Compare commits

..

37 Commits

Author SHA1 Message Date
Jamkris
85d33748e0 test(hooks): regression coverage for round 1 review fixes
9 new cases locking in the behavior added by the previous two
commits. Each was verified to fail before the fix and pass after.

Greptile — quote-aware depth counting:
  - blocks $(echo ")"; (npm run dev))
  - blocks (echo ")"; npm run dev)
  - allows $(echo "(npm run dev)") — () inside double-quoted body is literal

Greptile — brace groups:
  - blocks { npm run dev; }
  - blocks echo hi && { npm run dev; }
  - allows {npm run dev} — bash brace-group syntax requires a space after {

CodeRabbit — missing package-manager variants:
  - blocks yarn run dev (yarn 1.x convention)
  - blocks bun dev (bun bare form)

CodeRabbit nitpick — symmetric quote test:
  - blocks echo "$(npm run dev)" — double-quoted substitution still substitutes

The `{npm run dev}` allow case is intentional: bash treats `{` as
a reserved word only when followed by whitespace. The pre-fix code
already passed this through, but until now we never asserted it,
so a future change to brace handling could silently start blocking
literal `{npm` tokens.
2026-05-14 12:24:45 +09:00
Jamkris
e2eaf4ac2f fix(hooks): cover brace groups + yarn-run/bun-bare dev variants
Two false-negatives surfaced in PR #1889 review:

1. Brace-group bypass (Greptile).
   `{ npm run dev; }` evaluates the dev command in the *current*
   shell — semantically distinct from `( ... )` but with the same
   effect for this hook. `splitShellSegments` correctly cleaves the
   group at `;` into `["{ npm run dev", "}"]`, but the first segment's
   leading token under `readToken` is the bare `{`, which was not in
   `DEV_COMMAND_WORDS`, so the dev-pattern check was skipped.

   Fix: treat `{` and `}` as no-op tokens in `getLeadingCommandWord`
   so we keep walking to the real command word. Matches how shell
   itself parses brace groups (the braces are reserved words, not
   commands). Bash requires a space after `{` and a terminator before
   `}` for an actual group, so `{npm run dev}` correctly remains
   allowed (single token `{npm`, not in `DEV_COMMAND_WORDS`).

2. Missing yarn-run / bun-bare variants (CodeRabbit).
   Both `yarn dev` *and* `yarn run dev` are valid (the latter is what
   `package.json` actually wires `dev` to under yarn 1.x). The same
   `(run )?` symmetry applies to bun. The previous `DEV_PATTERN` only
   matched `yarn\s+dev` and `bun\s+run\s+dev`, allowing the cross
   forms to pass through silently.

   Fix: `yarn(?:\s+run)?\s+dev` and `bun(?:\s+run)?\s+dev` — same
   shape `pnpm(?:\s+run)?\s+dev` was already using.

Verified after this commit (every form now exits 2):

  { npm run dev; }
  { npm run dev ; }
  echo hi && { npm run dev; }
  ({ npm run dev; })
  $( { npm run dev; } )
  yarn run dev
  bun dev

Verified still allowed (no regression):

  echo "{ npm run dev; }"   # literal inside double quotes
  {npm run dev}             # not a brace group per bash syntax
2026-05-14 12:23:55 +09:00
Jamkris
70b86d81c4 fix(lib): track quote state inside command-substitution depth counters
Greptile flagged a bypass in PR #1889: `$(echo ")"; (npm run dev))`
threaded the depth-counting loops in `extractCommandSubstitutions`
and `extractSubshellGroups` to terminate early, because a literal `)`
inside double quotes was treated as a real closing paren. The
truncated body then ended in a dangling `"` that toggled `inDouble`
in the outer scan, masking the subsequent `(npm run dev)` group from
extraction.

Reproduced (before this commit) by piping the synthetic PreToolUse
payload `{"tool_input":{"command":"$(echo \")\"; (npm run dev))"}}`
into `scripts/hooks/pre-bash-dev-server-block.js` and observing
exit 0 (allow) where the dev pattern is clearly present.

Fix: each `$(...)` and `(...)` body loop now tracks its own
single/double quote state and only treats `(` / `)` as depth
delimiters when outside quotes. The quoted `)` no longer closes
the group early, the body now extends to the real closing paren,
and the outer scan's quote state remains untouched.

After this commit:
  $ echo '{"tool_input":{"command":"$(echo \")\"; (npm run dev))"}}' \
      | node scripts/hooks/pre-bash-dev-server-block.js; echo $?
  2

The symmetric form `$(echo "(npm run dev)")` correctly remains
allowed (bash does not honor `(...)` inside double quotes).
2026-05-14 12:22:22 +09:00
Jamkris
4f4654bf21 test(hooks): regression coverage for dev-server-block subshell bypass
Lock in the behavior added by the previous commit. Each new case was
verified to fail before the fix and pass after.

Bypasses now blocked (exit 2):
- \$(npm run dev)              command substitution
- \`npm run dev\`              backtick substitution
- echo \$(npm run dev)         substitution inside an argument
- (npm run dev)               plain subshell group
- \$(echo a; npm run dev)      substitution containing a sequenced segment
- (pnpm dev)                  plain subshell group, alt package manager

Allow cases — explicitly proven NOT to regress so the fix doesn't
over-block legitimate uses:
- (tmux new-session -d -s dev "npm run dev")   tmux launcher inside ()
- git commit -m '(npm run dev)'                literal in single quotes
- echo "(npm run dev)"                         literal in double quotes
  (bash does NOT subshell () inside double quotes)
- git commit -m '\$(npm run dev) fix'          literal in single quotes

Single- and double-quote allow cases are important: they distinguish a
real subshell construct from one that's just text inside a string,
which is what `extractSubshellGroups` / `extractCommandSubstitutions`
quote-awareness is for.
2026-05-14 11:22:44 +09:00
Jamkris
a7e51e8046 fix(hooks): close subshell bypass in pre-bash-dev-server-block
Before this commit the dev-server-block hook ran the leading-command
and dev-pattern check only against the top-level segments returned by
`splitShellSegments`, which doesn't split on `$(...)`, backticks, or
plain `(...)`. That left the policy bypassable by wrapping a dev
command in any of those constructs:

  $(npm run dev)
  `npm run dev`
  echo $(npm run dev)
  (npm run dev)

Each verified by piping a synthetic PreToolUse payload into the hook
on this branch: every form above returned exit 0 (allow) where a plain
`npm run dev` correctly returned exit 2 (block).

Fix: expand the check space before running the leading-command rule.
A small BFS walks the raw command, harvesting bodies from
`extractCommandSubstitutions` (`$(...)` and backticks) and from
`extractSubshellGroups` (plain `(...)`), then splits each harvested
body through `splitShellSegments` and feeds the result into the
existing `isBlockedDevSegment` check.

This preserves every existing allow case (`tmux new-session -d -s dev
"npm run dev"`, quoted-string mentions like `git commit -m "npm run
dev fix"`, `echo hi`) because the leading-command rule is unchanged —
only the set of segments it runs against grew.

Known limitation, not fixed here: `eval "$(echo npm run dev)"` still
slips through because the substitution body's leading command is
`echo`, and statically modeling echo's output to recover the executed
command is out of scope. The same class affects `gateguard-fact-force`
(via `eval "$(echo rm -rf /)"` etc.) and is best addressed in both
hooks together as a follow-up rather than as a one-off here.
2026-05-14 11:21:41 +09:00
Jamkris
04dc03c3af feat(lib): add extractSubshellGroups for plain (...) subshells
`extractCommandSubstitutions` only walks `$(...)` and backticks — the two
shell constructs whose bodies are captured as strings. Bash also has
plain `(...)` subshells (e.g. `(npm run dev)`), where the body executes
in a child shell but is not value-captured. Our PreToolUse hooks need
to peer inside those too, because a `(...)` group bypasses the
top-level segment splitter just like `$(...)` does.

This commit adds a sibling extractor with the same conventions as
`extractCommandSubstitutions`:

- single quotes literal — `'(npm run dev)'` is a string, ignored
- double quotes literal for parens — `"(npm run dev)"` is a string
  (bash only honors `$(...)`, not bare `(...)`, inside double quotes)
- skips `$(...)` and backtick spans so we don't double-extract
  bodies the other helper already handles
- recurses into its own bodies for nested groups

No consumer yet; the next commit wires both extractors into
`scripts/hooks/pre-bash-dev-server-block.js` to close the subshell
bypass surface.
2026-05-14 11:17:46 +09:00
Jamkris
0a380c3e85 feat(lib): extract shell command-substitution parser to shared lib
Extract the `extractCommandSubstitutions` function originally
introduced in scripts/hooks/gateguard-fact-force.js (PR #1853
round 2) into scripts/lib/shell-substitution.js so other PreToolUse
hooks can reuse the same single-quote-aware, double-quote-aware,
nested-subshell-aware parser without duplicating it.

No behavior change in this commit — the function body is copied
verbatim and exposed via `module.exports`. The next commit wires it
into scripts/hooks/pre-bash-dev-server-block.js to close that hook's
own subshell-bypass holes.

gateguard-fact-force.js still defines its own private copy of the
function; consolidating both call sites onto this shared lib is a
follow-up worth doing once this PR lands, but is intentionally out
of scope here to keep the diff focused on the dev-server-block fix.
2026-05-14 11:10:40 +09:00
Affaan Mustafa
a27831c13e Sync ECC Tools hosted status roadmap (#1886) 2026-05-13 21:49:42 -04:00
Affaan Mustafa
b24d762caa Sync ECC Tools hosted result history roadmap (#1885) 2026-05-13 21:31:08 -04:00
Affaan Mustafa
f94478e524 docs: sync roadmap after ECC-Tools hosted dispatch 2026-05-13 20:30:48 -04:00
Affaan Mustafa
6cdac19764 docs: sync roadmap after ECC-Tools depth-plan check 2026-05-13 20:10:38 -04:00
Affaan Mustafa
af3a206412 docs: sync roadmap after ECC-Tools team backlog job (#1880) 2026-05-13 19:44:49 -04:00
Affaan Mustafa
20f00c1410 docs: sync roadmap after ECC-Tools AI cost job (#1878) 2026-05-13 19:26:48 -04:00
Affaan Mustafa
e7a6f137e5 docs: sync roadmap after ECC-Tools reference-set job (#1877) 2026-05-13 19:09:35 -04:00
Affaan Mustafa
7596502092 docs: sync roadmap after ECC-Tools harness job (#1876) 2026-05-13 18:50:45 -04:00
Affaan Mustafa
c04baa8c25 docs: sync roadmap after ECC-Tools security evidence job (#1875) 2026-05-13 18:32:06 -04:00
Affaan Mustafa
9082bdedac docs: sync roadmap after ECC-Tools CI diagnostics (#1874) 2026-05-13 18:12:31 -04:00
Affaan Mustafa
3243a1c5d3 docs: sync roadmap after ECC-Tools hosted planning (#1872) 2026-05-13 12:48:50 -04:00
Affaan Mustafa
69401b28b3 docs: sync roadmap after ECC-Tools depth readiness (#1871) 2026-05-13 12:26:32 -04:00
Affaan Mustafa
9a5ed3223a docs: sync roadmap after AgentShield corpus expansion
Records AgentShield PR #82 and moves the next AgentShield roadmap slice to hosted evidence-pack workflow depth.
2026-05-13 09:04:34 -04:00
Affaan Mustafa
d844bd6bfc docs: sync roadmap after AgentShield remediation workflows
Records AgentShield PR #81 and advances the next AgentShield roadmap slice after remediation workflow phases landed.
2026-05-13 08:46:07 -04:00
Affaan Mustafa
cf54c791e4 docs: sync roadmap after AgentShield corpus recommendations
Syncs the ECC 2.0 GA roadmap after AgentShield PR #80 landed corpus accuracy recommendations.
2026-05-13 08:28:12 -04:00
Affaan Mustafa
bd4369e1d5 docs: sync roadmap after ECC-Tools PR draft tracking (#1865) 2026-05-13 08:11:09 -04:00
Affaan Mustafa
f2be190dcb docs: sync roadmap after AgentShield fingerprint hardening 2026-05-13 07:53:15 -04:00
Affaan Mustafa
2afef0f18b docs: sync roadmap after ECC-Tools hardening 2026-05-13 07:32:55 -04:00
Affaan Mustafa
967e5c6922 docs: mark JARVIS backend audit clean 2026-05-13 07:15:13 -04:00
Affaan Mustafa
2d29643dd4 docs: sync ECC 2.0 GA roadmap after hardening pass 2026-05-13 06:59:20 -04:00
Affaan Mustafa
c2762dd569 feat: add Ruby and Rails rules 2026-05-13 06:27:08 -04:00
Affaan Mustafa
cb3509ee19 docs: sync AgentShield adapter roadmap
Record AgentShield #68/#69 in the ECC GA roadmap and update the next enterprise slice.
2026-05-13 04:43:58 -04:00
Affaan Mustafa
42f04edc03 ci: gate observability on release safety evidence
Add release-safety evidence coverage to observability readiness and refresh rc.1 publication gate docs.
2026-05-13 04:14:47 -04:00
Affaan Mustafa
d4728a0d80 fix: fall back to ASCII instinct status bars
Fixes #1855
2026-05-13 02:59:58 -04:00
SeungHyun
0e169fecbc fix: harden GateGuard destructive bash tokenizer
Co-authored-by: Jamkris <dltmdgus1412@gmail.com>
2026-05-13 02:43:04 -04:00
Affaan Mustafa
b2506f82f6 docs: sync AgentShield evidence-pack roadmap (#1854) 2026-05-13 02:22:05 -04:00
Affaan Mustafa
f6e13ab520 docs: record post-hardening rc1 release evidence (#1852) 2026-05-13 01:32:58 -04:00
Affaan Mustafa
209abd403b ci: disable checkout credential persistence in privileged workflows (#1851) 2026-05-13 01:15:49 -04:00
Affaan Mustafa
2486732714 harden: remove shell access from read-only analyzers (#1850) 2026-05-13 01:00:26 -04:00
Affaan Mustafa
63f9bfc33f docs: gate ECC progress sync readiness
Make the ECC 2.0 GitHub/Linear/handoff/roadmap progress-sync model part of the local observability readiness gate instead of leaving it as roadmap prose only.

- add `docs/architecture/progress-sync-contract.md` for GitHub, Linear, handoff, roadmap, and work-items sync
- add a `Tracker Sync` check to `scripts/observability-readiness.js`
- update observability tests with passing and missing-contract coverage
- update observability and GA roadmap docs so the local readiness gate is now 18/18 and records #1848 supply-chain hardening evidence

Validation:
- node tests/scripts/observability-readiness.test.js (9 passed, 0 failed)
- npm run observability:ready -- --format json (18/18, ready true)
- npx markdownlint-cli 'docs/architecture/progress-sync-contract.md' 'docs/architecture/observability-readiness.md' 'docs/ECC-2.0-GA-ROADMAP.md'
- git diff --check
- node tests/docs/ecc2-release-surface.test.js (18 passed)
- node tests/run-all.js (2378 passed, 0 failed)
- GitHub CI for #1849 green across Ubuntu, Windows, and macOS

No release, tag, npm publish, plugin tag, marketplace submission, or announcement was performed.
2026-05-13 00:38:18 -04:00
37 changed files with 1901 additions and 58 deletions

View File

@@ -16,6 +16,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: '20.x'
@@ -27,6 +29,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: '20.x'

View File

@@ -18,6 +18,7 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
persist-credentials: false
- name: Setup Node.js
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0

View File

@@ -42,6 +42,7 @@ jobs:
with:
fetch-depth: 0
ref: ${{ inputs.tag }}
persist-credentials: false
- name: Setup Node.js
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0

View File

@@ -2,7 +2,7 @@
name: code-explorer
description: Deeply analyzes existing codebase features by tracing execution paths, mapping architecture layers, and documenting dependencies to inform new development.
model: sonnet
tools: [Read, Grep, Glob, Bash]
tools: [Read, Grep, Glob]
---
## Prompt Defense Baseline

View File

@@ -2,7 +2,7 @@
name: comment-analyzer
description: Analyze code comments for accuracy, completeness, maintainability, and comment rot risk.
model: sonnet
tools: [Read, Grep, Glob, Bash]
tools: [Read, Grep, Glob]
---
## Prompt Defense Baseline

View File

@@ -2,7 +2,7 @@
name: type-design-analyzer
description: Analyze type design for encapsulation, invariant expression, usefulness, and enforcement.
model: sonnet
tools: [Read, Grep, Glob, Bash]
tools: [Read, Grep, Glob]
---
## Prompt Defense Baseline

View File

@@ -16,12 +16,14 @@ so the live execution truth is split across:
As of 2026-05-13:
- Public 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
`ECC-Tools/ECC-website`.
- Public GitHub discussions are also clean across those tracked repos:
the latest GraphQL sweep found only closed discussions on the trunk
discussion surface, and satellite discussion surfaces are disabled or empty.
`ECC-Tools/ECC-website`: the latest sweep found 0 open PRs and 0 open
issues across all five repos.
- GitHub discussions are also clean across those tracked repos:
the latest GraphQL sweep found 52 total trunk discussions with 0 open,
and 0 total/open discussions on AgentShield, JARVIS, ECC-Tools, and the
ECC-Tools website.
- The final open public GitHub issue, #1314, was closed as a non-actionable
external badge/listing notification with a courtesy comment.
- Linear issue creation for this project was re-tested after GitHub cleanup and
@@ -30,10 +32,138 @@ As of 2026-05-13:
Linear project status updates remain the active tracking surfaces until the
workspace is upgraded or issue capacity is freed.
- `npm run harness:audit -- --format json` reports 70/70 on current `main`.
- `npm run observability:ready` reports 16/16 readiness on current `main`.
- `npm run observability:ready` reports 21/21 readiness on current `main`,
including the GitHub/Linear/handoff/roadmap progress-sync contract.
- PR #1846 merged as `797f283036904128bb1b348ae62019eb9f08cf39` and made
npm registry signature verification a durable workflow-security gate:
workflows that run `npm audit` now need `npm audit signatures`.
- PR #1848 merged as `cbecf5689d8d1bd5915e7031697a1d56aac538f2` and added
`docs/security/supply-chain-incident-response.md`, plus a workflow-security
validator rule blocking `pull_request_target` workflows from restoring or
saving shared dependency caches.
- PR #1850 merged as `248673271455e9dc85b8add2a6ab76107b718639` and removed
shell access from read-only analyzer agents and zh-CN copies, reducing
AgentShield high findings on that surface without changing operator agents.
- PR #1851 merged as `209abd403b7eaa968c6d4fa67be82e04b55706d6` and made
`persist-credentials: false` mandatory for `actions/checkout` in workflows
with write permissions.
- PR #1860 merged as `c2762dd5691a33aaa7f84a0a4901a5bab7980fc8` and closed
#1859 by adding the Ruby/Rails language pack surface, install aliases,
selective-install components, and focused install-manifest executor tests.
- AgentShield PR #78 merged as `1b19a985d6ae1346244089a78806a7d5eaaf270e`
and hardened the release workflow with `persist-credentials: false` plus
`npm ci --ignore-scripts` in the write/id-token release path.
- AgentShield PR #79 merged as `86a823c5f2c35ee97e6ecf6f99e9ac301d54119a`
and moved baseline/watch/remediation fingerprints to a shared hashed
evidence fingerprint helper. New baselines omit raw finding evidence while
older raw-evidence baselines remain comparable.
- AgentShield PR #80 merged as `8ed379d1de067b25640ac6273aa4d9f8e6735d43`
and added prioritized corpus accuracy recommendations to failed corpus gates,
mapping misses by category, missing rule, and config ID so enterprise
scanner-regression work has an actionable improvement plan.
- AgentShield PR #81 merged as `6583884e74ba2e896942113e1ce3146230e6fb76`
and added ordered remediation workflow phases to remediation plans, routing
safe auto-fixes, manual review, and verification through stable finding
fingerprints without copying raw evidence.
- AgentShield PR #82 merged as `51336ba074ad5e9fed2c0aa3237422be22147e76`
and expanded the built-in attack corpus with an env proxy hijack scenario
covering proxy/runtime mutation, env-token exfiltration, DNS exfiltration,
credential-store access, and clipboard access.
- JARVIS PR #13 merged as `127efabbfb5033ae53d7a53e1546aa3c33d6f962`
and hardened CI/deploy workflows with npm registry signature verification,
disabled persisted checkout credentials in write-permission jobs, and pinned
the Vercel CLI install instead of using `latest`.
- ECC-Tools PR #53 merged as `99018e943d03f024de8c9d278c91f66393d4f1ee`
and added npm registry signature verification before the existing production
dependency audit in CI.
- ECC-Tools PR #54 merged as `05df89721f49c1e19d8502c545e26f5694806998`
and made `/ecc-tools followups sync-linear` track copy-ready PR drafts in
the Linear/project backlog when `open-pr-drafts` is not used, preserving
useful stale-PR salvage work without opening extra PR shells.
- ECC-Tools PR #55 merged as `5d8c112cce4794cfa089d5b0ea661ba87a178be1`
and added analysis-depth readiness to `/ecc-tools analyze` comments,
separating commit-history-only repos from evidence-backed and deep-ready repos
using CI/CD, security, harness, reference/eval, AI routing/cost-control, and
team handoff evidence.
- ECC-Tools PR #56 merged as `5b729c88641eafe80f65364bab3fc74d0270f57b`
and added the authenticated `/api/analysis/depth-plan` contract that maps
analysis-depth readiness into concrete hosted jobs for CI diagnostics,
security evidence review, harness compatibility, reference-set evaluation,
AI routing/cost review, and team backlog routing.
- ECC-Tools PR #57 merged as `4cc61112a4cc9feec7b07af09321f360e34af6a4`
and added the first executable hosted analysis job:
`/api/analysis/jobs/ci-diagnostics` now gates on CI/CD readiness, inspects
workflow/test-runner/failure-evidence artifacts, returns CI hardening
findings and next actions, and charges usage only after successful execution.
- ECC-Tools PR #58 merged as `ce09dd8d9b46f65c6b88dc4f48cfb6b6227ae0bf`
and added the second executable hosted analysis job:
`/api/analysis/jobs/security-evidence-review` now gates on security-evidence
readiness, inspects capped AgentShield evidence-pack, policy, baseline,
SBOM, SARIF, and security-scan artifacts, returns supply-chain evidence
findings and next actions, and charges usage only after successful execution.
- ECC-Tools PR #59 merged as `505b372dbd8f75f996d9e2ed079effd30cec5ba5`
and added the third executable hosted analysis job:
`/api/analysis/jobs/harness-compatibility-audit` now gates on harness-config
readiness, inspects capped Claude, Codex, OpenCode, MCP, plugin, and
cross-harness documentation artifacts, excludes local secret-bearing config
paths from fetches, returns portability findings and next actions, and
charges usage only after successful execution.
- ECC-Tools PR #60 merged as `b75e0a49ba5672b1ec9a2a4880ddcfa2d07dc557`
and added the fourth executable hosted analysis job:
`/api/analysis/jobs/reference-set-evaluation` now gates on reference-evidence
readiness, evaluates analyzer corpus, RAG/evaluator, PR salvage/review,
harness, security, and CI failure-mode evidence, excludes obvious
secret-bearing fixture paths from fetches, returns reference coverage
findings and next actions, and charges usage only after successful execution.
- ECC-Tools PR #61 merged as `7b01b67cae0b80774b311cb515b7eca0aa038c65`
and added the fifth executable hosted analysis job:
`/api/analysis/jobs/ai-routing-cost-review` now gates on AI routing/cost
readiness, evaluates model routing, token budget, usage-limit, rate-limit,
billing/entitlement, cost-regression, and cost-policy evidence, excludes
obvious secret-bearing paths from fetches, returns cost-control findings and
next actions, and charges usage only after successful execution.
- ECC-Tools PR #62 merged as `781d6733e56f7556edb43fb96bdfb00b1f0a3aa6`
and added the sixth executable hosted analysis job:
`/api/analysis/jobs/team-backlog-routing` now gates on team handoff/project
tracking readiness, evaluates roadmap, runbook, handoff, release-plan,
issue-template, ownership, project-tracker, backlog, and follow-up evidence,
excludes obvious secret-bearing paths from fetches, returns team-routing
findings and next actions, and charges usage only after successful execution.
- ECC-Tools PR #63 merged as `fb9e4c5ceb9ccde50da74c7a69c3fa4bd321fc07`
and made the hosted execution plan operator-visible on queued PR analysis:
the queue now publishes a non-blocking `ECC Tools / Hosted Depth Plan`
check-run on the PR head SHA with ready/blocked hosted executor commands
and next action text, while keeping check-run publication best-effort so
bundle generation and analysis comments are not blocked.
- ECC-Tools PR #64 merged as `72020ef94db94840812977ea7ac37e9344036668`
and added PR-facing hosted job dispatch controls:
`/ecc-tools analyze --job ...` comments now queue hosted jobs against the
PR head SHA, execute them through the existing hosted readiness/evidence
gates, post artifacts/findings/next actions back to the PR, and scope
idempotency keys by job id so hosted jobs do not collide with bundle
analysis.
- ECC-Tools PR #65 merged as `bacd4adf6a3a629e8d403865456d15f127baaf4e`
and added hosted job result history/check-run summaries:
queued hosted jobs now cache both the latest result and immutable run records
for completed or blocked runs, then publish a non-blocking per-job check-run
on the PR head SHA with artifacts, findings, readiness blockers, and next
actions.
- ECC-Tools PR #66 merged as `4e1db48252d068ea5dcf4308b0bc11b0dfe0c9ce`
and added a read-only hosted status command:
`/ecc-tools analyze --job status` now reads the #65 latest-result cache for
the current PR head and posts a compact completed/blocked/not-run table with
the next hosted job command, without queueing work or billing usage.
- Handoff `ecc-supply-chain-audit-20260513-0645.md` under
`~/.cluster-swarm/handoffs/`
records the May 13 supply-chain sweep: no active lockfile/manifest hit for
TanStack/Mini Shai-Hulud indicators; npm audit/signature checks clean across
active npm lockfiles; `cargo audit` clean for `ecc2`; trunk `pip-audit`
clean; JARVIS backend pinned-graph Python audit clean under the supported
Python 3.12 target.
- PR #1861 validation refreshed `node scripts/harness-audit.js --format json`
at 70/70 and `npm run observability:ready` at 21/21.
- PR #1862 updated this roadmap after the JARVIS backend Python audit was
re-run against the supported Python 3.12 pinned graph.
- `docs/architecture/harness-adapter-compliance.md` maps Claude Code, Codex,
OpenCode, Cursor, Gemini, Zed-adjacent, dmux, Orca, Superset, Ghast, and
terminal-only support to install paths, verification commands, and risk
@@ -56,6 +186,12 @@ As of 2026-05-13:
release-readiness evidence refresh: 70/70 harness audit, adapter compliance
PASS, 16/16 observability readiness, 2376/2376 root Node tests, markdownlint,
release-surface and npm publish-surface tests, and 462/462 `ecc2` Rust tests.
- `docs/releases/2.0.0-rc.1/publication-evidence-2026-05-13-post-hardening.md`
records the post-hardening release-readiness refresh after PR #1850 and
PR #1851: 70/70 harness audit, adapter compliance PASS, 18/18 observability
readiness, 2380/2380 root Node tests, markdownlint, release-surface and
npm publish-surface tests, 462/462 `ecc2` Rust tests, npm audit/signature
checks, Rust advisory audit, and TanStack/Mini Shai-Hulud IOC checks.
- A detached clean worktree at
`bfacf37715b39655cbc2c48f12f2a35c67cb0253` verified Claude plugin tag
dry-run without `--force`, local marketplace discovery, temp-home local
@@ -114,6 +250,22 @@ As of 2026-05-13:
CLI command with severity filtering, JSON metadata output, README/API docs,
rebuilt CLI bundle, local TDD coverage, and green remote action/self-scan/Node
verification.
- AgentShield PR #65 pinned workflow actions for release/security CI hardening.
- AgentShield PR #66 disabled cache use in the release publish job so release
publication does not depend on mutable restored build state.
- AgentShield PR #67 added the first portable enterprise evidence-pack bundle:
`agentshield scan --evidence-pack <dir>` writes deterministic manifest,
README, JSON, HTML, SARIF, policy-evaluation, baseline-comparison, and
supply-chain artifacts with default redaction and `not-run` markers for
optional policy/baseline evidence.
- AgentShield PR #68 hardened evidence-pack redaction for enterprise credential
families including GitHub fine-grained PATs, GitLab PATs, npm tokens, Linear
API keys, Stripe keys, Google API keys, Hugging Face tokens, Vercel tokens,
AWS access key IDs, and JWT-shaped credentials.
- AgentShield PR #69 added the deterministic harness adapter registry. Scan
reports now surface local marker evidence for Claude Code, OpenCode, Codex,
Gemini, dmux, generic terminal agents, and project-local templates in JSON,
markdown, terminal, and HTML outputs.
- AgentShield PDF-export decision: defer a native PDF writer for now. The
self-contained HTML executive report remains the exportable buyer artifact
and can be printed to PDF when needed; native PDF generation should wait for
@@ -176,6 +328,64 @@ As of 2026-05-13:
quality, AgentShield policy exceptions, skill-quality evidence,
deep-analyzer evidence, and RAG/evaluator comparison evidence, with each
scenario exercising missing-evidence and evidence-backed diffs.
- ECC-Tools PR #41 hardened supply-chain dependencies.
- ECC-Tools PR #42 added AgentShield evidence-pack gap prediction and routed
missing policy/baseline/allowlist/suppression/supply-chain evidence into the
PR-risk taxonomy, follow-up drafts, and Linear-ready backlog table.
- ECC-Tools PR #43 recognized the concrete AgentShield #67 evidence-pack
artifact contract so canonical bundle files now satisfy the taxonomy and
generated follow-up PRs point maintainers at
`agentshield scan --evidence-pack <dir>`.
- ECC-Tools PR #55 added the first hosted/deeper-analysis readiness signal:
analysis comments now classify a repo as commit-history-only,
evidence-backed, or deep-ready before routing work into CI, AgentShield,
harness, reference-set, RAG/evaluator, AI-routing, cost-control, and
Linear/project-tracking lanes.
- ECC-Tools PR #56 turned that signal into a hosted execution-plan contract:
`/api/analysis/depth-plan` returns ready/blocked jobs and next action text
without charging analysis usage or creating bundle PRs.
- ECC-Tools PR #57 implemented the first job-specific hosted executor:
`/api/analysis/jobs/ci-diagnostics` reuses the depth-readiness gate, internal
API auth, installation ownership, repo-access billing checks, capped workflow
file reads, and usage accounting to return concrete CI hardening findings.
- ECC-Tools PR #58 implemented the second job-specific hosted executor:
`/api/analysis/jobs/security-evidence-review` applies the same hosted gates
to AgentShield evidence-pack, policy, baseline, SBOM, SARIF, and security
scanner artifacts.
- ECC-Tools PR #59 implemented the third job-specific hosted executor:
`/api/analysis/jobs/harness-compatibility-audit` applies the same hosted
gates to Claude, Codex, OpenCode, MCP, plugin, and cross-harness evidence
while avoiding local secret-bearing harness config fetches.
- ECC-Tools PR #60 implemented the fourth job-specific hosted executor:
`/api/analysis/jobs/reference-set-evaluation` applies the same hosted gates
to analyzer corpus, RAG/evaluator, PR salvage, harness, security, and CI
failure-mode reference evidence while avoiding obvious secret-bearing fixture
fetches.
- ECC-Tools PR #61 implemented the fifth job-specific hosted executor:
`/api/analysis/jobs/ai-routing-cost-review` applies the same hosted gates to
model-routing, token-budget, usage-limit, rate-limit, billing/entitlement,
cost-regression, and cost-policy evidence while avoiding obvious
secret-bearing path fetches.
- ECC-Tools PR #62 implemented the sixth job-specific hosted executor:
`/api/analysis/jobs/team-backlog-routing` applies the same hosted gates to
roadmap, runbook, handoff, release-plan, issue-template, ownership,
project-tracker, backlog, and follow-up evidence while avoiding obvious
secret-bearing path fetches.
- ECC-Tools PR #63 publishes the hosted depth-plan check-run after queued PR
analysis completes, making the six hosted executor commands visible on the
PR head SHA without turning the check into a merge blocker.
- ECC-Tools PR #64 wires those commands into the queue: maintainers can comment
`/ecc-tools analyze --job ci-diagnostics`, `security-evidence`,
`harness-compatibility`, `reference-set-evaluation`, `ai-routing-cost`, or
`team-backlog` on a PR and receive hosted job results in a PR comment.
- ECC-Tools PR #65 persists completed and blocked hosted job results to the
analysis cache for 30 days and publishes non-blocking `ECC Tools / Hosted
Job: ...` check-runs so maintainers can scan hosted outcomes from the PR
checks surface instead of rereading older comments.
- ECC-Tools PR #66 exposes the cached results from PR comments with
`/ecc-tools analyze --job status`, summarizing completed, blocked, and
not-yet-run hosted jobs for the PR head and recommending the next hosted job
command.
- ECC PR #1803 landed the contributor Quarkus handling branch after maintainer
cleanup, current-`main` alignment, full local validation, and preservation of
the author's removal of incomplete ja-JP and zh-CN Quarkus translations.
@@ -201,7 +411,7 @@ As of 2026-05-13:
- Keep public PRs and issues below 20, with zero as the preferred release-lane
target.
- Maintain 70/70 harness audit and 16/16 observability readiness after every
- Maintain 70/70 harness audit and 21/21 observability readiness after every
GA-readiness batch.
- Do not publish release or social announcements until the GitHub release,
npm/package state, billing state, and plugin submission surfaces are verified
@@ -218,32 +428,33 @@ is not complete unless the evidence column exists and has been freshly verified.
| Prompt requirement | Required artifact or gate | Current evidence | Status |
| --- | --- | --- | --- |
| Keep public PRs below 20 | Repo-family PR recheck | 0 open PRs across the tracked public repos on 2026-05-13 after merging #1846 | Complete for this checkpoint |
| Keep public issues below 20 | Repo-family issue recheck | 0 open issues across the tracked public repos on 2026-05-13 | Complete for this checkpoint |
| Manage repository discussions | Repo-family discussion recheck | Latest trunk discussion GraphQL sweep returned closed discussions only; satellite repos remain disabled or empty | Complete for this checkpoint |
| Manage PR discussions | PR review/comment closure plus merge/close state | #1846 merged after current-head CI; no open PRs remain | Complete for this checkpoint |
| Keep public PRs below 20 | Repo-family PR recheck | 0 open PRs across `everything-claude-code`, AgentShield, JARVIS, `ECC-Tools/ECC-Tools`, and `ECC-Tools/ECC-website` on 2026-05-13 after merging ECC #1860, AgentShield #78, JARVIS #13, and ECC-Tools #53 | Complete |
| Keep public issues below 20 | Repo-family issue recheck | 0 open issues across `everything-claude-code`, AgentShield, JARVIS, `ECC-Tools/ECC-Tools`, and `ECC-Tools/ECC-website` on 2026-05-13 | Complete |
| Manage repository discussions | Repo-family discussion recheck | GraphQL sweep returned 52 total trunk discussions with 0 open; AgentShield, JARVIS, ECC-Tools, and ECC-Tools website returned 0 total/open discussions | Complete |
| Manage PR discussions | PR review/comment closure plus merge/close state | ECC #1860, AgentShield #78, JARVIS #13, and ECC-Tools #53/#54 merged after current-head CI/builds; no open tracked PRs remain | Complete |
| Salvage useful stale work | `docs/stale-pr-salvage-ledger.md` | Ledger records salvaged, superseded, skipped, and manual-review tails; #1815-#1818 added cost tracking, skill scout, frontend design guidance, code-reviewer false-positive guardrails, and the May 12 gap pass | Complete except translation/manual review tail |
| ECC 2.0 preview pack ready | Release docs, quickstart, publication readiness, release notes | `docs/releases/2.0.0-rc.1/` and readiness docs are in-tree; May 13 evidence refresh records harness, adapter, observability, Node, lint, release-surface, npm publish-surface, and Rust checks | Needs final clean-checkout release approval |
| Hermes specialized skills included safely | Hermes setup/import docs and sanitized skill surface | Hermes setup and import playbook are public; secrets stay local | Needs final release review |
| Naming and rename readiness | Naming matrix across package/plugin/docs/social surfaces | `docs/releases/2.0.0-rc.1/naming-and-publication-matrix.md` records current package, repo, Claude plugin, Codex plugin, OpenCode, and npm availability evidence | Complete for rc.1; post-rc rename remains future work |
| Claude and Codex plugin publication | Contact/submission path with required artifacts and status | Publication readiness, naming matrix, and May 12 dry-run evidence document plugin validation, clean-checkout Claude tag/install smoke, and Codex marketplace CLI shape | Needs explicit approval for real tag/push and marketplace submission |
| Articles, tweets, and announcements | X thread, LinkedIn copy, GitHub release copy, push checklist | Draft launch collateral exists under rc.1 release docs | Needs URL-backed refresh |
| AgentShield enterprise iteration | Policy gates, SARIF, packs, provenance, corpus, HTML reports, exception lifecycle audit, baseline drift Action/CLI surfaces, enterprise research roadmap | PRs #53, #55-#64 landed with test evidence; native PDF export deferred in favor of self-contained HTML plus print-to-PDF until explicit enterprise demand appears; `docs/architecture/agentshield-enterprise-research-roadmap.md` selects baseline drift as the first control-plane slice | Baseline-drift Action and CLI write surfaces landed; evidence-pack routing remains |
| ECC Tools next-level app | Billing audit, PR checks, deep analyzer, sync backlog, evaluator/RAG corpus | PRs #26-#40 landed with test evidence | Needs capacity-backed Linear rollout |
| GitGuardian/Dependabot/CodeRabbit-style checks | Non-blocking taxonomy and deterministic follow-up checks | ECC-Tools risk taxonomy check plus follow-up signals landed, including Skill Quality, Deep Analyzer Evidence, Analyzer Corpus Evidence, RAG/Evaluator Evidence, and PR Review/Salvage Evidence | Partially complete |
| AgentShield enterprise iteration | Policy gates, SARIF, packs, provenance, corpus, HTML reports, exception lifecycle audit, baseline drift Action/CLI surfaces, evidence-pack redaction, harness adapter registry, enterprise research roadmap, supply-chain hardened release path, CI-safe baseline fingerprints, corpus accuracy recommendations, remediation workflow phases, env proxy hijack corpus coverage | PRs #53, #55-#64, #67-#69, and #78-#82 landed with test evidence; native PDF export deferred in favor of self-contained HTML plus print-to-PDF until explicit enterprise demand appears; `docs/architecture/agentshield-enterprise-research-roadmap.md` now has baseline drift, evidence-pack bundle, redaction, adapter-registry, supply-chain hardening, hashed baseline fingerprints, corpus accuracy recommendation, remediation workflow, and env proxy hijack corpus slices landed | Next hosted evidence-pack workflow depth |
| ECC Tools next-level app | Billing audit, PR checks, deep analyzer, sync backlog, evaluator/RAG corpus, analysis-depth readiness, hosted execution planning, hosted CI diagnostics, hosted security evidence review, hosted harness compatibility audit, hosted reference-set evaluation, hosted AI routing/cost review, hosted team backlog routing, hosted depth-plan check-run, PR-comment hosted job dispatch, hosted job result history/check-runs, hosted result status command | PRs #26-#43 plus #53-#66 landed with test evidence, including AgentShield evidence-pack gap routing, canonical bundle recognition, supply-chain signature gates, PR draft follow-up Linear tracking, evidence-backed/deep-ready repository classification, the `/api/analysis/depth-plan` hosted job plan, `/api/analysis/jobs/ci-diagnostics`, `/api/analysis/jobs/security-evidence-review`, `/api/analysis/jobs/harness-compatibility-audit`, `/api/analysis/jobs/reference-set-evaluation`, `/api/analysis/jobs/ai-routing-cost-review`, `/api/analysis/jobs/team-backlog-routing`, the `ECC Tools / Hosted Depth Plan` check-run, `/ecc-tools analyze --job ...` PR-comment dispatch, non-blocking per-hosted-job result check-runs backed by 30-day result cache records, and `/ecc-tools analyze --job status` cache lookup | Next work is evaluator-backed hosted promotion and status-aware depth-plan recommendations |
| GitGuardian/Dependabot/CodeRabbit-style checks | Non-blocking taxonomy, deterministic follow-up checks, and local supply-chain gates | ECC-Tools risk taxonomy check plus follow-up signals landed, including Skill Quality, Deep Analyzer Evidence, Analyzer Corpus Evidence, RAG/Evaluator Evidence, PR Review/Salvage Evidence, and AgentShield evidence-pack evidence; #1846 added npm registry signature gates; #1848 added the supply-chain incident-response playbook and `pull_request_target` cache-poisoning validator guard; #1851 added the privileged checkout credential-persistence guard; AgentShield #78, JARVIS #13, and ECC-Tools #53 applied the same hardening outside trunk | Current supply-chain gate complete; deeper hosted review features remain future |
| Harness-agnostic learning system | Audit, adapter matrix, observability, traces, promotion loop | Audit/adapters/observability gates plus `docs/architecture/evaluator-rag-prototype.md`, `examples/evaluator-rag-prototype/`, and ECC-Tools PR #40 define read-only stale-salvage, billing-readiness, CI-failure-diagnosis, harness-config-quality, AgentShield policy-exception, skill-quality evidence, deep-analyzer evidence, and RAG/evaluator comparison scenarios with trace, report, playbook, verifier, and predictive-check artifacts | Local corpus complete; hosted integration remains future |
| Linear roadmap is detailed | Linear project status plus repo mirror | Repo mirror exists; issue creation was retried on 2026-05-12 and remains blocked by the workspace free issue limit | Needs recurring status updates after each merge batch |
| Flow separation and progress tracking | Flow lanes with owner artifacts and update cadence | This roadmap defines lanes below | Active |
| Realtime Linear sync | Project updates while issue limit is blocked; issues later | ECC-Tools #39 implements opt-in Linear API sync for deferred follow-up backlog items | Needs workspace capacity/config rollout |
| Observability for self-use | Local readiness gate, traces, status snapshots, HUD/status contract, risk ledger | `npm run observability:ready` reports 16/16 | Complete for local gate |
| Linear roadmap is detailed | Linear project status plus repo mirror | Repo mirror exists; issue creation was retried on 2026-05-12 and remains blocked by the workspace free issue limit; this May 13 sync adds ECC #1860, AgentShield #78-#82, JARVIS #13, ECC-Tools #53-#66, resolved queue/discussion counts, and Linear project status updates through ECC-Tools #66 | Needs recurring status updates after each merge batch |
| Flow separation and progress tracking | Flow lanes with owner artifacts and update cadence | This roadmap defines lanes below and `docs/architecture/progress-sync-contract.md` makes GitHub/Linear/handoff/roadmap sync part of the readiness gate | Active |
| Realtime Linear sync | Project updates while issue limit is blocked; issues later | ECC-Tools #39 implements opt-in Linear API sync for deferred follow-up backlog items, and ECC-Tools #54 adds copy-ready PR drafts to that backlog when draft PR shells are not opened; `docs/architecture/progress-sync-contract.md` defines the local file-backed realtime boundary while issue capacity is blocked | Needs workspace capacity/config rollout |
| Observability for self-use | Local readiness gate, traces, status snapshots, HUD/status contract, risk ledger, progress-sync contract | `npm run observability:ready` reports 21/21 | Complete for local gate |
| Proper release and notifications | Release tag, npm publish state, plugin state, social posts | Publication readiness gate exists with May 12 dry-run and May 13 readiness evidence | Not complete; approval/live URLs required |
## Execution Lanes And Tracking Contract
Until Linear issue capacity is cleared, this document is the durable execution
ledger and Linear receives project status updates only. When capacity is
available, each lane below should become a small set of Linear issues linked
back to the repo evidence and merge commits.
ledger and Linear receives project status updates only. The sync contract lives
at `docs/architecture/progress-sync-contract.md`. When capacity is available,
each lane below should become a small set of Linear issues linked back to the
repo evidence and merge commits.
| Lane | Source of truth | Next tracked artifact | Update cadence |
| --- | --- | --- | --- |
@@ -251,9 +462,9 @@ back to the repo evidence and merge commits.
| Release and publication | rc.1 release docs, publication readiness doc | Naming matrix and plugin submission/contact checklist | Before any tag |
| Harness OS core | Audit, adapter matrix, observability docs, `ecc2/` | HUD/session-control acceptance spec | Weekly until GA |
| Evaluation and RAG | Reference-set validation, harness audit, traces, ECC-Tools corpus | Read-only evaluator/RAG prototype plus stale-salvage, billing-readiness, CI-failure-diagnosis, harness-config-quality, AgentShield policy-exception, skill-quality evidence, deep-analyzer evidence, and RAG/evaluator comparison fixtures | Hosted retrieval/check-run automation plan |
| AgentShield enterprise | AgentShield PR evidence and roadmap notes | Baseline-drift evidence-pack and backlog sync follow-up | Next implementation batch |
| ECC Tools app | ECC-Tools PR evidence, billing audit, risk taxonomy, evaluator/RAG corpus | Capacity-backed Linear rollout | Next implementation batch |
| Linear progress | Linear project status updates and this mirror | Status update with queue/evidence/missing gates | Every significant merge batch |
| AgentShield enterprise | AgentShield PR evidence and roadmap notes | Remediation workflow depth or corpus expansion follow-up | Next implementation batch |
| ECC Tools app | ECC-Tools PR evidence, billing audit, risk taxonomy, evaluator/RAG corpus | ECC-Tools #53 published the supply-chain workflow hardening branch, #54 tracks copy-ready PR drafts in the Linear/project backlog, #55 classifies analysis-depth readiness, #56 exposes the hosted execution plan, #57 executes the first hosted CI diagnostics job, #58 executes the hosted security evidence review job, #59 executes the hosted harness compatibility audit, #60 executes the hosted reference-set evaluation, #61 executes the hosted AI routing/cost review, #62 executes hosted team backlog routing, #63 publishes the hosted depth-plan check-run, and #64 dispatches hosted jobs from PR comments; next work is hosted result history/check-run summaries | Next implementation batch |
| Linear progress | Linear project status updates, `docs/architecture/progress-sync-contract.md`, and this mirror | Status update with queue/evidence/missing gates | Every significant merge batch |
The project status update should always include:
@@ -323,7 +534,7 @@ Target: 2026-06-07
Acceptance:
- Observability readiness remains 16/16 and is backed by JSONL traces, status
- Observability readiness remains 21/21 and is backed by JSONL traces, status
snapshots, risk ledger, and exportable handoff contracts.
- HUD/status model covers context, tool calls, active agents, todos, checks,
cost, risk, and queue state.
@@ -429,6 +640,9 @@ Acceptance:
- Linear sync maps deferred backlog findings to Linear issues without flooding
GitHub, creates or reuses exact-title Linear issues when configured, and
reports skipped sync when credentials or team configuration are absent.
- Linear/project backlog sync includes copy-ready PR drafts when
`/ecc-tools followups sync-linear` is used without `open-pr-drafts`, so
stale-PR salvage work remains tracked without opening extra PR shells.
- Follow-up generation caps automatic GitHub object creation and keeps overflow
findings in a copy-ready project sync backlog.
@@ -452,14 +666,26 @@ Acceptance:
## Next Engineering Slices
1. Finish the AgentShield baseline-drift control-plane slice from
1. Continue the AgentShield enterprise control-plane sequence from
`docs/architecture/agentshield-enterprise-research-roadmap.md`: PR #63
shipped the GitHub Action baseline outputs and job-summary evidence; PR #64
shipped GitHub Action baseline outputs and job-summary evidence; PR #64
shipped first-class baseline snapshot creation through
`agentshield baseline write`; the remaining work is evidence-pack routing
and ECC-Tools backlog sync integration.
2. Enable/configure the merged Linear backlog sync path after workspace issue
capacity clears or the Linear workspace is upgraded.
3. Use the ECC-Tools evaluator/RAG corpus as the promotion gate before adding
`agentshield baseline write`; PR #67 shipped the evidence-pack bundle; PR
#68 hardened evidence-pack redaction; PR #69 shipped the multi-harness
adapter registry; PR #78 hardened the release workflow for the current
supply-chain incident class; PR #79 moved baseline/watch/remediation
fingerprints to hashed evidence and stopped writing raw evidence into new
baselines; PR #80 added prioritized corpus accuracy recommendations for
failed regression gates; PR #81 added ordered remediation workflow phases;
PR #82 expanded corpus coverage for env proxy hijacks and out-of-band
exfiltration; and ECC-Tools PRs #42/#43 now route and recognize evidence
packs. The next slice is hosted evidence-pack workflow depth.
2. Feed the #66 status surface back into hosted depth-plan recommendations so
queued analysis can suggest the next unrun or newly blocked hosted job from
cached outcomes, not only static readiness.
3. Enable/configure the merged Linear backlog sync path after workspace issue
capacity clears or the Linear workspace is upgraded, then verify PR-draft
salvage items land in the expected project.
4. Use the ECC-Tools evaluator/RAG corpus as the promotion gate before adding
hosted retrieval, vector storage, model-backed judging, or automated
check-run promotion.

View File

@@ -32,6 +32,13 @@ operator needs.
`tool-usage.jsonl` events that ECC2 can sync.
- Risk ledger: `ecc2/src/observability/mod.rs` scores tool calls and stores a
paginated ledger for review.
- Progress sync: `docs/architecture/progress-sync-contract.md` defines how
GitHub, Linear, local handoffs, the repo roadmap, and `scripts/work-items.js`
stay aligned during merge batches and release-gate reviews.
- Release safety: `docs/releases/2.0.0-rc.1/publication-readiness.md`,
post-hardening evidence, supply-chain incident response, workflow-security
validation, npm pack checks, and release-surface tests must be present before
any public tag, package publish, plugin submission, or announcement action.
## Reference Pressure
@@ -64,9 +71,15 @@ later, but only after the local event model is useful enough to trust.
operator dashboard.
5. Run `node scripts/session-inspect.js --list-adapters` to confirm which
session surfaces are available.
6. Use ECC2 tool logs for risky operations, conflict analysis, and handoff
6. Run `node scripts/work-items.js sync-github --repo <owner/repo>` before
relying on local work-item status for a tracked repository.
7. Use ECC2 tool logs for risky operations, conflict analysis, and handoff
review before increasing autonomy.
8. Re-run the release-safety evidence checks before any public release action:
publication readiness, supply-chain incident response, workflow-security
validation, package surface, and release-surface tests.
The end-state is practical: before asking ECC to run larger multi-agent loops,
the operator can prove the system has live status, durable session traces,
baseline scorecards, and a local risk ledger.
baseline scorecards, a local risk ledger, and a progress-sync contract that
keeps GitHub, Linear, handoffs, and roadmap evidence from drifting apart.

View File

@@ -0,0 +1,67 @@
# Progress Sync Contract
ECC 2.0 tracks execution state across GitHub, Linear, local handoffs, and the
repo roadmap. This contract defines the minimum evidence required before a
status update can claim a lane is current.
## Sources Of Truth
| Surface | Role | Current rule |
| --- | --- | --- |
| GitHub PRs/issues/discussions | Public queue and review state | Recheck live counts before every significant merge batch and before release approval. |
| Linear project | Executive roadmap and stakeholder status update | Post project status updates while issue capacity blocks issue creation. Create/reuse issues only when workspace capacity is available. |
| Local handoff | Durable operator continuity | Update the active handoff after every merge batch, queue drain, skipped release gate, or blocked external action. |
| Repo roadmap | Auditable planning mirror | Keep `docs/ECC-2.0-GA-ROADMAP.md` aligned to merged PR evidence and unresolved gates. |
| `scripts/work-items.js` | Local tracker bridge | Sync GitHub PRs/issues into the SQLite work-items store for status snapshots and blocked follow-up. |
## Flow Lanes
The repo mirror uses these flow lanes so ECC work does not collapse into one
undifferentiated backlog:
- Queue hygiene and stale-work salvage
- Release, naming, plugin publication, and announcements
- Harness adapter compliance
- Local observability, HUD/status, and session control
- Evaluator/RAG and self-improving harness loops
- AgentShield enterprise security platform
- ECC Tools billing, PR-risk checks, deep analysis, and Linear sync
- Legacy artifact audit and translator/manual-review tails
Each flow lane needs one owner artifact, one current evidence source, and one
next action. A lane is not current if any of those three fields are missing.
## Significant Merge Batch Update
After a significant merge batch, update Linear and the handoff with:
1. Current public queue counts for tracked GitHub repos.
2. Merged PR numbers, commit IDs, and validation evidence.
3. Changed release gates, if any.
4. Deferred or skipped work and the explicit reason.
5. The next one or two implementation slices.
When Linear issue capacity is unavailable, use a project status update instead
of creating placeholder issues. When issue capacity is available, create or
reuse exact-title issues and link them to the repo evidence.
## Realtime Boundary
The local realtime path is file-backed by default:
- `node scripts/work-items.js sync-github --repo <owner/repo>` imports current
GitHub PR and issue state into the SQLite work-items store.
- `node scripts/status.js --json` and `node scripts/work-items.js list --json`
expose local state for a HUD, handoff, or later Linear sync.
- Linear remains the external status surface; the repo does not require hosted
telemetry to be release-ready.
Hosted telemetry such as PostHog can be added later, but it must consume the
same event model rather than becoming a second source of truth.
## Release Gate
Do not publish, tag, announce, submit marketplace packages, or claim plugin
availability from this contract alone. Release readiness still requires the
publication-readiness evidence documents, fresh queue checks, package checks,
plugin checks, and explicit maintainer approval.

View File

@@ -0,0 +1,98 @@
# ECC v2.0.0-rc.1 Publication Evidence - 2026-05-13 Post-Hardening
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 | `209abd403b7eaa968c6d4fa67be82e04b55706d6` |
| Evidence branch | `docs/post-hardening-release-evidence-20260513` |
| Evidence scope | Current `main` after PR #1850 and PR #1851 |
| Git remote | `https://github.com/affaan-m/everything-claude-code.git` |
| Local status caveat | Working tree had the unrelated untracked `docs/drafts/` directory |
The actual release operator should repeat these checks from the final release
commit with a clean checkout before publishing.
## Queue And Release State
| Surface | Command | Result |
| --- | --- | --- |
| GitHub PRs and issues | `gh pr list` / `gh issue list` across trunk, AgentShield, and JARVIS | 0 open PRs and 0 open issues on accessible `affaan-m` repos |
| Trunk discussions | GraphQL discussion count for `affaan-m/everything-claude-code` | 0 open discussions |
| Dependabot alerts | Dependabot alert API for trunk, AgentShield, and JARVIS | 0 open alerts |
| Release state | `gh release view v2.0.0-rc.1` | Still not created; release remains approval-gated |
ECC-Tools organization repo counts were not rechecked through the current
GraphQL token in this pass because the token cannot resolve those org repos.
The prior post-#42 local checkout handoff recorded both ECC-Tools repos at
0 open PRs and 0 open issues.
## Hardening Landed Since Previous Evidence
| PR | Merge commit | Evidence |
| --- | --- | --- |
| #1850 | `248673271455e9dc85b8add2a6ab76107b718639` | Removed `Bash` tool access from read-only analyzer agents and zh-CN copies; AgentShield high findings on that surface dropped 21 -> 18 with no new high findings |
| #1851 | `209abd403b7eaa968c6d4fa67be82e04b55706d6` | Disabled `actions/checkout` credential persistence in write-permission workflows and added a workflow-security validator rule to keep that guard in place |
## Required Command Evidence
| Evidence | Command | Result |
| --- | --- | --- |
| Harness audit | `npm run harness:audit -- --format json` | `overall_score: 70`, `max_score: 70`, no top actions |
| Adapter scorecard | `npm run harness:adapters -- --check` | `Harness Adapter Compliance: PASS`; 11 adapters |
| Observability readiness | `npm run observability:ready -- --format json` | `overall_score: 21`, `max_score: 21`, `ready: true`, no top actions; includes Release Safety 3/3 |
| Workflow security validator | `node scripts/ci/validate-workflow-security.js` | Validated 7 workflow files |
| Workflow validator tests | `node tests/ci/validate-workflow-security.test.js` | Passed 14/14 |
| Release surface | `node tests/docs/ecc2-release-surface.test.js` | Passed 18/18 |
| Package surface | `node tests/scripts/npm-publish-surface.test.js` | Passed 2/2 |
| Root suite | `node tests/run-all.js` | Passed 2381/2381, 0 failed |
| Markdown lint | `npx markdownlint-cli '**/*.md' --ignore node_modules --ignore docs/drafts` | Passed |
| Rust surface | `cd ecc2 && cargo test` | Passed 462/462; warnings only for unused functions/fields |
| GitGuardian Security Checks | GitHub check on post-hardening security PRs | Passed before merge |
## Supply-Chain Evidence
| Surface | Command or check | Result |
| --- | --- | --- |
| Local npm vulnerability audit | `npm audit --json` | 0 vulnerabilities |
| Local npm signature audit | `npm audit signatures` | 241 verified registry signatures and 30 verified attestations |
| Rust advisory audit | `cd ecc2 && cargo audit -q` | Passed silently |
| TanStack / Mini Shai-Hulud IOC check | Grep for affected package namespaces, payload filenames, and known commit marker | No runtime or lockfile dependency on affected packages; no worm IOC matches |
| GitGuardian Security Checks | GitHub check on post-hardening security PRs | Passed before merge |
## External Advisory Mapping
The May 2026 TanStack incident maps to ECC release risk through three workflow
classes:
- `pull_request_target` workflows that execute or checkout untrusted PR code;
- shared dependency caches crossing fork, base, and release workflow trust
boundaries;
- release jobs with writable tokens or OIDC tokens exposed to subsequent
process execution.
ECC's current guardrails cover those classes through:
- rejection of untrusted checkout refs in `workflow_run` and
`pull_request_target` workflows;
- rejection of shared caches in `pull_request_target` and `id-token: write`
workflows;
- mandatory `npm audit signatures` when workflows run `npm audit`;
- mandatory `npm ci --ignore-scripts` in workflows with write permissions;
- mandatory `persist-credentials: false` on `actions/checkout` in workflows
with write permissions.
## Blockers Still Requiring Approval Or External Action
- Create or verify GitHub prerelease `v2.0.0-rc.1`.
- Publish `ecc-universal@2.0.0-rc.1` with npm dist-tag `next`.
- Create and push the Claude plugin tag only after explicit approval.
- Confirm the live Claude/Codex/OpenCode marketplace submission path or record
the manual submission owner and status.
- Verify ECC Tools billing/App/Marketplace claims before using them in launch
copy.
- Refresh announcement copy with live URLs after release and package/plugin
URLs exist.

View File

@@ -10,6 +10,8 @@ For the May 12 dry-run evidence pass, see
[`publication-evidence-2026-05-12.md`](publication-evidence-2026-05-12.md).
For the May 13 release-readiness evidence refresh, see
[`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
[`publication-evidence-2026-05-13-post-hardening.md`](publication-evidence-2026-05-13-post-hardening.md).
## Release Identity Matrix
@@ -39,6 +41,7 @@ For the May 13 release-readiness evidence refresh, see
| 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 |
| 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 |
| 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
@@ -49,8 +52,10 @@ Record the exact commit SHA and command output before any publication action:
| Clean release branch | `git status --short --branch` | On intended release commit; no unrelated files | Pending final clean-checkout release pass; May 13 evidence branch still had unrelated untracked `docs/drafts/` |
| Harness audit | `npm run harness:audit -- --format json` | 70/70 passing | `publication-evidence-2026-05-13.md`: 70/70 |
| Adapter scorecard | `npm run harness:adapters -- --check` | PASS | `publication-evidence-2026-05-13.md`: PASS, 11 adapters |
| Observability readiness | `npm run observability:ready` | 16/16 passing | `publication-evidence-2026-05-13.md`: 16/16, ready true |
| Root suite | `node tests/run-all.js` | 0 failures | `publication-evidence-2026-05-13.md`: 2376 passed, 0 failed |
| Observability readiness | `npm run observability:ready` | 21/21 passing | `publication-evidence-2026-05-13-post-hardening.md`: 21/21, ready true after release-safety gate refresh |
| Release safety gate | `npm run observability:ready -- --format json` | Release Safety category passing with publication readiness, supply-chain, workflow security, package surface, and release-surface evidence | `publication-evidence-2026-05-13-post-hardening.md`: Release Safety 3/3 |
| Supply-chain verification | `npm audit --json`; `npm audit signatures`; `cd ecc2 && cargo audit -q`; Dependabot alerts; GitGuardian Security Checks | 0 vulnerabilities/alerts, registry signatures verified, GitGuardian clean | `publication-evidence-2026-05-13-post-hardening.md`: npm, cargo, Dependabot, TanStack/Mini Shai-Hulud, and GitGuardian evidence |
| Root suite | `node tests/run-all.js` | 0 failures | `publication-evidence-2026-05-13-post-hardening.md`: 2381 passed, 0 failed |
| Markdown lint | `npx markdownlint-cli '**/*.md' --ignore node_modules` | 0 failures | `publication-evidence-2026-05-13.md`: passed after zh-CN CLAUDE list-marker normalization |
| 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 |

View File

@@ -8,7 +8,9 @@ they do not prove that the workflow executed the intended code path.
## Current External Trigger
As of 2026-05-13, the active incident class is the May 2026 TanStack npm
supply-chain compromise:
supply-chain compromise. ECC also keeps Mini Shai-Hulud-style npm worm IOCs in
the same release-safety sweep because both incident classes target package
install/publish paths and developer credentials:
- TanStack reported 84 malicious versions across 42 `@tanstack/*` packages,
published on 2026-05-11 between 19:20 and 19:26 UTC.

View File

@@ -2,7 +2,7 @@
name: code-explorer
description: 通过追踪执行路径、映射架构层和记录依赖关系,深入分析现有代码库功能,为新的开发提供信息。
model: sonnet
tools: [Read, Grep, Glob, Bash]
tools: [Read, Grep, Glob]
---
# 代码探索代理

View File

@@ -2,7 +2,7 @@
name: comment-analyzer
description: 分析代码注释的准确性、完整性、可维护性和注释腐烂风险。
model: sonnet
tools: [Read, Grep, Glob, Bash]
tools: [Read, Grep, Glob]
---
# 注释分析代理

View File

@@ -2,7 +2,7 @@
name: type-design-analyzer
description: 分析封装、不变式表达、实用性和强制性的类型设计。
model: sonnet
tools: [Read, Grep, Glob, Bash]
tools: [Read, Grep, Glob]
---
# 类型设计分析代理

View File

@@ -243,6 +243,24 @@
"security"
]
},
{
"id": "lang:ruby",
"family": "language",
"description": "Ruby and Rails coding, testing, and security guidance. Resolves through framework-language and security modules.",
"modules": [
"framework-language",
"security"
]
},
{
"id": "framework:rails",
"family": "framework",
"description": "Rails 8 application guidance for MVC, Hotwire, Solid Queue/Cache/Cable, authentication, testing, and security.",
"modules": [
"framework-language",
"security"
]
},
{
"id": "lang:rust",
"family": "language",

View File

@@ -21,6 +21,7 @@ rules/
├── web/ # Web and frontend specific
├── swift/ # Swift specific
├── php/ # PHP specific
├── ruby/ # Ruby / Rails specific
└── arkts/ # HarmonyOS / ArkTS specific
```
@@ -40,6 +41,7 @@ rules/
./install.sh web
./install.sh swift
./install.sh php
./install.sh ruby
./install.sh arkts
# Install multiple languages at once
@@ -66,6 +68,7 @@ cp -r rules/golang ~/.claude/rules/golang
cp -r rules/web ~/.claude/rules/web
cp -r rules/swift ~/.claude/rules/swift
cp -r rules/php ~/.claude/rules/php
cp -r rules/ruby ~/.claude/rules/ruby
cp -r rules/arkts ~/.claude/rules/arkts
# Attention ! ! ! Configure according to your actual project requirements; the configuration here is for reference only.

View File

@@ -0,0 +1,46 @@
---
paths:
- "**/*.rb"
- "**/*.rake"
- "**/Gemfile"
- "**/*.gemspec"
- "**/config.ru"
---
# Ruby Coding Style
> This file extends [common/coding-style.md](../common/coding-style.md) with Ruby and Rails specific content.
## Standards
- Target **Ruby 3.3+** for new Rails work unless the project already pins an older supported runtime.
- Enable **YJIT** in production only after measuring boot time, memory, and request/job throughput.
- Add `# frozen_string_literal: true` to new Ruby files when the project uses that convention.
- Prefer clear Ruby over clever metaprogramming; isolate DSL-heavy code behind narrow, tested boundaries.
## Formatting And Linting
- Use the project's checked-in RuboCop config. For Rails 8+ apps, start from `rubocop-rails-omakase` and customize only where the codebase has a real convention.
- Keep formatter/linter commands behind binstubs or scripts so CI and local runs match:
```bash
bundle exec rubocop
bundle exec rubocop -A
```
- Do not silence cops inline unless the exception is narrow, documented, and harder to express cleanly in code.
## Rails Style
- Follow Rails naming and directory conventions before adding custom structure.
- Keep controllers transport-focused: authentication, authorization, parameter handling, response shape.
- Put reusable domain behavior in models, concerns, service objects, query objects, or form objects based on actual complexity, not as default ceremony.
- Prefer `bin/rails`, `bin/rake`, and checked-in binstubs over globally installed commands.
## Error Handling
- Rescue specific exceptions. Avoid broad `rescue StandardError` blocks unless they re-raise or preserve enough context for operators.
- Use `ActiveSupport::Notifications` or the app's logger for operational events; do not leave `puts`, `pp`, or `debugger` in committed application code.
## Reference
See skill: `backend-patterns` for broader service/repository layering guidance.

37
rules/ruby/hooks.md Normal file
View File

@@ -0,0 +1,37 @@
---
paths:
- "**/*.rb"
- "**/*.rake"
- "**/Gemfile"
- "**/Gemfile.lock"
- "**/config/routes.rb"
---
# Ruby Hooks
> This file extends [common/hooks.md](../common/hooks.md) with Ruby and Rails specific content.
## PostToolUse Hooks
Configure project-local hooks to prefer binstubs and checked-in tooling:
- **RuboCop**: run `bundle exec rubocop -A <file>` or the project's safer formatter command after Ruby edits.
- **Brakeman**: run `bundle exec brakeman --no-pager` after security-sensitive Rails changes.
- **Tests**: run the narrowest matching `bin/rails test ...` or `bundle exec rspec ...` command for touched files.
- **Bundler audit**: run `bundle exec bundle-audit check --update` when `Gemfile` or `Gemfile.lock` changes and the project has bundler-audit installed.
## Warnings
- Warn on committed `debugger`, `binding.irb`, `binding.pry`, `puts`, `pp`, or `p` calls in application code.
- Warn when an edit disables CSRF protection, expands mass-assignment, or adds raw SQL without parameterization.
- Warn when a migration changes data destructively without a reversible path or documented rollout plan.
## CI Gate Suggestions
```bash
bundle exec rubocop
bundle exec brakeman --no-pager
bin/rails test
bundle exec rspec
```
Use only the commands that are present in the project; do not install new hook dependencies without maintainer approval.

44
rules/ruby/patterns.md Normal file
View File

@@ -0,0 +1,44 @@
---
paths:
- "**/*.rb"
- "**/*.rake"
- "**/Gemfile"
- "**/app/**/*.erb"
- "**/config/routes.rb"
---
# Ruby Patterns
> This file extends [common/patterns.md](../common/patterns.md) with Ruby and Rails specific content.
## Rails Way First
- Start with plain Rails MVC and Active Record conventions for small and medium features.
- Introduce service objects, query objects, form objects, decorators, or presenters when the model/controller boundary is carrying multiple responsibilities.
- Name extracted objects after the business operation they perform, not after generic layers like `Manager` or `Processor`.
## Persistence
- Prefer PostgreSQL for multi-host production Rails apps unless the existing platform has a clear reason for MySQL or SQLite.
- Treat Rails 8 SQLite-backed defaults as viable for single-host or modest deployments, not as an automatic fit for shared multi-service systems.
- Keep raw SQL behind query objects or model scopes and parameterize every dynamic value.
## Background Jobs And Runtime Services
- Use **Solid Queue** for greenfield Rails 8 apps with modest throughput and simple deployment needs.
- Use **Sidekiq** when the app needs mature observability, high throughput, existing Redis infrastructure, or Pro/Enterprise features.
- Use **Solid Cache** and **Solid Cable** when their deployment model matches the app; use Redis when shared cross-service behavior, high fanout, or advanced data structures matter.
## Frontend
- Prefer **Hotwire** with Turbo, Stimulus, Importmap, and Propshaft for server-rendered Rails apps.
- Use React, Vue, Inertia.js, or a separate SPA when interaction complexity, existing product architecture, or team ownership justifies the extra client surface.
- Keep view components, partials, and presenters focused on rendering decisions; keep persistence and authorization out of templates.
## Authentication
- Use the Rails 8 authentication generator for straightforward session auth and password reset needs.
- Use Devise or another established auth system when requirements include OAuth, MFA, confirmable/lockable flows, multi-model auth, or a large existing Devise footprint.
## Reference
See skill: `backend-patterns` for service boundaries and adapter patterns.

51
rules/ruby/security.md Normal file
View File

@@ -0,0 +1,51 @@
---
paths:
- "**/*.rb"
- "**/*.rake"
- "**/Gemfile"
- "**/Gemfile.lock"
- "**/config/routes.rb"
- "**/config/credentials*.yml.enc"
---
# Ruby Security
> This file extends [common/security.md](../common/security.md) with Ruby and Rails specific content.
## Rails Defaults
- Keep CSRF protection enabled for state-changing browser requests.
- Use strong parameters or typed boundary objects before mass assignment.
- Store secrets in Rails credentials, environment variables, or a secret manager. Never commit plaintext keys, tokens, private credentials, or copied `.env` values.
## SQL And Active Record
- Prefer Active Record query APIs and parameterized SQL.
- Never interpolate request, cookie, header, job, or webhook values into SQL strings.
- Scope model callbacks carefully; security-sensitive side effects should be explicit and covered by tests.
## Authentication And Sessions
- Use the Rails 8 authentication generator for simple session auth, or Devise when OAuth, MFA, confirmable, lockable, multi-model auth, or existing Devise conventions are required.
- Rotate sessions after sign-in and privilege changes.
- Protect account recovery flows with expiry, single-use tokens, rate limiting, and audit logging.
## Dependencies
- Run dependency checks when the lockfile changes:
```bash
bundle audit check --update
bundle exec brakeman --no-pager
```
- Review new gems for maintainer activity, native extension risk, transitive dependencies, and whether the same behavior can be implemented with Rails core.
## Web Safety
- Escape template output by default. Treat `html_safe`, `raw`, and custom sanitizers as security-sensitive code.
- Validate file uploads by content type, extension, size, and storage destination.
- Treat background jobs, webhooks, Action Cable messages, and Turbo Stream inputs as untrusted boundaries.
## Reference
See skill: `security-review` for secure-by-default review patterns.

51
rules/ruby/testing.md Normal file
View File

@@ -0,0 +1,51 @@
---
paths:
- "**/*.rb"
- "**/*.rake"
- "**/Gemfile"
- "**/test/**/*.rb"
- "**/spec/**/*.rb"
- "**/config/routes.rb"
---
# Ruby Testing
> This file extends [common/testing.md](../common/testing.md) with Ruby and Rails specific content.
## Framework
- Use **Minitest** when the Rails app follows the default Rails test stack.
- Use **RSpec** when it is already established in the project or the team has explicit production conventions around it.
- Do not mix Minitest and RSpec inside the same feature area without a migration reason.
## Test Pyramid
- Put fast domain behavior in model, service, query, policy, and job tests.
- Use request/controller tests for HTTP contracts, auth behavior, redirects, status codes, and response shapes.
- Use system tests with Capybara for browser-critical flows only; keep them focused and stable.
- Cover background jobs with unit tests for behavior and integration tests for queue/enqueue contracts.
## Fixtures And Factories
- Use Rails fixtures when they are the project default and the data graph is small.
- Use `factory_bot` when scenarios need explicit object construction or complex traits.
- Keep test data close to the behavior being asserted; avoid global fixtures that hide setup cost.
## Commands
Prefer project-local commands:
```bash
bin/rails test
bin/rails test test/models/user_test.rb
bundle exec rspec
bundle exec rspec spec/models/user_spec.rb
```
## Coverage
- Use SimpleCov when coverage is enforced; keep thresholds in CI and avoid gaming branch coverage with low-value tests.
- Add regression tests for bug fixes before changing production code.
## Reference
See skill: `tdd-workflow` for the repo-wide RED -> GREEN -> REFACTOR loop.

View File

@@ -108,6 +108,18 @@ function findViolations(filePath, source) {
}
if (WRITE_PERMISSION_PATTERN.test(source)) {
for (const step of checkoutSteps) {
if (!/persist-credentials:\s*['"]?false['"]?\b/m.test(step.text)) {
violations.push({
filePath,
event: 'write-permission checkout',
description: 'workflows with write permissions must disable checkout credential persistence',
expression: 'actions/checkout without persist-credentials: false',
line: step.startLine,
});
}
}
for (const match of source.matchAll(NPM_CI_PATTERN)) {
violations.push({
filePath,

View File

@@ -42,7 +42,374 @@ const EDIT_WRITE_HOOK_ID = 'pre:edit-write:gateguard-fact-force';
const BASH_HOOK_ID = 'pre:bash:gateguard-fact-force';
const ECC_DISABLE_VALUES = new Set(['0', 'false', 'off', 'disabled', 'disable']);
const DESTRUCTIVE_BASH = /\b(rm\s+-rf|git\s+reset\s+--hard|git\s+checkout\s+--|git\s+clean\s+-f|drop\s+table|delete\s+from|truncate|git\s+push\s+--force(?!-with-lease)|git\s+commit\s+--amend|dd\s+if=)\b/i;
// SQL-keyword + dd patterns stay as a single regex — they are stable
// phrases without shell-flag ordering concerns. Quoted strings are
// stripped before this regex runs so a commit message mentioning
// "drop table" no longer triggers a false positive.
const DESTRUCTIVE_SQL_DD = /\b(drop\s+table|delete\s+from|truncate|dd\s+if=)\b/i;
/**
* Strip the contents of single- and double-quoted strings so phrases
* mentioned inside a commit message or echoed argument do not trigger
* the destructive detector. Command substitutions are scanned separately
* before this runs because they execute even inside double quotes.
*
* @param {string} input
* @returns {string}
*/
function stripQuotedStrings(input) {
return input
.replace(/'(?:[^'\\]|\\.)*'/g, "''")
.replace(/"(?:[^"\\]|\\.)*"/g, '""');
}
/**
* Promote subshell delimiters to top-level segment separators so the
* destructive check applies inside `$(...)` and backtick subshells.
* Without this, `echo y | $(rm -rf /tmp)` and ``echo y | `rm -rf /tmp` ``
* slip past the segment splitter because the destructive command lives
* inside a sub-expression. Run iteratively to handle a layer of nesting.
*
* @param {string} input
* @returns {string}
*/
function explodeSubshells(input) {
let out = input;
for (let i = 0; i < 4; i += 1) {
const before = out;
out = out.replace(/\$\(([^()`]*)\)/g, ';$1;');
out = out.replace(/`([^`]*)`/g, ';$1;');
if (out === before) break;
}
return out;
}
/**
* Extract executable command-substitution bodies from a shell line. Single
* quotes are literal, so substitutions inside them are ignored; double quotes
* still permit substitutions, so those bodies are scanned before quoted text
* is stripped.
*
* @param {string} input
* @returns {string[]}
*/
function extractCommandSubstitutions(input) {
const source = String(input || '');
const substitutions = [];
let inSingle = false;
let inDouble = false;
for (let i = 0; i < source.length; i++) {
const ch = source[i];
const prev = source[i - 1];
if (ch === '\\' && !inSingle) {
i += 1;
continue;
}
if (ch === "'" && !inDouble && prev !== '\\') {
inSingle = !inSingle;
continue;
}
if (ch === '"' && !inSingle && prev !== '\\') {
inDouble = !inDouble;
continue;
}
if (inSingle) {
continue;
}
if (ch === '`') {
let body = '';
i += 1;
while (i < source.length) {
const inner = source[i];
if (inner === '\\') {
body += inner;
if (i + 1 < source.length) {
body += source[i + 1];
i += 2;
continue;
}
}
if (inner === '`') {
break;
}
body += inner;
i += 1;
}
if (body.trim()) {
substitutions.push(body);
substitutions.push(...extractCommandSubstitutions(body));
}
continue;
}
if (ch === '$' && source[i + 1] === '(') {
let depth = 1;
let body = '';
i += 2;
while (i < source.length && depth > 0) {
const inner = source[i];
if (inner === '\\') {
body += inner;
if (i + 1 < source.length) {
body += source[i + 1];
i += 2;
continue;
}
}
if (inner === '(') {
depth += 1;
} else if (inner === ')') {
depth -= 1;
if (depth === 0) {
break;
}
}
body += inner;
i += 1;
}
if (body.trim()) {
substitutions.push(body);
substitutions.push(...extractCommandSubstitutions(body));
}
}
}
return substitutions;
}
/**
* Split a command line into top-level segments at unquoted shell
* separators (`;`, `|`, `&`, `&&`, `||`) and across subshells
* (`$(...)` / backticks). Quoted strings are stripped first so
* separators inside quotes are not split on. Per-segment comments
* are also stripped.
*
* @param {string} input
* @returns {string[]}
*/
function splitCommandSegments(input) {
const stripped = explodeSubshells(stripQuotedStrings(input));
return stripped
.split(/[;|&]+/)
.map(segment => segment.replace(/(^|\s)#.*/, '$1').trim())
.filter(Boolean);
}
/**
* Tokenize a single command segment by whitespace. Quoted strings
* are already collapsed to empty quotes by `stripQuotedStrings`, so
* naive whitespace splitting is sufficient.
*
* @param {string} segment
* @returns {string[]}
*/
function tokenize(segment) {
return segment.split(/\s+/).filter(Boolean);
}
/**
* Strip a leading path and trailing `.exe` from a command token so
* `/usr/bin/git`, `git.exe`, and `GIT` all normalize to `git`.
*
* @param {string} token
* @returns {string}
*/
function commandBasename(token) {
if (!token) return '';
return token.replace(/^.*[\\/]/, '').replace(/\.exe$/i, '').toLowerCase();
}
/**
* Detect `rm` invocations that recursively force-delete files. Handles
* combined (`-rf`, `-fr`, `-Rf`) and split (`-r -f`) flag forms.
*
* @param {string[]} tokens
* @returns {boolean}
*/
function isDestructiveRm(tokens) {
if (tokens.length === 0 || commandBasename(tokens[0]) !== 'rm') return false;
let hasR = false;
let hasF = false;
for (const t of tokens.slice(1)) {
if (t === '--recursive') {
hasR = true;
continue;
}
if (t === '--force') {
hasF = true;
continue;
}
if (!t.startsWith('-') || t.startsWith('--')) continue;
const body = t.slice(1);
if (/[rR]/.test(body)) hasR = true;
if (/f/.test(body)) hasF = true;
}
return hasR && hasF;
}
/**
* Locate the git subcommand within a token list, skipping over git's
* global options like `-c key=value`, `-C <path>`, `--git-dir=...`,
* `--work-tree=...`, `--namespace=...`, `--super-prefix=...`.
*
* @param {string[]} tokens
* @returns {{ command: string, rest: string[] } | null}
*/
function findGitSubcommand(tokens) {
if (tokens.length === 0 || commandBasename(tokens[0]) !== 'git') return null;
const valueConsumingShort = new Set(['-c', '-C']);
const valueConsumingLong = new Set(['--git-dir', '--work-tree', '--namespace', '--super-prefix']);
let i = 1;
while (i < tokens.length) {
const t = tokens[i];
if (valueConsumingShort.has(t) || valueConsumingLong.has(t)) {
i += 2;
continue;
}
if (t.startsWith('--git-dir=') || t.startsWith('--work-tree=') || t.startsWith('--namespace=') || t.startsWith('--super-prefix=')) {
i += 1;
continue;
}
if (t.startsWith('-')) {
// Unknown global option — skip without consuming a value.
i += 1;
continue;
}
return { command: t.toLowerCase(), rest: tokens.slice(i + 1) };
}
return null;
}
/**
* Detect destructive `git` invocations: `reset --hard`, `checkout --`,
* `clean -f...`, `push --force` (but not `--force-with-lease`),
* `commit --amend`, `rm -rf`.
*
* @param {string[]} tokens
* @returns {boolean}
*/
function isDestructiveGit(tokens) {
const sub = findGitSubcommand(tokens);
if (!sub) return false;
const { command, rest } = sub;
if (command === 'reset') {
return rest.includes('--hard');
}
if (command === 'checkout') {
return rest.includes('--');
}
if (command === 'clean') {
// `git clean -f`, `-fd`, `-fdx`, `-df`, `--force`
return rest.some(t => {
if (t === '--force') return true;
if (!t.startsWith('-') || t.startsWith('--')) return false;
return t.slice(1).includes('f');
});
}
if (command === 'push') {
// Only `--force-with-lease` qualifies as a safety-checked force.
// `--force-if-includes` is a no-op when used WITHOUT
// `--force-with-lease` (per git-scm.com/docs/git-push), and when
// combined with a bare `--force` the bare force is still in effect.
// So `--force --force-if-includes` must be treated as destructive.
//
// A `+` refspec prefix (e.g. `git push origin +main`,
// `+refs/heads/main:refs/heads/main`) also forces a non-fast-forward
// update of that ref and is destructive on its own.
let withLease = false;
let bareForce = false;
let plusRefspecForce = false;
for (const t of rest) {
if (t === '--force-with-lease' || t.startsWith('--force-with-lease=')) {
withLease = true;
continue;
}
if (t === '--force' || t.startsWith('--force=')) {
bareForce = true;
continue;
}
if (t.startsWith('-') && !t.startsWith('--') && t.slice(1).includes('f')) {
bareForce = true;
continue;
}
// Refspec prefix: `+<src>[:<dst>]`. Match tokens like `+main`,
// `+refs/heads/main`, `+HEAD:branch`, `+:branch`. Exclude bare
// `+` and numeric-only `+123` which are not refspecs.
if (t.startsWith('+') && t.length > 1 && /^\+(?:[a-zA-Z_/.:]|HEAD)/.test(t)) {
plusRefspecForce = true;
}
}
return bareForce || (plusRefspecForce && !withLease);
}
if (command === 'commit') {
return rest.includes('--amend');
}
if (command === 'rm') {
// `git rm -r` / `-rf` / `-r -f` — destructive within the index too.
let hasR = false;
for (const t of rest) {
if (!t.startsWith('-') || t.startsWith('--')) continue;
if (/[rR]/.test(t.slice(1))) hasR = true;
}
return hasR;
}
if (command === 'switch') {
// `git switch` can discard local working-tree changes in three forms:
// --discard-changes explicit discard
// --force / -f ignore conflicts and overwrite
// -C <branch> force-create (overwrites existing branch)
return rest.some(t => {
if (t === '--discard-changes' || t === '--force') return true;
if (!t.startsWith('-') || t.startsWith('--')) return false;
// Short combined form: -f, -fC, -Cf, -C
const body = t.slice(1);
return /[fC]/.test(body);
});
}
return false;
}
/**
* Decide whether a bash command line contains a destructive action
* the fact-forcing gate should challenge. Combines SQL-keyword
* detection (regex on quote-stripped input) with per-segment shell
* tokenization for shell commands.
*
* @param {string} command
* @returns {boolean}
*/
function isDestructiveBash(command) {
// The SQL/dd phrases live in command bodies, not as flag-bearing
// arguments, so we still match them by regex — but on the input
// after quoting AND subshell delimiters are normalized so phrases
// inside `$(...)` or backticks are also caught.
const raw = String(command || '');
const flattened = explodeSubshells(stripQuotedStrings(raw));
if (DESTRUCTIVE_SQL_DD.test(flattened)) return true;
const segments = [raw, ...extractCommandSubstitutions(raw)].flatMap(splitCommandSegments);
for (const segment of segments) {
if (DESTRUCTIVE_SQL_DD.test(stripQuotedStrings(segment))) return true;
const tokens = tokenize(segment);
if (isDestructiveRm(tokens)) return true;
if (isDestructiveGit(tokens)) return true;
}
return false;
}
// --- State management (per-session, atomic writes, bounded) ---
@@ -483,7 +850,7 @@ function run(rawInput) {
return rawInput;
}
if (DESTRUCTIVE_BASH.test(command)) {
if (isDestructiveBash(command)) {
// Gate destructive commands on first attempt; allow retry after facts presented
const key = '__destructive__' + crypto.createHash('sha256').update(command).digest('hex').slice(0, 16);
if (!isChecked(key)) {

View File

@@ -4,6 +4,10 @@
const MAX_STDIN = 1024 * 1024;
const path = require('path');
const { splitShellSegments } = require('../lib/shell-split');
const {
extractCommandSubstitutions,
extractSubshellGroups
} = require('../lib/shell-substitution');
const DEV_COMMAND_WORDS = new Set([
'npm',
@@ -123,6 +127,8 @@ function getLeadingCommandWord(segment) {
continue;
}
if (token === '{' || token === '}') continue;
if (/^[A-Za-z_][A-Za-z0-9_]*=.*/.test(token)) continue;
const normalizedToken = normalizeCommandWord(token);
@@ -154,23 +160,55 @@ process.stdin.on('data', chunk => {
}
});
const TMUX_LAUNCHER = /^\s*tmux\s+(new|new-session|new-window|split-window)\b/;
const DEV_PATTERN = /\b(npm\s+run\s+dev|pnpm(?:\s+run)?\s+dev|yarn(?:\s+run)?\s+dev|bun(?:\s+run)?\s+dev)\b/;
/**
* Collect every command-line segment we should evaluate. Returns the top-level
* segments first, then segments harvested from `$(...)` / backtick command
* substitutions and plain `(...)` subshell groups, recursively.
*
* Without this expansion the leading-command and dev-pattern check below only
* sees the outermost command, so wrappers like `$(npm run dev)` and
* `(npm run dev)` (which still spawn a dev server) sneak past.
*/
function collectCheckSegments(cmd) {
const segments = [...splitShellSegments(cmd)];
const queue = [cmd];
const seen = new Set();
while (queue.length) {
const current = queue.shift();
if (seen.has(current)) continue;
seen.add(current);
for (const body of extractCommandSubstitutions(current)) {
for (const seg of splitShellSegments(body)) segments.push(seg);
queue.push(body);
}
for (const body of extractSubshellGroups(current)) {
for (const seg of splitShellSegments(body)) segments.push(seg);
queue.push(body);
}
}
return segments;
}
function isBlockedDevSegment(segment) {
const commandWord = getLeadingCommandWord(segment);
if (!commandWord || !DEV_COMMAND_WORDS.has(commandWord)) return false;
return DEV_PATTERN.test(segment) && !TMUX_LAUNCHER.test(segment);
}
process.stdin.on('end', () => {
try {
const input = JSON.parse(raw);
const cmd = String(input.tool_input?.command || '');
if (process.platform !== 'win32') {
const segments = splitShellSegments(cmd);
const tmuxLauncher = /^\s*tmux\s+(new|new-session|new-window|split-window)\b/;
const devPattern = /\b(npm\s+run\s+dev|pnpm(?:\s+run)?\s+dev|yarn\s+dev|bun\s+run\s+dev)\b/;
const hasBlockedDev = segments.some(segment => {
const commandWord = getLeadingCommandWord(segment);
if (!commandWord || !DEV_COMMAND_WORDS.has(commandWord)) {
return false;
}
return devPattern.test(segment) && !tmuxLauncher.test(segment);
});
const segments = collectCheckSegments(cmd);
const hasBlockedDev = segments.some(isBlockedDevSegment);
if (hasBlockedDev) {
console.error('[Hook] BLOCKED: Dev server must run in tmux for log access');

View File

@@ -51,6 +51,8 @@ const LEGACY_LANGUAGE_ALIAS_TO_CANONICAL = Object.freeze({
perl: 'perl',
php: 'php',
python: 'python',
rails: 'ruby',
ruby: 'ruby',
rust: 'rust',
swift: 'swift',
typescript: 'typescript',
@@ -66,6 +68,7 @@ const LEGACY_LANGUAGE_EXTRA_MODULE_IDS = Object.freeze({
perl: [],
php: [],
python: ['framework-language'],
ruby: ['framework-language', 'security'],
rust: ['framework-language'],
swift: [],
typescript: ['framework-language'],

View File

@@ -0,0 +1,246 @@
'use strict';
/**
* Extract executable command-substitution bodies from a shell line.
*
* Single quotes are literal, so substitutions inside them are ignored;
* double quotes still permit substitutions, so those bodies are scanned
* before quoted text is stripped. Returns each substitution body plus
* any nested substitutions discovered recursively.
*
* Originally introduced in scripts/hooks/gateguard-fact-force.js
* (PR #1853 round 2). Extracted to a shared lib so other PreToolUse
* hooks that need the same "scan inside `$(...)` and backticks"
* behavior can reuse it without duplicating the parser.
*
* @param {string} input
* @returns {string[]}
*/
function extractCommandSubstitutions(input) {
const source = String(input || '');
const substitutions = [];
let inSingle = false;
let inDouble = false;
for (let i = 0; i < source.length; i++) {
const ch = source[i];
const prev = source[i - 1];
if (ch === '\\' && !inSingle) {
i += 1;
continue;
}
if (ch === "'" && !inDouble && prev !== '\\') {
inSingle = !inSingle;
continue;
}
if (ch === '"' && !inSingle && prev !== '\\') {
inDouble = !inDouble;
continue;
}
if (inSingle) {
continue;
}
if (ch === '`') {
let body = '';
i += 1;
while (i < source.length) {
const inner = source[i];
if (inner === '\\') {
body += inner;
if (i + 1 < source.length) {
body += source[i + 1];
i += 2;
continue;
}
}
if (inner === '`') {
break;
}
body += inner;
i += 1;
}
if (body.trim()) {
substitutions.push(body);
substitutions.push(...extractCommandSubstitutions(body));
}
continue;
}
if (ch === '$' && source[i + 1] === '(') {
let depth = 1;
let body = '';
let bodyInSingle = false;
let bodyInDouble = false;
i += 2;
while (i < source.length && depth > 0) {
const inner = source[i];
const innerPrev = source[i - 1];
if (inner === '\\' && !bodyInSingle) {
body += inner;
if (i + 1 < source.length) {
body += source[i + 1];
i += 2;
continue;
}
}
if (inner === "'" && !bodyInDouble && innerPrev !== '\\') {
bodyInSingle = !bodyInSingle;
} else if (inner === '"' && !bodyInSingle && innerPrev !== '\\') {
bodyInDouble = !bodyInDouble;
} else if (!bodyInSingle && !bodyInDouble) {
if (inner === '(') {
depth += 1;
} else if (inner === ')') {
depth -= 1;
if (depth === 0) {
break;
}
}
}
body += inner;
i += 1;
}
if (body.trim()) {
substitutions.push(body);
substitutions.push(...extractCommandSubstitutions(body));
}
}
}
return substitutions;
}
/**
* Extract bodies of plain `(...)` subshell groups.
*
* Bash treats `(npm run dev)` as a subshell that executes its contents, but
* the regex-light segment splitters used by our PreToolUse hooks don't peer
* inside those parens. This helper finds top-level `(...)` groups (skipping
* `$(...)` command substitutions and backticks, which `extractCommandSubstitutions`
* already covers) and returns each body, recursing for nested groups.
*
* Quote semantics:
* - Single quotes are literal: `'( ... )'` is a string, not a subshell.
* - Double quotes are literal *for parens*: `"( ... )"` is a string too —
* bash only honors `$( )` inside double quotes, not bare `( )`.
*
* @param {string} input
* @returns {string[]}
*/
function extractSubshellGroups(input) {
const source = String(input || '');
const groups = [];
let inSingle = false;
let inDouble = false;
for (let i = 0; i < source.length; i++) {
const ch = source[i];
const prev = source[i - 1];
if (ch === '\\' && !inSingle) {
i += 1;
continue;
}
if (ch === "'" && !inDouble && prev !== '\\') {
inSingle = !inSingle;
continue;
}
if (ch === '"' && !inSingle && prev !== '\\') {
inDouble = !inDouble;
continue;
}
if (inSingle || inDouble) {
continue;
}
if (ch === '$' && source[i + 1] === '(') {
let depth = 1;
let skipInSingle = false;
let skipInDouble = false;
i += 2;
while (i < source.length && depth > 0) {
const inner = source[i];
const innerPrev = source[i - 1];
if (inner === '\\' && !skipInSingle) {
i += 2;
continue;
}
if (inner === "'" && !skipInDouble && innerPrev !== '\\') {
skipInSingle = !skipInSingle;
} else if (inner === '"' && !skipInSingle && innerPrev !== '\\') {
skipInDouble = !skipInDouble;
} else if (!skipInSingle && !skipInDouble) {
if (inner === '(') depth += 1;
else if (inner === ')') depth -= 1;
}
i += 1;
}
i -= 1;
continue;
}
if (ch === '`') {
i += 1;
while (i < source.length && source[i] !== '`') {
if (source[i] === '\\' && i + 1 < source.length) {
i += 2;
continue;
}
i += 1;
}
continue;
}
if (ch === '(') {
let depth = 1;
let body = '';
let bodyInSingle = false;
let bodyInDouble = false;
i += 1;
while (i < source.length && depth > 0) {
const inner = source[i];
const innerPrev = source[i - 1];
if (inner === '\\' && !bodyInSingle) {
body += inner;
if (i + 1 < source.length) {
body += source[i + 1];
i += 2;
continue;
}
}
if (inner === "'" && !bodyInDouble && innerPrev !== '\\') {
bodyInSingle = !bodyInSingle;
} else if (inner === '"' && !bodyInSingle && innerPrev !== '\\') {
bodyInDouble = !bodyInDouble;
} else if (!bodyInSingle && !bodyInDouble) {
if (inner === '(') {
depth += 1;
} else if (inner === ')') {
depth -= 1;
if (depth === 0) {
break;
}
}
}
body += inner;
i += 1;
}
if (body.trim()) {
groups.push(body);
groups.push(...extractSubshellGroups(body));
}
}
}
return groups;
}
module.exports = { extractCommandSubstitutions, extractSubshellGroups };

View File

@@ -124,6 +124,16 @@ function buildChecks(rootDir) {
const sessionManagerRust = readText(rootDir, 'ecc2/src/session/manager.rs');
const readinessDoc = readText(rootDir, 'docs/architecture/observability-readiness.md');
const hudStatusContract = readText(rootDir, 'docs/architecture/hud-status-session-control.md');
const progressSyncContract = readText(rootDir, 'docs/architecture/progress-sync-contract.md');
const gaRoadmap = readText(rootDir, 'docs/ECC-2.0-GA-ROADMAP.md');
const workItems = readText(rootDir, 'scripts/work-items.js');
const publicationReadiness = readText(rootDir, 'docs/releases/2.0.0-rc.1/publication-readiness.md');
const postHardeningEvidence = readText(rootDir, 'docs/releases/2.0.0-rc.1/publication-evidence-2026-05-13-post-hardening.md');
const supplyChainIncidentResponse = readText(rootDir, 'docs/security/supply-chain-incident-response.md');
const workflowSecurityValidator = readText(rootDir, 'scripts/ci/validate-workflow-security.js');
const workflowSecurityValidatorTests = readText(rootDir, 'tests/ci/validate-workflow-security.test.js');
const publishSurfaceTest = readText(rootDir, 'tests/scripts/npm-publish-surface.test.js');
const releaseSurfaceTest = readText(rootDir, 'tests/docs/ecc2-release-surface.test.js');
const hudStatusFixture = safeParseJson(readText(rootDir, 'examples/hud-status-contract.json')) || {};
const quickstart = readText(rootDir, 'docs/releases/2.0.0-rc.1/quickstart.md');
const releaseNotes = readText(rootDir, 'docs/releases/2.0.0-rc.1/release-notes.md');
@@ -238,6 +248,90 @@ function buildChecks(rootDir) {
&& releaseNotes.includes('observability-readiness.md'),
fix: 'Add the observability readiness doc and link it from rc.1 release docs.'
},
{
id: 'progress-sync-contract',
category: 'Tracker Sync',
points: 2,
path: 'docs/architecture/progress-sync-contract.md',
description: 'Linear, GitHub, handoff, and roadmap progress sync has an evidence-backed contract',
pass: fileExists(rootDir, 'docs/architecture/progress-sync-contract.md')
&& includesAll(progressSyncContract, [
'Linear',
'GitHub',
'handoff',
'work-items',
'issue capacity',
'status update',
'queue counts',
'release gate',
'flow lanes',
'evidence'
])
&& includesAll(gaRoadmap, [
'Execution Lanes And Tracking Contract',
'docs/architecture/progress-sync-contract.md',
'Linear progress',
'Every significant merge batch'
])
&& includesAll(workItems, [
'sync-github',
'github-pr',
'github-issue',
'sourceClosedAt',
'ecc-work-items-sync-github'
]),
fix: 'Add the progress sync contract, link it from the GA roadmap, and preserve work-items GitHub sync.'
},
{
id: 'release-safety-evidence',
category: 'Release Safety',
points: 3,
path: 'docs/releases/2.0.0-rc.1/publication-readiness.md',
description: 'Release readiness includes package, workflow, and supply-chain evidence before publication',
pass: fileExists(rootDir, 'docs/releases/2.0.0-rc.1/publication-readiness.md')
&& fileExists(rootDir, 'docs/releases/2.0.0-rc.1/publication-evidence-2026-05-13-post-hardening.md')
&& fileExists(rootDir, 'docs/security/supply-chain-incident-response.md')
&& fileExists(rootDir, 'scripts/ci/validate-workflow-security.js')
&& fileExists(rootDir, 'tests/ci/validate-workflow-security.test.js')
&& fileExists(rootDir, 'tests/scripts/npm-publish-surface.test.js')
&& fileExists(rootDir, 'tests/docs/ecc2-release-surface.test.js')
&& includesAll(publicationReadiness, [
'Publication Gates',
'Required Command Evidence',
'Do Not Publish If',
'npm dist-tag',
'GitGuardian',
'Dependabot alerts',
'npm audit signatures'
])
&& includesAll(postHardeningEvidence, [
'npm audit --json',
'npm audit signatures',
'cargo audit',
'Dependabot alert API',
'TanStack',
'Mini Shai-Hulud',
'GitGuardian Security Checks'
])
&& includesAll(supplyChainIncidentResponse, [
'TanStack',
'Mini Shai-Hulud',
'npm audit signatures',
'trusted publishing',
'pull_request_target',
'id-token: write'
])
&& includesAll(workflowSecurityValidator, [
'persist-credentials: false',
'npm audit signatures',
'pull_request_target',
'id-token: write'
])
&& includesAll(workflowSecurityValidatorTests, ['npm audit signatures', 'persist-credentials: false'])
&& includesAll(publishSurfaceTest, ['npm pack', 'Python bytecode'])
&& includesAll(releaseSurfaceTest, ['publication-readiness.md']),
fix: 'Refresh publication readiness, post-hardening evidence, supply-chain response docs, workflow-security validator coverage, and package/release surface tests.'
},
{
id: 'package-exposes-readiness-gate',
category: 'Packaging',

View File

@@ -82,6 +82,27 @@ def _normalize_remote_url(remote_url: str) -> str:
return normalized.lower() if is_network else normalized
def _stream_can_encode(text: str, stream=None) -> bool:
stream = stream or sys.stdout
encoding = getattr(stream, "encoding", None) or sys.getdefaultencoding()
try:
text.encode(encoding)
except (LookupError, UnicodeEncodeError):
return False
return True
def _confidence_bar(confidence, stream=None) -> str:
try:
filled = int(float(confidence) * 10)
except (TypeError, ValueError):
filled = 5
filled = max(0, min(10, filled))
full, empty = ("\u2588", "\u2591") if _stream_can_encode("\u2588\u2591", stream) else ("#", ".")
return full * filled + empty * (10 - filled)
def _project_hash(value: str) -> str:
return hashlib.sha256(value.encode("utf-8")).hexdigest()[:12]
@@ -550,7 +571,7 @@ def _print_instincts_by_domain(instincts: list[dict]) -> None:
for inst in sorted(domain_instincts, key=lambda x: -x.get('confidence', 0.5)):
conf = inst.get('confidence', 0.5)
conf_bar = '\u2588' * int(conf * 10) + '\u2591' * (10 - int(conf * 10))
conf_bar = _confidence_bar(conf)
trigger = inst.get('trigger', 'unknown trigger')
scope_tag = f"[{inst.get('scope', '?')}]"

View File

@@ -45,6 +45,7 @@ _find_cross_project_instincts = _mod._find_cross_project_instincts
load_registry = _mod.load_registry
_validate_instinct_id = _mod._validate_instinct_id
_update_registry = _mod._update_registry
_confidence_bar = _mod._confidence_bar
# ─────────────────────────────────────────────
@@ -642,6 +643,39 @@ def test_cmd_status_with_instincts(patch_globals, monkeypatch, capsys):
assert "GLOBAL" in out
def test_confidence_bar_uses_unicode_when_supported():
"""Confidence bars should retain block glyphs on UTF-8 streams."""
stream = SimpleNamespace(encoding="utf-8")
assert _confidence_bar(0.8, stream=stream) == "\u2588" * 8 + "\u2591" * 2
def test_confidence_bar_uses_ascii_when_stream_rejects_block_glyphs():
"""Windows cp1252 streams cannot encode block glyphs."""
stream = SimpleNamespace(encoding="cp1252")
assert _confidence_bar(0.8, stream=stream) == "########.."
def test_print_instincts_by_domain_is_cp1252_safe(monkeypatch):
"""Status rendering should not crash on Windows cp1252 stdout."""
raw = io.BytesIO()
stream = io.TextIOWrapper(raw, encoding="cp1252")
monkeypatch.setattr(_mod.sys, "stdout", stream)
_mod._print_instincts_by_domain([{
"id": "windows-safe",
"trigger": "when stdout uses cp1252",
"confidence": 0.8,
"domain": "platform",
"scope": "project",
}])
stream.flush()
out = raw.getvalue().decode("cp1252")
assert "########.." in out
assert "\u2588" not in out
assert "\u2591" not in out
def test_cmd_status_returns_int(patch_globals, monkeypatch):
"""cmd_status should always return an int."""
tree = patch_globals

View File

@@ -122,6 +122,21 @@ function run() {
assert.strictEqual(result.status, 0, result.stderr || result.stdout);
})) passed++; else failed++;
if (test('rejects checkout credential persistence in workflows with write permissions', () => {
const result = runValidator({
'unsafe-write-checkout.yml': `name: Unsafe\non:\n workflow_dispatch:\npermissions:\n contents: write\njobs:\n release:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n - run: npm ci --ignore-scripts\n`,
});
assert.notStrictEqual(result.status, 0, 'Expected validator to fail on credential-persisting checkout');
assert.match(result.stderr, /write permissions must disable checkout credential persistence/);
})) passed++; else failed++;
if (test('allows checkout with disabled credential persistence in workflows with write permissions', () => {
const result = runValidator({
'safe-write-checkout.yml': `name: Safe\non:\n workflow_dispatch:\npermissions:\n contents: write\njobs:\n release:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n with:\n persist-credentials: false\n - run: npm ci --ignore-scripts\n`,
});
assert.strictEqual(result.status, 0, result.stderr || result.stdout);
})) passed++; else failed++;
if (test('rejects actions/cache in workflows with id-token write', () => {
const result = runValidator({
'unsafe-oidc-cache.yml': `name: Unsafe\non:\n push:\npermissions:\n contents: read\n id-token: write\njobs:\n release:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/cache@v5\n with:\n path: ~/.npm\n key: cache\n`,

View File

@@ -1143,6 +1143,145 @@ function runTests() {
'second subagent edit should pass even on a new file');
})) passed++; else failed++;
// --- Shell-words tokenizer: bypasses the old regex missed ---
function expectDestructiveDeny(command, label) {
clearState();
const input = { tool_name: 'Bash', tool_input: { command } };
const result = runBashHook(input);
assert.strictEqual(result.code, 0, `${label}: exit code should be 0`);
const output = parseOutput(result.stdout);
assert.ok(output, `${label}: should produce JSON output`);
assert.strictEqual(output.hookSpecificOutput.permissionDecision, 'deny', `${label}: should deny`);
assert.ok(output.hookSpecificOutput.permissionDecisionReason.includes('Destructive'),
`${label}: reason should mention "Destructive"`);
}
function expectAllow(command, label) {
clearState();
writeState({ checked: ['__bash_session__'], last_active: Date.now() });
const input = { tool_name: 'Bash', tool_input: { command } };
const result = runBashHook(input);
assert.strictEqual(result.code, 0, `${label}: exit code should be 0`);
const output = parseOutput(result.stdout);
assert.ok(output, `${label}: should produce JSON output`);
if (output.hookSpecificOutput) {
assert.notStrictEqual(output.hookSpecificOutput.permissionDecision, 'deny', `${label}: should not deny`);
} else {
assert.strictEqual(output.tool_name, 'Bash', `${label}: pass-through should preserve input`);
}
}
if (test('denies short-form git push -f as destructive', () => {
expectDestructiveDeny('git push -f origin main', 'git push -f');
})) passed++; else failed++;
if (test('denies git reset --hard even with intervening -c global option', () => {
expectDestructiveDeny('git -c core.foo=bar reset --hard', 'git -c ... reset --hard');
})) passed++; else failed++;
if (test('denies rm -fr (reverse flag order)', () => {
expectDestructiveDeny('rm -fr /tmp/junk', 'rm -fr');
})) passed++; else failed++;
if (test('denies rm -r -f (split flag form)', () => {
expectDestructiveDeny('rm -r -f /tmp/junk', 'rm -r -f');
})) passed++; else failed++;
if (test('denies rm --recursive --force (long flag form)', () => {
expectDestructiveDeny('rm --recursive --force /tmp/junk', 'rm --recursive --force');
})) passed++; else failed++;
if (test('denies git reset HEAD --hard (with intervening ref)', () => {
expectDestructiveDeny('git reset HEAD --hard', 'git reset HEAD --hard');
})) passed++; else failed++;
if (test('denies git clean -fd (combined force+dirs flag)', () => {
expectDestructiveDeny('git clean -fd', 'git clean -fd');
})) passed++; else failed++;
if (test('denies destructive command in second chained segment', () => {
expectDestructiveDeny('echo y | rm -rf /tmp/junk', 'echo y | rm -rf');
})) passed++; else failed++;
if (test('denies destructive command inside command substitution', () => {
expectDestructiveDeny('echo $(rm -rf /tmp/junk)', 'rm -rf inside $()');
})) passed++; else failed++;
if (test('denies destructive command inside backticks', () => {
expectDestructiveDeny('echo `git push -f origin main`', 'git push -f inside backticks');
})) passed++; else failed++;
if (test('allows destructive phrase quoted inside a commit message', () => {
expectAllow('git commit -m "fix: rm -rf race in worker"', 'rm -rf in -m');
})) passed++; else failed++;
if (test('allows SQL phrase quoted inside a commit message', () => {
expectAllow('git commit -m "docs: explain when drop table is safe"', 'drop table in -m');
})) passed++; else failed++;
if (test('allows git push --force-if-includes as a safety-checked variant', () => {
expectAllow('git push --force-with-lease --force-if-includes origin main',
'git push --force-if-includes');
})) passed++; else failed++;
// --- Review-round-2 findings ---
if (test('denies git push --force even with --force-if-includes present', () => {
expectDestructiveDeny('git push --force --force-if-includes origin main',
'git push --force --force-if-includes');
})) passed++; else failed++;
if (test('denies git push when bare --force is mixed with lease flags', () => {
expectDestructiveDeny('git push --force-with-lease --force origin main',
'git push --force-with-lease --force');
})) passed++; else failed++;
if (test('denies git push with +refspec prefix (bare branch)', () => {
expectDestructiveDeny('git push origin +main', 'git push origin +main');
})) passed++; else failed++;
if (test('denies git push with +refspec prefix (full ref)', () => {
expectDestructiveDeny('git push origin +refs/heads/main:refs/heads/main',
'git push origin +refs/heads/main:refs/heads/main');
})) passed++; else failed++;
if (test('denies git switch --discard-changes', () => {
expectDestructiveDeny('git switch --discard-changes feature',
'git switch --discard-changes');
})) passed++; else failed++;
if (test('denies git switch --force', () => {
expectDestructiveDeny('git switch --force main', 'git switch --force');
})) passed++; else failed++;
if (test('denies git switch -f short form', () => {
expectDestructiveDeny('git switch -f main', 'git switch -f');
})) passed++; else failed++;
if (test('denies git switch -C force-create', () => {
expectDestructiveDeny('git switch -C feature', 'git switch -C');
})) passed++; else failed++;
if (test('still allows plain git switch', () => {
expectAllow('git switch feature', 'git switch feature');
})) passed++; else failed++;
if (test('denies rm -rf nested inside a backtick subshell', () => {
expectDestructiveDeny('echo y | `rm -rf /tmp/junk`',
'backtick subshell');
})) passed++; else failed++;
if (test('denies rm -rf nested inside a $(...) subshell', () => {
expectDestructiveDeny('echo y | $(rm -rf /tmp/junk)',
'dollar-paren subshell');
})) passed++; else failed++;
if (test('denies rm -rf inside double-quoted command substitution', () => {
expectDestructiveDeny('echo "$(rm -rf /tmp/junk)"',
'double-quoted dollar-paren subshell');
})) passed++; else failed++;
// Cleanup only the temp directory created by this test file.
try {
if (fs.existsSync(stateDir)) {

View File

@@ -89,6 +89,110 @@ function runTests() {
assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`);
}) ? passed++ : failed++);
// --- Subshell bypass regression (issue: dev server slipped past via $(), ``, ()) ---
if (!isWindows) {
(test('blocks $(npm run dev) — command substitution', () => {
const result = runScript('$(npm run dev)');
assert.strictEqual(result.code, 2, `Expected exit code 2, got ${result.code}`);
assert.ok(result.stderr.includes('BLOCKED'), 'expected BLOCKED in stderr');
}) ? passed++ : failed++);
(test('blocks `npm run dev` — backtick substitution', () => {
const result = runScript('`npm run dev`');
assert.strictEqual(result.code, 2, `Expected exit code 2, got ${result.code}`);
}) ? passed++ : failed++);
(test('blocks echo $(npm run dev) — substitution nested in argument', () => {
const result = runScript('echo $(npm run dev)');
assert.strictEqual(result.code, 2, `Expected exit code 2, got ${result.code}`);
}) ? passed++ : failed++);
(test('blocks (npm run dev) — plain subshell group', () => {
const result = runScript('(npm run dev)');
assert.strictEqual(result.code, 2, `Expected exit code 2, got ${result.code}`);
}) ? passed++ : failed++);
(test('blocks $(echo a; npm run dev) — substitution with sequenced segments', () => {
const result = runScript('$(echo a; npm run dev)');
assert.strictEqual(result.code, 2, `Expected exit code 2, got ${result.code}`);
}) ? passed++ : failed++);
(test('blocks (pnpm dev) — plain subshell group with pnpm', () => {
const result = runScript('(pnpm dev)');
assert.strictEqual(result.code, 2, `Expected exit code 2, got ${result.code}`);
}) ? passed++ : failed++);
(test('allows tmux launcher inside subshell wrapping (exit code 0)', () => {
const result = runScript('(tmux new-session -d -s dev "npm run dev")');
assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`);
}) ? passed++ : failed++);
(test('allows single-quoted "(npm run dev)" — literal string, not a subshell', () => {
const result = runScript("git commit -m '(npm run dev)'");
assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`);
}) ? passed++ : failed++);
(test('allows double-quoted "(npm run dev)" — literal in double quotes (bash does not subshell)', () => {
const result = runScript('echo "(npm run dev)"');
assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`);
}) ? passed++ : failed++);
(test("allows single-quoted '$(npm run dev)' — literal string, no substitution", () => {
const result = runScript("git commit -m '$(npm run dev) fix'");
assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`);
}) ? passed++ : failed++);
}
// --- Round 1 review fixes (Greptile + CodeRabbit on PR #1889) ---
if (!isWindows) {
(test('blocks $(echo ")"; (npm run dev)) — quoted ) does not terminate $() early', () => {
const result = runScript('$(echo ")"; (npm run dev))');
assert.strictEqual(result.code, 2, `Expected exit code 2, got ${result.code}`);
}) ? passed++ : failed++);
(test('blocks (echo ")"; npm run dev) — quoted ) does not terminate (...) early', () => {
const result = runScript('(echo ")"; npm run dev)');
assert.strictEqual(result.code, 2, `Expected exit code 2, got ${result.code}`);
}) ? passed++ : failed++);
(test('allows $(echo "(npm run dev)") — () inside double-quoted substitution body is literal', () => {
const result = runScript('$(echo "(npm run dev)")');
assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`);
}) ? passed++ : failed++);
(test('blocks { npm run dev; } — brace group runs in current shell', () => {
const result = runScript('{ npm run dev; }');
assert.strictEqual(result.code, 2, `Expected exit code 2, got ${result.code}`);
}) ? passed++ : failed++);
(test('blocks echo hi && { npm run dev; } — brace group after &&', () => {
const result = runScript('echo hi && { npm run dev; }');
assert.strictEqual(result.code, 2, `Expected exit code 2, got ${result.code}`);
}) ? passed++ : failed++);
(test('allows {npm run dev} — bash requires space after { to form a group', () => {
const result = runScript('{npm run dev}');
assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`);
}) ? passed++ : failed++);
(test('blocks yarn run dev — yarn 1.x convention', () => {
const result = runScript('yarn run dev');
assert.strictEqual(result.code, 2, `Expected exit code 2, got ${result.code}`);
}) ? passed++ : failed++);
(test('blocks bun dev — bun bare form', () => {
const result = runScript('bun dev');
assert.strictEqual(result.code, 2, `Expected exit code 2, got ${result.code}`);
}) ? passed++ : failed++);
(test('blocks "$(npm run dev)" — double-quoted substitution still substitutes', () => {
const result = runScript('echo "$(npm run dev)"');
assert.strictEqual(result.code, 2, `Expected exit code 2, got ${result.code}`);
}) ? passed++ : failed++);
}
// --- Edge cases ---
(test('empty/invalid input passes through (exit code 0)', () => {

View File

@@ -143,6 +143,8 @@ function runTests() {
const languages = listAvailableLanguages(sourceRoot);
assert.ok(languages.includes('typescript'));
assert.ok(languages.includes('ruby'));
assert.ok(languages.includes('rails'));
assert.ok(languages.includes('zig'));
assert.ok(!languages.includes('common'));
assert.deepStrictEqual([...languages].sort(), languages);

View File

@@ -176,6 +176,8 @@ function runTests() {
assert.ok(languages.includes('golang'));
assert.ok(languages.includes('kotlin'));
assert.ok(languages.includes('rust'));
assert.ok(languages.includes('ruby'));
assert.ok(languages.includes('rails'));
assert.ok(languages.includes('cpp'));
assert.ok(languages.includes('c'));
assert.ok(languages.includes('csharp'));
@@ -432,6 +434,22 @@ function runTests() {
'fsharp should resolve to framework-language module');
})) passed++; else failed++;
if (test('resolves ruby and rails legacy compatibility into framework-language and security modules', () => {
const selection = resolveLegacyCompatibilitySelection({
target: 'cursor',
legacyLanguages: ['ruby', 'rails'],
});
assert.deepStrictEqual(selection.canonicalLegacyLanguages, ['ruby', 'ruby']);
assert.ok(selection.moduleIds.includes('rules-core'));
assert.strictEqual(selection.moduleIds.filter(moduleId => moduleId === 'framework-language').length, 1);
assert.strictEqual(selection.moduleIds.filter(moduleId => moduleId === 'security').length, 1);
assert.ok(selection.moduleIds.includes('framework-language'),
'ruby should resolve to framework-language module');
assert.ok(selection.moduleIds.includes('security'),
'rails alias should add security guidance for Rails apps');
})) passed++; else failed++;
if (test('keeps antigravity legacy compatibility selections target-safe', () => {
const selection = resolveLegacyCompatibilitySelection({
target: 'antigravity',

View File

@@ -236,6 +236,7 @@ function runTests() {
assert.ok(components.some(c => c.id === 'lang:python'), 'Should have lang:python');
assert.ok(components.some(c => c.id === 'lang:go'), 'Should have lang:go');
assert.ok(components.some(c => c.id === 'lang:java'), 'Should have lang:java');
assert.ok(components.some(c => c.id === 'lang:ruby'), 'Should have lang:ruby');
})) passed++; else failed++;
if (test('component catalog includes framework: family entries', () => {
@@ -244,6 +245,7 @@ function runTests() {
assert.ok(components.some(c => c.id === 'framework:nextjs'), 'Should have framework:nextjs');
assert.ok(components.some(c => c.id === 'framework:django'), 'Should have framework:django');
assert.ok(components.some(c => c.id === 'framework:springboot'), 'Should have framework:springboot');
assert.ok(components.some(c => c.id === 'framework:rails'), 'Should have framework:rails');
})) passed++; else failed++;
if (test('component catalog includes capability: family entries', () => {

View File

@@ -57,11 +57,22 @@ function seedMinimalRepo(rootDir, overrides = {}) {
'scripts/session-inspect.js': '--list-adapters --write inspectSessionTarget',
'scripts/lib/session-adapters/registry.js': 'module.exports = {};',
'scripts/harness-audit.js': 'Deterministic harness audit --format overall_score',
'scripts/work-items.js': 'sync-github github-pr github-issue sourceClosedAt ecc-work-items-sync-github',
'scripts/hooks/session-activity-tracker.js': 'tool-usage.jsonl session_id tool_name',
'ecc2/src/observability/mod.rs': 'ToolCallEvent RiskAssessment ToolLogger',
'ecc2/src/session/store.rs': 'insert_tool_log query_tool_logs',
'ecc2/src/session/manager.rs': 'sync_tool_activity_metrics tool-usage.jsonl',
'docs/architecture/observability-readiness.md': 'node scripts/observability-readiness.js --format json',
'docs/architecture/progress-sync-contract.md': [
'Linear GitHub handoff work-items issue capacity status update',
'queue counts release gate flow lanes evidence'
].join('\n'),
'docs/ECC-2.0-GA-ROADMAP.md': [
'Execution Lanes And Tracking Contract',
'docs/architecture/progress-sync-contract.md',
'Linear progress',
'Every significant merge batch'
].join('\n'),
'docs/architecture/hud-status-session-control.md': [
'context toolCalls activeAgents todos checks cost risk queueState',
'create resume status stop diff pr mergeQueue conflictQueue',
@@ -81,7 +92,43 @@ function seedMinimalRepo(rootDir, overrides = {}) {
sync: {}
}, null, 2),
'docs/releases/2.0.0-rc.1/quickstart.md': 'observability-readiness.md',
'docs/releases/2.0.0-rc.1/release-notes.md': 'observability-readiness.md'
'docs/releases/2.0.0-rc.1/release-notes.md': 'observability-readiness.md',
'docs/releases/2.0.0-rc.1/publication-readiness.md': [
'Publication Gates',
'Required Command Evidence',
'Do Not Publish If',
'npm dist-tag',
'GitGuardian',
'Dependabot alerts',
'npm audit signatures'
].join('\n'),
'docs/releases/2.0.0-rc.1/publication-evidence-2026-05-13-post-hardening.md': [
'npm audit --json',
'npm audit signatures',
'cargo audit',
'Dependabot alert API',
'TanStack',
'Mini Shai-Hulud',
'GitGuardian Security Checks'
].join('\n'),
'docs/security/supply-chain-incident-response.md': [
'TanStack',
'Mini Shai-Hulud',
'npm audit signatures',
'trusted publishing',
'pull_request_target',
'id-token: write'
].join('\n'),
'scripts/ci/validate-workflow-security.js': [
'persist-credentials: false',
'npm audit signatures',
'pull_request_target',
'id-token: write',
'shared cache'
].join('\n'),
'tests/ci/validate-workflow-security.test.js': 'npm audit signatures persist-credentials: false',
'tests/scripts/npm-publish-surface.test.js': 'npm pack --dry-run Python bytecode',
'tests/docs/ecc2-release-surface.test.js': 'publication-readiness.md',
};
for (const [relativePath, content] of Object.entries({ ...files, ...overrides })) {
@@ -230,6 +277,40 @@ function runTests() {
}
})) passed++; else failed++;
if (test('missing progress sync contract fails without disturbing core tool checks', () => {
const projectRoot = createTempDir('observability-readiness-sync-fail-');
try {
seedMinimalRepo(projectRoot, {
'docs/architecture/progress-sync-contract.md': null
});
const report = buildReport(projectRoot);
assert.strictEqual(report.ready, false);
assert.ok(report.checks.some(check => check.id === 'progress-sync-contract' && !check.pass));
assert.ok(report.checks.some(check => check.id === 'loop-status-live-signal' && check.pass));
} finally {
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('missing release safety evidence fails without disturbing live status checks', () => {
const projectRoot = createTempDir('observability-readiness-release-safety-fail-');
try {
seedMinimalRepo(projectRoot, {
'docs/releases/2.0.0-rc.1/publication-evidence-2026-05-13-post-hardening.md': 'npm audit --json only'
});
const report = buildReport(projectRoot);
assert.strictEqual(report.ready, false);
assert.ok(report.checks.some(check => check.id === 'release-safety-evidence' && !check.pass));
assert.ok(report.checks.some(check => check.id === 'loop-status-live-signal' && check.pass));
} finally {
cleanup(projectRoot);
}
})) passed++; else failed++;
console.log('\nResults:');
console.log(` Passed: ${passed}`);
console.log(` Failed: ${failed}`);