mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-06-14 04:01:30 +08:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1c079908e2 | |||
| 1f901ab582 | |||
| acbc152375 | |||
| 13585f1092 | |||
| ee85e1482e | |||
| 5b9acd1d92 |
@@ -29,6 +29,9 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci --ignore-scripts
|
run: npm ci --ignore-scripts
|
||||||
|
|
||||||
|
- name: Run supply-chain IOC scan
|
||||||
|
run: npm run security:ioc-scan
|
||||||
|
|
||||||
- name: Verify OpenCode package payload
|
- name: Verify OpenCode package payload
|
||||||
run: node tests/scripts/build-opencode.test.js
|
run: node tests/scripts/build-opencode.test.js
|
||||||
|
|
||||||
|
|||||||
@@ -53,6 +53,9 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci --ignore-scripts
|
run: npm ci --ignore-scripts
|
||||||
|
|
||||||
|
- name: Run supply-chain IOC scan
|
||||||
|
run: npm run security:ioc-scan
|
||||||
|
|
||||||
- name: Verify OpenCode package payload
|
- name: Verify OpenCode package payload
|
||||||
run: node tests/scripts/build-opencode.test.js
|
run: node tests/scripts/build-opencode.test.js
|
||||||
|
|
||||||
|
|||||||
+22
-19
@@ -1,36 +1,39 @@
|
|||||||
# ECC 2.0 GA Roadmap
|
# ECC 2.0 GA Roadmap
|
||||||
|
|
||||||
This roadmap is the durable repo mirror for the Linear project:
|
This roadmap is the durable repo mirror for the active Linear project:
|
||||||
|
|
||||||
<https://linear.app/ecctools/project/ecc-20-ga-harness-os-security-platform-de2a0ecace6f>
|
<https://linear.app/itomarkets/project/ecc-platform-roadmap-52b328ee03e1>
|
||||||
|
|
||||||
Linear issue creation is currently blocked by the workspace active issue limit,
|
Linear issue creation is available again in the Ito Markets workspace. The live
|
||||||
so the live execution truth is split across:
|
execution truth is split across:
|
||||||
|
|
||||||
- the Linear project description, status updates, and milestones;
|
- the Linear project documents, issue lanes, dependencies, and milestones;
|
||||||
- this repo document;
|
- this repo document;
|
||||||
- merged PR evidence;
|
- merged PR evidence;
|
||||||
- handoffs under `~/.cluster-swarm/handoffs/`.
|
- handoffs under `~/.cluster-swarm/handoffs/`.
|
||||||
|
|
||||||
## Current Evidence
|
## Current Evidence
|
||||||
|
|
||||||
As of 2026-05-13:
|
As of 2026-05-15:
|
||||||
|
|
||||||
- GitHub queues are clean across `affaan-m/everything-claude-code`,
|
- GitHub queues are clean across `affaan-m/everything-claude-code`,
|
||||||
`affaan-m/agentshield`, `affaan-m/JARVIS`, `ECC-Tools/ECC-Tools`, and
|
`affaan-m/agentshield`, `affaan-m/JARVIS`, `ECC-Tools/ECC-Tools`, and
|
||||||
`ECC-Tools/ECC-website`: the latest sweep found 0 open PRs and 0 open
|
`ECC-Tools/ECC-website`: the latest sweep found 0 open PRs and 0 open issues
|
||||||
issues across all five repos.
|
across all five repos. ECC Tools org verification requires
|
||||||
- GitHub discussions are also clean across those tracked repos:
|
`env -u GITHUB_TOKEN` in this shell so the configured GitHub host credential
|
||||||
the latest GraphQL sweep found 52 total trunk discussions with 0 open,
|
is used instead of the incompatible environment token.
|
||||||
and 0 total/open discussions on AgentShield, JARVIS, ECC-Tools, and the
|
- GitHub discussions are current across those tracked repos:
|
||||||
ECC-Tools website.
|
`affaan-m/everything-claude-code` has 57 total discussions and 0 without
|
||||||
- The final open public GitHub issue, #1314, was closed as a non-actionable
|
maintainer touch after May 15 maintainer updates on #73 and #1239; AgentShield,
|
||||||
external badge/listing notification with a courtesy comment.
|
JARVIS, ECC Tools, and the ECC Tools website have discussions disabled or 0
|
||||||
- Linear issue creation for this project was re-tested after GitHub cleanup and
|
total discussions.
|
||||||
is still blocked by the workspace free issue limit. Seven roadmap-lane issue
|
- The current Linear roadmap contains 16 issue lanes (`ITO-44` through
|
||||||
creation attempts all returned the same limit error, so this repo mirror and
|
`ITO-59`) and five milestones: Security and Access Baseline, ECC 2.0 Preview
|
||||||
Linear project status updates remain the active tracking surfaces until the
|
and Publication, AgentShield Enterprise Iteration, ECC Tools Next-Level
|
||||||
workspace is upgraded or issue capacity is freed.
|
Platform, and Legacy Audit and Salvage.
|
||||||
|
- `docs/releases/2.0.0-rc.1/publication-evidence-2026-05-15.md` records the
|
||||||
|
queue, discussion, Linear roadmap, ECC Tools access, and PR #1921
|
||||||
|
Mini Shai-Hulud/TanStack follow-up evidence refresh.
|
||||||
- `npm run harness:audit -- --format json` reports 70/70 on current `main`.
|
- `npm run harness:audit -- --format json` reports 70/70 on current `main`.
|
||||||
- `npm run observability:ready` reports 21/21 readiness on current `main`,
|
- `npm run observability:ready` reports 21/21 readiness on current `main`,
|
||||||
including the GitHub/Linear/handoff/roadmap progress-sync contract.
|
including the GitHub/Linear/handoff/roadmap progress-sync contract.
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Use these templates as launch-ready starting points. Review channel tone before
|
|||||||
## X Post: Release Announcement
|
## X Post: Release Announcement
|
||||||
|
|
||||||
```text
|
```text
|
||||||
ECC v2.0.0-rc.1 is live.
|
ECC v2.0.0-rc.1 preview pack is ready for final release review.
|
||||||
|
|
||||||
The repo is moving from a Claude Code config pack into a cross-harness operating system for agentic work.
|
The repo is moving from a Claude Code config pack into a cross-harness operating system for agentic work.
|
||||||
|
|
||||||
@@ -55,7 +55,7 @@ ECC v2.0.0-rc.1 pushes that further: reusable skills, thin harness adapters, and
|
|||||||
## LinkedIn Post: Partner-Friendly Summary
|
## LinkedIn Post: Partner-Friendly Summary
|
||||||
|
|
||||||
```text
|
```text
|
||||||
ECC v2.0.0-rc.1 is live.
|
ECC v2.0.0-rc.1 preview pack is ready for final release review.
|
||||||
|
|
||||||
The practical shift: ECC is no longer just a Claude Code config pack. It is becoming a cross-harness operating system for agentic work.
|
The practical shift: ECC is no longer just a Claude Code config pack. It is becoming a cross-harness operating system for agentic work.
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,13 @@
|
|||||||
|
|
||||||
- verify local `main` is synced to `origin/main`
|
- verify local `main` is synced to `origin/main`
|
||||||
- verify `docs/ECC-2.0-GA-ROADMAP.md` reflects the current Linear milestone plan
|
- verify `docs/ECC-2.0-GA-ROADMAP.md` reflects the current Linear milestone plan
|
||||||
|
and the May 15 `ECC Platform Roadmap` project under the Ito Markets workspace
|
||||||
- verify `docs/HERMES-SETUP.md` is present
|
- verify `docs/HERMES-SETUP.md` is present
|
||||||
- verify `docs/architecture/cross-harness.md` is present
|
- verify `docs/architecture/cross-harness.md` is present
|
||||||
- verify this release directory is committed
|
- verify this release directory is committed
|
||||||
|
- verify `preview-pack-manifest.md` lists the public release, Hermes, adapter,
|
||||||
|
observability, publication, and announcement artifacts before running final
|
||||||
|
publish checks
|
||||||
- keep private tokens, personal docs, and raw workspace exports out of the repo
|
- keep private tokens, personal docs, and raw workspace exports out of the repo
|
||||||
|
|
||||||
## Release Surface
|
## Release Surface
|
||||||
@@ -14,6 +18,8 @@
|
|||||||
- verify package, plugin, marketplace, OpenCode, and agent metadata stays at `2.0.0-rc.1`
|
- verify package, plugin, marketplace, OpenCode, and agent metadata stays at `2.0.0-rc.1`
|
||||||
- verify `ecc2/Cargo.toml` stays at `0.1.0` for rc.1; `ecc2/` remains an alpha control-plane scaffold
|
- verify `ecc2/Cargo.toml` stays at `0.1.0` for rc.1; `ecc2/` remains an alpha control-plane scaffold
|
||||||
- complete `publication-readiness.md` with fresh evidence before any GitHub release, npm publish, plugin submission, or announcement post
|
- complete `publication-readiness.md` with fresh evidence before any GitHub release, npm publish, plugin submission, or announcement post
|
||||||
|
- include `publication-evidence-2026-05-15.md` in the final evidence review,
|
||||||
|
then rerun publish-facing checks from the exact release commit
|
||||||
- update release metadata in one dedicated release-version PR
|
- update release metadata in one dedicated release-version PR
|
||||||
- run the root test suite
|
- run the root test suite
|
||||||
- run `cd ecc2 && cargo test`
|
- run `cd ecc2 && cargo test`
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# LinkedIn Draft - ECC v2.0.0-rc.1
|
# LinkedIn Draft - ECC v2.0.0-rc.1
|
||||||
|
|
||||||
ECC v2.0.0-rc.1 is live as the first release-candidate pass at the 2.0 direction.
|
ECC v2.0.0-rc.1 is ready for final release review as the first release-candidate pass at the 2.0 direction.
|
||||||
|
|
||||||
The practical shift is simple: ECC is no longer framed as only a Claude Code plugin or config bundle.
|
The practical shift is simple: ECC is no longer framed as only a Claude Code plugin or config bundle.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
# ECC v2.0.0-rc.1 Preview Pack Manifest
|
||||||
|
|
||||||
|
This manifest defines the reviewed preview pack for `2.0.0-rc.1`. It is not a
|
||||||
|
release action by itself. Use it to verify that the public launch surface is
|
||||||
|
assembled before creating the GitHub prerelease, publishing npm, tagging plugin
|
||||||
|
surfaces, or posting announcements.
|
||||||
|
|
||||||
|
## Pack Contents
|
||||||
|
|
||||||
|
| Artifact | Role | Gate |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `README.md` | Public onramp and install surface | Links Hermes setup, rc.1 notes, plugin install, manual install, reset, and uninstall guidance |
|
||||||
|
| `docs/HERMES-SETUP.md` | Public Hermes operator topology | No raw workspace export, credentials, private account names, or local-only operator state |
|
||||||
|
| `skills/hermes-imports/SKILL.md` | Sanitized Hermes-to-ECC import workflow | Includes import rules, sanitization checklist, conversion pattern, and output contract |
|
||||||
|
| `docs/architecture/cross-harness.md` | Shared substrate model for Claude Code, Codex, OpenCode, Cursor, Gemini, Hermes, and terminal-only use | Names portability boundaries and does not claim unsupported native parity |
|
||||||
|
| `docs/architecture/harness-adapter-compliance.md` | Adapter matrix and scorecard | Verified by `npm run harness:adapters -- --check` |
|
||||||
|
| `docs/architecture/observability-readiness.md` | Local operator-readiness gate | Verified by `npm run observability:ready` |
|
||||||
|
| `docs/architecture/progress-sync-contract.md` | GitHub, Linear, handoff, roadmap, and work-item sync boundary | Checked by `node scripts/platform-audit.js --format json --allow-untracked docs/drafts/` |
|
||||||
|
| `docs/releases/2.0.0-rc.1/release-notes.md` | GitHub release copy source | Must be refreshed with final live release/package/plugin URLs before publication |
|
||||||
|
| `docs/releases/2.0.0-rc.1/quickstart.md` | Clone-to-first-workflow path | Covers clone, install, verify, first skill, and harness switch |
|
||||||
|
| `docs/releases/2.0.0-rc.1/launch-checklist.md` | Operator launch checklist | Must remain approval-gated for release, package, plugin, and announcement actions |
|
||||||
|
| `docs/releases/2.0.0-rc.1/publication-readiness.md` | Release gate | Requires fresh evidence from the exact release commit |
|
||||||
|
| `docs/releases/2.0.0-rc.1/publication-evidence-2026-05-15.md` | Current May 15 queue, roadmap, security, and AgentShield evidence | Must be superseded by a final clean-checkout evidence file before real publication |
|
||||||
|
| `docs/releases/2.0.0-rc.1/naming-and-publication-matrix.md` | Naming, slug, and publication-path decision record | Keeps `Everything Claude Code / ECC`, npm `ecc-universal`, and plugin slug `ecc` for rc.1 |
|
||||||
|
| `docs/releases/2.0.0-rc.1/x-thread.md` | X launch draft | Must replace placeholders with live URLs after release/package/plugin publication |
|
||||||
|
| `docs/releases/2.0.0-rc.1/linkedin-post.md` | LinkedIn launch draft | Must replace placeholders with live URLs after release/package/plugin publication |
|
||||||
|
| `docs/releases/2.0.0-rc.1/article-outline.md` | Longform launch outline | Must stay release-candidate framed until GA evidence exists |
|
||||||
|
| `docs/releases/2.0.0-rc.1/telegram-handoff.md` | Internal/shareable handoff copy | Must not include private workspace or credential details |
|
||||||
|
| `docs/releases/2.0.0-rc.1/demo-prompts.md` | Demo prompts and proof-of-work prompts | Must keep private Hermes workflows abstracted into public examples |
|
||||||
|
|
||||||
|
## Hermes Skill Boundary
|
||||||
|
|
||||||
|
The preview pack includes one public Hermes-specialized skill:
|
||||||
|
|
||||||
|
- `skills/hermes-imports/SKILL.md`
|
||||||
|
|
||||||
|
That is intentional for rc.1. The skill is a sanitization and conversion
|
||||||
|
workflow, not a dump of private Hermes automations. Additional Hermes-generated
|
||||||
|
skills should enter ECC only after they pass the same rules:
|
||||||
|
|
||||||
|
- no raw workspace exports;
|
||||||
|
- no live account names, client data, finance data, CRM data, health data, or
|
||||||
|
private contact graph;
|
||||||
|
- provider requirements described by capability, not by secret value;
|
||||||
|
- repo-relative examples instead of local absolute paths;
|
||||||
|
- tests or docs proving the workflow is useful without private state.
|
||||||
|
|
||||||
|
## Reference-Inspired Adapter Direction
|
||||||
|
|
||||||
|
The preview pack uses outside systems as design pressure, not as copy targets:
|
||||||
|
|
||||||
|
| Reference pressure | ECC preview-pack interpretation |
|
||||||
|
| --- | --- |
|
||||||
|
| Claude Code | Native plugin, skills, commands, hooks, MCP conventions, and statusline-oriented workflows |
|
||||||
|
| Codex | Instruction-backed plugin metadata, shared skills, MCP reference config, and explicit hook-parity caveats |
|
||||||
|
| OpenCode | Adapter-backed package/plugin surface with shared hook logic at the edge |
|
||||||
|
| Zed-adjacent tools | Instruction-backed portability until a verified native adapter exists |
|
||||||
|
| dmux | Session/runtime orchestration signals and handoff exports, not a replacement for repo validation |
|
||||||
|
| Orca, Superset, Ghast | Reference-only pressure for worktree lifecycle, session grouping, notifications, and workspace presets |
|
||||||
|
| Hermes Agent, meta-harness, autocontext-style systems | Evaluation, memory, and context-routing pressure routed through public artifacts, verifier outputs, and the evaluator/RAG prototype |
|
||||||
|
|
||||||
|
## Final Verification Commands
|
||||||
|
|
||||||
|
Run these from the exact release commit before publication:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git status --short --branch
|
||||||
|
node scripts/platform-audit.js --format json --allow-untracked docs/drafts/
|
||||||
|
npm run harness:adapters -- --check
|
||||||
|
npm run harness:audit -- --format json
|
||||||
|
npm run observability:ready
|
||||||
|
npm run security:ioc-scan
|
||||||
|
npm audit --audit-level=high
|
||||||
|
npm audit signatures
|
||||||
|
node tests/docs/ecc2-release-surface.test.js
|
||||||
|
node tests/run-all.js
|
||||||
|
cd ecc2 && cargo test
|
||||||
|
```
|
||||||
|
|
||||||
|
## Publication Blockers
|
||||||
|
|
||||||
|
The preview pack is assembled, but publication is still blocked until these live
|
||||||
|
surfaces exist and are recorded in a final evidence file:
|
||||||
|
|
||||||
|
- GitHub prerelease `v2.0.0-rc.1`;
|
||||||
|
- npm `ecc-universal@2.0.0-rc.1` on the `next` dist-tag;
|
||||||
|
- Claude plugin tag / marketplace propagation for `ecc@ecc`;
|
||||||
|
- Codex plugin publication or owner-approved manual submission path;
|
||||||
|
- final announcement URLs in X, LinkedIn, GitHub release, and longform copy;
|
||||||
|
- ECC Tools billing/product readiness evidence before any native-payments
|
||||||
|
announcement copy is published.
|
||||||
|
|
||||||
|
## Result
|
||||||
|
|
||||||
|
The rc.1 preview pack is ready for a final clean-checkout release gate, but not
|
||||||
|
for public publication without the approval-gated release, package, plugin, and
|
||||||
|
announcement steps above.
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
# ECC v2.0.0-rc.1 Publication Evidence - 2026-05-15
|
||||||
|
|
||||||
|
This is release-readiness evidence only. It does not create a GitHub release,
|
||||||
|
npm publication, plugin tag, marketplace submission, or announcement post.
|
||||||
|
|
||||||
|
## Source Commit
|
||||||
|
|
||||||
|
| Field | Evidence |
|
||||||
|
| --- | --- |
|
||||||
|
| Upstream main base | `acbc152375c215b4fe2a20abb29dfb733727c4cb` |
|
||||||
|
| Evidence branch | `docs/ecc2-rc1-preview-pack-refresh` |
|
||||||
|
| Evidence scope | Current `main` after PR #1921, #1924, #1925, #1926, and AgentShield #83 follow-up |
|
||||||
|
| Git remote | `https://github.com/affaan-m/everything-claude-code.git` |
|
||||||
|
| Local status caveat | Working tree had the unrelated untracked `docs/drafts/` directory before this docs refresh |
|
||||||
|
|
||||||
|
The actual release operator should repeat all publish-facing checks from the
|
||||||
|
final release commit with a clean checkout before publishing.
|
||||||
|
|
||||||
|
## Queue And Discussion State
|
||||||
|
|
||||||
|
| Surface | Command | Result |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Trunk PRs/issues | `gh pr list` and `gh issue list` for `affaan-m/everything-claude-code` | 0 open PRs, 0 open issues |
|
||||||
|
| AgentShield PRs/issues | `gh pr list` and `gh issue list` for `affaan-m/agentshield` | 0 open PRs, 0 open issues |
|
||||||
|
| JARVIS PRs/issues | `gh pr list` and `gh issue list` for `affaan-m/JARVIS` | 0 open PRs, 0 open issues |
|
||||||
|
| ECC Tools PRs/issues | `env -u GITHUB_TOKEN gh pr list` and `env -u GITHUB_TOKEN gh issue list` for `ECC-Tools/ECC-Tools` | 0 open PRs, 0 open issues |
|
||||||
|
| ECC website PRs/issues | `env -u GITHUB_TOKEN gh pr list` and `env -u GITHUB_TOKEN gh issue list` for `ECC-Tools/ECC-website` | 0 open PRs, 0 open issues |
|
||||||
|
| Trunk discussions | GraphQL discussion count and maintainer-touch sweep | 58 total discussions; 0 without maintainer touch after May 15 maintainer comments |
|
||||||
|
| Other repo discussions | GraphQL discussion count for AgentShield, JARVIS, ECC Tools, and ECC website | Discussions disabled or 0 total |
|
||||||
|
|
||||||
|
The ECC Tools organization is reachable with the configured GitHub host
|
||||||
|
credential. In this shell, the exported `GITHUB_TOKEN` overrides that credential
|
||||||
|
and causes false 404/403 failures for `ECC-Tools/*`. Use `env -u GITHUB_TOKEN`
|
||||||
|
for ECC Tools verification commands until that environment override is cleaned
|
||||||
|
up.
|
||||||
|
|
||||||
|
## Linear Roadmap State
|
||||||
|
|
||||||
|
The detailed execution roadmap now lives in Linear project:
|
||||||
|
|
||||||
|
<https://linear.app/itomarkets/project/ecc-platform-roadmap-52b328ee03e1>
|
||||||
|
|
||||||
|
The project contains 16 issue-level lanes and 5 milestones:
|
||||||
|
|
||||||
|
| Milestone | Issues |
|
||||||
|
| --- | --- |
|
||||||
|
| Security and Access Baseline | `ITO-44`, `ITO-57`, `ITO-58` |
|
||||||
|
| ECC 2.0 Preview and Publication | `ITO-45`, `ITO-46`, `ITO-47`, `ITO-56` |
|
||||||
|
| AgentShield Enterprise Iteration | `ITO-48`, `ITO-49` |
|
||||||
|
| ECC Tools Next-Level Platform | `ITO-50`, `ITO-51`, `ITO-52`, `ITO-53`, `ITO-54`, `ITO-59` |
|
||||||
|
| Legacy Audit and Salvage | `ITO-55` |
|
||||||
|
|
||||||
|
Project documents added in Linear:
|
||||||
|
|
||||||
|
- Roadmap Index and Current Execution Baseline
|
||||||
|
- Status Update 2026-05-15
|
||||||
|
- GitHub Queue Snapshot 2026-05-15
|
||||||
|
- Completion Audit Snapshot 2026-05-15
|
||||||
|
- Discussion Queue Evidence 2026-05-15
|
||||||
|
- ECC-Tools Access Evidence 2026-05-15
|
||||||
|
|
||||||
|
## Supply-Chain Evidence
|
||||||
|
|
||||||
|
| Surface | Evidence |
|
||||||
|
| --- | --- |
|
||||||
|
| PR #1921 | Merged supply-chain IOC expansion for Mini Shai-Hulud/TanStack follow-up |
|
||||||
|
| Node IPC follow-up / PR #1924 | Added May 14 `node-ipc` malicious-version, hash, DNS, and runtime IOC coverage |
|
||||||
|
| PR #1926 | Added `platform:audit` and `security-ioc-scan` command surfaces plus release workflow IOC gates |
|
||||||
|
| AgentShield PR #83 | Merged Mini Shai-Hulud IOC coverage for TanStack, Mistral, OpenSearch, Guardrails, UiPath, Squawk, Claude Code / VS Code persistence, and dead-man switch artifacts |
|
||||||
|
| Trunk merge commits | `f04702bdac132662c8496e817bcd850c86e2b854`, `ee85e1482e3d6322ddb2706392ea0fc97469bd26`, `13585f1092c92fa3f20ffe0d756e40c5720b0de5` |
|
||||||
|
| AgentShield merge commit | `f899b27ba3fa60ec7e0dca41cc2dadcb1a1fb75d` |
|
||||||
|
| Local IOC tests | `node tests/ci/scan-supply-chain-iocs.test.js` passed 12/12 |
|
||||||
|
| Unicode safety | `node scripts/ci/check-unicode-safety.js` passed |
|
||||||
|
| IOC scan | `npm run security:ioc-scan` passed |
|
||||||
|
| Root suite | `npm test` passed 2427/2427, 0 failed |
|
||||||
|
| Repo sweeps | `node scripts/ci/scan-supply-chain-iocs.js --root <ECC-workspace> --home` passed with 1238 files inspected; targeted persistence path checks found no active `gh-token-monitor`, `pgsql-monitor`, `transformers.pyz`, or `pgmonitor.py` artifacts |
|
||||||
|
|
||||||
|
The May 15 IOC expansion added coverage for OpenSearch/Mistral/Guardrails/
|
||||||
|
UiPath/Squawk-style campaign variants, `opensearch_init.js`, `vite_setup.mjs`,
|
||||||
|
dead-drop/session protocol strings, and AI-tooling persistence surfaces without
|
||||||
|
committing full high-entropy indicators that trip secret scanners.
|
||||||
|
The May 15 node-ipc follow-up blocks `node-ipc@9.1.6`, `9.2.3`, `10.1.1`,
|
||||||
|
`10.1.2`, `11.0.0`, `11.1.0`, and `12.0.1`, plus the `node-ipc.cjs` payload
|
||||||
|
hash, malicious tarball hashes, DNS exfil domains, and runtime markers reported
|
||||||
|
by Socket.
|
||||||
|
AgentShield PR #83 adds the matching scanner-side enterprise coverage:
|
||||||
|
version-pinned package detections, `.claude` / `.vscode` automation-surface
|
||||||
|
discovery, `gh-token-monitor` LaunchAgent/systemd/local-bin artifact detection,
|
||||||
|
network/payload IOCs, built action/CLI bundles, 1758/1758 local tests, and
|
||||||
|
green GitHub Actions verification before merge.
|
||||||
|
|
||||||
|
## Preview Pack State
|
||||||
|
|
||||||
|
`preview-pack-manifest.md` now assembles the rc.1 preview-pack boundary:
|
||||||
|
|
||||||
|
- release notes, quickstart, launch checklist, publication readiness, naming
|
||||||
|
matrix, and May 15 evidence;
|
||||||
|
- `docs/HERMES-SETUP.md` and `skills/hermes-imports/SKILL.md` as the public
|
||||||
|
Hermes-specialized surface;
|
||||||
|
- cross-harness, harness-adapter, observability, and progress-sync docs;
|
||||||
|
- X, LinkedIn, article, Telegram, and demo collateral that must receive final
|
||||||
|
live URLs after release/package/plugin publication;
|
||||||
|
- explicit blockers for GitHub release, npm `next` publish, Claude plugin,
|
||||||
|
Codex plugin, ECC Tools billing/product-readiness, and announcements.
|
||||||
|
|
||||||
|
The preview pack is assembled for final clean-checkout gating, but it is still
|
||||||
|
not a publication action.
|
||||||
|
|
||||||
|
## Current Publication Blockers
|
||||||
|
|
||||||
|
- GitHub prerelease `v2.0.0-rc.1` is still not created in this pass.
|
||||||
|
- npm `ecc-universal@2.0.0-rc.1` is still not published to the `next` dist-tag.
|
||||||
|
- Claude plugin tag and marketplace propagation remain approval-gated.
|
||||||
|
- Codex plugin public marketplace/manual submission path still needs final
|
||||||
|
owner verification.
|
||||||
|
- ECC Tools billing claims are now GitHub-access-verifiable, but the billing
|
||||||
|
product surface still needs a dedicated payment-readiness audit before any
|
||||||
|
public payment announcement.
|
||||||
|
- Release notes, X, LinkedIn, and longform copy still need final live URLs after
|
||||||
|
release/package/plugin URLs exist.
|
||||||
|
|
||||||
|
## Result
|
||||||
|
|
||||||
|
The queue, discussion, Linear roadmap, and supply-chain evidence are fresher
|
||||||
|
than the May 13 publication evidence. They improve readiness, but they do not
|
||||||
|
replace the final clean-checkout publish pass required by
|
||||||
|
`publication-readiness.md`.
|
||||||
@@ -6,12 +6,17 @@ URLs from the exact commit being released.
|
|||||||
|
|
||||||
For the current rc.1 naming decision and package/plugin publication path, see
|
For the current rc.1 naming decision and package/plugin publication path, see
|
||||||
[`naming-and-publication-matrix.md`](naming-and-publication-matrix.md).
|
[`naming-and-publication-matrix.md`](naming-and-publication-matrix.md).
|
||||||
|
For the assembled rc.1 preview pack boundary, see
|
||||||
|
[`preview-pack-manifest.md`](preview-pack-manifest.md).
|
||||||
For the May 12 dry-run evidence pass, see
|
For the May 12 dry-run evidence pass, see
|
||||||
[`publication-evidence-2026-05-12.md`](publication-evidence-2026-05-12.md).
|
[`publication-evidence-2026-05-12.md`](publication-evidence-2026-05-12.md).
|
||||||
For the May 13 release-readiness evidence refresh, see
|
For the May 13 release-readiness evidence refresh, see
|
||||||
[`publication-evidence-2026-05-13.md`](publication-evidence-2026-05-13.md).
|
[`publication-evidence-2026-05-13.md`](publication-evidence-2026-05-13.md).
|
||||||
For the May 13 post-hardening evidence refresh after PR #1850 and PR #1851, see
|
For the May 13 post-hardening evidence refresh after PR #1850 and PR #1851, see
|
||||||
[`publication-evidence-2026-05-13-post-hardening.md`](publication-evidence-2026-05-13-post-hardening.md).
|
[`publication-evidence-2026-05-13-post-hardening.md`](publication-evidence-2026-05-13-post-hardening.md).
|
||||||
|
For the May 15 queue, discussion, Linear roadmap, and Mini Shai-Hulud/TanStack
|
||||||
|
follow-up evidence refresh after PR #1921, see
|
||||||
|
[`publication-evidence-2026-05-15.md`](publication-evidence-2026-05-15.md).
|
||||||
|
|
||||||
## Release Identity Matrix
|
## Release Identity Matrix
|
||||||
|
|
||||||
@@ -39,8 +44,8 @@ For the May 13 post-hardening evidence refresh after PR #1850 and PR #1851, see
|
|||||||
| Claude plugin | Manifest validates, marketplace JSON points to public repo, install docs match slug | `claude plugin validate .claude-plugin/plugin.json`; `claude plugin tag .claude-plugin --dry-run`; isolated temp-home install smoke | `Blocker: real tag creation/push requires approval` | Plugin owner | Clean-checkout dry-run and install smoke recorded |
|
| Claude plugin | Manifest validates, marketplace JSON points to public repo, install docs match slug | `claude plugin validate .claude-plugin/plugin.json`; `claude plugin tag .claude-plugin --dry-run`; isolated temp-home install smoke | `Blocker: real tag creation/push requires approval` | Plugin owner | Clean-checkout dry-run and install smoke recorded |
|
||||||
| Codex plugin | Manifest version matches package and docs, hook limitations are explicit | `node tests/docs/ecc2-release-surface.test.js` | `Blocker: marketplace submission path still manual/owner-gated` | Plugin owner | Evidence recorded |
|
| Codex plugin | Manifest version matches package and docs, hook limitations are explicit | `node tests/docs/ecc2-release-surface.test.js` | `Blocker: marketplace submission path still manual/owner-gated` | Plugin owner | Evidence recorded |
|
||||||
| OpenCode package | Build output is regenerated from source and package metadata is current | `npm run build:opencode` | `Blocker: none for local build; public distribution still follows npm/plugin release` | Package owner | Evidence recorded |
|
| OpenCode package | Build output is regenerated from source and package metadata is current | `npm run build:opencode` | `Blocker: none for local build; public distribution still follows npm/plugin release` | Package owner | Evidence recorded |
|
||||||
| ECC Tools billing reference | Any billing claim links to verified Marketplace/App state | `gh api repos/ECC-Tools/ECC-Tools` plus app/marketplace URL check | `Blocker:` | ECC Tools owner | Pending |
|
| ECC Tools billing reference | Any billing claim links to verified Marketplace/App state | `env -u GITHUB_TOKEN gh repo view ECC-Tools/ECC-Tools --json nameWithOwner,isPrivate,viewerPermission` plus app/marketplace URL check | `Blocker: repo access verified on 2026-05-15; billing/product readiness still requires dedicated ECC Tools audit` | ECC Tools owner | Access verified; billing audit pending |
|
||||||
| Announcement copy | X, LinkedIn, GitHub release, and longform copy point to live URLs | `rg -n "TODO" docs/releases/2.0.0-rc.1` and repeat for `TBD` | `Blocker:` | Release owner | Pending |
|
| Announcement copy | X, LinkedIn, GitHub release, and longform copy point to live URLs | `rg -n "TODO" docs/releases/2.0.0-rc.1` and repeat for `TBD` | `Blocker: final live release/npm/plugin URLs do not exist yet` | Release owner | Pending |
|
||||||
| Privileged workflow hardening | Release and maintenance workflows avoid persisted checkout tokens | `node scripts/ci/validate-workflow-security.js` | `Blocker:` | Release owner | Evidence recorded in post-hardening refresh |
|
| Privileged workflow hardening | Release and maintenance workflows avoid persisted checkout tokens | `node scripts/ci/validate-workflow-security.js` | `Blocker:` | Release owner | Evidence recorded in post-hardening refresh |
|
||||||
|
|
||||||
## Required Command Evidence
|
## Required Command Evidence
|
||||||
@@ -60,6 +65,9 @@ Record the exact commit SHA and command output before any publication action:
|
|||||||
| Package surface | `node tests/scripts/npm-publish-surface.test.js` | 0 failures; no Python bytecode in npm tarball | `2/2` passed in May 12 evidence pass |
|
| Package surface | `node tests/scripts/npm-publish-surface.test.js` | 0 failures; no Python bytecode in npm tarball | `2/2` passed in May 12 evidence pass |
|
||||||
| Release surface | `node tests/docs/ecc2-release-surface.test.js` | 0 failures | `publication-evidence-2026-05-13.md`: 18/18 passed |
|
| Release surface | `node tests/docs/ecc2-release-surface.test.js` | 0 failures | `publication-evidence-2026-05-13.md`: 18/18 passed |
|
||||||
| Optional Rust surface | `cd ecc2 && cargo test` | 0 failures or explicit deferral | `publication-evidence-2026-05-13.md`: 462/462 passed, warnings only |
|
| Optional Rust surface | `cd ecc2 && cargo test` | 0 failures or explicit deferral | `publication-evidence-2026-05-13.md`: 462/462 passed, warnings only |
|
||||||
|
| Queue baseline | `gh pr list` / `gh issue list` across trunk, AgentShield, JARVIS, ECC Tools, and ECC website | Under 20 open PRs and under 20 open issues | `publication-evidence-2026-05-15.md`: 0 open PRs and 0 open issues across checked repos |
|
||||||
|
| Discussion baseline | GraphQL discussion count and maintainer-touch sweep | No unmanaged active discussion queue | `publication-evidence-2026-05-15.md`: 58 trunk discussions, 0 without maintainer touch; other tracked repos disabled or 0 |
|
||||||
|
| Linear roadmap | Linear project and issue readback | Detailed roadmap exists with release, security, AgentShield, ECC Tools, legacy, and observability lanes | `publication-evidence-2026-05-15.md`: project and 16 issue lanes recorded |
|
||||||
|
|
||||||
## Do Not Publish If
|
## Do Not Publish If
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ Claude Code remains a core target. Codex, OpenCode, Cursor, Gemini, and other ha
|
|||||||
- Documented the cross-harness portability model for skills, hooks, MCPs, rules, and instructions.
|
- Documented the cross-harness portability model for skills, hooks, MCPs, rules, and instructions.
|
||||||
- Added a Hermes import playbook for turning local operator patterns into publishable ECC skills.
|
- Added a Hermes import playbook for turning local operator patterns into publishable ECC skills.
|
||||||
- Added a local [observability readiness gate](../../architecture/observability-readiness.md) for loop status, session traces, harness audit, and ECC2 tool-risk logs.
|
- Added a local [observability readiness gate](../../architecture/observability-readiness.md) for loop status, session traces, harness audit, and ECC2 tool-risk logs.
|
||||||
|
- Refreshed the release-readiness evidence after the May 2026 Mini Shai-Hulud/TanStack campaign follow-up, including expanded IOC coverage, clean queue/discussion checks, and a detailed Linear roadmap gate.
|
||||||
|
|
||||||
## Why This Matters
|
## Why This Matters
|
||||||
|
|
||||||
@@ -37,6 +38,7 @@ What ships in this surface:
|
|||||||
- release notes and launch collateral
|
- release notes and launch collateral
|
||||||
- cross-harness architecture documentation
|
- cross-harness architecture documentation
|
||||||
- Hermes import guidance for sanitized operator workflows
|
- Hermes import guidance for sanitized operator workflows
|
||||||
|
- publication-readiness evidence for queue state, discussion state, Linear roadmap coverage, and supply-chain follow-up
|
||||||
|
|
||||||
What stays local:
|
What stays local:
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,10 @@ credentials:
|
|||||||
- Follow-on reporting from StepSecurity, Socket, Aikido, and Wiz describes the
|
- Follow-on reporting from StepSecurity, Socket, Aikido, and Wiz describes the
|
||||||
same campaign expanding into packages associated with Mistral AI, UiPath,
|
same campaign expanding into packages associated with Mistral AI, UiPath,
|
||||||
OpenSearch, Guardrails AI, Squawk, and other npm/PyPI packages.
|
OpenSearch, Guardrails AI, Squawk, and other npm/PyPI packages.
|
||||||
|
- Socket's 2026-05-14 `node-ipc` report describes a separate active npm
|
||||||
|
compromise affecting `node-ipc` versions `9.1.6`, `9.2.3`, and `12.0.1`,
|
||||||
|
with historical malicious `node-ipc` versions also blocked by ECC because
|
||||||
|
they carried destructive or unauthorized file-writing behavior.
|
||||||
- The live IOC set includes persistence through Claude Code
|
- The live IOC set includes persistence through Claude Code
|
||||||
`.claude/settings.json`, VS Code `.vscode/tasks.json`, and OS-level
|
`.claude/settings.json`, VS Code `.vscode/tasks.json`, and OS-level
|
||||||
`gh-token-monitor` LaunchAgent/systemd services. Some variants add a
|
`gh-token-monitor` LaunchAgent/systemd services. Some variants add a
|
||||||
@@ -35,6 +39,12 @@ credentials:
|
|||||||
`opensearch_init.js`, `vite_setup.mjs`, campaign salt `svksjrhjkcejg`,
|
`opensearch_init.js`, `vite_setup.mjs`, campaign salt `svksjrhjkcejg`,
|
||||||
Session protocol strings, `claude@users.noreply.github.com` dead-drop
|
Session protocol strings, `claude@users.noreply.github.com` dead-drop
|
||||||
commits, `dependabout/` branch names, and `OhNoWhatsGoingOnWithGitHub`.
|
commits, `dependabout/` branch names, and `OhNoWhatsGoingOnWithGitHub`.
|
||||||
|
- The `node-ipc` sweep watches for `node-ipc.cjs` payload hash
|
||||||
|
`96097e06...d9034144`, tarball hashes for the malicious `9.1.6`, `9.2.3`,
|
||||||
|
and `12.0.1` artifacts, `sh.azurestaticprovider.net`, `bt.node.js`,
|
||||||
|
`37.16.75.69`, DNS exfil labels `xh` / `xd` / `xf` where present in
|
||||||
|
artifacts, `__ntw`, `__ntRun`, `/nt-` temp archives, and archive entries such
|
||||||
|
as `uname.txt`, `envs.txt`, and `fixtures/_paths.txt`.
|
||||||
- The attack chain combined `pull_request_target`, GitHub Actions cache
|
- The attack chain combined `pull_request_target`, GitHub Actions cache
|
||||||
poisoning across a fork/base trust boundary, and OIDC token extraction from a
|
poisoning across a fork/base trust boundary, and OIDC token extraction from a
|
||||||
GitHub Actions runner.
|
GitHub Actions runner.
|
||||||
@@ -47,6 +57,7 @@ Primary references:
|
|||||||
- <https://tanstack.com/blog/npm-supply-chain-compromise-postmortem>
|
- <https://tanstack.com/blog/npm-supply-chain-compromise-postmortem>
|
||||||
- <https://github.com/advisories/GHSA-g7cv-rxg3-hmpx>
|
- <https://github.com/advisories/GHSA-g7cv-rxg3-hmpx>
|
||||||
- <https://tanstack.com/blog/incident-followup>
|
- <https://tanstack.com/blog/incident-followup>
|
||||||
|
- <https://socket.dev/blog/node-ipc-package-compromised>
|
||||||
- <https://docs.npmjs.com/trusted-publishers/>
|
- <https://docs.npmjs.com/trusted-publishers/>
|
||||||
- <https://www.cisa.gov/news-events/alerts/2025/09/23/widespread-supply-chain-compromise-impacting-npm-ecosystem>
|
- <https://www.cisa.gov/news-events/alerts/2025/09/23/widespread-supply-chain-compromise-impacting-npm-ecosystem>
|
||||||
|
|
||||||
|
|||||||
@@ -63,6 +63,7 @@
|
|||||||
"rules/",
|
"rules/",
|
||||||
"schemas/",
|
"schemas/",
|
||||||
"scripts/catalog.js",
|
"scripts/catalog.js",
|
||||||
|
"scripts/ci/scan-supply-chain-iocs.js",
|
||||||
"scripts/consult.js",
|
"scripts/consult.js",
|
||||||
"scripts/auto-update.js",
|
"scripts/auto-update.js",
|
||||||
"scripts/claw.js",
|
"scripts/claw.js",
|
||||||
@@ -74,6 +75,7 @@
|
|||||||
"scripts/harness-adapter-compliance.js",
|
"scripts/harness-adapter-compliance.js",
|
||||||
"scripts/harness-audit.js",
|
"scripts/harness-audit.js",
|
||||||
"scripts/observability-readiness.js",
|
"scripts/observability-readiness.js",
|
||||||
|
"scripts/platform-audit.js",
|
||||||
"scripts/hooks/",
|
"scripts/hooks/",
|
||||||
"scripts/install-apply.js",
|
"scripts/install-apply.js",
|
||||||
"scripts/install-plan.js",
|
"scripts/install-plan.js",
|
||||||
@@ -293,6 +295,7 @@
|
|||||||
"harness:adapters": "node scripts/harness-adapter-compliance.js",
|
"harness:adapters": "node scripts/harness-adapter-compliance.js",
|
||||||
"harness:audit": "node scripts/harness-audit.js",
|
"harness:audit": "node scripts/harness-audit.js",
|
||||||
"observability:ready": "node scripts/observability-readiness.js",
|
"observability:ready": "node scripts/observability-readiness.js",
|
||||||
|
"platform:audit": "node scripts/platform-audit.js",
|
||||||
"security:ioc-scan": "node scripts/ci/scan-supply-chain-iocs.js",
|
"security:ioc-scan": "node scripts/ci/scan-supply-chain-iocs.js",
|
||||||
"claw": "node scripts/claw.js",
|
"claw": "node scripts/claw.js",
|
||||||
"orchestrate:status": "node scripts/orchestration-status.js",
|
"orchestrate:status": "node scripts/orchestration-status.js",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
|
const crypto = require('crypto');
|
||||||
const os = require('os');
|
const os = require('os');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
@@ -204,6 +205,7 @@ const MALICIOUS_PACKAGE_VERSIONS = {
|
|||||||
'mbt': ['1.2.48'],
|
'mbt': ['1.2.48'],
|
||||||
'mistralai': ['2.4.6'],
|
'mistralai': ['2.4.6'],
|
||||||
'ml-toolkit-ts': ['1.0.4', '1.0.5'],
|
'ml-toolkit-ts': ['1.0.4', '1.0.5'],
|
||||||
|
'node-ipc': ['9.1.6', '9.2.3', '10.1.1', '10.1.2', '11.0.0', '11.1.0', '12.0.1'],
|
||||||
'nextmove-mcp': ['0.1.3', '0.1.4', '0.1.5', '0.1.7'],
|
'nextmove-mcp': ['0.1.3', '0.1.4', '0.1.5', '0.1.7'],
|
||||||
'safe-action': ['0.8.3', '0.8.4'],
|
'safe-action': ['0.8.3', '0.8.4'],
|
||||||
'ts-dna': ['3.0.1', '3.0.2', '3.0.3', '3.0.4', '3.0.5'],
|
'ts-dna': ['3.0.1', '3.0.2', '3.0.3', '3.0.4', '3.0.5'],
|
||||||
@@ -251,7 +253,6 @@ const CRITICAL_TEXT_INDICATORS = [
|
|||||||
'seed2.getsession.org',
|
'seed2.getsession.org',
|
||||||
'seed3.getsession.org',
|
'seed3.getsession.org',
|
||||||
'signalservice',
|
'signalservice',
|
||||||
'snode',
|
|
||||||
'git-tanstack.com',
|
'git-tanstack.com',
|
||||||
'litter.catbox.moe/h8nc9u.js',
|
'litter.catbox.moe/h8nc9u.js',
|
||||||
'litter.catbox.moe/7rrc6l.mjs',
|
'litter.catbox.moe/7rrc6l.mjs',
|
||||||
@@ -266,8 +267,60 @@ const CRITICAL_TEXT_INDICATORS = [
|
|||||||
'PUSH UR T3MPRR',
|
'PUSH UR T3MPRR',
|
||||||
'codeql_analysis.yml',
|
'codeql_analysis.yml',
|
||||||
'shai-hulud-workflow.yml',
|
'shai-hulud-workflow.yml',
|
||||||
|
[
|
||||||
|
'96097e0612d9575c',
|
||||||
|
'b133021017fb1a5c',
|
||||||
|
'68a03b60f9f3d24e',
|
||||||
|
'bdc0e628d9034144',
|
||||||
|
].join(''),
|
||||||
|
[
|
||||||
|
'449e4265979b5fdb',
|
||||||
|
'2d3446c021af437e',
|
||||||
|
'815debd66de7da2f',
|
||||||
|
'e54f1ad93cbcc75e',
|
||||||
|
].join(''),
|
||||||
|
[
|
||||||
|
'c2f4dc64aec46315',
|
||||||
|
'40a568e88932b61d',
|
||||||
|
'aebbfb7e8281b812',
|
||||||
|
'fa01b7215f9be9ea',
|
||||||
|
].join(''),
|
||||||
|
[
|
||||||
|
'78a82d93b4f58083',
|
||||||
|
'5f5823b85a3d9ee1',
|
||||||
|
'f03a15ee6f0e01b',
|
||||||
|
'4eac86252a7002981',
|
||||||
|
].join(''),
|
||||||
|
'sh.azurestaticprovider.net',
|
||||||
|
'37.16.75.69',
|
||||||
|
'bt.node.js',
|
||||||
|
'__ntw',
|
||||||
|
'__ntRun',
|
||||||
|
'/nt-',
|
||||||
|
'uname.txt',
|
||||||
|
'envs.txt',
|
||||||
|
'fixtures/_paths.txt',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const MALICIOUS_FILE_HASHES = {
|
||||||
|
'96097e0612d9575cb133021017fb1a5c68a03b60f9f3d24ebdc0e628d9034144': {
|
||||||
|
indicator: 'node-ipc.cjs sha256',
|
||||||
|
message: 'Known malicious node-ipc CommonJS payload hash is present',
|
||||||
|
},
|
||||||
|
'449e4265979b5fdb2d3446c021af437e815debd66de7da2fe54f1ad93cbcc75e': {
|
||||||
|
indicator: 'node-ipc-9.1.6.tgz sha256',
|
||||||
|
message: 'Known malicious node-ipc tarball hash is present',
|
||||||
|
},
|
||||||
|
'c2f4dc64aec4631540a568e88932b61daebbfb7e8281b812fa01b7215f9be9ea': {
|
||||||
|
indicator: 'node-ipc-9.2.3.tgz sha256',
|
||||||
|
message: 'Known malicious node-ipc tarball hash is present',
|
||||||
|
},
|
||||||
|
'78a82d93b4f580835f5823b85a3d9ee1f03a15ee6f0e01b4eac86252a7002981': {
|
||||||
|
indicator: 'node-ipc-12.0.1.tar.gz sha256',
|
||||||
|
message: 'Known malicious node-ipc tarball hash is present',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const DEPENDENCY_FILENAMES = new Set([
|
const DEPENDENCY_FILENAMES = new Set([
|
||||||
'package.json',
|
'package.json',
|
||||||
'package-lock.json',
|
'package-lock.json',
|
||||||
@@ -279,6 +332,13 @@ const DEPENDENCY_FILENAMES = new Set([
|
|||||||
'requirements.txt',
|
'requirements.txt',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const INSPECT_ONLY_FILENAMES = new Set([
|
||||||
|
'node-ipc.cjs',
|
||||||
|
'node-ipc-9.1.6.tgz',
|
||||||
|
'node-ipc-9.2.3.tgz',
|
||||||
|
'node-ipc-12.0.1.tar.gz',
|
||||||
|
]);
|
||||||
|
|
||||||
const PERSISTENCE_FILENAMES = new Set([
|
const PERSISTENCE_FILENAMES = new Set([
|
||||||
'settings.json',
|
'settings.json',
|
||||||
'tasks.json',
|
'tasks.json',
|
||||||
@@ -342,6 +402,7 @@ function shouldInspectFile(filePath) {
|
|||||||
if (DEPENDENCY_FILENAMES.has(base)) return true;
|
if (DEPENDENCY_FILENAMES.has(base)) return true;
|
||||||
if (PERSISTENCE_FILENAMES.has(base) && isInSpecialConfigPath(filePath)) return true;
|
if (PERSISTENCE_FILENAMES.has(base) && isInSpecialConfigPath(filePath)) return true;
|
||||||
if (PAYLOAD_FILENAMES.has(base) && filePath.includes(`${path.sep}node_modules${path.sep}`)) return true;
|
if (PAYLOAD_FILENAMES.has(base) && filePath.includes(`${path.sep}node_modules${path.sep}`)) return true;
|
||||||
|
if (INSPECT_ONLY_FILENAMES.has(base)) return true;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -392,7 +453,13 @@ function walkNodeModules(nodeModulesDir, files) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function inspectPackageDir(packageDir, files) {
|
function inspectPackageDir(packageDir, files) {
|
||||||
for (const filename of [...DEPENDENCY_FILENAMES, ...PAYLOAD_FILENAMES, 'setup.mjs', 'execution.js']) {
|
for (const filename of [
|
||||||
|
...DEPENDENCY_FILENAMES,
|
||||||
|
...PAYLOAD_FILENAMES,
|
||||||
|
...INSPECT_ONLY_FILENAMES,
|
||||||
|
'setup.mjs',
|
||||||
|
'execution.js',
|
||||||
|
]) {
|
||||||
const candidate = path.join(packageDir, filename);
|
const candidate = path.join(packageDir, filename);
|
||||||
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
|
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
|
||||||
files.push(candidate);
|
files.push(candidate);
|
||||||
@@ -408,6 +475,14 @@ function readText(filePath) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sha256File(filePath) {
|
||||||
|
try {
|
||||||
|
return crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex');
|
||||||
|
} catch {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function lineForIndex(text, index) {
|
function lineForIndex(text, index) {
|
||||||
return text.slice(0, index).split(/\r?\n/).length;
|
return text.slice(0, index).split(/\r?\n/).length;
|
||||||
}
|
}
|
||||||
@@ -425,6 +500,18 @@ function scanFile(filePath, rootDir, findings) {
|
|||||||
const relativePath = path.relative(rootDir, filePath) || filePath;
|
const relativePath = path.relative(rootDir, filePath) || filePath;
|
||||||
const text = readText(filePath);
|
const text = readText(filePath);
|
||||||
const lowerText = normalizeForMatch(text);
|
const lowerText = normalizeForMatch(text);
|
||||||
|
const hashFinding = MALICIOUS_FILE_HASHES[sha256File(filePath)];
|
||||||
|
|
||||||
|
if (hashFinding) {
|
||||||
|
addFinding(
|
||||||
|
findings,
|
||||||
|
'critical',
|
||||||
|
relativePath,
|
||||||
|
1,
|
||||||
|
hashFinding.indicator,
|
||||||
|
hashFinding.message,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (PAYLOAD_FILENAMES.has(base)) {
|
if (PAYLOAD_FILENAMES.has(base)) {
|
||||||
addFinding(
|
addFinding(
|
||||||
@@ -492,8 +579,14 @@ function runtimeTargets() {
|
|||||||
return [
|
return [
|
||||||
'/tmp/transformers.pyz',
|
'/tmp/transformers.pyz',
|
||||||
'/tmp/pgmonitor.py',
|
'/tmp/pgmonitor.py',
|
||||||
|
'/tmp/node-ipc-9.1.6.tgz',
|
||||||
|
'/tmp/node-ipc-9.2.3.tgz',
|
||||||
|
'/tmp/node-ipc-12.0.1.tar.gz',
|
||||||
'/private/tmp/transformers.pyz',
|
'/private/tmp/transformers.pyz',
|
||||||
'/private/tmp/pgmonitor.py',
|
'/private/tmp/pgmonitor.py',
|
||||||
|
'/private/tmp/node-ipc-9.1.6.tgz',
|
||||||
|
'/private/tmp/node-ipc-9.2.3.tgz',
|
||||||
|
'/private/tmp/node-ipc-12.0.1.tar.gz',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -526,7 +619,9 @@ function parseArgs(argv) {
|
|||||||
const options = {};
|
const options = {};
|
||||||
for (let i = 0; i < argv.length; i++) {
|
for (let i = 0; i < argv.length; i++) {
|
||||||
const arg = argv[i];
|
const arg = argv[i];
|
||||||
if (arg === '--root') {
|
if (arg === '--help' || arg === '-h') {
|
||||||
|
options.help = true;
|
||||||
|
} else if (arg === '--root') {
|
||||||
options.rootDir = argv[++i];
|
options.rootDir = argv[++i];
|
||||||
} else if (arg === '--home') {
|
} else if (arg === '--home') {
|
||||||
options.home = true;
|
options.home = true;
|
||||||
@@ -542,6 +637,26 @@ function parseArgs(argv) {
|
|||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function printHelp() {
|
||||||
|
console.log(`Usage: node scripts/ci/scan-supply-chain-iocs.js [options]
|
||||||
|
|
||||||
|
Scan dependency manifests, lockfiles, installed package payloads, and AI-tool
|
||||||
|
persistence paths for active supply-chain IOC markers.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--root <dir> Directory to scan (default: repo root)
|
||||||
|
--home Also scan user-level Claude, VS Code, LaunchAgent, systemd,
|
||||||
|
and /tmp persistence targets
|
||||||
|
--home-dir <dir> Home directory to use with --home
|
||||||
|
--json Emit JSON instead of text
|
||||||
|
--help, -h Show this help
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
node scripts/ci/scan-supply-chain-iocs.js --home
|
||||||
|
node scripts/ci/scan-supply-chain-iocs.js --root /path/to/project --json
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
function printReport(result, json = false) {
|
function printReport(result, json = false) {
|
||||||
if (json) {
|
if (json) {
|
||||||
console.log(JSON.stringify(result, null, 2));
|
console.log(JSON.stringify(result, null, 2));
|
||||||
@@ -564,6 +679,10 @@ function printReport(result, json = false) {
|
|||||||
if (require.main === module) {
|
if (require.main === module) {
|
||||||
try {
|
try {
|
||||||
const options = parseArgs(process.argv.slice(2));
|
const options = parseArgs(process.argv.slice(2));
|
||||||
|
if (options.help) {
|
||||||
|
printHelp();
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
const result = scanSupplyChainIocs(options);
|
const result = scanSupplyChainIocs(options);
|
||||||
printReport(result, options.json);
|
printReport(result, options.json);
|
||||||
process.exit(result.findings.length > 0 ? 1 : 0);
|
process.exit(result.findings.length > 0 ? 1 : 0);
|
||||||
@@ -575,6 +694,7 @@ if (require.main === module) {
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
CRITICAL_TEXT_INDICATORS,
|
CRITICAL_TEXT_INDICATORS,
|
||||||
|
MALICIOUS_FILE_HASHES,
|
||||||
MALICIOUS_PACKAGE_VERSIONS,
|
MALICIOUS_PACKAGE_VERSIONS,
|
||||||
scanSupplyChainIocs,
|
scanSupplyChainIocs,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -45,6 +45,14 @@ const COMMANDS = {
|
|||||||
script: 'status.js',
|
script: 'status.js',
|
||||||
description: 'Query the ECC SQLite state store status summary',
|
description: 'Query the ECC SQLite state store status summary',
|
||||||
},
|
},
|
||||||
|
'platform-audit': {
|
||||||
|
script: 'platform-audit.js',
|
||||||
|
description: 'Audit GitHub queues, discussions, roadmap, release, and security evidence',
|
||||||
|
},
|
||||||
|
'security-ioc-scan': {
|
||||||
|
script: 'ci/scan-supply-chain-iocs.js',
|
||||||
|
description: 'Scan dependency and AI-tool persistence surfaces for active supply-chain IOCs',
|
||||||
|
},
|
||||||
sessions: {
|
sessions: {
|
||||||
script: 'sessions-cli.js',
|
script: 'sessions-cli.js',
|
||||||
description: 'List or inspect ECC sessions from the SQLite state store',
|
description: 'List or inspect ECC sessions from the SQLite state store',
|
||||||
@@ -77,6 +85,8 @@ const PRIMARY_COMMANDS = [
|
|||||||
'repair',
|
'repair',
|
||||||
'auto-update',
|
'auto-update',
|
||||||
'status',
|
'status',
|
||||||
|
'platform-audit',
|
||||||
|
'security-ioc-scan',
|
||||||
'sessions',
|
'sessions',
|
||||||
'work-items',
|
'work-items',
|
||||||
'session-inspect',
|
'session-inspect',
|
||||||
@@ -115,6 +125,8 @@ Examples:
|
|||||||
ecc status --json
|
ecc status --json
|
||||||
ecc status --exit-code
|
ecc status --exit-code
|
||||||
ecc status --markdown --write status.md
|
ecc status --markdown --write status.md
|
||||||
|
ecc platform-audit --json --allow-untracked docs/drafts/
|
||||||
|
ecc security-ioc-scan --home
|
||||||
ecc sessions
|
ecc sessions
|
||||||
ecc sessions session-active --json
|
ecc sessions session-active --json
|
||||||
ecc work-items upsert linear-ecc-20 --source linear --source-id ECC-20 --title "Review control-plane contract" --status blocked
|
ecc work-items upsert linear-ecc-20 --source linear --source-id ECC-20 --title "Review control-plane contract" --status blocked
|
||||||
|
|||||||
@@ -0,0 +1,630 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const os = require('os');
|
||||||
|
const path = require('path');
|
||||||
|
const { spawnSync } = require('child_process');
|
||||||
|
|
||||||
|
const SCHEMA_VERSION = 'ecc.platform-audit.v1';
|
||||||
|
const DEFAULT_REPOS = Object.freeze([
|
||||||
|
'affaan-m/everything-claude-code',
|
||||||
|
'affaan-m/agentshield',
|
||||||
|
'affaan-m/JARVIS',
|
||||||
|
'ECC-Tools/ECC-Tools',
|
||||||
|
'ECC-Tools/ECC-website',
|
||||||
|
]);
|
||||||
|
const DEFAULT_THRESHOLDS = Object.freeze({
|
||||||
|
maxOpenPrs: 20,
|
||||||
|
maxOpenIssues: 20,
|
||||||
|
maxDirtyFiles: 0,
|
||||||
|
});
|
||||||
|
const MAINTAINER_ASSOCIATIONS = new Set(['OWNER', 'MEMBER', 'COLLABORATOR']);
|
||||||
|
const DISCUSSION_QUERY = 'query($owner: String!, $name: String!, $first: Int!) { repository(owner: $owner, name: $name) { hasDiscussionsEnabled discussions(first: $first, orderBy: {field: UPDATED_AT, direction: DESC}) { totalCount nodes { number title url updatedAt authorAssociation comments(first: 20) { nodes { authorAssociation } } } } } }';
|
||||||
|
|
||||||
|
function usage() {
|
||||||
|
console.log([
|
||||||
|
'Usage: node scripts/platform-audit.js [options]',
|
||||||
|
'',
|
||||||
|
'Operator readiness audit for ECC queue, discussion, roadmap, release, and security evidence.',
|
||||||
|
'',
|
||||||
|
'Options:',
|
||||||
|
' --format <text|json> Output format (default: text)',
|
||||||
|
' --root <dir> Repository root to inspect (default: cwd)',
|
||||||
|
' --repo <owner/repo> GitHub repo to inspect; repeatable',
|
||||||
|
' --skip-github Skip live GitHub queue/discussion checks',
|
||||||
|
' --max-open-prs <n> Fail when open PR count is above n (default: 20)',
|
||||||
|
' --max-open-issues <n> Fail when open issue count is above n (default: 20)',
|
||||||
|
' --max-dirty-files <n> Fail when blocking dirty file count is above n (default: 0)',
|
||||||
|
' --allow-untracked <path> Ignore untracked files under path; repeatable',
|
||||||
|
' --use-env-github-token Keep GITHUB_TOKEN when invoking gh',
|
||||||
|
' --exit-code Return 2 when the audit is not ready',
|
||||||
|
' --help, -h Show this help',
|
||||||
|
].join('\n'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function readValue(args, index, flagName) {
|
||||||
|
const value = args[index + 1];
|
||||||
|
if (!value || value.startsWith('--')) {
|
||||||
|
throw new Error(`${flagName} requires a value`);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseIntegerFlag(value, flagName) {
|
||||||
|
const parsed = Number.parseInt(value, 10);
|
||||||
|
if (!Number.isFinite(parsed) || parsed < 0) {
|
||||||
|
throw new Error(`Invalid ${flagName}: ${value}`);
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(argv) {
|
||||||
|
const args = argv.slice(2);
|
||||||
|
const parsed = {
|
||||||
|
allowUntracked: [],
|
||||||
|
exitCode: false,
|
||||||
|
format: 'text',
|
||||||
|
help: false,
|
||||||
|
repos: [],
|
||||||
|
root: path.resolve(process.cwd()),
|
||||||
|
skipGithub: false,
|
||||||
|
thresholds: { ...DEFAULT_THRESHOLDS },
|
||||||
|
useEnvGithubToken: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let index = 0; index < args.length; index += 1) {
|
||||||
|
const arg = args[index];
|
||||||
|
|
||||||
|
if (arg === '--help' || arg === '-h') {
|
||||||
|
parsed.help = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg === '--format') {
|
||||||
|
parsed.format = readValue(args, index, arg).toLowerCase();
|
||||||
|
index += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg.startsWith('--format=')) {
|
||||||
|
parsed.format = arg.slice('--format='.length).toLowerCase();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg === '--root') {
|
||||||
|
parsed.root = path.resolve(readValue(args, index, arg));
|
||||||
|
index += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg.startsWith('--root=')) {
|
||||||
|
parsed.root = path.resolve(arg.slice('--root='.length));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg === '--repo') {
|
||||||
|
parsed.repos.push(readValue(args, index, arg));
|
||||||
|
index += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg.startsWith('--repo=')) {
|
||||||
|
parsed.repos.push(arg.slice('--repo='.length));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg === '--skip-github') {
|
||||||
|
parsed.skipGithub = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg === '--allow-untracked') {
|
||||||
|
parsed.allowUntracked.push(readValue(args, index, arg));
|
||||||
|
index += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg.startsWith('--allow-untracked=')) {
|
||||||
|
parsed.allowUntracked.push(arg.slice('--allow-untracked='.length));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg === '--max-open-prs') {
|
||||||
|
parsed.thresholds.maxOpenPrs = parseIntegerFlag(readValue(args, index, arg), arg);
|
||||||
|
index += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg.startsWith('--max-open-prs=')) {
|
||||||
|
parsed.thresholds.maxOpenPrs = parseIntegerFlag(arg.slice('--max-open-prs='.length), '--max-open-prs');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg === '--max-open-issues') {
|
||||||
|
parsed.thresholds.maxOpenIssues = parseIntegerFlag(readValue(args, index, arg), arg);
|
||||||
|
index += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg.startsWith('--max-open-issues=')) {
|
||||||
|
parsed.thresholds.maxOpenIssues = parseIntegerFlag(arg.slice('--max-open-issues='.length), '--max-open-issues');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg === '--max-dirty-files') {
|
||||||
|
parsed.thresholds.maxDirtyFiles = parseIntegerFlag(readValue(args, index, arg), arg);
|
||||||
|
index += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg.startsWith('--max-dirty-files=')) {
|
||||||
|
parsed.thresholds.maxDirtyFiles = parseIntegerFlag(arg.slice('--max-dirty-files='.length), '--max-dirty-files');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg === '--use-env-github-token') {
|
||||||
|
parsed.useEnvGithubToken = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg === '--exit-code') {
|
||||||
|
parsed.exitCode = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Unknown argument: ${arg}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!['text', 'json'].includes(parsed.format)) {
|
||||||
|
throw new Error(`Invalid format: ${parsed.format}. Use text or json.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
parsed.allowUntracked = parsed.allowUntracked.map(normalizeRelativePrefix);
|
||||||
|
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeRelativePrefix(value) {
|
||||||
|
return String(value || '')
|
||||||
|
.replace(/\\/g, '/')
|
||||||
|
.replace(/^\.\/+/, '')
|
||||||
|
.replace(/\/+$/, '') + (String(value || '').endsWith('/') ? '/' : '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function runCommand(command, args, options = {}) {
|
||||||
|
const result = spawnSync(command, args, {
|
||||||
|
cwd: options.cwd,
|
||||||
|
env: options.env || process.env,
|
||||||
|
encoding: 'utf8',
|
||||||
|
maxBuffer: 10 * 1024 * 1024,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
throw new Error(`${command} ${args.join(' ')} failed: ${result.error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.status !== 0) {
|
||||||
|
throw new Error(`${command} ${args.join(' ')} failed: ${(result.stderr || result.stdout || '').trim()}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.stdout || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function runGhJson(args, options = {}) {
|
||||||
|
const shimPath = process.env.ECC_GH_SHIM;
|
||||||
|
const command = shimPath ? process.execPath : 'gh';
|
||||||
|
const commandArgs = shimPath ? [shimPath, ...args] : args;
|
||||||
|
const env = { ...process.env };
|
||||||
|
|
||||||
|
if (!options.useEnvGithubToken) {
|
||||||
|
delete env.GITHUB_TOKEN;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stdout = runCommand(command, commandArgs, { env });
|
||||||
|
try {
|
||||||
|
return JSON.parse(stdout || 'null');
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`gh ${args.join(' ')} returned invalid JSON: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function readText(rootDir, relativePath) {
|
||||||
|
try {
|
||||||
|
return fs.readFileSync(path.join(rootDir, relativePath), 'utf8');
|
||||||
|
} catch (_error) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function safeParseJson(text) {
|
||||||
|
if (!text || !text.trim()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.parse(text);
|
||||||
|
} catch (_error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function includesAll(text, needles) {
|
||||||
|
return needles.every(needle => text.includes(needle));
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCheck(id, status, summary, details = {}) {
|
||||||
|
return { id, status, summary, ...details };
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseGitStatus(output) {
|
||||||
|
const lines = output.split(/\r?\n/).filter(Boolean);
|
||||||
|
const branchLine = lines[0] || '';
|
||||||
|
const dirtyLines = lines.slice(1);
|
||||||
|
return {
|
||||||
|
branch: branchLine.replace(/^##\s*/, '') || null,
|
||||||
|
dirtyLines,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAllowedUntracked(statusLine, allowUntracked) {
|
||||||
|
if (!statusLine.startsWith('?? ')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const relativePath = statusLine.slice(3).replace(/\\/g, '/');
|
||||||
|
return allowUntracked.some(prefix => relativePath === prefix || relativePath.startsWith(prefix));
|
||||||
|
}
|
||||||
|
|
||||||
|
function inspectGit(rootDir, options) {
|
||||||
|
try {
|
||||||
|
const parsed = parseGitStatus(runCommand('git', ['status', '--short', '--branch'], { cwd: rootDir }));
|
||||||
|
const ignoredDirty = parsed.dirtyLines.filter(line => isAllowedUntracked(line, options.allowUntracked));
|
||||||
|
const blockingDirty = parsed.dirtyLines.filter(line => !isAllowedUntracked(line, options.allowUntracked));
|
||||||
|
|
||||||
|
return {
|
||||||
|
available: true,
|
||||||
|
branch: parsed.branch,
|
||||||
|
dirtyLines: parsed.dirtyLines,
|
||||||
|
ignoredDirty,
|
||||||
|
blockingDirty,
|
||||||
|
blockingDirtyCount: blockingDirty.length,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
available: false,
|
||||||
|
error: error.message,
|
||||||
|
branch: null,
|
||||||
|
dirtyLines: [],
|
||||||
|
ignoredDirty: [],
|
||||||
|
blockingDirty: [],
|
||||||
|
blockingDirtyCount: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function discussionNeedsMaintainerTouch(discussion) {
|
||||||
|
if (MAINTAINER_ASSOCIATIONS.has(discussion.authorAssociation)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const comments = discussion.comments && Array.isArray(discussion.comments.nodes)
|
||||||
|
? discussion.comments.nodes
|
||||||
|
: [];
|
||||||
|
return !comments.some(comment => MAINTAINER_ASSOCIATIONS.has(comment.authorAssociation));
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitRepo(repo) {
|
||||||
|
const [owner, name] = String(repo || '').split('/');
|
||||||
|
if (!owner || !name) {
|
||||||
|
throw new Error(`Invalid repo: ${repo}`);
|
||||||
|
}
|
||||||
|
return { owner, name };
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetchDiscussionSummary(repo, options) {
|
||||||
|
const { owner, name } = splitRepo(repo);
|
||||||
|
const payload = runGhJson([
|
||||||
|
'api',
|
||||||
|
'graphql',
|
||||||
|
'-f',
|
||||||
|
`owner=${owner}`,
|
||||||
|
'-f',
|
||||||
|
`name=${name}`,
|
||||||
|
'-F',
|
||||||
|
'first=100',
|
||||||
|
'-f',
|
||||||
|
`query=${DISCUSSION_QUERY}`,
|
||||||
|
], options);
|
||||||
|
const repository = payload && payload.data && payload.data.repository;
|
||||||
|
const discussions = repository && repository.discussions;
|
||||||
|
const nodes = discussions && Array.isArray(discussions.nodes) ? discussions.nodes : [];
|
||||||
|
const needingTouch = nodes.filter(discussionNeedsMaintainerTouch);
|
||||||
|
|
||||||
|
return {
|
||||||
|
enabled: Boolean(repository && repository.hasDiscussionsEnabled),
|
||||||
|
totalCount: discussions && Number.isFinite(discussions.totalCount) ? discussions.totalCount : 0,
|
||||||
|
sampledCount: nodes.length,
|
||||||
|
needingMaintainerTouch: needingTouch.map(discussion => ({
|
||||||
|
number: discussion.number,
|
||||||
|
title: discussion.title,
|
||||||
|
url: discussion.url,
|
||||||
|
updatedAt: discussion.updatedAt,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetchGithubRepo(repo, options) {
|
||||||
|
const prs = runGhJson([
|
||||||
|
'pr',
|
||||||
|
'list',
|
||||||
|
'--repo',
|
||||||
|
repo,
|
||||||
|
'--state',
|
||||||
|
'open',
|
||||||
|
'--json',
|
||||||
|
'number,title,isDraft,mergeStateStatus,updatedAt,url,author',
|
||||||
|
], options);
|
||||||
|
const issues = runGhJson([
|
||||||
|
'issue',
|
||||||
|
'list',
|
||||||
|
'--repo',
|
||||||
|
repo,
|
||||||
|
'--state',
|
||||||
|
'open',
|
||||||
|
'--json',
|
||||||
|
'number,title,updatedAt,url,author,labels',
|
||||||
|
], options);
|
||||||
|
const discussionSummary = fetchDiscussionSummary(repo, options);
|
||||||
|
|
||||||
|
return {
|
||||||
|
repo,
|
||||||
|
openPrs: Array.isArray(prs) ? prs.length : 0,
|
||||||
|
openIssues: Array.isArray(issues) ? issues.length : 0,
|
||||||
|
discussions: discussionSummary,
|
||||||
|
dirtyPrs: (Array.isArray(prs) ? prs : []).filter(pr => pr.mergeStateStatus === 'DIRTY').map(pr => ({
|
||||||
|
number: pr.number,
|
||||||
|
title: pr.title,
|
||||||
|
url: pr.url,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildGithubReport(options) {
|
||||||
|
const repos = options.repos.length > 0 ? options.repos : DEFAULT_REPOS;
|
||||||
|
|
||||||
|
if (options.skipGithub) {
|
||||||
|
return {
|
||||||
|
skipped: true,
|
||||||
|
repos: repos.map(repo => ({ repo, skipped: true })),
|
||||||
|
totals: {
|
||||||
|
openPrs: 0,
|
||||||
|
openIssues: 0,
|
||||||
|
discussionsNeedingMaintainerTouch: 0,
|
||||||
|
dirtyPrs: 0,
|
||||||
|
errors: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const repoReports = repos.map(repo => {
|
||||||
|
try {
|
||||||
|
return fetchGithubRepo(repo, options);
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
repo,
|
||||||
|
error: error.message,
|
||||||
|
openPrs: 0,
|
||||||
|
openIssues: 0,
|
||||||
|
discussions: {
|
||||||
|
enabled: false,
|
||||||
|
totalCount: 0,
|
||||||
|
sampledCount: 0,
|
||||||
|
needingMaintainerTouch: [],
|
||||||
|
},
|
||||||
|
dirtyPrs: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
skipped: false,
|
||||||
|
repos: repoReports,
|
||||||
|
totals: {
|
||||||
|
openPrs: repoReports.reduce((sum, repo) => sum + repo.openPrs, 0),
|
||||||
|
openIssues: repoReports.reduce((sum, repo) => sum + repo.openIssues, 0),
|
||||||
|
discussionsNeedingMaintainerTouch: repoReports.reduce((sum, repo) => sum + repo.discussions.needingMaintainerTouch.length, 0),
|
||||||
|
dirtyPrs: repoReports.reduce((sum, repo) => sum + repo.dirtyPrs.length, 0),
|
||||||
|
errors: repoReports.filter(repo => repo.error).length,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildLocalEvidenceChecks(rootDir) {
|
||||||
|
const packageJson = safeParseJson(readText(rootDir, 'package.json')) || {};
|
||||||
|
const packageScripts = packageJson.scripts || {};
|
||||||
|
const roadmap = readText(rootDir, 'docs/ECC-2.0-GA-ROADMAP.md');
|
||||||
|
const progressSync = readText(rootDir, 'docs/architecture/progress-sync-contract.md');
|
||||||
|
const supplyChain = readText(rootDir, 'docs/security/supply-chain-incident-response.md');
|
||||||
|
const evidence = readText(rootDir, 'docs/releases/2.0.0-rc.1/publication-evidence-2026-05-15.md');
|
||||||
|
|
||||||
|
return [
|
||||||
|
buildCheck(
|
||||||
|
'platform-audit-cli-surface',
|
||||||
|
packageScripts['platform:audit'] === 'node scripts/platform-audit.js' ? 'pass' : 'fail',
|
||||||
|
'package.json exposes the platform audit command',
|
||||||
|
{ fix: 'Add "platform:audit": "node scripts/platform-audit.js" to package.json.' }
|
||||||
|
),
|
||||||
|
buildCheck(
|
||||||
|
'roadmap-linear-mirror',
|
||||||
|
includesAll(roadmap, ['linear.app/itomarkets/project/ecc-platform-roadmap', 'ITO-44', 'ITO-59']) ? 'pass' : 'fail',
|
||||||
|
'repo roadmap mirrors the Linear roadmap and security/operator lanes',
|
||||||
|
{ path: 'docs/ECC-2.0-GA-ROADMAP.md' }
|
||||||
|
),
|
||||||
|
buildCheck(
|
||||||
|
'progress-sync-contract',
|
||||||
|
includesAll(progressSync, ['GitHub PRs/issues/discussions', 'Linear project', 'local handoff', 'repo roadmap', 'scripts/work-items.js']) ? 'pass' : 'fail',
|
||||||
|
'progress sync contract names GitHub, Linear, handoff, roadmap, and work-items surfaces',
|
||||||
|
{ path: 'docs/architecture/progress-sync-contract.md' }
|
||||||
|
),
|
||||||
|
buildCheck(
|
||||||
|
'supply-chain-runbook',
|
||||||
|
includesAll(supplyChain, ['TanStack', 'Mini Shai-Hulud', 'node-ipc', 'scan-supply-chain-iocs.js']) ? 'pass' : 'fail',
|
||||||
|
'supply-chain runbook covers the current TanStack/Mini Shai-Hulud/node-ipc scanner lane',
|
||||||
|
{ path: 'docs/security/supply-chain-incident-response.md' }
|
||||||
|
),
|
||||||
|
buildCheck(
|
||||||
|
'release-evidence-current',
|
||||||
|
includesAll(evidence, ['TanStack', 'Mini Shai-Hulud', 'Node IPC follow-up', 'node-ipc', 'IOC scan']) ? 'pass' : 'fail',
|
||||||
|
'rc.1 evidence includes current supply-chain verification artifacts',
|
||||||
|
{ path: 'docs/releases/2.0.0-rc.1/publication-evidence-2026-05-15.md' }
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildReport(options) {
|
||||||
|
const rootDir = path.resolve(options.root);
|
||||||
|
const git = inspectGit(rootDir, options);
|
||||||
|
const github = buildGithubReport(options);
|
||||||
|
const checks = [];
|
||||||
|
|
||||||
|
checks.push(buildCheck(
|
||||||
|
'git-worktree-blockers',
|
||||||
|
!git.available ? 'warn' : (git.blockingDirtyCount <= options.thresholds.maxDirtyFiles ? 'pass' : 'fail'),
|
||||||
|
!git.available
|
||||||
|
? 'git status is unavailable for this root'
|
||||||
|
: `blocking dirty files: ${git.blockingDirtyCount}`,
|
||||||
|
{
|
||||||
|
branch: git.branch,
|
||||||
|
ignoredDirtyCount: git.ignoredDirty.length,
|
||||||
|
blockingDirty: git.blockingDirty,
|
||||||
|
fix: 'Commit, stash, or explicitly allow unrelated untracked files before claiming release readiness.',
|
||||||
|
}
|
||||||
|
));
|
||||||
|
|
||||||
|
checks.push(buildCheck(
|
||||||
|
'github-fetch',
|
||||||
|
github.skipped ? 'warn' : (github.totals.errors === 0 ? 'pass' : 'fail'),
|
||||||
|
github.skipped ? 'live GitHub checks skipped' : `GitHub fetch errors: ${github.totals.errors}`,
|
||||||
|
{ fix: 'Re-run with working gh authentication or ECC_GH_SHIM for deterministic tests.' }
|
||||||
|
));
|
||||||
|
|
||||||
|
checks.push(buildCheck(
|
||||||
|
'github-open-pr-budget',
|
||||||
|
github.totals.openPrs <= options.thresholds.maxOpenPrs ? 'pass' : 'fail',
|
||||||
|
`open PRs: ${github.totals.openPrs}/${options.thresholds.maxOpenPrs}`,
|
||||||
|
{ fix: 'Triage, merge, close, or attach open PRs to roadmap issues until under budget.' }
|
||||||
|
));
|
||||||
|
|
||||||
|
checks.push(buildCheck(
|
||||||
|
'github-open-issue-budget',
|
||||||
|
github.totals.openIssues <= options.thresholds.maxOpenIssues ? 'pass' : 'fail',
|
||||||
|
`open issues: ${github.totals.openIssues}/${options.thresholds.maxOpenIssues}`,
|
||||||
|
{ fix: 'Triage, close, or attach open issues to Linear/project lanes until under budget.' }
|
||||||
|
));
|
||||||
|
|
||||||
|
checks.push(buildCheck(
|
||||||
|
'github-discussion-touch',
|
||||||
|
github.totals.discussionsNeedingMaintainerTouch === 0 ? 'pass' : 'fail',
|
||||||
|
`discussions needing maintainer touch: ${github.totals.discussionsNeedingMaintainerTouch}`,
|
||||||
|
{ fix: 'Respond to or route discussions without maintainer touch before marking the queue current.' }
|
||||||
|
));
|
||||||
|
|
||||||
|
checks.push(buildCheck(
|
||||||
|
'github-conflict-queue',
|
||||||
|
github.totals.dirtyPrs === 0 ? 'pass' : 'fail',
|
||||||
|
`conflicting open PRs: ${github.totals.dirtyPrs}`,
|
||||||
|
{ fix: 'Update, rebase, salvage, or close conflicting open PRs.' }
|
||||||
|
));
|
||||||
|
|
||||||
|
checks.push(...buildLocalEvidenceChecks(rootDir));
|
||||||
|
|
||||||
|
const topActions = checks
|
||||||
|
.filter(check => check.status === 'fail')
|
||||||
|
.map(check => ({
|
||||||
|
id: check.id,
|
||||||
|
summary: check.summary,
|
||||||
|
fix: check.fix || 'Review and remediate this failed check.',
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
schema_version: SCHEMA_VERSION,
|
||||||
|
generatedAt: new Date().toISOString(),
|
||||||
|
root: rootDir,
|
||||||
|
ready: topActions.length === 0,
|
||||||
|
thresholds: options.thresholds,
|
||||||
|
git,
|
||||||
|
github,
|
||||||
|
checks,
|
||||||
|
top_actions: topActions,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderText(report) {
|
||||||
|
const lines = [
|
||||||
|
`ECC Platform Audit: ${report.ready ? 'ready' : 'attention required'}`,
|
||||||
|
`Generated: ${report.generatedAt}`,
|
||||||
|
`Root: ${report.root}`,
|
||||||
|
'',
|
||||||
|
`Git: ${report.git.available ? report.git.branch : 'unavailable'}`,
|
||||||
|
`Blocking dirty files: ${report.git.blockingDirtyCount}`,
|
||||||
|
`Ignored dirty files: ${report.git.ignoredDirty.length}`,
|
||||||
|
'',
|
||||||
|
`GitHub skipped: ${report.github.skipped ? 'yes' : 'no'}`,
|
||||||
|
`Open PRs: ${report.github.totals.openPrs}/${report.thresholds.maxOpenPrs}`,
|
||||||
|
`Open issues: ${report.github.totals.openIssues}/${report.thresholds.maxOpenIssues}`,
|
||||||
|
`Discussions needing maintainer touch: ${report.github.totals.discussionsNeedingMaintainerTouch}`,
|
||||||
|
`Conflicting open PRs: ${report.github.totals.dirtyPrs}`,
|
||||||
|
'',
|
||||||
|
'Checks:',
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const check of report.checks) {
|
||||||
|
lines.push(` ${check.status.toUpperCase()} ${check.id}: ${check.summary}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push('', 'Top actions:');
|
||||||
|
if (report.top_actions.length === 0) {
|
||||||
|
lines.push(' none');
|
||||||
|
} else {
|
||||||
|
for (const action of report.top_actions) {
|
||||||
|
lines.push(` - ${action.id}: ${action.fix}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${lines.join('\n')}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
try {
|
||||||
|
const options = parseArgs(process.argv);
|
||||||
|
if (options.help) {
|
||||||
|
usage();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const report = buildReport(options);
|
||||||
|
const output = options.format === 'json'
|
||||||
|
? `${JSON.stringify(report, null, 2)}\n`
|
||||||
|
: renderText(report);
|
||||||
|
process.stdout.write(output);
|
||||||
|
|
||||||
|
if (options.exitCode && !report.ready) {
|
||||||
|
process.exitCode = 2;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error: ${error.message}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
|
main();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
buildReport,
|
||||||
|
parseArgs,
|
||||||
|
renderText,
|
||||||
|
runGhJson,
|
||||||
|
};
|
||||||
@@ -366,6 +366,65 @@ def stop_recording(proc):
|
|||||||
proc.stdin.write(b"q"); proc.stdin.flush(); proc.wait(timeout=10)
|
proc.stdin.write(b"q"); proc.stdin.flush(); proc.wait(timeout=10)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Per-Step Trace (opt-in)
|
||||||
|
|
||||||
|
The default failure screenshot is often too thin for diagnosing flaky tests. The step-level trace below is **off by default** — enable it only when reproducing a flaky case.
|
||||||
|
|
||||||
|
### Enable
|
||||||
|
|
||||||
|
```bash
|
||||||
|
E2E_TRACE=1 pytest tests/test_login.py -v
|
||||||
|
# Include typed text in the JSONL log (DO NOT use on tests that type credentials/PII):
|
||||||
|
E2E_TRACE=1 E2E_TRACE_INCLUDE_TEXT=1 pytest ...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Patch into BasePage
|
||||||
|
|
||||||
|
```python
|
||||||
|
import os, json, time
|
||||||
|
TRACE_ENABLED = os.environ.get("E2E_TRACE") == "1"
|
||||||
|
TRACE_INCLUDE_TEXT = os.environ.get("E2E_TRACE_INCLUDE_TEXT") == "1"
|
||||||
|
|
||||||
|
class BasePage:
|
||||||
|
_step = 0
|
||||||
|
|
||||||
|
def _trace(self, action, spec=None, text=None):
|
||||||
|
if not TRACE_ENABLED:
|
||||||
|
return
|
||||||
|
BasePage._step += 1
|
||||||
|
idx = f"{BasePage._step:03d}"
|
||||||
|
os.makedirs(ARTIFACT_DIR, exist_ok=True)
|
||||||
|
try:
|
||||||
|
self.window.capture_as_image().save(
|
||||||
|
os.path.join(ARTIFACT_DIR, f"step_{idx}_{action}.png"))
|
||||||
|
except Exception:
|
||||||
|
pass # capture failure must not break the test
|
||||||
|
rec = {
|
||||||
|
"ts": time.time(), "step": BasePage._step, "action": action,
|
||||||
|
"locator": getattr(spec, "criteria", None),
|
||||||
|
"text": text if TRACE_INCLUDE_TEXT else ("<redacted>" if text else None),
|
||||||
|
}
|
||||||
|
with open(os.path.join(ARTIFACT_DIR, "trace.jsonl"), "a") as f:
|
||||||
|
f.write(json.dumps(rec) + "\n")
|
||||||
|
|
||||||
|
def click(self, spec):
|
||||||
|
self.wait_visible(spec); self._trace("click_before", spec)
|
||||||
|
spec.click_input(); self._trace("click_after", spec)
|
||||||
|
|
||||||
|
def type_text(self, spec, text):
|
||||||
|
self.wait_visible(spec); self._trace("type_before", spec, text)
|
||||||
|
# ... existing set_edit_text / keyboard fallback ...
|
||||||
|
self._trace("type_after", spec)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Caveats
|
||||||
|
|
||||||
|
- **PII / credentials**: `type_text` content is `<redacted>` by default. Never set `E2E_TRACE_INCLUDE_TEXT=1` on login or payment flows.
|
||||||
|
- **Overhead**: ~50–200ms per action + one PNG per step on disk. Don't enable on the default CI matrix — only on a dedicated flake-repro job.
|
||||||
|
- **Artifact bloat**: a long flow produces tens of MB; tune `retention-days` accordingly.
|
||||||
|
- **Parallel/rerun hygiene**: this simple example appends to `trace.jsonl` and uses a class-level counter. Clear the artifact directory before reruns, and use per-worker artifact dirs for parallel tests.
|
||||||
|
- **Coverage gap**: actions performed outside `BasePage` (raw `pywinauto` calls in test code) are not traced.
|
||||||
|
|
||||||
## Flaky Test Handling
|
## Flaky Test Handling
|
||||||
|
|
||||||
```python
|
```python
|
||||||
@@ -387,6 +446,8 @@ Common causes and fixes:
|
|||||||
| Animation in progress | `wait_until(lambda: not loading_indicator.exists())` |
|
| Animation in progress | `wait_until(lambda: not loading_indicator.exists())` |
|
||||||
| Dialog timing | `wait_window(title, timeout=15)` |
|
| Dialog timing | `wait_window(title, timeout=15)` |
|
||||||
| CI display not ready | Set `DISPLAY` or use virtual desktop in CI |
|
| CI display not ready | Set `DISPLAY` or use virtual desktop in CI |
|
||||||
|
| `set_edit_text` raises NotImplementedError | UIA ValuePattern missing (common on Qt 5.x) — `BasePage.type_text` already falls back to `keyboard.send_keys` |
|
||||||
|
| Control exists but `wait_visible` times out | Window minimised or off-screen — call `win.restore()` + `win.set_focus()` before waiting |
|
||||||
|
|
||||||
## Test Isolation & Sandbox
|
## Test Isolation & Sandbox
|
||||||
|
|
||||||
@@ -719,6 +780,44 @@ def click_image(template_path, confidence=0.85):
|
|||||||
pyautogui.click(*pos)
|
pyautogui.click(*pos)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### DPI / Scaling Rules (screenshot mode only)
|
||||||
|
|
||||||
|
Screenshot matching is brutally sensitive to Windows display scaling (100% / 125% / 150%). Three hard rules:
|
||||||
|
|
||||||
|
1. **Capture templates at the same scale as the target machine.** Don't try to rescue a mismatch with `PIL.Image.resize` — `cv2.matchTemplate` is very fragile against resampling artefacts.
|
||||||
|
2. **Pin the CI display scaling.** On `windows-latest` add a step like `Set-DisplayResolution 1920 1080 -Force` and disable per-monitor DPI scaling, so screenshot dimensions are reproducible.
|
||||||
|
3. **Record the scale alongside each artefact.** On capture, write `GetDpiForWindow(hwnd) / 96` to `artifacts/<test>/metadata.json` — postmortems become obvious instead of guess-work.
|
||||||
|
|
||||||
|
> Process-level DPI awareness (`SetProcessDpiAwarenessContext`) **can conflict with Qt's own DPI handling** when the app under test is Qt-based. Prefer "same-scale templates + CI pin" over flipping process-wide DPI mode in fixtures.
|
||||||
|
|
||||||
|
### Debugging Match Confidence
|
||||||
|
|
||||||
|
When tuning the `confidence` threshold, the only sane workflow is to **see** where the match landed. The helper below is diagnosis-only — do not call it from test code.
|
||||||
|
|
||||||
|
```python
|
||||||
|
def debug_match(template_path, out="artifacts/match_debug.png", confidence=0.85):
|
||||||
|
"""Diagnosis-only. Draw the best-match rectangle + score back on the current screen.
|
||||||
|
|
||||||
|
NOT for production tests — use when calibrating confidence or chasing false matches.
|
||||||
|
"""
|
||||||
|
import os, cv2, pyautogui, numpy as np
|
||||||
|
screen = np.array(pyautogui.screenshot())[:, :, ::-1]
|
||||||
|
tpl = cv2.imread(template_path)
|
||||||
|
if tpl is None:
|
||||||
|
raise RuntimeError(f"Template unreadable: {template_path}")
|
||||||
|
res = cv2.matchTemplate(screen, tpl, cv2.TM_CCOEFF_NORMED)
|
||||||
|
_, mv, _, ml = cv2.minMaxLoc(res)
|
||||||
|
h, w = tpl.shape[:2]
|
||||||
|
colour = (0, 255, 0) if mv >= confidence else (0, 0, 255) # green pass / red fail
|
||||||
|
cv2.rectangle(screen, ml, (ml[0]+w, ml[1]+h), colour, 2)
|
||||||
|
cv2.putText(screen, f"score={mv:.3f} thr={confidence}",
|
||||||
|
(ml[0], max(20, ml[1]-6)),
|
||||||
|
cv2.FONT_HERSHEY_SIMPLEX, 0.7, colour, 2)
|
||||||
|
os.makedirs(os.path.dirname(out) or ".", exist_ok=True)
|
||||||
|
cv2.imwrite(out, screen)
|
||||||
|
return mv
|
||||||
|
```
|
||||||
|
|
||||||
**Use sparingly** — image matching breaks on DPI changes, theme switches, and partial occlusion.
|
**Use sparingly** — image matching breaks on DPI changes, theme switches, and partial occlusion.
|
||||||
Always try UIA first; fall back to screenshots only for genuinely unreachable controls.
|
Always try UIA first; fall back to screenshots only for genuinely unreachable controls.
|
||||||
|
|
||||||
|
|||||||
@@ -104,6 +104,41 @@ function run() {
|
|||||||
});
|
});
|
||||||
})) passed++; else failed++;
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('rejects node-ipc campaign package versions and CJS indicators', () => {
|
||||||
|
withFixture({
|
||||||
|
'package-lock.json': JSON.stringify({
|
||||||
|
packages: {
|
||||||
|
'node_modules/node-ipc': {
|
||||||
|
version: '12.0.1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, null, 2),
|
||||||
|
'node_modules/node-ipc/package.json': JSON.stringify({
|
||||||
|
name: 'node-ipc',
|
||||||
|
version: '9.2.3',
|
||||||
|
}, null, 2),
|
||||||
|
'node_modules/node-ipc/node-ipc.cjs': [
|
||||||
|
'const host = "sh.azurestaticprovider.net";',
|
||||||
|
'const zone = "bt.node.js";',
|
||||||
|
'process.env.__ntw = "1";',
|
||||||
|
'module.exports.__ntRun = true;',
|
||||||
|
'const archive = "/nt-/sample.tar.gz";',
|
||||||
|
'const entries = ["uname.txt", "envs.txt", "fixtures/_paths.txt"];',
|
||||||
|
].join('\n'),
|
||||||
|
}, rootDir => {
|
||||||
|
const result = scanSupplyChainIocs({ rootDir });
|
||||||
|
const indicators = result.findings.map(finding => finding.indicator);
|
||||||
|
assert.ok(indicators.includes('node-ipc@12.0.1'));
|
||||||
|
assert.ok(indicators.includes('node-ipc@9.2.3'));
|
||||||
|
assert.ok(indicators.includes('sh.azurestaticprovider.net'));
|
||||||
|
assert.ok(indicators.includes('bt.node.js'));
|
||||||
|
assert.ok(indicators.includes('__ntw'));
|
||||||
|
assert.ok(indicators.includes('__ntRun'));
|
||||||
|
assert.ok(indicators.includes('/nt-'));
|
||||||
|
assert.ok(indicators.includes('fixtures/_paths.txt'));
|
||||||
|
});
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
if (test('passes clean versions of watched packages', () => {
|
if (test('passes clean versions of watched packages', () => {
|
||||||
withFixture({
|
withFixture({
|
||||||
'package-lock.json': JSON.stringify({
|
'package-lock.json': JSON.stringify({
|
||||||
@@ -119,6 +154,21 @@ function run() {
|
|||||||
});
|
});
|
||||||
})) passed++; else failed++;
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('does not flag benign substrings in clean package scripts', () => {
|
||||||
|
withFixture({
|
||||||
|
'node_modules/uuid/package.json': JSON.stringify({
|
||||||
|
name: 'uuid',
|
||||||
|
version: '9.0.1',
|
||||||
|
scripts: {
|
||||||
|
test: 'BABEL_ENV=commonjsNode node --throw-deprecation node_modules/.bin/jest test/unit/',
|
||||||
|
},
|
||||||
|
}, null, 2),
|
||||||
|
}, rootDir => {
|
||||||
|
const result = scanSupplyChainIocs({ rootDir });
|
||||||
|
assert.deepStrictEqual(result.findings, []);
|
||||||
|
});
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
if (test('rejects malicious optional dependency markers', () => {
|
if (test('rejects malicious optional dependency markers', () => {
|
||||||
withFixture({
|
withFixture({
|
||||||
'package-lock.json': JSON.stringify({
|
'package-lock.json': JSON.stringify({
|
||||||
@@ -206,7 +256,6 @@ function run() {
|
|||||||
assert.ok(indicators.includes('claude@users.noreply.github.com'));
|
assert.ok(indicators.includes('claude@users.noreply.github.com'));
|
||||||
assert.ok(indicators.includes('dependabout/'));
|
assert.ok(indicators.includes('dependabout/'));
|
||||||
assert.ok(indicators.includes('signalservice'));
|
assert.ok(indicators.includes('signalservice'));
|
||||||
assert.ok(indicators.includes('snode'));
|
|
||||||
});
|
});
|
||||||
})) passed++; else failed++;
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ const expectedReleaseFiles = [
|
|||||||
'telegram-handoff.md',
|
'telegram-handoff.md',
|
||||||
'demo-prompts.md',
|
'demo-prompts.md',
|
||||||
'quickstart.md',
|
'quickstart.md',
|
||||||
|
'preview-pack-manifest.md',
|
||||||
'publication-readiness.md',
|
'publication-readiness.md',
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -104,6 +105,10 @@ test('release docs do not contain unresolved public-link placeholders', () => {
|
|||||||
test('business launch copy stays aligned with the rc.1 public surface', () => {
|
test('business launch copy stays aligned with the rc.1 public surface', () => {
|
||||||
const source = read('docs/business/social-launch-copy.md');
|
const source = read('docs/business/social-launch-copy.md');
|
||||||
assert.ok(source.includes('ECC v2.0.0-rc.1'), 'business launch copy should use the rc.1 release');
|
assert.ok(source.includes('ECC v2.0.0-rc.1'), 'business launch copy should use the rc.1 release');
|
||||||
|
assert.ok(
|
||||||
|
source.includes('preview pack is ready for final release review'),
|
||||||
|
'business launch copy should stay pre-publication until release URLs exist'
|
||||||
|
);
|
||||||
assert.ok(
|
assert.ok(
|
||||||
source.includes('https://github.com/affaan-m/everything-claude-code'),
|
source.includes('https://github.com/affaan-m/everything-claude-code'),
|
||||||
'business launch copy should include the public repo URL'
|
'business launch copy should include the public repo URL'
|
||||||
@@ -118,6 +123,21 @@ test('business launch copy stays aligned with the rc.1 public surface', () => {
|
|||||||
assert.ok(!source.includes('v1.8.0'), 'business launch copy should not stay pinned to v1.8.0');
|
assert.ok(!source.includes('v1.8.0'), 'business launch copy should not stay pinned to v1.8.0');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('announcement drafts avoid live-release claims before publication', () => {
|
||||||
|
const announcementFiles = [
|
||||||
|
'docs/releases/2.0.0-rc.1/linkedin-post.md',
|
||||||
|
'docs/business/social-launch-copy.md',
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const relativePath of announcementFiles) {
|
||||||
|
const source = read(relativePath);
|
||||||
|
assert.ok(
|
||||||
|
!/ECC v2\.0\.0-rc\.1 is live\./.test(source),
|
||||||
|
`${relativePath} must not claim rc.1 is live before the release gate completes`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('Hermes setup uses release-candidate wording for the rc.1 surface', () => {
|
test('Hermes setup uses release-candidate wording for the rc.1 surface', () => {
|
||||||
const source = read('docs/HERMES-SETUP.md');
|
const source = read('docs/HERMES-SETUP.md');
|
||||||
assert.ok(source.includes('Public Release Candidate Scope'));
|
assert.ok(source.includes('Public Release Candidate Scope'));
|
||||||
@@ -144,6 +164,34 @@ test('release notes route new contributors through the rc.1 quickstart', () => {
|
|||||||
assert.ok(releaseNotes.includes('[rc.1 quickstart](quickstart.md)'));
|
assert.ok(releaseNotes.includes('[rc.1 quickstart](quickstart.md)'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('preview pack manifest assembles release, Hermes, and publication gates', () => {
|
||||||
|
const manifest = read('docs/releases/2.0.0-rc.1/preview-pack-manifest.md');
|
||||||
|
|
||||||
|
for (const artifact of [
|
||||||
|
'docs/HERMES-SETUP.md',
|
||||||
|
'skills/hermes-imports/SKILL.md',
|
||||||
|
'docs/architecture/harness-adapter-compliance.md',
|
||||||
|
'docs/releases/2.0.0-rc.1/publication-readiness.md',
|
||||||
|
'docs/releases/2.0.0-rc.1/naming-and-publication-matrix.md',
|
||||||
|
]) {
|
||||||
|
assert.ok(manifest.includes(artifact), `preview pack manifest missing ${artifact}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const blocker of [
|
||||||
|
'GitHub prerelease `v2.0.0-rc.1`',
|
||||||
|
'npm `ecc-universal@2.0.0-rc.1`',
|
||||||
|
'Claude plugin tag',
|
||||||
|
'Codex plugin publication',
|
||||||
|
'ECC Tools billing/product readiness',
|
||||||
|
]) {
|
||||||
|
assert.ok(manifest.includes(blocker), `preview pack manifest missing blocker ${blocker}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.ok(manifest.includes('no raw workspace exports'));
|
||||||
|
assert.ok(manifest.includes('Final Verification Commands'));
|
||||||
|
assert.ok(manifest.includes('Reference-Inspired Adapter Direction'));
|
||||||
|
});
|
||||||
|
|
||||||
test('rc.1 quickstart gives a clone-to-cross-harness path', () => {
|
test('rc.1 quickstart gives a clone-to-cross-harness path', () => {
|
||||||
const quickstart = read('docs/releases/2.0.0-rc.1/quickstart.md');
|
const quickstart = read('docs/releases/2.0.0-rc.1/quickstart.md');
|
||||||
for (const heading of ['Clone', 'Install', 'Verify', 'First Skill', 'Switch Harness']) {
|
for (const heading of ['Clone', 'Install', 'Verify', 'First Skill', 'Switch Harness']) {
|
||||||
@@ -178,6 +226,7 @@ test('launch checklist records the ecc2 alpha version policy', () => {
|
|||||||
|
|
||||||
test('publication readiness checklist gates public release actions on evidence', () => {
|
test('publication readiness checklist gates public release actions on evidence', () => {
|
||||||
const source = read('docs/releases/2.0.0-rc.1/publication-readiness.md');
|
const source = read('docs/releases/2.0.0-rc.1/publication-readiness.md');
|
||||||
|
const may15Evidence = read('docs/releases/2.0.0-rc.1/publication-evidence-2026-05-15.md');
|
||||||
|
|
||||||
for (const section of [
|
for (const section of [
|
||||||
'## Release Identity Matrix',
|
'## Release Identity Matrix',
|
||||||
@@ -211,6 +260,15 @@ test('publication readiness checklist gates public release actions on evidence',
|
|||||||
]) {
|
]) {
|
||||||
assert.ok(source.includes(surface), `publication readiness missing ${surface}`);
|
assert.ok(source.includes(surface), `publication readiness missing ${surface}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
assert.ok(source.includes('publication-evidence-2026-05-15.md'));
|
||||||
|
assert.ok(may15Evidence.includes('PR #1921'));
|
||||||
|
assert.ok(may15Evidence.includes('AgentShield PR #83'));
|
||||||
|
assert.ok(may15Evidence.includes('| Trunk discussions | GraphQL discussion count and maintainer-touch sweep | 58 total discussions;'));
|
||||||
|
assert.ok(source.includes('58 trunk discussions, 0 without maintainer touch'));
|
||||||
|
assert.ok(may15Evidence.includes('env -u GITHUB_TOKEN'));
|
||||||
|
assert.ok(may15Evidence.includes('ITO-44'));
|
||||||
|
assert.ok(may15Evidence.includes('0 open PRs, 0 open issues'));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('release checklist and roadmap link to publication readiness evidence gate', () => {
|
test('release checklist and roadmap link to publication readiness evidence gate', () => {
|
||||||
|
|||||||
@@ -72,6 +72,8 @@ function main() {
|
|||||||
assert.match(result.stdout, /consult/);
|
assert.match(result.stdout, /consult/);
|
||||||
assert.match(result.stdout, /loop-status/);
|
assert.match(result.stdout, /loop-status/);
|
||||||
assert.match(result.stdout, /work-items/);
|
assert.match(result.stdout, /work-items/);
|
||||||
|
assert.match(result.stdout, /platform-audit/);
|
||||||
|
assert.match(result.stdout, /security-ioc-scan/);
|
||||||
}],
|
}],
|
||||||
['delegates explicit install command', () => {
|
['delegates explicit install command', () => {
|
||||||
const result = runCli(['install', '--dry-run', '--json', 'typescript']);
|
const result = runCli(['install', '--dry-run', '--json', 'typescript']);
|
||||||
@@ -207,6 +209,28 @@ function main() {
|
|||||||
assert.strictEqual(result.status, 0, result.stderr);
|
assert.strictEqual(result.status, 0, result.stderr);
|
||||||
assert.match(result.stdout, /node scripts\/work-items\.js upsert/);
|
assert.match(result.stdout, /node scripts\/work-items\.js upsert/);
|
||||||
}],
|
}],
|
||||||
|
['supports help for the platform-audit subcommand', () => {
|
||||||
|
const result = runCli(['help', 'platform-audit']);
|
||||||
|
assert.strictEqual(result.status, 0, result.stderr);
|
||||||
|
assert.match(result.stdout, /Usage: node scripts\/platform-audit\.js/);
|
||||||
|
}],
|
||||||
|
['supports help for the security-ioc-scan subcommand', () => {
|
||||||
|
const result = runCli(['help', 'security-ioc-scan']);
|
||||||
|
assert.strictEqual(result.status, 0, result.stderr);
|
||||||
|
assert.match(result.stdout, /Usage: node scripts\/ci\/scan-supply-chain-iocs\.js/);
|
||||||
|
}],
|
||||||
|
['delegates security-ioc-scan command', () => {
|
||||||
|
const projectRoot = createTempDir('ecc-cli-ioc-scan-');
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(projectRoot, 'package.json'),
|
||||||
|
JSON.stringify({ dependencies: { leftpad: '1.0.0' } }, null, 2)
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = runCli(['security-ioc-scan', '--root', projectRoot, '--json']);
|
||||||
|
assert.strictEqual(result.status, 0, result.stderr);
|
||||||
|
const payload = parseJson(result.stdout);
|
||||||
|
assert.deepStrictEqual(payload.findings, []);
|
||||||
|
}],
|
||||||
['fails on unknown commands instead of treating them as installs', () => {
|
['fails on unknown commands instead of treating them as installs', () => {
|
||||||
const result = runCli(['bogus']);
|
const result = runCli(['bogus']);
|
||||||
assert.strictEqual(result.status, 1);
|
assert.strictEqual(result.status, 1);
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ function buildExpectedPublishPaths(repoRoot) {
|
|||||||
"manifests",
|
"manifests",
|
||||||
"scripts/ecc.js",
|
"scripts/ecc.js",
|
||||||
"scripts/catalog.js",
|
"scripts/catalog.js",
|
||||||
|
"scripts/ci/scan-supply-chain-iocs.js",
|
||||||
"scripts/consult.js",
|
"scripts/consult.js",
|
||||||
"scripts/claw.js",
|
"scripts/claw.js",
|
||||||
"scripts/doctor.js",
|
"scripts/doctor.js",
|
||||||
@@ -54,6 +55,7 @@ function buildExpectedPublishPaths(repoRoot) {
|
|||||||
"scripts/list-installed.js",
|
"scripts/list-installed.js",
|
||||||
"scripts/loop-status.js",
|
"scripts/loop-status.js",
|
||||||
"scripts/observability-readiness.js",
|
"scripts/observability-readiness.js",
|
||||||
|
"scripts/platform-audit.js",
|
||||||
"scripts/skill-create-output.js",
|
"scripts/skill-create-output.js",
|
||||||
"scripts/repair.js",
|
"scripts/repair.js",
|
||||||
"scripts/harness-adapter-compliance.js",
|
"scripts/harness-adapter-compliance.js",
|
||||||
@@ -119,8 +121,10 @@ function main() {
|
|||||||
|
|
||||||
for (const requiredPath of [
|
for (const requiredPath of [
|
||||||
"scripts/catalog.js",
|
"scripts/catalog.js",
|
||||||
|
"scripts/ci/scan-supply-chain-iocs.js",
|
||||||
"scripts/consult.js",
|
"scripts/consult.js",
|
||||||
"scripts/work-items.js",
|
"scripts/work-items.js",
|
||||||
|
"scripts/platform-audit.js",
|
||||||
".gemini/GEMINI.md",
|
".gemini/GEMINI.md",
|
||||||
".qwen/QWEN.md",
|
".qwen/QWEN.md",
|
||||||
".claude-plugin/plugin.json",
|
".claude-plugin/plugin.json",
|
||||||
|
|||||||
@@ -0,0 +1,328 @@
|
|||||||
|
/**
|
||||||
|
* Tests for scripts/platform-audit.js
|
||||||
|
*/
|
||||||
|
|
||||||
|
const assert = require('assert');
|
||||||
|
const fs = require('fs');
|
||||||
|
const os = require('os');
|
||||||
|
const path = require('path');
|
||||||
|
const { execFileSync, spawnSync } = require('child_process');
|
||||||
|
|
||||||
|
const SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'platform-audit.js');
|
||||||
|
|
||||||
|
function createTempDir(prefix) {
|
||||||
|
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanup(dirPath) {
|
||||||
|
fs.rmSync(dirPath, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeFile(rootDir, relativePath, content) {
|
||||||
|
const targetPath = path.join(rootDir, relativePath);
|
||||||
|
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
||||||
|
fs.writeFileSync(targetPath, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
function seedRepo(rootDir, overrides = {}) {
|
||||||
|
const files = {
|
||||||
|
'package.json': JSON.stringify({
|
||||||
|
name: 'everything-claude-code',
|
||||||
|
scripts: {
|
||||||
|
'platform:audit': 'node scripts/platform-audit.js',
|
||||||
|
'observability:ready': 'node scripts/observability-readiness.js',
|
||||||
|
'security:ioc-scan': 'node scripts/ci/scan-supply-chain-iocs.js',
|
||||||
|
'harness:audit': 'node scripts/harness-audit.js'
|
||||||
|
}
|
||||||
|
}, null, 2),
|
||||||
|
'docs/ECC-2.0-GA-ROADMAP.md': [
|
||||||
|
'ECC Platform Roadmap',
|
||||||
|
'https://linear.app/itomarkets/project/ecc-platform-roadmap-52b328ee03e1',
|
||||||
|
'ITO-44',
|
||||||
|
'ITO-59'
|
||||||
|
].join('\n'),
|
||||||
|
'docs/architecture/progress-sync-contract.md': [
|
||||||
|
'GitHub PRs/issues/discussions',
|
||||||
|
'Linear project',
|
||||||
|
'local handoff',
|
||||||
|
'repo roadmap',
|
||||||
|
'scripts/work-items.js'
|
||||||
|
].join('\n'),
|
||||||
|
'docs/security/supply-chain-incident-response.md': [
|
||||||
|
'TanStack',
|
||||||
|
'Mini Shai-Hulud',
|
||||||
|
'node-ipc',
|
||||||
|
'scan-supply-chain-iocs.js'
|
||||||
|
].join('\n'),
|
||||||
|
'docs/releases/2.0.0-rc.1/publication-evidence-2026-05-15.md': [
|
||||||
|
'TanStack',
|
||||||
|
'Mini Shai-Hulud',
|
||||||
|
'Node IPC follow-up',
|
||||||
|
'node-ipc',
|
||||||
|
'IOC scan'
|
||||||
|
].join('\n')
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const [relativePath, content] of Object.entries({ ...files, ...overrides })) {
|
||||||
|
if (content === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
writeFile(rootDir, relativePath, content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeGhShim(rootDir, responses) {
|
||||||
|
const shimPath = path.join(rootDir, 'gh-shim.js');
|
||||||
|
fs.writeFileSync(shimPath, `
|
||||||
|
const responses = ${JSON.stringify(responses)};
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
const key = args.join(' ');
|
||||||
|
if (process.env.GITHUB_TOKEN) {
|
||||||
|
console.error('GITHUB_TOKEN should be unset by default');
|
||||||
|
process.exit(42);
|
||||||
|
}
|
||||||
|
if (!Object.prototype.hasOwnProperty.call(responses, key)) {
|
||||||
|
console.error('Unexpected gh args: ' + key);
|
||||||
|
process.exit(3);
|
||||||
|
}
|
||||||
|
process.stdout.write(JSON.stringify(responses[key]));
|
||||||
|
`);
|
||||||
|
return shimPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
function run(args = [], options = {}) {
|
||||||
|
const env = {
|
||||||
|
...process.env,
|
||||||
|
...(options.env || {})
|
||||||
|
};
|
||||||
|
|
||||||
|
return execFileSync('node', [SCRIPT, ...args], {
|
||||||
|
cwd: options.cwd || path.join(__dirname, '..', '..'),
|
||||||
|
env,
|
||||||
|
encoding: 'utf8',
|
||||||
|
stdio: ['pipe', 'pipe', 'pipe'],
|
||||||
|
timeout: 10000
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function runProcess(args = [], options = {}) {
|
||||||
|
const env = {
|
||||||
|
...process.env,
|
||||||
|
...(options.env || {})
|
||||||
|
};
|
||||||
|
|
||||||
|
return spawnSync('node', [SCRIPT, ...args], {
|
||||||
|
cwd: options.cwd || path.join(__dirname, '..', '..'),
|
||||||
|
env,
|
||||||
|
encoding: 'utf8',
|
||||||
|
stdio: ['pipe', 'pipe', 'pipe'],
|
||||||
|
timeout: 10000
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function test(name, fn) {
|
||||||
|
try {
|
||||||
|
fn();
|
||||||
|
console.log(` PASS ${name}`);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.log(` FAIL ${name}`);
|
||||||
|
console.log(` Error: ${error.message}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function runTests() {
|
||||||
|
console.log('\n=== Testing platform-audit.js ===\n');
|
||||||
|
|
||||||
|
let passed = 0;
|
||||||
|
let failed = 0;
|
||||||
|
|
||||||
|
if (test('parseArgs accepts supported flags and rejects invalid values', () => {
|
||||||
|
const { parseArgs } = require(SCRIPT);
|
||||||
|
const rootDir = createTempDir('platform-audit-args-');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = parseArgs([
|
||||||
|
'node',
|
||||||
|
'script',
|
||||||
|
'--format=json',
|
||||||
|
`--root=${rootDir}`,
|
||||||
|
'--repo',
|
||||||
|
'affaan-m/everything-claude-code',
|
||||||
|
'--max-open-prs',
|
||||||
|
'5',
|
||||||
|
'--max-open-issues',
|
||||||
|
'6',
|
||||||
|
'--allow-untracked',
|
||||||
|
'docs/drafts/'
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert.strictEqual(parsed.format, 'json');
|
||||||
|
assert.strictEqual(parsed.root, path.resolve(rootDir));
|
||||||
|
assert.deepStrictEqual(parsed.repos, ['affaan-m/everything-claude-code']);
|
||||||
|
assert.strictEqual(parsed.thresholds.maxOpenPrs, 5);
|
||||||
|
assert.strictEqual(parsed.thresholds.maxOpenIssues, 6);
|
||||||
|
assert.deepStrictEqual(parsed.allowUntracked, ['docs/drafts/']);
|
||||||
|
|
||||||
|
assert.throws(() => parseArgs(['node', 'script', '--format', 'xml']), /Invalid format/);
|
||||||
|
assert.throws(() => parseArgs(['node', 'script', '--repo']), /--repo requires a value/);
|
||||||
|
assert.throws(() => parseArgs(['node', 'script', '--max-open-prs', 'x']), /Invalid --max-open-prs/);
|
||||||
|
assert.throws(() => parseArgs(['node', 'script', '--unknown']), /Unknown argument/);
|
||||||
|
} finally {
|
||||||
|
cleanup(rootDir);
|
||||||
|
}
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('skip-github report checks local release and security evidence', () => {
|
||||||
|
const projectRoot = createTempDir('platform-audit-local-');
|
||||||
|
|
||||||
|
try {
|
||||||
|
seedRepo(projectRoot);
|
||||||
|
const parsed = JSON.parse(run(['--format=json', `--root=${projectRoot}`, '--skip-github'], { cwd: projectRoot }));
|
||||||
|
|
||||||
|
assert.strictEqual(parsed.schema_version, 'ecc.platform-audit.v1');
|
||||||
|
assert.strictEqual(parsed.ready, true);
|
||||||
|
assert.strictEqual(parsed.github.skipped, true);
|
||||||
|
assert.ok(parsed.checks.some(check => check.id === 'roadmap-linear-mirror' && check.status === 'pass'));
|
||||||
|
assert.ok(parsed.checks.some(check => check.id === 'supply-chain-runbook' && check.status === 'pass'));
|
||||||
|
assert.deepStrictEqual(parsed.top_actions, []);
|
||||||
|
} finally {
|
||||||
|
cleanup(projectRoot);
|
||||||
|
}
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('github queue and discussion budgets pass with maintainer touch', () => {
|
||||||
|
const projectRoot = createTempDir('platform-audit-github-pass-');
|
||||||
|
|
||||||
|
try {
|
||||||
|
seedRepo(projectRoot);
|
||||||
|
const shimPath = writeGhShim(projectRoot, {
|
||||||
|
'pr list --repo affaan-m/everything-claude-code --state open --json number,title,isDraft,mergeStateStatus,updatedAt,url,author': [],
|
||||||
|
'issue list --repo affaan-m/everything-claude-code --state open --json number,title,updatedAt,url,author,labels': [],
|
||||||
|
'api graphql -f owner=affaan-m -f name=everything-claude-code -F first=100 -f query=query($owner: String!, $name: String!, $first: Int!) { repository(owner: $owner, name: $name) { hasDiscussionsEnabled discussions(first: $first, orderBy: {field: UPDATED_AT, direction: DESC}) { totalCount nodes { number title url updatedAt authorAssociation comments(first: 20) { nodes { authorAssociation } } } } } }': {
|
||||||
|
data: {
|
||||||
|
repository: {
|
||||||
|
hasDiscussionsEnabled: true,
|
||||||
|
discussions: {
|
||||||
|
totalCount: 1,
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
number: 73,
|
||||||
|
title: 'Compacting during workflow',
|
||||||
|
url: 'https://github.com/example/discussions/73',
|
||||||
|
updatedAt: '2026-05-15T00:00:00Z',
|
||||||
|
authorAssociation: 'NONE',
|
||||||
|
comments: { nodes: [{ authorAssociation: 'OWNER' }] }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const parsed = JSON.parse(run([
|
||||||
|
'--format=json',
|
||||||
|
`--root=${projectRoot}`,
|
||||||
|
'--repo',
|
||||||
|
'affaan-m/everything-claude-code'
|
||||||
|
], {
|
||||||
|
cwd: projectRoot,
|
||||||
|
env: {
|
||||||
|
ECC_GH_SHIM: shimPath,
|
||||||
|
GITHUB_TOKEN: 'must-be-removed'
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
assert.strictEqual(parsed.ready, true);
|
||||||
|
assert.strictEqual(parsed.github.totals.openPrs, 0);
|
||||||
|
assert.strictEqual(parsed.github.totals.openIssues, 0);
|
||||||
|
assert.strictEqual(parsed.github.totals.discussionsNeedingMaintainerTouch, 0);
|
||||||
|
assert.ok(parsed.checks.some(check => check.id === 'github-discussion-touch' && check.status === 'pass'));
|
||||||
|
} finally {
|
||||||
|
cleanup(projectRoot);
|
||||||
|
}
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('threshold failures and untouched discussions become top actions', () => {
|
||||||
|
const projectRoot = createTempDir('platform-audit-github-fail-');
|
||||||
|
|
||||||
|
try {
|
||||||
|
seedRepo(projectRoot);
|
||||||
|
const prs = Array.from({ length: 3 }, (_, index) => ({
|
||||||
|
number: index + 1,
|
||||||
|
title: `PR ${index + 1}`,
|
||||||
|
isDraft: false,
|
||||||
|
mergeStateStatus: 'CLEAN',
|
||||||
|
updatedAt: '2026-05-15T00:00:00Z',
|
||||||
|
url: `https://github.com/example/pull/${index + 1}`,
|
||||||
|
author: { login: 'contributor' }
|
||||||
|
}));
|
||||||
|
const shimPath = writeGhShim(projectRoot, {
|
||||||
|
'pr list --repo affaan-m/everything-claude-code --state open --json number,title,isDraft,mergeStateStatus,updatedAt,url,author': prs,
|
||||||
|
'issue list --repo affaan-m/everything-claude-code --state open --json number,title,updatedAt,url,author,labels': [],
|
||||||
|
'api graphql -f owner=affaan-m -f name=everything-claude-code -F first=100 -f query=query($owner: String!, $name: String!, $first: Int!) { repository(owner: $owner, name: $name) { hasDiscussionsEnabled discussions(first: $first, orderBy: {field: UPDATED_AT, direction: DESC}) { totalCount nodes { number title url updatedAt authorAssociation comments(first: 20) { nodes { authorAssociation } } } } } }': {
|
||||||
|
data: {
|
||||||
|
repository: {
|
||||||
|
hasDiscussionsEnabled: true,
|
||||||
|
discussions: {
|
||||||
|
totalCount: 1,
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
number: 1239,
|
||||||
|
title: 'Losing context',
|
||||||
|
url: 'https://github.com/example/discussions/1239',
|
||||||
|
updatedAt: '2026-05-15T00:00:00Z',
|
||||||
|
authorAssociation: 'NONE',
|
||||||
|
comments: { nodes: [] }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const parsed = JSON.parse(run([
|
||||||
|
'--format=json',
|
||||||
|
`--root=${projectRoot}`,
|
||||||
|
'--repo',
|
||||||
|
'affaan-m/everything-claude-code',
|
||||||
|
'--max-open-prs',
|
||||||
|
'2'
|
||||||
|
], {
|
||||||
|
cwd: projectRoot,
|
||||||
|
env: { ECC_GH_SHIM: shimPath }
|
||||||
|
}));
|
||||||
|
|
||||||
|
assert.strictEqual(parsed.ready, false);
|
||||||
|
assert.ok(parsed.top_actions.some(action => action.id === 'github-open-pr-budget'));
|
||||||
|
assert.ok(parsed.top_actions.some(action => action.id === 'github-discussion-touch'));
|
||||||
|
assert.strictEqual(parsed.github.totals.discussionsNeedingMaintainerTouch, 1);
|
||||||
|
} finally {
|
||||||
|
cleanup(projectRoot);
|
||||||
|
}
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('cli help and invalid args exit cleanly', () => {
|
||||||
|
const help = runProcess(['--help']);
|
||||||
|
assert.strictEqual(help.status, 0);
|
||||||
|
assert.ok(help.stdout.includes('Usage: node scripts/platform-audit.js'));
|
||||||
|
|
||||||
|
const invalid = runProcess(['--format', 'xml']);
|
||||||
|
assert.strictEqual(invalid.status, 1);
|
||||||
|
assert.ok(invalid.stderr.includes('Invalid format'));
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
console.log(`\nPassed: ${passed}`);
|
||||||
|
console.log(`Failed: ${failed}`);
|
||||||
|
|
||||||
|
if (failed > 0) {
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
|
runTests();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user