diff --git a/.changeset/proud-sloths-frolic.md b/.changeset/proud-sloths-frolic.md new file mode 100644 index 000000000..a96ac1a4b --- /dev/null +++ b/.changeset/proud-sloths-frolic.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 3823 +--- +**A merged acknowledgment fragment no longer hard-blocks a later PR that grows the same workflow.** The `guard-no-ack-on-next` job only ever watched the legacy `tests/emitted-drift-ack.json`, on the premise that per-PR fragments cannot conflict. They do not share a file, but they do share a path key space — so a fully-spent fragment on `next` kept owning paths it could no longer gate, and the next PR to touch one of them could declare it neither there nor in its own fragment. The guard now sweeps fully-spent fragments, the duplicate-ack error names both resolutions, and the 45 spent fragments on `next` are removed. (#3078) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b21708949..a868d9de8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -838,6 +838,36 @@ jobs: timeout-minutes: 1 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + # The guard compares each surviving fragment against its copy at the tip of + # `next` BEFORE this push. Depth 2 covers the script's local `HEAD^` fallback; + # at the default depth of 1 even that commit is absent, `git rev-parse HEAD^` + # fails, every fragment reads as brand-new, and the job passes VACUOUSLY — + # exactly how the legacy-file half spent months guarding a file that had not + # existed since #2914 (#3078). + fetch-depth: 2 - - name: Assert tests/emitted-drift-ack.json is absent - run: node scripts/lint-emitted-drift-ack.cjs --guard-next + # `github.event.before` is the authoritative pre-push tip, and `HEAD^` is NOT a + # substitute for it: the default branch allows REBASE merges + # (.github/rulesets/main-protection.json, allowed_merge_methods), so one push can + # carry N commits. With `HEAD^` a 2-commit rebase-merge whose first commit adds a + # fragment would read that fragment as already-present and demand `git rm` on the + # very push that introduced it. Zeros mean the branch was just created: there is no + # pre-push tip, so the guard is given none and falls back to resolving one locally. + - name: Fetch the pre-push tip of next + if: github.event.before != '0000000000000000000000000000000000000000' + env: + BEFORE: ${{ github.event.before }} + run: git fetch --no-tags --depth=1 origin "$BEFORE" + + - name: Assert no spent ack survives on next (legacy file and fragments) + env: + BEFORE: ${{ github.event.before }} + # The sha goes through the environment rather than `${{ }}` interpolation into the + # script body, so nothing from the event can ever be parsed as shell. + run: | + if [ "$BEFORE" = "0000000000000000000000000000000000000000" ]; then + node scripts/lint-emitted-drift-ack.cjs --guard-next + else + node scripts/lint-emitted-drift-ack.cjs --guard-next --base-ref "$BEFORE" + fi diff --git a/CONTEXT.md b/CONTEXT.md index 03551d2d1..662b027da 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -537,7 +537,7 @@ Default-off Capability (`capabilities/live-dom-uat/capability.json`, `role: feat The enforced cross-phase defect register operationalizing GSD's no-defer discipline as a tracked artifact (#1950). Markdown file at `.planning/WINDOWS.md` (project-level, cross-phase) with YAML frontmatter carrying scalar counts (`schema_version`, `open_count`, `waived_count`, `fixed_count`, `total_count`, `last_updated`) for the FAST path the gate reads via jq without parsing JSON, plus a JSON code block as the AUTHORITATIVE entries source; the two cross-check and fail closed on drift. Each entry: `{ id, kind, phase, file, line, description, status, reason, recorded_at, resolved_at }`; kinds are closed (`stub | todo | fixme | skipped-test | lint-warning | unmet-truth | unrun-verify | deviation`); statuses are closed (`open | waived | fixed`). The `broken-windows` Capability (`capabilities/broken-windows/capability.json`) registers one `ship:pre` gate with predicate `artifact-frontmatter-equals WINDOWS.md open_count == 0`; federated config key `workflow.windows_enforce` (default `false` — opt-in enforcement, tracking-only by default so a project can adopt the ledger before turning the gate on). Population is best-effort and never blocks execution: `agents/gsd-executor.md` appends stubs/skipped-tests/unrun-verifies via `gsd_run windows append` after writing SUMMARY.md. Source of truth: `src/broken-windows.cts` → `gsd-core/bin/lib/broken-windows.cjs` (pure `parseLedger`/`renderLedger`/`appendWindow`/`markWaived`/`markFixed` + I/O `cmdWindowsStatus`/`Append`/`Waive`/`MarkFixed`); CLI surface `gsd-tools windows status|append|waive|fixed`. Ship gate enforcement is a `capId == "broken-windows"` named specialization inside `gsd-core/workflows/ship.md` preflight's generic `kind == "gate"` dispatch loop (sibling to the `security` specialization; #3559 made that loop generic, so every OTHER capability's `ship:pre` gate is now evaluated through `gsd_run check predicate` instead of being resolved and silently dropped, while these two keep their bespoke fail-closed reads and are each visited exactly once); it reads `gsd_run windows status --raw` and fails closed on a non-zero/non-numeric `open_count` (an unparseable ledger is itself a broken window). `/gsd:progress` surfaces the open+waived count. The ledger is optional and backward-compatible: a project with no `.planning/WINDOWS.md` reports `open_count: 0` and ships cleanly, and with `workflow.windows_enforce=false` (the default) ship never blocks on it. Frozen `REASON` enum: `WINDOWS_LEDGER_MISSING | WINDOWS_LEDGER_MALFORMED | WINDOWS_ID_NOT_FOUND | WINDOWS_ALREADY_RESOLVED | WINDOWS_WAIVE_REASON_EMPTY | WINDOWS_INVALID_KIND | WINDOWS_INVALID_FILE | WINDOWS_INVALID_ID | WINDOWS_APPEND_MISSING_FIELD | WINDOWS_USAGE | WINDOWS_OK` — surfaced through `--json-errors` for typed test assertions. Test seam: `tests/broken-windows.test.cjs`. Origin: *The Pragmatic Programmer* Topic 3 (Hunt & Thomas — software transplant of Wilson & Kelling's broken-windows metaphor) plus Cunningham's debt metaphor (decay accrues interest ⇒ accounting, not just habit). ### Emitted Artifact Provenance -Cross-seam principle (ADR-2719, epic #2719): a committed artifact that is a pure function of the source tree is not reviewable state — it is derived state wearing a review costume, and it must be *attributable* rather than *pinned*. Concept, not a Module: it ships nothing, so it takes no `Module` suffix (follows the `### Resolution Provenance` precedent). Scope is the emitted-artifact family named by `RULESET.EMITTED_ATTRIBUTION`. The principle: every emitted path whose hash moved between `next` HEAD and PR HEAD must be attributable — through a declarative provenance table — to a path the pull request actually changed; unattributable deltas are a hard failure that *names them* rather than an anomaly a reviewer must notice inside 7,500 lines of hex. Totality is enforced, so an emitted path matching no rule fails loudly instead of passing through unattributed. The escape hatch is a committed acknowledgment — a per-PR fragment under `tests/emitted-drift-acks/` (#2914; the legacy single `tests/emitted-drift-ack.json` is still read and unioned in for pre-#2914 branches, with a duplicate key across two sources a hard, loudly-reported error rather than silent last-wins), deliberately not a flag or env var — a fragment appears in the changed-files list ONLY when something rippled unexpectedly, so adding one IS the alarm, whereas today 100% of emitted-byte changes touch fixtures and touching them signals nothing. Fragments exist because the single legacy file, rewritten wholesale by every PR needing an ack, was a guaranteed merge-conflict cell between any two such PRs (5 of 6 conflicting PRs in one open queue collided on it and nothing else) — the same shape `.changeset/` already solves the same way. The same differential machine carries the size ratchet: growth is reported with exact byte deltas and needs the same acknowledgment, so anti-creep survives without pinning a number. Distinguish from the absolute check that remains: `tests/fixtures/install-tree/*.json` stays committed and normally-merged (ADR-2719 §7) because "the installer stopped shipping X" must fail with no attribution reasoning involved. Delivery was phased — #2721 naming + interim merge relief, #2722 provenance table + totality guard, #2723 differential check dual-run beside `golden-install-parity.test.cjs`, #2724 cutover (COMPLETE: the dual-run window observed agreement on real PRs after fixing #2750/#2760, and the golden fixtures/test/generator/merge-driver bridge are now deleted; the differential is the sole gate). The table LANDED in #2722 as `tests/helpers/emitted-provenance.cjs` (19 rules, guarded by `tests/emitted-provenance.test.cjs`); it maps emitted path → repo source and is TOTAL over EVERY emitted path in all 19 manifests — exactly one rule per path, with zero-match, two-match, AND dead-rule (a rule matching nothing) all hard failures, so table rot is loud in both directions. Deliberately NO path/family counts are recorded here: those move with every shipped-content edit, and a hand-maintained number in glossary canon is the exact silent-drift failure this whole seam exists to end. The guard recomputes them from the fixtures on every run — read them from a failure message, never from prose. What IS stable is the rule count, which changes only when a new emitted family or host appears. Note the surface is materially wider than #2722 estimated from `claude.json` alone (its "13 families / 15-20 rules" was a single-runtime sample; the 19-manifest surface spans runtime-specific roots — `.agents/`, `.kimi/hooks/`, `command/`, `agents/subagents/`, `.clinerules/`, `plugins/`, `extensions/`, `.gsd/`, the hermes `skills/gsd/` category and the #69 nested `skills//skills//` layout). Two design invariants carry forward to #2723: emitted SHAPES are hard-coded (deriving them from the installer would make the guard tautological — it would follow any installer change silently), while source PATHS may read a first-party descriptor where that descriptor is the sole declaration (`hostBehaviors.nativePlugin.source`); and attribution is keyed on `(rel, runtime)`, never `rel` alone, because one emitted path has different sources per host (`plugins/gsd-core.js` ← `.opencode/` vs `.kilo/`). Emitted skills attribute to `commands/gsd/*.md`, NEVER the repo `skills/` dir — that dir is itself generated from `commands/gsd` by `scripts/gen-plugin-skills.cjs`, so attributing to it is false attribution that still passes totality. Totality does NOT catch a rule pointing at the WRONG source (the recorded residual); the guard against that is the companion assertion that every attributed source EXISTS in the repo, which caught three real cases while the table was built (Copilot's `.agent.md` rename, Kimi's code-literal `agents/gsd.{yaml,md}` root agent, and Copilot's `hooks/gsd-session.json`). A `sources` entry ending in `/` is a PREFIX, not a file — and prefix matching is SEGMENT-AWARE, so a source of `agents/` must not attribute `agentsfoo/x.md`. The differential check LANDED in #2723 as `tests/helpers/emitted-diff.cjs` (the conservation law, a PURE function — no fs/git/installer/clock) + `tests/helpers/emitted-baseline.cjs` (baseline resolution), guarded by `tests/emitted-attribution.test.cjs`. It ran DUAL beside `golden-install-parity.test.cjs` through the #2723 dual-run window with both green and fixtures untouched; #2724 deleted the golden fixtures/test/generator and the check is now the sole gate. Purity is deliberate and load-bearing: the naive one-big-integration-test shape would need ~38 installer spawns per assertion, so the four failing-first criteria would not in practice get written — which is exactly how a phase ships promised-but-not-built. Buckets are CONSERVED: every moved emitted path lands in exactly one of `attributed | unattributable | acked` (property-tested), and a path the provenance table cannot resolve surfaces as an ERROR rather than a silent skip. Four asymmetries worth knowing: an ADDED emitted key is a ripple too (not just modified ones); `synthesized` paths are exempt but `code-derived` ones are NOT (that is why Phase 2 refused to mark them exempt — exempt means permanently blind); SHRINKAGE needs no ack while growth does (gating shrinkage would punish what the ratchet wants); and a STALE ack is a hard failure — but ONLY for an ack THIS diff wrote or reworded (#2789). An ack is SCOPED TO THE DIFF THAT INTRODUCED IT: `diffEmitted` takes the document at the base ref (`baseAck`, read by `readAckFileAtRef`) alongside the working-tree one, and an entry already present at the base is SPENT — its ripple is absorbed into the base, so it can no longer clear a delta and is never reported stale (surfaced as `spentAcks`, informational, gating nothing). Before #2789 the ack set was the one ABSOLUTE input to an otherwise base-relative machine — `baseline` vs `current`, `changedPaths` from `git diff base...HEAD` — and that mismatch made a MERGED ack indistinguishable from one that never explained anything, since `staleAcks` asks only "did a delta consume you?": merging an ack the PR lane had already accepted reddened `next` and every PR branching off it (#2768). Making spent entries inert is also what finally closes the pre-clearing hazard the original design NAMED but could not prevent — a leftover ack used to silently clear the next ripple on its path; now that ripple must be explained on its own terms, and a reworded reason is how a contributor re-arms an ack deliberately. `baseAck` is REQUIRED once an ack DECLARES ENTRIES (omission is an error, never a silent "inherit nothing", same discipline as `changedPaths`; an entry is the only thing that can be misclassified, so an empty-but-legal document needs no base side). Absent AT THE REF returns null — the healthy steady state — but every other read failure THROWS, and that asymmetry is load-bearing in the direction that is easy to invert: returning null looks armed because every entry stays LIVE, yet a live entry's defining power is that it CONSUMES a delta, so null is armed on the staleness axis and DISARMED on the consumption axis — a genuinely new unexplained ripple on a path carrying an already-merged ack would come back `acked` instead of `unattributable`, silently restoring the whole pre-#2789 gate. `git show` cannot tell absence from fault (both say "does not exist in"), so absence is established with `ls-tree`. Re-arming a spent ack is legitimate and deliberate, but it costs ACTUAL PROSE: the comparison collapses internal whitespace and ignores `runtime`, because a doubled space or a decorative field would otherwise re-arm an ack whose recorded justification still describes the PREVIOUS ripple, showing a reviewer nothing new in the diff. Because a corrupt document ON THE BASE is expensive (it reds every PR carrying an ack until repaired), `scripts/lint-emitted-drift-ack.cjs` runs in `lint:ci` and refuses the merge before one can land — invalid JSON, a non-object, a bad version, a reasonless entry, or a present-but-entryless/`null` document. It is deliberately STANDALONE rather than importing `parseAck` (`scripts/` ships in the npm package and `tests/` does not, so the require would be MODULE_NOT_FOUND once published); the duplication is bounded by a parity test that runs both surfaces over one corpus and fails on any disagreement about schema validity. The two are MEANT to differ on exactly one axis: an entryless or `null` document is legal to PARSE (it is the gate's own absent-equals-no-acks sentinel) and still refused for COMMIT. Deadlock is separately foreclosed at the call site — a tree carrying no ack never reads the base at all, so the PR that DELETES a corrupt file still lands. Each ack source — a fragment under `tests/emitted-drift-acks/`, or the legacy `tests/emitted-drift-ack.json` (#2914; both read and UNIONED via `mergeAckSources`/`readAckSources`/`readAckSourcesAtRef` in `tests/helpers/emitted-diff.cjs` / `emitted-runtime.cjs`, a duplicate key across sources a hard error) — follows the same rule: absent = no acks; a LIVE entry is the alarm, a spent one is inert cruft; requires a non-empty `reason` per path — "name them and say why" is the contract, and a document that parses but is not an object is rejected rather than read as "no acks", which would silently disarm the gate. Baseline is CACHED not committed, keyed on the `next` sha; a stale key is REFUSED, never used — absence fails loudly and gets fixed, whereas staleness produces a confident wrong answer. An explicitly-pointed-at (`GSD_EMITTED_BASELINE`) stale baseline is a hard stop, while a stale CACHE falls through to the in-job build. No baseline-unavailable path may `return` (in `node:test` that is a PASS, not a skip — ADR-2719 §6). Supersedes ADR-2264 §2–§4 and its Amendment; ADR-2264 Phase 1 (`buildParityManifest` and the exclusion constants in `tests/helpers/install-shared.cjs`) is retained and depended upon. +Cross-seam principle (ADR-2719, epic #2719): a committed artifact that is a pure function of the source tree is not reviewable state — it is derived state wearing a review costume, and it must be *attributable* rather than *pinned*. Concept, not a Module: it ships nothing, so it takes no `Module` suffix (follows the `### Resolution Provenance` precedent). Scope is the emitted-artifact family named by `RULESET.EMITTED_ATTRIBUTION`. The principle: every emitted path whose hash moved between `next` HEAD and PR HEAD must be attributable — through a declarative provenance table — to a path the pull request actually changed; unattributable deltas are a hard failure that *names them* rather than an anomaly a reviewer must notice inside 7,500 lines of hex. Totality is enforced, so an emitted path matching no rule fails loudly instead of passing through unattributed. The escape hatch is a committed acknowledgment — a per-PR fragment under `tests/emitted-drift-acks/` (#2914; the legacy single `tests/emitted-drift-ack.json` is still read and unioned in for pre-#2914 branches, with a duplicate key across two sources a hard, loudly-reported error rather than silent last-wins), deliberately not a flag or env var — a fragment appears in the changed-files list ONLY when something rippled unexpectedly, so adding one IS the alarm, whereas today 100% of emitted-byte changes touch fixtures and touching them signals nothing. Fragments exist because the single legacy file, rewritten wholesale by every PR needing an ack, was a guaranteed merge-conflict cell between any two such PRs (5 of 6 conflicting PRs in one open queue collided on it and nothing else) — the same shape `.changeset/` already solves the same way. Fragments end the FILE conflict but not the KEY conflict: two sources may never name the same path, so a fully-spent fragment left on `next` still walls off every path it owns until it is swept (#3078; see `RULESET.EMITTED_ATTRIBUTION`). The same differential machine carries the size ratchet: growth is reported with exact byte deltas and needs the same acknowledgment, so anti-creep survives without pinning a number. Distinguish from the absolute check that remains: `tests/fixtures/install-tree/*.json` stays committed and normally-merged (ADR-2719 §7) because "the installer stopped shipping X" must fail with no attribution reasoning involved. Delivery was phased — #2721 naming + interim merge relief, #2722 provenance table + totality guard, #2723 differential check dual-run beside `golden-install-parity.test.cjs`, #2724 cutover (COMPLETE: the dual-run window observed agreement on real PRs after fixing #2750/#2760, and the golden fixtures/test/generator/merge-driver bridge are now deleted; the differential is the sole gate). The table LANDED in #2722 as `tests/helpers/emitted-provenance.cjs` (19 rules, guarded by `tests/emitted-provenance.test.cjs`); it maps emitted path → repo source and is TOTAL over EVERY emitted path in all 19 manifests — exactly one rule per path, with zero-match, two-match, AND dead-rule (a rule matching nothing) all hard failures, so table rot is loud in both directions. Deliberately NO path/family counts are recorded here: those move with every shipped-content edit, and a hand-maintained number in glossary canon is the exact silent-drift failure this whole seam exists to end. The guard recomputes them from the fixtures on every run — read them from a failure message, never from prose. What IS stable is the rule count, which changes only when a new emitted family or host appears. Note the surface is materially wider than #2722 estimated from `claude.json` alone (its "13 families / 15-20 rules" was a single-runtime sample; the 19-manifest surface spans runtime-specific roots — `.agents/`, `.kimi/hooks/`, `command/`, `agents/subagents/`, `.clinerules/`, `plugins/`, `extensions/`, `.gsd/`, the hermes `skills/gsd/` category and the #69 nested `skills//skills//` layout). Two design invariants carry forward to #2723: emitted SHAPES are hard-coded (deriving them from the installer would make the guard tautological — it would follow any installer change silently), while source PATHS may read a first-party descriptor where that descriptor is the sole declaration (`hostBehaviors.nativePlugin.source`); and attribution is keyed on `(rel, runtime)`, never `rel` alone, because one emitted path has different sources per host (`plugins/gsd-core.js` ← `.opencode/` vs `.kilo/`). Emitted skills attribute to `commands/gsd/*.md`, NEVER the repo `skills/` dir — that dir is itself generated from `commands/gsd` by `scripts/gen-plugin-skills.cjs`, so attributing to it is false attribution that still passes totality. Totality does NOT catch a rule pointing at the WRONG source (the recorded residual); the guard against that is the companion assertion that every attributed source EXISTS in the repo, which caught three real cases while the table was built (Copilot's `.agent.md` rename, Kimi's code-literal `agents/gsd.{yaml,md}` root agent, and Copilot's `hooks/gsd-session.json`). A `sources` entry ending in `/` is a PREFIX, not a file — and prefix matching is SEGMENT-AWARE, so a source of `agents/` must not attribute `agentsfoo/x.md`. The differential check LANDED in #2723 as `tests/helpers/emitted-diff.cjs` (the conservation law, a PURE function — no fs/git/installer/clock) + `tests/helpers/emitted-baseline.cjs` (baseline resolution), guarded by `tests/emitted-attribution.test.cjs`. It ran DUAL beside `golden-install-parity.test.cjs` through the #2723 dual-run window with both green and fixtures untouched; #2724 deleted the golden fixtures/test/generator and the check is now the sole gate. Purity is deliberate and load-bearing: the naive one-big-integration-test shape would need ~38 installer spawns per assertion, so the four failing-first criteria would not in practice get written — which is exactly how a phase ships promised-but-not-built. Buckets are CONSERVED: every moved emitted path lands in exactly one of `attributed | unattributable | acked` (property-tested), and a path the provenance table cannot resolve surfaces as an ERROR rather than a silent skip. Four asymmetries worth knowing: an ADDED emitted key is a ripple too (not just modified ones); `synthesized` paths are exempt but `code-derived` ones are NOT (that is why Phase 2 refused to mark them exempt — exempt means permanently blind); SHRINKAGE needs no ack while growth does (gating shrinkage would punish what the ratchet wants); and a STALE ack is a hard failure — but ONLY for an ack THIS diff wrote or reworded (#2789). An ack is SCOPED TO THE DIFF THAT INTRODUCED IT: `diffEmitted` takes the document at the base ref (`baseAck`, read by `readAckFileAtRef`) alongside the working-tree one, and an entry already present at the base is SPENT — its ripple is absorbed into the base, so it can no longer clear a delta and is never reported stale (surfaced as `spentAcks`, informational, gating nothing). Before #2789 the ack set was the one ABSOLUTE input to an otherwise base-relative machine — `baseline` vs `current`, `changedPaths` from `git diff base...HEAD` — and that mismatch made a MERGED ack indistinguishable from one that never explained anything, since `staleAcks` asks only "did a delta consume you?": merging an ack the PR lane had already accepted reddened `next` and every PR branching off it (#2768). Making spent entries inert is also what finally closes the pre-clearing hazard the original design NAMED but could not prevent — a leftover ack used to silently clear the next ripple on its path; now that ripple must be explained on its own terms, and a reworded reason is how a contributor re-arms an ack deliberately. `baseAck` is REQUIRED once an ack DECLARES ENTRIES (omission is an error, never a silent "inherit nothing", same discipline as `changedPaths`; an entry is the only thing that can be misclassified, so an empty-but-legal document needs no base side). Absent AT THE REF returns null — the healthy steady state — but every other read failure THROWS, and that asymmetry is load-bearing in the direction that is easy to invert: returning null looks armed because every entry stays LIVE, yet a live entry's defining power is that it CONSUMES a delta, so null is armed on the staleness axis and DISARMED on the consumption axis — a genuinely new unexplained ripple on a path carrying an already-merged ack would come back `acked` instead of `unattributable`, silently restoring the whole pre-#2789 gate. `git show` cannot tell absence from fault (both say "does not exist in"), so absence is established with `ls-tree`. Re-arming a spent ack is legitimate and deliberate, but it costs ACTUAL PROSE: the comparison collapses internal whitespace and ignores `runtime`, because a doubled space or a decorative field would otherwise re-arm an ack whose recorded justification still describes the PREVIOUS ripple, showing a reviewer nothing new in the diff. Because a corrupt document ON THE BASE is expensive (it reds every PR carrying an ack until repaired), `scripts/lint-emitted-drift-ack.cjs` runs in `lint:ci` and refuses the merge before one can land — invalid JSON, a non-object, a bad version, a reasonless entry, or a present-but-entryless/`null` document. It is deliberately STANDALONE rather than importing `parseAck` (`scripts/` ships in the npm package and `tests/` does not, so the require would be MODULE_NOT_FOUND once published); the duplication is bounded by a parity test that runs both surfaces over one corpus and fails on any disagreement about schema validity. The two are MEANT to differ on exactly one axis: an entryless or `null` document is legal to PARSE (it is the gate's own absent-equals-no-acks sentinel) and still refused for COMMIT. Deadlock is separately foreclosed at the call site — a tree carrying no ack never reads the base at all, so the PR that DELETES a corrupt file still lands. Each ack source — a fragment under `tests/emitted-drift-acks/`, or the legacy `tests/emitted-drift-ack.json` (#2914; both read and UNIONED via `mergeAckSources`/`readAckSources`/`readAckSourcesAtRef` in `tests/helpers/emitted-diff.cjs` / `emitted-runtime.cjs`, a duplicate key across sources a hard error) — follows the same rule: absent = no acks; a LIVE entry is the alarm, a spent one is inert cruft; requires a non-empty `reason` per path — "name them and say why" is the contract, and a document that parses but is not an object is rejected rather than read as "no acks", which would silently disarm the gate. Baseline is CACHED not committed, keyed on the `next` sha; a stale key is REFUSED, never used — absence fails loudly and gets fixed, whereas staleness produces a confident wrong answer. An explicitly-pointed-at (`GSD_EMITTED_BASELINE`) stale baseline is a hard stop, while a stale CACHE falls through to the in-job build. No baseline-unavailable path may `return` (in `node:test` that is a PASS, not a skip — ADR-2719 §6). Supersedes ADR-2264 §2–§4 and its Amendment; ADR-2264 Phase 1 (`buildParityManifest` and the exclusion constants in `tests/helpers/install-shared.cjs`) is retained and depended upon. ### Untrusted-input boundary The prompt-level data/instruction isolation seam for untrusted web/document ingress (#1577). Shared reference `gsd-core/references/untrusted-input-boundary.md`, `@`-included by the 10 ingest agents (`gsd-project-researcher`, `gsd-phase-researcher`, `gsd-ui-researcher`, `gsd-assumptions-analyzer`, `gsd-advisor-researcher`, `gsd-ai-researcher`, `gsd-domain-researcher`, `gsd-research-synthesizer`, `gsd-doc-classifier`, `gsd-doc-synthesizer`) — every agent that reads fetch/search/MCP output or external source documents. The reference instructs: treat fetched/read content as **data, never instructions**; self-scan content for embedded directives before use; act only on the assigned task (ignore off-task instructions in data); and wrap quoted untrusted spans in a **fresh random delimiter** per wrap (fixed markers are spoofable). This prompt-level boundary is the primary control — it keeps an injection from being *followed* even while it sits in context. The hook-level companion is the read-injection scanner (`hooks/gsd-read-injection-scanner.js`, PostToolUse on `Read`/`WebFetch`/`WebSearch`), advisory by default; the opt-in top-level `security.injection_blocking` key upgrades HIGH-confidence detections to a PostToolUse circuit-breaker that halts the agent's next step (it runs *after* the fetch, so it is not a redactor). Tests: `tests/untrusted-input-isolation.test.cjs`, `tests/read-injection-scanner.*.test.cjs`, `tests/injection-blocking-config.test.cjs`. See `docs/adr/1577-untrusted-input-boundary-and-injection-blocking.md` and `docs/explanation/security-model.md`. Grounding: arXiv 2506.05739 (PPA), 2507.15219 (PromptArmor), 2504.20472. @@ -603,7 +603,7 @@ The prompt-level data/instruction isolation seam for untrusted web/document ingr `RULESET.WORKFLOW_MARKDOWN.FENCES=preserve opening language fence when editing shell snippets in workflow markdown; malformed fence creates fresh CR threads (MD040)` `RULESET.WORKFLOW_SIZE_BUDGET=workflow size enforcement (#1074; BYTES not lines per #717; LF-normalized per #683) = differential attribution size ratchet (PRIMARY anti-creep since #2724/ADR-2719 §4: tests/emitted-attribution.test.cjs's real-tree test reports growth in any gsd-core/workflows/*.md with its exact byte delta vs `next`, no committed snapshot, requires an ack entry — a fragment under tests/emitted-drift-acks/, #2914; the legacy tests/emitted-drift-ack.json is still honored and unioned in) + loose tier hard caps (outer red lines, NEVER raised on approach: XL<=98304 / LARGE<=61440 / DEFAULT<=40960) + discuss-phase<32000; a file that grew fails the differential guard — add an ack entry naming the file and reason, justify the growth in the PR (or extract LAZILY-loaded content; eager @-imports don't reduce loaded context); crossing a hard cap means EXTRACT, not bump. The prior per-file baseline (tests/workflow-size-baseline.json, `npm run size:baseline`) is REMOVED by #2724. Its new-file cap (ADR-1610 Decision point 3, un-baselined files <=32768, the Codex anchor) is REVIVED inside the differential's size ratchet itself (`NEW_FILE_CAP` in tests/helpers/emitted-diff.cjs) rather than lost: "not yet baselined" is exactly "present in sizeCurrent, absent from sizeBaseline", a signal the ratchet already computes for its own reasons. NOT ack-able — same as the tier hard caps, the fix is extraction. Narrower than the original: this check cannot see XL/LARGE tiering (tests/workflow-size-budget.test.cjs's classification, invisible to the pure differential module), so a legitimately large NEW file must extract rather than tier in, one release earlier than an existing file would need to — a disclosed, deliberate simplification` `RULESET.AGENT_SIZE_BUDGET=agent-size-budget (#1074; sibling of WORKFLOW_SIZE_BUDGET; BYTES not lines per #717/#683, rebased from lines in PR 3/3) = differential attribution size ratchet (PRIMARY anti-creep since #2724/ADR-2719 §4, same mechanism and same ack fragments (tests/emitted-drift-acks/, #2914; legacy tests/emitted-drift-ack.json still honored) as WORKFLOW_SIZE_BUDGET, scoped to agents/gsd-*.md) + loose tier hard caps (red lines, never raised on approach: XL<=57344 / LARGE<=49152 / DEFAULT<=24576); net-new agents are DEFAULT-tier (no separate new-file cap). Sizes are measured via the shared scripts/workflow-size.cjs measureMdFiles(dir,predicate) counter (tests/helpers/emitted-runtime.cjs's currentSizes() and the guard's own tier-cap checks both import it). A grown agent fails the differential guard — ack + justify, or extract LAZILY to gsd-core/references/. DISTINCT from DEFECT.AGENT-FILE-SIZE-CAP-BREACH (a separate 45K-CHAR extraction-evidence threshold on gsd-planner via planner-decomposition/reachability tests): that guard proves mode-sections were extracted; this one bounds total agent bytes. Two guards, two units (chars vs bytes), two purposes. The prior per-file baseline (tests/agent-size-baseline.json, `npm run size:baseline`) is REMOVED by #2724` -`RULESET.EMITTED_ATTRIBUTION=the emitted-artifact family (ADR-2719, epic #2719) — POST-CUTOVER (#2724, Phase 4). Historically tests/fixtures/golden-install-parity/*.json (19 path→hash manifests) + tests/workflow-size-baseline.json + tests/agent-size-baseline.json were all committed, PURE FUNCTIONS of the source tree whose correct merge was ALWAYS "recompute" — 140 of 143 conflicted-file instances across the open PR queue were these files. #2724 DELETES all three, the golden test (tests/golden-install-parity.test.cjs), the generator (scripts/gen-golden-install-parity-zcode.cjs), `npm run gen:golden`, `UPDATE_GOLDEN`, the merge-driver bridge (scripts/git-merge-regen-driver.cjs, `npm run setup:merge-driver`, the .gitattributes merge=gsd-regen block), and scripts/update-size-baseline.cjs (`npm run size:baseline`). The differential attribution check (tests/emitted-attribution.test.cjs + tests/emitted-provenance.test.cjs) is now the SOLE gate for emitted-artifact propagation AND size growth — no committed artifact, nothing to hand-merge, nothing to regenerate. `npm run regen:derived` still exists for what remains committed and derived: build, registry, ADR index, capability matrix, inventory manifest, manifest versions, and `tests/fixtures/install-tree/*.json` (now `npm run gen:install-tree`, folded into `regen:derived`). tests/fixtures/install-tree/*.json is DELIBERATELY EXCLUDED from the cutover (ADR-2719 §7): it conflicts on 0 of 7, its diffs are readable, and it preserves "the installer stopped shipping X" as a hard absolute failure — capturing it would convert that absolute into an attribution-free auto-resolve. The baseline the differential compares against is now published by `scripts/gen-emitted-baseline.cjs` on every push to `next` (cached, keyed on sha) and restored in PR lanes via `GSD_EMITTED_BASELINE`/`resolveBaseline()` (tests/helpers/emitted-baseline.cjs); a cache miss falls back to an in-job build via a throwaway `git worktree` (tests/helpers/emitted-runtime.cjs's `buildBaselineAtRef`). REMEDIATION IS PART OF THE GATE (#2778): the failure output names its own remedy, because a gate that states a requirement and withholds the means of satisfying it is a maintainer round-trip, not a gate — ADR-2719 §3's "conspicuous declaration" only works if the contributor can discover how to make it. Both failing branches name a NEW fragment to create under `tests/emitted-drift-acks/` (#2914; pick a name nobody else is using), say it may not exist yet (absence is the healthy steady state), print a minimal valid document, and repeat "do NOT regenerate anything" — post-#2724 there is nothing left to regenerate, and hunting for a deleted baseline is the predictable wrong guess. The two branches key on DIFFERENT spaces and each says which: the hash pass keys on the EMITTED PATH (always contains a `/`), the size ratchet keys on the BARE FILENAME (`currentSizes` writes `sizes[entry.name]` from readdirSync over `gsd-core/workflows/` + `agents/`). A stale-ack failure additionally says to delete the FILE when removing its last entry, since an empty-but-present ack parses fine yet signals nothing; post-#2789 it also offers CORRECTING the entry to name the ripple actually made, which is the other honest resolution and the one a contributor usually wants. NOT ack-able and deliberately given no ack text: the `NEW_FILE_CAP` branch, whose remedy is extraction. Text is sourced from one frozen `REMEDIATION` export in tests/helpers/emitted-diff.cjs whose example document is rendered from `ACK_VERSION` via `JSON.stringify`, so the taught schema cannot drift from the accepted one (a round-trip test feeds the printed document back through `parseAck`); the message teaches ONE canonical shape even though `parseAck` also accepts a bare-string reason and a missing `version` — liberal in what it accepts, conservative in what it sends. Note the ADR's Consequences originally called the #2724 migration "terminal"; #2778 corrected that — it is terminal only for a PR that grows no shipped file. #2914 replaced the single shared ack file with per-PR fragments under `tests/emitted-drift-acks/` — exactly the shape `.changeset/` already uses for the identical "every PR rewrites one shared document" conflict problem — so two PRs needing an ack can no longer collide with each other, and a fragment left on `next` after merge is inert rather than a shared cell; the legacy file is still read and unioned in for branches that predate the split, and a duplicate path key across two sources is a hard, loudly-reported error, never silent last-wins. `tests/emitted-drift-ack.json` (the LEGACY file specifically, NOT the fragment directory) must NEVER persist on `next` (#2914): every entry is scoped to the diff that introduced it, so once merged it is by definition already at the base — spent and inert regardless of shape — and a persistent copy makes that ONE file a shared merge-conflict cell across every open PR that also carries an ack, exactly the "140 of 143" cost this whole cutover exists to remove; a persisting FRAGMENT is harmless by construction and is deliberately not what this guard checks. This is enforced on `next` itself only, never as a PR-lane check: the `guard-no-ack-on-next` job in `.github/workflows/test.yml` (push-to-`next` trigger) runs `scripts/lint-emitted-drift-ack.cjs --guard-next` (`assertAbsentOnNext`), which fails on the LEGACY file's PRESENCE alone, valid or not — a PR-lane "base ack must be absent" check would red every open PR the instant a spent ack merged, which is the #2768 shape #2789 already ended. cf `RULESET.WORKFLOW_SIZE_BUDGET`, `RULESET.AGENT_SIZE_BUDGET`; see `### Emitted Artifact Provenance`` +`RULESET.EMITTED_ATTRIBUTION=the emitted-artifact family (ADR-2719, epic #2719) — POST-CUTOVER (#2724, Phase 4). Historically tests/fixtures/golden-install-parity/*.json (19 path→hash manifests) + tests/workflow-size-baseline.json + tests/agent-size-baseline.json were all committed, PURE FUNCTIONS of the source tree whose correct merge was ALWAYS "recompute" — 140 of 143 conflicted-file instances across the open PR queue were these files. #2724 DELETES all three, the golden test (tests/golden-install-parity.test.cjs), the generator (scripts/gen-golden-install-parity-zcode.cjs), `npm run gen:golden`, `UPDATE_GOLDEN`, the merge-driver bridge (scripts/git-merge-regen-driver.cjs, `npm run setup:merge-driver`, the .gitattributes merge=gsd-regen block), and scripts/update-size-baseline.cjs (`npm run size:baseline`). The differential attribution check (tests/emitted-attribution.test.cjs + tests/emitted-provenance.test.cjs) is now the SOLE gate for emitted-artifact propagation AND size growth — no committed artifact, nothing to hand-merge, nothing to regenerate. `npm run regen:derived` still exists for what remains committed and derived: build, registry, ADR index, capability matrix, inventory manifest, manifest versions, and `tests/fixtures/install-tree/*.json` (now `npm run gen:install-tree`, folded into `regen:derived`). tests/fixtures/install-tree/*.json is DELIBERATELY EXCLUDED from the cutover (ADR-2719 §7): it conflicts on 0 of 7, its diffs are readable, and it preserves "the installer stopped shipping X" as a hard absolute failure — capturing it would convert that absolute into an attribution-free auto-resolve. The baseline the differential compares against is now published by `scripts/gen-emitted-baseline.cjs` on every push to `next` (cached, keyed on sha) and restored in PR lanes via `GSD_EMITTED_BASELINE`/`resolveBaseline()` (tests/helpers/emitted-baseline.cjs); a cache miss falls back to an in-job build via a throwaway `git worktree` (tests/helpers/emitted-runtime.cjs's `buildBaselineAtRef`). REMEDIATION IS PART OF THE GATE (#2778): the failure output names its own remedy, because a gate that states a requirement and withholds the means of satisfying it is a maintainer round-trip, not a gate — ADR-2719 §3's "conspicuous declaration" only works if the contributor can discover how to make it. Both failing branches name a NEW fragment to create under `tests/emitted-drift-acks/` (#2914; pick a name nobody else is using), say it may not exist yet (absence is the healthy steady state), print a minimal valid document, and repeat "do NOT regenerate anything" — post-#2724 there is nothing left to regenerate, and hunting for a deleted baseline is the predictable wrong guess. The two branches key on DIFFERENT spaces and each says which: the hash pass keys on the EMITTED PATH (always contains a `/`), the size ratchet keys on the BARE FILENAME (`currentSizes` writes `sizes[entry.name]` from readdirSync over `gsd-core/workflows/` + `agents/`). A stale-ack failure additionally says to delete the FILE when removing its last entry, since an empty-but-present ack parses fine yet signals nothing; post-#2789 it also offers CORRECTING the entry to name the ripple actually made, which is the other honest resolution and the one a contributor usually wants. NOT ack-able and deliberately given no ack text: the `NEW_FILE_CAP` branch, whose remedy is extraction. Text is sourced from one frozen `REMEDIATION` export in tests/helpers/emitted-diff.cjs whose example document is rendered from `ACK_VERSION` via `JSON.stringify`, so the taught schema cannot drift from the accepted one (a round-trip test feeds the printed document back through `parseAck`); the message teaches ONE canonical shape even though `parseAck` also accepts a bare-string reason and a missing `version` — liberal in what it accepts, conservative in what it sends. Note the ADR's Consequences originally called the #2724 migration "terminal"; #2778 corrected that — it is terminal only for a PR that grows no shipped file. #2914 replaced the single shared ack file with per-PR fragments under `tests/emitted-drift-acks/` — exactly the shape `.changeset/` already uses for the identical "every PR rewrites one shared document" conflict problem — so two PRs needing an ack can no longer collide with each other on the FILE; the legacy file is still read and unioned in for branches that predate the split, and a duplicate path key across two sources is a hard, loudly-reported error, never silent last-wins — #3078 made that error name its two resolutions (git rm an already-merged, spent owner; APPEND prose to a still-live one, which re-arms it), because the guard runs post-merge and cannot stop the colliding PR. `tests/emitted-drift-ack.json` (the LEGACY file specifically) must NEVER persist on `next` (#2914): every entry is scoped to the diff that introduced it, so once merged it is by definition already at the base — spent and inert regardless of shape — and a persistent copy makes that ONE file a shared merge-conflict cell across every open PR that also carries an ack, exactly the "140 of 143" cost this whole cutover exists to remove; #2914 asserted a persisting FRAGMENT was harmless by construction and deliberately exempted the directory; #3078 REVERSED that — fragments do not share a FILE but they DO share a PATH KEY SPACE, so a fully-spent fragment on `next` owns keys it can no longer gate and the next PR growing one of those paths can declare it neither there (spent) nor in its own (duplicate), which is the #2914 wall one level down (measured at the sweep: 45 fragments owning 403 paths, up from 13/272 at triage 19 days earlier). A fragment is judged on INERTNESS, not presence: swept once EVERY entry is spent, left alone while PARTIALLY spent — the asymmetry is what keeps the re-arm-by-appending route (#2639, #2993) working, and the `0000` legacy-migration bucket #2923 created for the old shared file's 35 entries was NOT permanent (the issue's own open question resolved to NO) and went with the rest. This is enforced on `next` itself only, never as a PR-lane check: the `guard-no-ack-on-next` job in `.github/workflows/test.yml` (push-to-`next` trigger) runs `scripts/lint-emitted-drift-ack.cjs --guard-next`, which is now BOTH halves — `assertAbsentOnNext` (legacy file, fails on PRESENCE alone, valid or not) and `assertNoAllSpentFragments` (fragments, fails on all-entries-spent vs the copy at the PRE-PUSH TIP of next — CI passes `github.event.before` via `--base-ref`, because the default branch allows REBASE merges so one push can carry N commits and a bare `HEAD^` would flag a fragment the same push introduced; `HEAD^` remains only the local/manual fallback, using the SAME zero-width/whitespace-stripping prose comparison as `isSpent` so an invisible reword cannot fake a re-arm; duplicated across the scripts-ship/tests-do-not line and held by a parity test). The job's checkout REQUIRES `fetch-depth: 2` plus an explicit `git fetch --depth=1 origin $BEFORE` — at depth 1 no base commit exists locally, every fragment reads as brand-new, and the guard passes vacuously, which is exactly how the legacy half went blind after #2914 removed the file it was watching. The gate's `INVISIBLE`/`normalizeAckReason` are EXPORTED from tests/helpers/emitted-diff.cjs for the sole purpose of letting the parity test compare them against the script's duplicate; before #3078 neither was exported, so the "parity test" the comments promised was a tautology checking the script against itself. A PR-lane "base ack must be absent" check would red every open PR the instant a spent ack merged, which is the #2768 shape #2789 already ended — so this alerts AFTER the merge by design and never stops the offending PR. cf `RULESET.WORKFLOW_SIZE_BUDGET`, `RULESET.AGENT_SIZE_BUDGET`; see `### Emitted Artifact Provenance`` `RULESET.WORKFLOW_FILE_NAMES=workflow files use hyphens; XML attributes must match (extract-learnings not extract_learnings); tests should pin exact hyphenated name` `RULESET.WORKFLOW_EXECUTION_CONTEXT=@-ref in commands/gsd/*.md must resolve to an existing file on disk; regression test in tests/docs-update.test.cjs (folds former \`bug-3135-capture-backlog-workflow\`, consolidation epic #1969); INVENTORY.md row + INVENTORY-MANIFEST.json families.workflows must stay in sync; "Invoked by" attribution must move when a flag absorbs a micro-skill` `RULESET.WORKFLOW_EXECUTE_END_TO_END=standard for single-workflow commands is "Execute end-to-end." (no bolded **Follow the X workflow** fragments); flag-dispatch routing uses "execute the X workflow end-to-end." in routing bullets — convention verified live across ~20 commands/gsd/*.md files; no ADR currently documents this specific phrasing rule (ADR-0002 covers the adjacent but distinct command-contract/@-ref-resolution seam, not this convention)` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 47dbc37bf..33dcd02bd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1012,22 +1012,42 @@ file every such PR touches guarantees a merge conflict between any two of them ( conflicting PRs in one open queue collided on this file and nothing else), and it means spent, already-merged entries pile up on `next`. A fragment per PR — the same shape `.changeset/` already uses for the identical problem — means two PRs can never conflict on -this seam again, and a fragment left on `next` after merge is inert rather than a shared -cell. Two ack sources (two fragments, or a fragment and the legacy file) may **never** name -the same path; that is a hard, loudly-reported error, not a silent last-wins. +this seam again. Two ack sources (two fragments, or a fragment and the legacy file) may +**never** name the same path; that is a hard, loudly-reported error, not a silent +last-wins. -`tests/emitted-drift-ack.json` (the legacy single file, specifically — NOT the fragment -directory) must never persist on `next` (#2914): every entry is scoped to the diff that -introduced it, so once merged it is, by definition, already at the base — spent and inert, -regardless of shape, and its persistence is what makes it a shared merge-conflict cell. A -fragment persisting on `next` is harmless, since fragments are independently named and -cannot conflict with anything, so this guard is deliberately scoped to the legacy file -alone. This is enforced only on `next` itself, by the `guard-no-ack-on-next` job in +**When two sources collide (#3078).** The error names both resolutions, because which one +applies depends on the fragment that already owns the path. If the owning fragment is +already **merged**, its entry is spent — it is at the base, so it gates nothing — and the +answer is to delete it (`git rm tests/emitted-drift-acks/.json`) and keep your own. +If it is still **live** on your branch, append your explanation to its existing entry +instead; that re-arms it, and re-arming deliberately costs an actual new sentence, because +the reason is the whole artifact a reviewer reads. Never rename the path to dodge the +error, and never declare it twice. + +**Neither ack source may persist on `next`, and a fragment is not exempt (#3078).** +`tests/emitted-drift-ack.json`, the legacy single file, must never survive there at all: +every entry is scoped to the diff that introduced it, so once merged it is by definition +already at the base — spent and inert, regardless of shape — and its persistence is what +makes it a shared merge-conflict cell. A **fragment** is judged on a different rule but the +same law. #2914 originally exempted the fragment directory on the premise that a persisting +fragment is harmless, since fragments are independently named and cannot conflict. That +premise was wrong: fragments do not share a *file*, but they do share a *path key space*, +and a path claimed by two sources is the hard error above. So a fully-spent fragment left on +`next` owns keys it can no longer gate, and the next PR that grows one of those paths can +declare it neither there (spent) nor in its own fragment (duplicate) — the exact wall #2914 +removed for the legacy file, one level down. A fragment is therefore swept once **every** +entry in it is spent; a **partially** spent one is left alone, which is what keeps the +re-arm-by-appending route above working. Both rules are enforced only on `next` itself, by +the `guard-no-ack-on-next` job in `.github/workflows/test.yml` (push-to-`next` trigger, `scripts/lint-emitted-drift-ack.cjs --guard-next`), never as a PR-lane check — a PR-lane "base ack must be absent" check would red every open PR the moment one landed (the #2768 -shape #2789 exists to prevent). If you ever see the legacy file present on `next`, delete -it; do not try to make it well-formed. +shape #2789 exists to prevent), which means the job alerts **after** the merge and cannot +stop the offending PR — that is why the collision error above has to teach the resolution +too. If you ever see the legacy file present on `next`, delete it; do not try to make it +well-formed. If the job names a spent fragment, run the `git rm` it prints — that is the +whole remedy, and there is nothing to regenerate. `npm run regen:derived` still exists for the artifacts that ARE committed and derived — `sync-manifest-versions`, the ADR index, the capability matrix, the inventory manifest, diff --git a/docs/CONTEXT-INDEX.json b/docs/CONTEXT-INDEX.json index 8ca145484..bdde35eee 100644 --- a/docs/CONTEXT-INDEX.json +++ b/docs/CONTEXT-INDEX.json @@ -992,7 +992,7 @@ { "id": "RULESET.EMITTED_ATTRIBUTION", "klass": "RULESET", - "value": "the emitted-artifact family (ADR-2719, epic #2719) — POST-CUTOVER (#2724, Phase 4). Historically tests/fixtures/golden-install-parity/*.json (19 path→hash manifests) + tests/workflow-size-baseline.json + tests/agent-size-baseline.json were all committed, PURE FUNCTIONS of the source tree whose correct merge was ALWAYS \"recompute\" — 140 of 143 conflicted-file instances across the open PR queue were these files. #2724 DELETES all three, the golden test (tests/golden-install-parity.test.cjs), the generator (scripts/gen-golden-install-parity-zcode.cjs), `npm run gen:golden`, `UPDATE_GOLDEN`, the merge-driver bridge (scripts/git-merge-regen-driver.cjs, `npm run setup:merge-driver`, the .gitattributes merge=gsd-regen block), and scripts/update-size-baseline.cjs (`npm run size:baseline`). The differential attribution check (tests/emitted-attribution.test.cjs + tests/emitted-provenance.test.cjs) is now the SOLE gate for emitted-artifact propagation AND size growth — no committed artifact, nothing to hand-merge, nothing to regenerate. `npm run regen:derived` still exists for what remains committed and derived: build, registry, ADR index, capability matrix, inventory manifest, manifest versions, and `tests/fixtures/install-tree/*.json` (now `npm run gen:install-tree`, folded into `regen:derived`). tests/fixtures/install-tree/*.json is DELIBERATELY EXCLUDED from the cutover (ADR-2719 §7): it conflicts on 0 of 7, its diffs are readable, and it preserves \"the installer stopped shipping X\" as a hard absolute failure — capturing it would convert that absolute into an attribution-free auto-resolve. The baseline the differential compares against is now published by `scripts/gen-emitted-baseline.cjs` on every push to `next` (cached, keyed on sha) and restored in PR lanes via `GSD_EMITTED_BASELINE`/`resolveBaseline()` (tests/helpers/emitted-baseline.cjs); a cache miss falls back to an in-job build via a throwaway `git worktree` (tests/helpers/emitted-runtime.cjs's `buildBaselineAtRef`). REMEDIATION IS PART OF THE GATE (#2778): the failure output names its own remedy, because a gate that states a requirement and withholds the means of satisfying it is a maintainer round-trip, not a gate — ADR-2719 §3's \"conspicuous declaration\" only works if the contributor can discover how to make it. Both failing branches name a NEW fragment to create under `tests/emitted-drift-acks/` (#2914; pick a name nobody else is using), say it may not exist yet (absence is the healthy steady state), print a minimal valid document, and repeat \"do NOT regenerate anything\" — post-#2724 there is nothing left to regenerate, and hunting for a deleted baseline is the predictable wrong guess. The two branches key on DIFFERENT spaces and each says which: the hash pass keys on the EMITTED PATH (always contains a `/`), the size ratchet keys on the BARE FILENAME (`currentSizes` writes `sizes[entry.name]` from readdirSync over `gsd-core/workflows/` + `agents/`). A stale-ack failure additionally says to delete the FILE when removing its last entry, since an empty-but-present ack parses fine yet signals nothing; post-#2789 it also offers CORRECTING the entry to name the ripple actually made, which is the other honest resolution and the one a contributor usually wants. NOT ack-able and deliberately given no ack text: the `NEW_FILE_CAP` branch, whose remedy is extraction. Text is sourced from one frozen `REMEDIATION` export in tests/helpers/emitted-diff.cjs whose example document is rendered from `ACK_VERSION` via `JSON.stringify`, so the taught schema cannot drift from the accepted one (a round-trip test feeds the printed document back through `parseAck`); the message teaches ONE canonical shape even though `parseAck` also accepts a bare-string reason and a missing `version` — liberal in what it accepts, conservative in what it sends. Note the ADR's Consequences originally called the #2724 migration \"terminal\"; #2778 corrected that — it is terminal only for a PR that grows no shipped file. #2914 replaced the single shared ack file with per-PR fragments under `tests/emitted-drift-acks/` — exactly the shape `.changeset/` already uses for the identical \"every PR rewrites one shared document\" conflict problem — so two PRs needing an ack can no longer collide with each other, and a fragment left on `next` after merge is inert rather than a shared cell; the legacy file is still read and unioned in for branches that predate the split, and a duplicate path key across two sources is a hard, loudly-reported error, never silent last-wins. `tests/emitted-drift-ack.json` (the LEGACY file specifically, NOT the fragment directory) must NEVER persist on `next` (#2914): every entry is scoped to the diff that introduced it, so once merged it is by definition already at the base — spent and inert regardless of shape — and a persistent copy makes that ONE file a shared merge-conflict cell across every open PR that also carries an ack, exactly the \"140 of 143\" cost this whole cutover exists to remove; a persisting FRAGMENT is harmless by construction and is deliberately not what this guard checks. This is enforced on `next` itself only, never as a PR-lane check: the `guard-no-ack-on-next` job in `.github/workflows/test.yml` (push-to-`next` trigger) runs `scripts/lint-emitted-drift-ack.cjs --guard-next` (`assertAbsentOnNext`), which fails on the LEGACY file's PRESENCE alone, valid or not — a PR-lane \"base ack must be absent\" check would red every open PR the instant a spent ack merged, which is the #2768 shape #2789 already ended. cf `RULESET.WORKFLOW_SIZE_BUDGET`, `RULESET.AGENT_SIZE_BUDGET`; see `### Emitted Artifact Provenance`" + "value": "the emitted-artifact family (ADR-2719, epic #2719) — POST-CUTOVER (#2724, Phase 4). Historically tests/fixtures/golden-install-parity/*.json (19 path→hash manifests) + tests/workflow-size-baseline.json + tests/agent-size-baseline.json were all committed, PURE FUNCTIONS of the source tree whose correct merge was ALWAYS \"recompute\" — 140 of 143 conflicted-file instances across the open PR queue were these files. #2724 DELETES all three, the golden test (tests/golden-install-parity.test.cjs), the generator (scripts/gen-golden-install-parity-zcode.cjs), `npm run gen:golden`, `UPDATE_GOLDEN`, the merge-driver bridge (scripts/git-merge-regen-driver.cjs, `npm run setup:merge-driver`, the .gitattributes merge=gsd-regen block), and scripts/update-size-baseline.cjs (`npm run size:baseline`). The differential attribution check (tests/emitted-attribution.test.cjs + tests/emitted-provenance.test.cjs) is now the SOLE gate for emitted-artifact propagation AND size growth — no committed artifact, nothing to hand-merge, nothing to regenerate. `npm run regen:derived` still exists for what remains committed and derived: build, registry, ADR index, capability matrix, inventory manifest, manifest versions, and `tests/fixtures/install-tree/*.json` (now `npm run gen:install-tree`, folded into `regen:derived`). tests/fixtures/install-tree/*.json is DELIBERATELY EXCLUDED from the cutover (ADR-2719 §7): it conflicts on 0 of 7, its diffs are readable, and it preserves \"the installer stopped shipping X\" as a hard absolute failure — capturing it would convert that absolute into an attribution-free auto-resolve. The baseline the differential compares against is now published by `scripts/gen-emitted-baseline.cjs` on every push to `next` (cached, keyed on sha) and restored in PR lanes via `GSD_EMITTED_BASELINE`/`resolveBaseline()` (tests/helpers/emitted-baseline.cjs); a cache miss falls back to an in-job build via a throwaway `git worktree` (tests/helpers/emitted-runtime.cjs's `buildBaselineAtRef`). REMEDIATION IS PART OF THE GATE (#2778): the failure output names its own remedy, because a gate that states a requirement and withholds the means of satisfying it is a maintainer round-trip, not a gate — ADR-2719 §3's \"conspicuous declaration\" only works if the contributor can discover how to make it. Both failing branches name a NEW fragment to create under `tests/emitted-drift-acks/` (#2914; pick a name nobody else is using), say it may not exist yet (absence is the healthy steady state), print a minimal valid document, and repeat \"do NOT regenerate anything\" — post-#2724 there is nothing left to regenerate, and hunting for a deleted baseline is the predictable wrong guess. The two branches key on DIFFERENT spaces and each says which: the hash pass keys on the EMITTED PATH (always contains a `/`), the size ratchet keys on the BARE FILENAME (`currentSizes` writes `sizes[entry.name]` from readdirSync over `gsd-core/workflows/` + `agents/`). A stale-ack failure additionally says to delete the FILE when removing its last entry, since an empty-but-present ack parses fine yet signals nothing; post-#2789 it also offers CORRECTING the entry to name the ripple actually made, which is the other honest resolution and the one a contributor usually wants. NOT ack-able and deliberately given no ack text: the `NEW_FILE_CAP` branch, whose remedy is extraction. Text is sourced from one frozen `REMEDIATION` export in tests/helpers/emitted-diff.cjs whose example document is rendered from `ACK_VERSION` via `JSON.stringify`, so the taught schema cannot drift from the accepted one (a round-trip test feeds the printed document back through `parseAck`); the message teaches ONE canonical shape even though `parseAck` also accepts a bare-string reason and a missing `version` — liberal in what it accepts, conservative in what it sends. Note the ADR's Consequences originally called the #2724 migration \"terminal\"; #2778 corrected that — it is terminal only for a PR that grows no shipped file. #2914 replaced the single shared ack file with per-PR fragments under `tests/emitted-drift-acks/` — exactly the shape `.changeset/` already uses for the identical \"every PR rewrites one shared document\" conflict problem — so two PRs needing an ack can no longer collide with each other on the FILE; the legacy file is still read and unioned in for branches that predate the split, and a duplicate path key across two sources is a hard, loudly-reported error, never silent last-wins — #3078 made that error name its two resolutions (git rm an already-merged, spent owner; APPEND prose to a still-live one, which re-arms it), because the guard runs post-merge and cannot stop the colliding PR. `tests/emitted-drift-ack.json` (the LEGACY file specifically) must NEVER persist on `next` (#2914): every entry is scoped to the diff that introduced it, so once merged it is by definition already at the base — spent and inert regardless of shape — and a persistent copy makes that ONE file a shared merge-conflict cell across every open PR that also carries an ack, exactly the \"140 of 143\" cost this whole cutover exists to remove; #2914 asserted a persisting FRAGMENT was harmless by construction and deliberately exempted the directory; #3078 REVERSED that — fragments do not share a FILE but they DO share a PATH KEY SPACE, so a fully-spent fragment on `next` owns keys it can no longer gate and the next PR growing one of those paths can declare it neither there (spent) nor in its own (duplicate), which is the #2914 wall one level down (measured at the sweep: 45 fragments owning 403 paths, up from 13/272 at triage 19 days earlier). A fragment is judged on INERTNESS, not presence: swept once EVERY entry is spent, left alone while PARTIALLY spent — the asymmetry is what keeps the re-arm-by-appending route (#2639, #2993) working, and the `0000` legacy-migration bucket #2923 created for the old shared file's 35 entries was NOT permanent (the issue's own open question resolved to NO) and went with the rest. This is enforced on `next` itself only, never as a PR-lane check: the `guard-no-ack-on-next` job in `.github/workflows/test.yml` (push-to-`next` trigger) runs `scripts/lint-emitted-drift-ack.cjs --guard-next`, which is now BOTH halves — `assertAbsentOnNext` (legacy file, fails on PRESENCE alone, valid or not) and `assertNoAllSpentFragments` (fragments, fails on all-entries-spent vs the copy at the PRE-PUSH TIP of next — CI passes `github.event.before` via `--base-ref`, because the default branch allows REBASE merges so one push can carry N commits and a bare `HEAD^` would flag a fragment the same push introduced; `HEAD^` remains only the local/manual fallback, using the SAME zero-width/whitespace-stripping prose comparison as `isSpent` so an invisible reword cannot fake a re-arm; duplicated across the scripts-ship/tests-do-not line and held by a parity test). The job's checkout REQUIRES `fetch-depth: 2` plus an explicit `git fetch --depth=1 origin $BEFORE` — at depth 1 no base commit exists locally, every fragment reads as brand-new, and the guard passes vacuously, which is exactly how the legacy half went blind after #2914 removed the file it was watching. The gate's `INVISIBLE`/`normalizeAckReason` are EXPORTED from tests/helpers/emitted-diff.cjs for the sole purpose of letting the parity test compare them against the script's duplicate; before #3078 neither was exported, so the \"parity test\" the comments promised was a tautology checking the script against itself. A PR-lane \"base ack must be absent\" check would red every open PR the instant a spent ack merged, which is the #2768 shape #2789 already ended — so this alerts AFTER the merge by design and never stops the offending PR. cf `RULESET.WORKFLOW_SIZE_BUDGET`, `RULESET.AGENT_SIZE_BUDGET`; see `### Emitted Artifact Provenance`" }, { "id": "RULESET.GENERATIVE-FIX", diff --git a/docs/TESTING-SUITES.md b/docs/TESTING-SUITES.md index 65feb9a51..4686c3ca3 100644 --- a/docs/TESTING-SUITES.md +++ b/docs/TESTING-SUITES.md @@ -142,15 +142,24 @@ The differential attribution check reports the file and the byte delta. To resol section and `CONTEXT.md`'s `### Emitted Artifact Provenance` entry. Name the fragment for your issue or PR (something nobody else is using) — the failure output prints a minimal valid document you can paste. This is deliberately a - per-PR fragment, not one shared file: fragments can never conflict across - PRs, and a fragment appearing in your diff *is* the visible signal. If the - failure instead names a path a merged PR already acknowledged (a **spent** - entry sitting in an existing fragment), reword that fragment's `reason` in - place to explain the new ripple — do not add a duplicate entry for the same - path; two ack sources naming the same path is a hard, loudly-reported error. + per-PR fragment, not one shared file: two fragments can never *merge-conflict* + with each other, and a fragment appearing in your diff *is* the visible signal. + They do, however, share a path key space. If the failure instead names a path a + merged PR already acknowledged (a **spent** entry sitting in an existing + fragment), you have two routes and the error text names both: `git rm` that + fragment if every entry in it is spent — it gates nothing and only holds the + keys — or, if it is still live, reword/extend its `reason` in place to explain + the new ripple. Either way, do not add a duplicate entry for the same path; two + ack sources naming the same path is a hard, loudly-reported error. The legacy single `tests/emitted-drift-ack.json` is still read and unioned in for branches that carry it, but new acknowledgments never go there. -3. **Or shrink it instead of acknowledging.** Prefer extraction when the growth +3. **Delete your fragment once it has merged (#3078).** A fragment on `next` is + spent by definition — its prose is already at the base, so it can no longer + clear anything — while still owning its path keys, which walls off the next PR + that grows one of them. The `guard-no-ack-on-next` job reds `next` and prints + the exact `git rm` for every fully-spent fragment. A *partially* spent fragment + is deliberately left alone. +4. **Or shrink it instead of acknowledging.** Prefer extraction when the growth is incidental: for a workflow, move per-mode bodies to `workflows//modes/`, templates to `workflows//templates/`, and shared prose to `gsd-core/references/`; for an agent, lift shared boilerplate diff --git a/examples/dynamic-context-management/CONTEXT-INDEX.json b/examples/dynamic-context-management/CONTEXT-INDEX.json index 2ecb66750..cd02a3da1 100644 --- a/examples/dynamic-context-management/CONTEXT-INDEX.json +++ b/examples/dynamic-context-management/CONTEXT-INDEX.json @@ -28,1567 +28,1567 @@ "id": "ARCH.SKILL.improve-codebase.next-candidates", "klass": "ARCH", "value": "[Workstream Progress Projection Module]", - "line": 658 + "line": 661 }, { "id": "CI.GATE.changeset-lint", "klass": "CI", "value": "hard-fail for user-facing code diffs unless .changeset/* or PR has no-changelog label", - "line": 642 + "line": 645 }, { "id": "CI.GATE.issue-link-required", "klass": "CI", "value": "hard-fail if PR body lacks closes/fixes/resolves #", - "line": 641 + "line": 644 }, { "id": "CONFIG.LOCATION.SEAM.in-process-scrub", "klass": "CONFIG", "value": "TEST_ENV_BASE reaches CHILD env only; a test calling install() IN-PROCESS must additionally use helpers.scrubConfigLocationEnv() in beforeEach + its restorer in afterEach — HOME/USERPROFILE sandboxing is NOT sufficient because getGlobalConfigDir is env-FIRST", - "line": 676 + "line": 679 }, { "id": "CONFIG.LOCATION.SEAM.kimi-two-homes", "klass": "CONFIG", "value": "kimi declares TWO config-location vars: KIMI_CONFIG_DIR (registry, generic Agent-Skills root via resolveKimiGlobalDir) and KIMI_SHARE_DIR (KIMI_HOOKS_TOML_DESCRIPTOR, kimi's OWN native config.toml carrying GSD's [[hooks]] block via resolveKimiHooksTomlDir); a registry-only derivation covers the first and silently misses the second", - "line": 675 + "line": 678 }, { "id": "CONFIG.LOCATION.SEAM.scrub-set", "klass": "CONFIG", "value": "tests/helpers.cjs CONFIG_LOCATION_ENV_KEYS is DERIVED from five sources rather than maintained as one hand-written list (source 4 IS a literal residue list, for vars that fit no other rung — what is never hand-listed is the SET): capability-registry runtimes[].runtime.configHome.env AND [].configHome.skillsHome.env + runtime-homes NON_REGISTRY_CONFIG_HOME_DESCRIPTORS[].env AND [].skillsHome.env (a descriptor is a descriptor — BOTH descriptor rungs walk skillsHome, which resolves independently via resolveSkillsBaseFromDescriptor) + runtime-homes GSD_LOCATION_ENV_KEYS + a residue list (GROK_AGENTS_HOME, GSD_RUNTIME, GSD_PROJECT, GSD_WORKSTREAM) + WRITE_ESCAPE_PERMISSION_ENV_KEYS (GSD_ALLOW_SYMLINKED_DEST — a permission, not a location: it names no path but disarms the symlink-escape guard, so blanking it makes the guard STRICTER, never looser); adding a config-location var means making it ENUMERABLE at one of those sources, not appending a literal", - "line": 673 + "line": 676 }, { "id": "CONFIG.LOCATION.SEAM.two-families", "klass": "CONFIG", "value": "runtime configHomes (where a third-party runtime keeps config, registry- or descriptor-declared) and GSD's OWN location vars (GSD_HOME -> $GSD_HOME/.gsd store, GSD_AGENTS_DIR -> getAgentsDir priority 1) are DISTINCT families; no registry derivation reaches the second, and treating a miss there as a registry gap is what produced review round 2", - "line": 674 + "line": 677 }, { "id": "CONFIG.SEAM.loadConfig-context", "klass": "CONFIG", "value": "loadConfig(cwd,{workstream}) replaces env-mutation fallback; no temporary process.env GSD_WORKSTREAM rewrites", - "line": 672 + "line": 675 }, { "id": "EXEC.CLASSIFY.classes", "klass": "EXEC", "value": "{class:'quota-exceeded'|'classify-handoff-bug'|'unknown-failure', sentinel?, retryAfterSeconds?}", - "line": 891 + "line": 894 }, { "id": "EXEC.CLASSIFY.cross-runtime", "klass": "EXEC", "value": "Anthropic/CC: usage limit|rate limit|quota|429|retry-after; Copilot CLI: rate_limit (stem); Codex CLI: 429|usage_limit_reached|too many requests", - "line": 893 + "line": 896 }, { "id": "EXEC.CLASSIFY.handler", "klass": "EXEC", "value": "gsd-core/bin/lib/agent-command-router.cjs:classifyAgentFailure (registered via command-aliases.cjs; mutation:false outputMode:json)", - "line": 889 + "line": 892 }, { "id": "EXEC.CLASSIFY.precedence", "klass": "EXEC", "value": "quota sentinel wins over classifyHandoffIfNeeded bug when both appear", - "line": 894 + "line": 897 }, { "id": "EXEC.CLASSIFY.proactive-signal-not-usable", "klass": "EXEC", "value": "Anthropic exposes anthropic-ratelimit-* headers + Agent SDK RateLimitEvent; Claude Code subprocess does NOT forward to hooks/statusline today (upstream #33820, #22407, #32796)", - "line": 896 + "line": 899 }, { "id": "EXEC.CLASSIFY.retry-after-parser", "klass": "EXEC", "value": "\\bretry[-_ ]after[:\\s]+(\\d+)\\b avoids embedded-word false matches like noretry-after", - "line": 895 + "line": 898 }, { "id": "EXEC.CLASSIFY.sentinel-order", "klass": "EXEC", "value": "most specific first: 429 beats too-many-requests; resource_exhausted beats quota (array order in src/agent-command-router.cts QUOTA_SENTINELS checks resource_exhausted before quota); case-insensitive; canonical sentinel value is lower-cased form", - "line": 892 + "line": 895 }, { "id": "EXEC.CLASSIFY.workflow", "klass": "EXEC", "value": "gsd-core/workflows/execute-phase.md step 7; class-distinct prompts (quota-to-wait-for-reset; classify-handoff-bug-to-spot-check; unknown-to-continue/stop)", - "line": 890 + "line": 893 }, { "id": "GSD-RESEARCH.CONTEXT-DISCIPLINE", "klass": "GSD-RESEARCH", "value": "less-context levers: subagent isolation + compact provider output + fetches-to-disk + cache-returns-digest; API clear_tool_uses/memory tool are the conceptual model, not a Claude Code harness knob", - "line": 429 + "line": 432 }, { "id": "GSD-RESEARCH.INTEGRATION.L2-hybrid", "klass": "GSD-RESEARCH", "value": "code owns cache+legitimacy+confidence+provider-pick (gsd-tools query research-plan/research-store/package-legitimacy); MCP owns the fetch; agent returns RESEARCH.md path, never raw fetches", - "line": 427 + "line": 430 }, { "id": "GSD-RESEARCH.MODULE.package-legitimacy", "klass": "GSD-RESEARCH", "value": "registry-API verdicts (npm/PyPI/crates.io injectable adapters) computed from thresholds {minAgeDays:30,minWeeklyDownloads:1000,requireRepo:true}; verdict OK|SUS|SLOP per package; slopcheck=optional adapter that can only escalate, never the install-or-degrade gate", - "line": 426 + "line": 429 }, { "id": "GSD-RESEARCH.MODULE.research-provider", "klass": "GSD-RESEARCH", "value": "single source of truth PROVIDER_WATERFALL (docs Context7->Ref->Jina->websearch; web Exa->Tavily->Perplexity->Brave->websearch; scrape Firecrawl->Jina); planResearch returns cache-hits+fetch-plan; classifyConfidence stamps HIGH|MEDIUM|LOW by provider AUTHORITY + verification EVIDENCE (HIGH requires code-computed ground-truth corroboration e.g. legitimacyVerdict OK; provider authority alone caps at MEDIUM; SLOP caps at LOW); Firecrawl is scrape-only (not in the docs or web legs)", - "line": 425 + "line": 428 }, { "id": "GSD-RESEARCH.MODULE.research-store", "klass": "GSD-RESEARCH", "value": "content-addressed cache; key=sha256(ecosystem+library+version+query+kind); getResearch->{hit,stale} never throws (mirrors graphify staleness); ttlForSource curated HIGH 30d|MED 7d|web LOW 1d; tiers: curated-doc kinds -> ~/.gsd/research-cache (cross-project), web/synthesis -> project .planning/research/.cache", - "line": 424 + "line": 427 }, { "id": "GSD-RESEARCH.PROVIDER.availability", "klass": "GSD-RESEARCH", "value": "config flags brave_search/exa_search/firecrawl/tavily_search/ref_search/perplexity/jina (env _API_KEY or ~/.gsd/_api_key); context7/jina/websearch always available; planResearch falls through waterfall to websearch terminal", - "line": 428 + "line": 431 }, { "id": "LEARNING.prompt-budget.boundary-gap", "klass": "LEARNING", "value": "PR #3708 commit 2df566ed reserved NOTE_RESERVE_TOKENS in pressure-threshold AND in minSet pre-check; both buggy paths only fire when baseTokens ∈ (effectiveBudget - NOTE_RESERVE_TOKENS, effectiveBudget]; original test suite used budgets far from that band so neither path was exercised; fix bde1ae8f confines NOTE_RESERVE accounting to post-trim assembly path only; future budget/limit code MUST add boundary fixtures per RULESET.TESTS.boundary-coverage.fixtures", - "line": 589 + "line": 592 }, { "id": "LIVE-CONFIG.GUARD.SEAM.ci-blind", "klass": "LIVE-CONFIG", "value": "the AMBIENT-ENV half stays CI-blind — CI never has these vars set, so green CI is not evidence for it; what strict mode catches in CI is the suite's own default-root leaks (HOME/USERPROFILE-derived), the guard remains the only loud signal for ambient-var escapes", - "line": 682 + "line": 685 }, { "id": "LIVE-CONFIG.GUARD.SEAM.module", "klass": "LIVE-CONFIG", "value": "scripts/live-config-guard.cjs (deliberately NOT scripts/lib/, which the installer copies to users wholesale while uninstall removes only an allowlist; excluded from the npm tarball via package.json files[] together with its whole require chain run-tests.cjs/affected-tests-lib.cjs/run-affected-tests.cjs — a partial exclusion trips the #2858 shipped-requires-only-shipped gate); exports [resolveLiveConfigRoots, resolveExtraWatchTargets, snapshotLiveConfig, diffLiveConfig, formatViolations, newestMtime]; driven by scripts/run-tests.cjs pre/post suite", - "line": 677 + "line": 680 }, { "id": "LIVE-CONFIG.GUARD.SEAM.non-root-targets", "klass": "LIVE-CONFIG", "value": "resolveExtraWatchTargets covers THREE live write surfaces that are not runtime config ROOTS (skills bases are a DELIBERATE non-target — the config-root layout misfires beneath them, so they need their own layout): $GSD_HOME/.gsd watched WHOLESALE (exclusively GSD-owned, so the shared-root trap does not apply) plus ONE config.toml per NON_REGISTRY_CONFIG_HOME_DESCRIPTORS entry, each watched as a SINGLE FILE (those roots belong to their products) — today three targets, since #2755 split Kimi CLI (~/.kimi, KIMI_SHARE_DIR) from Kimi Code (~/.kimi-code, KIMI_CODE_HOME); the targets are DERIVED by iterating that array, never by calling a named resolver, so a further descriptor is picked up without editing the guard PROVIDED it owns the same NON_REGISTRY_OWNED_FILE ('config.toml') — one that owns a different filename needs a per-descriptor mapping, the named residual the guard states at its own definition. SECOND RESIDUAL: config.toml is not all GSD writes into those roots — installSharedHooksBundle also populates /hooks/, which is UNWATCHED; closing it is a layout decision, like skills bases; passed to snapshotLiveConfig explicitly so a fixture-root caller cannot pull the real ~/.gsd into its snapshot", - "line": 679 + "line": 682 }, { "id": "LIVE-CONFIG.GUARD.SEAM.scope", "klass": "LIVE-CONFIG", "value": "ownership-based, never whole-root: GSD_OWNED_ENTRIES top-level footprint + children whose name startsWith GSD_ARTIFACT_PREFIX ('gsd-') under GSD_PREFIXED_PARENTS (dirs shared with the host agent); watching a shared root wholesale false-positives on the host's own writes and a guard that cries wolf gets disabled", - "line": 678 + "line": 681 }, { "id": "LIVE-CONFIG.GUARD.SEAM.severity", "klass": "LIVE-CONFIG", "value": "reports by default locally; CI wires GSD_STRICT_LIVE_CONFIG_GUARD=1 on Linux/macOS lanes (test.yml, all three test jobs) so a suite-produced leak FAILS those runs; Windows lanes stay report-only pending the documented pre-existing USERPROFILE sweep (~190 test sites sandbox HOME alone) — promote once that lands; skipped by GSD_SKIP_LIVE_CONFIG_GUARD=1", - "line": 681 + "line": 684 }, { "id": "LIVE-CONFIG.GUARD.SEAM.truncation", "klass": "LIVE-CONFIG", "value": "MAX_ENTRIES/MAX_DEPTH bound the walk; a bound hit sets truncated and diffLiveConfig emits kind:'unverified' — a truncated scan MUST NOT read as clean; boundary covered at {limit-1,limit,limit+1} via newestMtime's injected budget plus fast-check monotonicity, per RULESET.TESTS.boundary-coverage + RULESET.TESTS.property-based-testing", - "line": 680 + "line": 683 }, { "id": "META.RULE.brief-must-cite-doc", "klass": "META", "value": "agent prompts MUST quote the canonical doc line being applied; paraphrasing from predicate memory drifts and produces violations", - "line": 733 + "line": 736 }, { "id": "META.RULE.brief-no-paraphrase", "klass": "META", "value": "writing \"k040 — never leave changelog box unchecked\" caused 5 of 8 agents to edit CHANGELOG.md in violation of CONTRIBUTING.md L110", - "line": 734 + "line": 737 }, { "id": "META.RULE.canonical-source-precedence", "klass": "META", "value": "CONTRIBUTING.md > docs/adr/* > CONTEXT.md > agent memory", - "line": 731 + "line": 734 }, { "id": "META.RULE.read-contributing-first", "klass": "META", "value": "read CONTRIBUTING.md sections \"Pull Request Guidelines\" + \"CHANGELOG Entries\" before EVERY agent dispatch", - "line": 732 + "line": 735 }, { "id": "PLANNING.PATH.PARITY.project-scope", "klass": "PLANNING", "value": ".planning/ (never .planning/projects/); mirror planning-workspace.cjs planningDir()", - "line": 667 + "line": 670 }, { "id": "PLANNING.PATH.SEAM.helpers", "klass": "PLANNING", "value": "helpers.planningPaths delegates to workspacePlanningPaths + resolveWorkspaceContext; precedence explicit-ws > env-ws > env-project > root", - "line": 668 + "line": 671 }, { "id": "PLANNING.PATH.SEAM.init-handlers", "klass": "PLANNING", "value": "[initExecutePhase, initPlanPhase, initPhaseOp, initMilestoneOp] consume helpers.planningPaths().planning (no direct relPlanningPath join)", - "line": 669 + "line": 672 }, { "id": "PR.3267.POSTMORTEM.recovery", "klass": "PR", "value": "[issue#3270 created, label approved-enhancement applied, PR reopened, body includes \"Closes #3270\", label no-changelog applied]", - "line": 646 + "line": 649 }, { "id": "PR.3267.POSTMORTEM.root-cause", "klass": "PR", "value": "[missing issue link, missing changeset/no-changelog]", - "line": 645 + "line": 648 }, { "id": "PRED.k320.canonical-source", "klass": "PRED", "value": "CONTRIBUTING.md L193-211", - "line": 737 + "line": 740 }, { "id": "PRED.k320.ci-enforcement", "klass": "PRED", "value": "scripts/changeset/lint.cjs", - "line": 743 + "line": 746 }, { "id": "PRED.k320.ci-paths-monitored", "klass": "PRED", "value": "bin/ gsd-core/ src/ agents/ commands/ hooks/ sdk/src/ sdk/prompts/", - "line": 744 + "line": 747 }, { "id": "PRED.k320.cure", "klass": "PRED", "value": "drop .changeset/--.md fragment ONLY", - "line": 739 + "line": 742 }, { "id": "PRED.k320.evidence", "klass": "PRED", "value": "PR #3302 merge-conflict against #3308 CHANGELOG.md row 2026-05-09", - "line": 746 + "line": 749 }, { "id": "PRED.k320.opt-out-label", "klass": "PRED", "value": "no-changelog", - "line": 742 + "line": 745 }, { "id": "PRED.k320.recovery", "klass": "PRED", "value": "open Removed-typed cleanup PR deleting only the redundant row", - "line": 745 + "line": 748 }, { "id": "PRED.k320.rule", "klass": "PRED", "value": "do not edit CHANGELOG.md in feature/fix/enhancement PRs", - "line": 738 + "line": 741 }, { "id": "PRED.k320.signal", "klass": "PRED", "value": "changelog-direct-edit-forbidden", - "line": 736 + "line": 739 }, { "id": "PRED.k320.tool", "klass": "PRED", "value": "npm run changeset -- --type --pr --body \"...\"", - "line": 740 + "line": 743 }, { "id": "PRED.k320.types", "klass": "PRED", "value": "Added|Changed|Deprecated|Removed|Fixed|Security", - "line": 741 + "line": 744 }, { "id": "PRED.k321.evidence", "klass": "PRED", "value": "PRs #3304/#3305 (2026-05-09): real Minor/Major findings in body, 0 threads", - "line": 752 + "line": 755 }, { "id": "PRED.k321.poll-shape", "klass": "PRED", "value": "parse pulls//reviews body AND graphql reviewThreads", - "line": 750 + "line": 753 }, { "id": "PRED.k321.resolution", "klass": "PRED", "value": "address in code; no GraphQL resolveReviewThread needed for body-only findings", - "line": 751 + "line": 754 }, { "id": "PRED.k321.shape", "klass": "PRED", "value": "CR posts \"[!CAUTION] outside the diff\" findings in review BODY, not in reviewThreads", - "line": 749 + "line": 752 }, { "id": "PRED.k321.signal", "klass": "PRED", "value": "cr-outside-diff-range-finding", - "line": 748 + "line": 751 }, { "id": "PRED.k322.cure-1", "klass": "PRED", "value": "2nd retrigger ~10min after first ack", - "line": 757 + "line": 760 }, { "id": "PRED.k322.cure-2", "klass": "PRED", "value": "if silent at 50min, treat as silent-pass with maintainer flag in merge-commit body", - "line": 758 + "line": 761 }, { "id": "PRED.k322.distinct-from", "klass": "PRED", "value": "k080", - "line": 755 + "line": 758 }, { "id": "PRED.k322.evidence", "klass": "PRED", "value": "PR #3306 (2026-05-09): 0 reviews after 50min + 2 retriggers", - "line": 760 + "line": 763 }, { "id": "PRED.k322.merge-gate-impact", "klass": "PRED", "value": "k070 real_coderabbit_review_present unsatisfied; requires maintainer judgment", - "line": 759 + "line": 762 }, { "id": "PRED.k322.shape", "klass": "PRED", "value": "ack posted, real review never lands within [5s, 410s] cooldown after burst of N PRs <15min", - "line": 756 + "line": 759 }, { "id": "PRED.k322.signal", "klass": "PRED", "value": "cr-sustained-throttle", - "line": 754 + "line": 757 }, { "id": "PRED.k323.cure-alt", "klass": "PRED", "value": "consolidate into single PR when 2+ issues share root cause", - "line": 765 + "line": 768 }, { "id": "PRED.k323.cure-pre-dispatch", "klass": "PRED", "value": "brief one agent canonical-owner; brief others to EXCLUDE shared site", - "line": 764 + "line": 767 }, { "id": "PRED.k323.evidence", "klass": "PRED", "value": "#3300 (#3297) overlapped #3306 (#3298) on add-backlog.md hunks 2026-05-09", - "line": 767 + "line": 770 }, { "id": "PRED.k323.recovery", "klass": "PRED", "value": "close smaller PR as \"subsumed by #N\" or rebase second to drop overlap hunk", - "line": 766 + "line": 769 }, { "id": "PRED.k323.shape", "klass": "PRED", "value": "2+ open issues touch same canonical bug site; each fix's sibling-audit produces overlapping diff", - "line": 763 + "line": 766 }, { "id": "PRED.k323.signal", "klass": "PRED", "value": "sibling-audit-cross-pr-overlap", - "line": 762 + "line": 765 }, { "id": "PRED.k324.cure", "klass": "PRED", "value": "verify via gh api on every agent-completion notification; never trust narrative", - "line": 771 + "line": 774 }, { "id": "PRED.k324.evidence", "klass": "PRED", "value": "2026-05-09 session: 5+ mid-monitor terminations across PRs #3232/#3271/#3251/#3255/#3262", - "line": 773 + "line": 776 }, { "id": "PRED.k324.k095-restatement", "klass": "PRED", "value": "k095 confirmed shape: agent reports \"waiting for monitor\" / \"tests still running\" then terminates", - "line": 770 + "line": 773 }, { "id": "PRED.k324.poll-shape", "klass": "PRED", "value": "gh pr view --json mergeStateStatus,statusCheckRollup + pulls//reviews + graphql reviewThreads + issues//comments tail", - "line": 772 + "line": 775 }, { "id": "PRED.k324.signal", "klass": "PRED", "value": "agent-terminates-mid-monitor", - "line": 769 + "line": 772 }, { "id": "PRED.k325.cleanup", "klass": "PRED", "value": "git worktree remove --force for aged agent worktrees", - "line": 778 + "line": 781 }, { "id": "PRED.k325.cure", "klass": "PRED", "value": "detached-HEAD: git checkout --detach $(git ls-remote origin ); modify; commit; git push --force-with-lease=: origin HEAD:refs/heads/", - "line": 777 + "line": 780 }, { "id": "PRED.k325.evidence", "klass": "PRED", "value": "2026-05-09 CHANGELOG.md strip on PRs #3300/#3302/#3304/#3305 required detached-HEAD", - "line": 779 + "line": 782 }, { "id": "PRED.k325.shape", "klass": "PRED", "value": "git checkout errors \"already used by worktree at \"", - "line": 776 + "line": 779 }, { "id": "PRED.k325.signal", "klass": "PRED", "value": "worktree-branch-lock-on-force-push", - "line": 775 + "line": 778 }, { "id": "PRED.k326.cure", "klass": "PRED", "value": "quote canonical doc verbatim in brief; mentally simulate \"if all N agents follow this brief literally, do they violate any rule?\"", - "line": 783 + "line": 786 }, { "id": "PRED.k326.evidence", "klass": "PRED", "value": "2026-05-09 brief \"k040 — update CHANGELOG.md\" → 5 of 8 agents violated CONTRIBUTING.md L110", - "line": 784 + "line": 787 }, { "id": "PRED.k326.shape", "klass": "PRED", "value": "N parallel agents amplify a single brief-vs-doc contradiction into N violations", - "line": 782 + "line": 785 }, { "id": "PRED.k326.signal", "klass": "PRED", "value": "brief-contradicts-canonical-doc", - "line": 781 + "line": 784 }, { "id": "PRED.k327.ack-shape", "klass": "PRED", "value": "body \"✅ Actions performed - Full review triggered\"", - "line": 787 + "line": 790 }, { "id": "PRED.k327.cooldown-normal", "klass": "PRED", "value": "[5s, 410s]", - "line": 790 + "line": 793 }, { "id": "PRED.k327.cooldown-throttled", "klass": "PRED", "value": "k322", - "line": 791 + "line": 794 }, { "id": "PRED.k327.distinguish-key", "klass": "PRED", "value": "len(pulls//reviews) — ack=0, real=≥1", - "line": 789 + "line": 792 }, { "id": "PRED.k327.real-review-shape", "klass": "PRED", "value": "body starts \"Actionable comments posted: N\" OR \"[!CAUTION] Some comments are outside the diff\"", - "line": 788 + "line": 791 }, { "id": "PRED.k327.signal", "klass": "PRED", "value": "cr-ack-vs-real-review", - "line": 786 + "line": 789 }, { "id": "PRED.k328.audit-list", "klass": "PRED", "value": "[heading-matches-class, closing-keyword-present, changeset-fragment-or-no-changelog-label]", - "line": 796 + "line": 799 }, { "id": "PRED.k328.canonical-source", "klass": "PRED", "value": "CONTRIBUTING.md L48,L64,L81 (template links) + .github/PULL_REQUEST_TEMPLATE/{fix,enhancement,feature}.md L1 (heading text)", - "line": 794 + "line": 797 }, { "id": "PRED.k328.k100-restatement", "klass": "PRED", "value": "heading must match issue class: bug→## Fix PR, enhancement→## Enhancement PR, feature→## Feature PR", - "line": 795 + "line": 798 }, { "id": "PRED.k328.signal", "klass": "PRED", "value": "pr-template-typed-heading-required", - "line": 793 + "line": 796 }, { "id": "PRED.k329.body", "klass": "PRED", "value": "**** — . (#)", - "line": 802 + "line": 805 }, { "id": "PRED.k329.canonical-source", "klass": "PRED", "value": "CONTRIBUTING.md L196-202 + .changeset/README.md", - "line": 799 + "line": 802 }, { "id": "PRED.k329.filename", "klass": "PRED", "value": ".changeset/--.md", - "line": 800 + "line": 803 }, { "id": "PRED.k329.frontmatter", "klass": "PRED", "value": "---\\\\ntype: \\\\npr: \\\\n---", - "line": 801 + "line": 804 }, { "id": "PRED.k329.observed-clean", "klass": "PRED", "value": "#3299 sunny-ibex-wave, #3301 sturdy-rams-caper, #3306 3298-phase-dir-prefix-drift-workflows", - "line": 803 + "line": 806 }, { "id": "PRED.k329.signal", "klass": "PRED", "value": "changeset-fragment-canonical-shape", - "line": 798 + "line": 801 }, { "id": "PRED.k330.fallback", "klass": "PRED", "value": "append predicate-format findings directly to CONTEXT.md", - "line": 807 + "line": 810 }, { "id": "PRED.k330.shape", "klass": "PRED", "value": "mempalace MCP tools require explicit user call; AI cannot trigger", - "line": 806 + "line": 809 }, { "id": "PRED.k330.signal", "klass": "PRED", "value": "mempalace-diary-not-callable-by-ai", - "line": 805 + "line": 808 }, { "id": "PRED.k331.cure", "klass": "PRED", "value": "gh pr close with NO --comment flag", - "line": 812 + "line": 815 }, { "id": "PRED.k331.evidence", "klass": "PRED", "value": "2026-05-09 wave-3: violation on #3300 close, deleted within 30s", - "line": 814 + "line": 817 }, { "id": "PRED.k331.k101-restatement", "klass": "PRED", "value": "k101 includes close-time --comment flag; rationale belongs in subsuming PR's squash-merge body", - "line": 811 + "line": 814 }, { "id": "PRED.k331.recovery", "klass": "PRED", "value": "if violation lands, gh api -X DELETE repos///issues/comments/", - "line": 813 + "line": 816 }, { "id": "PRED.k331.shape", "klass": "PRED", "value": "instruction \"close with no comment (rationale)\" — parenthetical is rationale, NOT comment body", - "line": 810 + "line": 813 }, { "id": "PRED.k331.signal", "klass": "PRED", "value": "close-with-no-comment-is-literal", - "line": 809 + "line": 812 }, { "id": "PROBE.ci.surface", "klass": "PROBE", "value": "the contract (parse/validate, projection round-trip, fail-closed guards), NEVER the LLM judgment (ADR-550 D5)", - "line": 558 + "line": 561 }, { "id": "PROBE.core.seam", "klass": "PROBE", "value": "analyzeCoverage(items,resolutions?,validators) ingests ALREADY-proposed items; does NOT assume deterministic propose (ADR-550 D7b)", - "line": 551 + "line": 554 }, { "id": "PROBE.edge.verification", "klass": "PROBE", "value": "explicit|backstop", - "line": 553 + "line": 556 }, { "id": "PROBE.family", "klass": "PROBE", "value": "edge-probe(shape-axis)+prohibition-probe(must-NOT-axis)+ui-consideration-probe(UI-state-axis), shared probe-core, run as spec-phase/ui-phase soft gates (ADR-550 D7; #1867)", - "line": 549 + "line": 552 }, { "id": "PROBE.item.axes", "klass": "PROBE", "value": "status{resolved|dismissed|unresolved} x verification{|null} — orthogonal; the lifecycle enum carries no verification fact (ADR-550 D7a)", - "line": 552 + "line": 555 }, { "id": "PROBE.principle", "klass": "PROBE", "value": "verifier-reach-equals-spec-reach (a goal-backward verifier only checks assertions that exist; probes make omitted assertions exist before code) — ADR-857 verification-substrate boundary; docs/design/verifier-reach.md", - "line": 548 + "line": 551 }, { "id": "PROBE.prohib.verification", "klass": "PROBE", "value": "test|judgment", - "line": 554 + "line": 557 }, { "id": "PROBE.protocol", "klass": "PROBE", "value": "recall(adversarial over-generate)->precision(drop routine-engineering); dismissals require a non-empty reason", - "line": 550 + "line": 553 }, { "id": "PROBE.ui.axis", "klass": "PROBE", "value": "MIXED — closed compiled shape-rooted 8 (empty/loading/error/populated/partial/overflow/zero-one-many/long-text) via ui-consideration-probe adapter; open UX (real-time/a11y/i18n-RTL) prose-owned in references/domain-probes.md, NOT compiled (#1867)", - "line": 556 + "line": 559 }, { "id": "PROBE.ui.seam", "klass": "PROBE", "value": "ui-phase Step 9.5 post-verification: element-cue classify -> propose-then-confirm (partial-cue mitigation, Goodhart) -> autoResolve --auto floor (never dismiss; unclassified stays unresolved #1110) -> ## UI Considerations write-back -> plan-phase `## UI Considerations` lift rule (#1867)", - "line": 557 + "line": 560 }, { "id": "PROBE.ui.verification", "klass": "PROBE", "value": "explicit|backstop", - "line": 555 + "line": 558 }, { "id": "PROC.AGENT-DISPATCH.completion-verify", "klass": "PROC", "value": "run k324.poll-shape on every agent-completion notification", - "line": 818 + "line": 821 }, { "id": "PROC.AGENT-DISPATCH.parallel-overlap-audit", "klass": "PROC", "value": "before dispatching N sibling-audit fixers, compute file-set union and assign canonical owners", - "line": 817 + "line": 820 }, { "id": "PROC.AGENT-DISPATCH.preflight", "klass": "PROC", "value": "[read-CONTRIBUTING.md-fresh, read-relevant-ADRs, cite-specific-line-in-brief, require-closing-keyword, require-changeset-fragment, forbid-CHANGELOG.md-edit, require-isolation-worktree, forbid-self-PR-comment, mandate-trust-but-verify]", - "line": 816 + "line": 819 }, { "id": "PROC.MERGE-WAVE.changelog-strip-pattern", "klass": "PROC", "value": "detached-HEAD per k325 + git checkout main -- CHANGELOG.md + commit + force-with-lease", - "line": 822 + "line": 825 }, { "id": "PROC.MERGE-WAVE.merge-tool", "klass": "PROC", "value": "gh pr merge --squash --delete-branch", - "line": 823 + "line": 826 }, { "id": "PROC.MERGE-WAVE.merge-tool-warning", "klass": "PROC", "value": "delete-branch may fail with \"used by worktree at\" — harmless; remote branch still deleted", - "line": 824 + "line": 827 }, { "id": "PROC.MERGE-WAVE.ordering", "klass": "PROC", "value": "[wave1: isolated-files, wave2: CHANGELOG-only-overlap (better: strip per k320), wave3: same-file-overlap with explicit decision]", - "line": 820 + "line": 823 }, { "id": "PROC.MERGE-WAVE.preflight", "klass": "PROC", "value": "gh pr view --json files for every PR; identify overlap pairs; surface to maintainer", - "line": 821 + "line": 824 }, { "id": "PROC.PARALLEL-FIX-DISPATCH.observed", "klass": "PROC", "value": "#3541 + #3542 dispatched simultaneously this session; PRs #3546 #3547 opened green; one syntax slip caught by AGENT-RETIRED-SLASH-SYNTAX-DRIFT and fixed before second PR opened", - "line": 900 + "line": 903 }, { "id": "PROC.PARALLEL-FIX-DISPATCH.pattern", "klass": "PROC", "value": "bot triage brief → worktree per branch → parallel sub-agents do rubber-duck/RCA/TDD implementation only → top-level orchestrator owns commit + gsd-test + push + PR + changeset-pr-backfill", - "line": 898 + "line": 901 }, { "id": "PROC.PARALLEL-FIX-DISPATCH.rationale", "klass": "PROC", "value": "long-running test runs need cross-turn notifications (orchestrator-only); CONTRIBUTING.md gh-templates-first hook requires session-scoped Read calls sub-agents wouldn't otherwise make; sequencing test runs avoids GSD-TEST-CONCURRENT-OUTPUT-COLLISION", - "line": 899 + "line": 902 }, { "id": "PROC.TRIAGE.comment-shape", "klass": "PROC", "value": "lead with \"duplicate of #NNNN, fixed by PR #MMMM, in v1.X.Y\"; show current code snippet proving bug-surface gone; give @latest and @next upgrade commands; close", - "line": 903 + "line": 906 }, { "id": "PROC.TRIAGE.no-duplicate-label", "klass": "PROC", "value": "this repo has no duplicate label; framing lives in comment text + closing the issue", - "line": 904 + "line": 907 }, { "id": "PROC.TRIAGE.routing-incoming", "klass": "PROC", "value": "stale-bug-already-fixed to close as duplicate of originating issue + cite fix PR + first stable tag; release-publish-or-backport to ready-for-human; reporter-can-self-test to awaiting-retest", - "line": 902 + "line": 905 }, { "id": "PROHIB.canon-referral", "klass": "PROHIB", "value": "OWASP/GDPR/fairness-canon are REFERRED to /gsd:secure-phase+eslint, never minted as prohibitions (ADR-550 D6)", - "line": 560 + "line": 563 }, { "id": "PROHIB.descriptor.shape", "klass": "PROHIB", "value": "5 FLAT scalars (check_kind,check_target,check_rule,check_violation_fixture,check_clean_fixture) — NEVER a nested check:{} (parseMustHavesBlock is a flat parser, src/frontmatter.cts)", - "line": 565 + "line": 568 }, { "id": "PROHIB.enforce.adr", "klass": "PROHIB", "value": "docs/adr/1606-prohibition-enforcement-verify-seam.md (verify-time enforcement seam) + docs/adr/550-spec-phase-probe-contract.md (spec-phase contract)", - "line": 568 + "line": 571 }, { "id": "PROHIB.enforce.causation", "klass": "PROHIB", "value": "clean-fixture control proves the red is content-caused not env-var-set; MANDATORY for node-test (#1906 supersedes #1346 opt-in) — absent clean-fixture ⇒ node-test un-provable/fail-closed; lint-rule needs none (its subject IS the linted file)", - "line": 564 + "line": 567 }, { "id": "PROHIB.enforce.failfirst", "klass": "PROHIB", "value": "MACHINE-PROVEN against an author-supplied violation fixture (#1279); caller failFirst attestation DEMOTED to a non-authoritative hint (FF-08)", - "line": 563 + "line": 566 }, { "id": "PROHIB.enforce.green-rule", "klass": "PROHIB", "value": "passed iff provenFailFirst===true && run.passed===true (runProhibitionEnforcement); every miss/fail/un-provable HARD-GATES both modes via dispositionForProhibition's fail-closed default", - "line": 561 + "line": 564 }, { "id": "PROHIB.enforce.kinds", "klass": "PROHIB", "value": "node-test (non-vacuous red via isNonVacuousNodeTestRed; pass-side vacuity via isNonVacuousNodeTestPass) | lint-rule (eslint --format json filtered by ruleId)", - "line": 562 + "line": 565 }, { "id": "PROHIB.judgment-tier", "klass": "PROHIB", "value": "never-silent / never-hard-halt soft gate; autonomous emits \"unverified-prohibition — human review recommended\" (exogenous grading, ADR-550 D4)", - "line": 567 + "line": 570 }, { "id": "PROHIB.rail", "klass": "PROHIB", "value": "core verify rail, non-toggleable (ADR-857 verification-substrate boundary / decision #6); the verifier<->predicate contract is NOT an off-by-default capability", - "line": 566 + "line": 569 }, { "id": "PROHIB.recall", "klass": "PROHIB", "value": "LLM-prose; no compiled prohibition-probe recall engine (only the schema/projection layer is code, ADR-550 D7b)", - "line": 559 + "line": 562 }, { "id": "RELEASE-NOTES.ANTI-PATTERN", "klass": "RELEASE-NOTES", "value": "raw \"What's Changed\" PR list as final body for hotfix or feature release; \"Full Changelog only\" body for tagged release with >0 user-facing fixes", - "line": 713 + "line": 716 }, { "id": "RELEASE-NOTES.ANTI-PATTERN.implementation-first", "klass": "RELEASE-NOTES", "value": "do not lead bullet with file path or function name; lead with symptom/user-visible behavior", - "line": 714 + "line": 717 }, { "id": "RELEASE-NOTES.ANTI-PATTERN.risk-commentary", "klass": "RELEASE-NOTES", "value": "do not include \"may break\", \"be careful\", \"test thoroughly\" - release notes state what changed, not hedges about what might go wrong", - "line": 715 + "line": 718 }, { "id": "RELEASE-NOTES.DEFAULT-STATE", "klass": "RELEASE-NOTES", "value": "auto-generated body is \"What's Changed\" PR list + Full Changelog link; treat as draft, not final", - "line": 689 + "line": 692 }, { "id": "RELEASE-NOTES.EXAMPLE.hotfix", "klass": "RELEASE-NOTES", "value": "v1.41.1 (https://github.com/open-gsd/gsd-core/releases/tag/v1.41.1) - 14 fixes grouped by 6 subgroups", - "line": 717 + "line": 720 }, { "id": "RELEASE-NOTES.EXAMPLE.minor-auto-acceptable", "klass": "RELEASE-NOTES", "value": "v1.41.0 - kept auto-generated body; many small fixes with clean conventional-commit titles", - "line": 719 + "line": 722 }, { "id": "RELEASE-NOTES.EXAMPLE.rc", "klass": "RELEASE-NOTES", "value": "v1.7.0-rc.1 (https://github.com/open-gsd/gsd-core/releases/tag/v1.7.0-rc.1) - intro + Added/Changed/Fixed/Documentation taxonomy", - "line": 718 + "line": 721 }, { "id": "RELEASE-NOTES.GATE.hotfix", "klass": "RELEASE-NOTES", "value": "manual edit required; auto-generated body for vX.Y.{Z>0} is \"Full Changelog only\" and must be replaced with structured body", - "line": 690 + "line": 693 }, { "id": "RELEASE-NOTES.GATE.minor", "klass": "RELEASE-NOTES", "value": "auto-generated body acceptable when PR titles are clean; promote to structured body when >20 PRs or contains feature+refactor+fix mix", - "line": 692 + "line": 695 }, { "id": "RELEASE-NOTES.GATE.rc", "klass": "RELEASE-NOTES", "value": "manual edit recommended; auto-generated PR list is acceptable for early RCs but final RC before vX.Y.0 should match standard", - "line": 691 + "line": 694 }, { "id": "RELEASE-NOTES.RELEASE-STREAM.main-branch", "klass": "RELEASE-NOTES", "value": "next (RCs) + latest (stable); install via @next or @latest", - "line": 724 + "line": 727 }, { "id": "RELEASE-NOTES.RELEASE-STREAM.rule", "klass": "RELEASE-NOTES", "value": "streams do not mix; do not document @next in hotfix/stable notes", - "line": 725 + "line": 728 }, { "id": "RELEASE-NOTES.SCOPE", "klass": "RELEASE-NOTES", "value": "GitHub Releases body for tags vX.Y.Z, vX.Y.Z-rc.N; not CHANGELOG.md (changeset workflow owns that)", - "line": 688 + "line": 691 }, { "id": "RELEASE-NOTES.SOURCE.changesets", "klass": "RELEASE-NOTES", "value": ".changeset/*.md (frontmatter pr: + body bullets)", - "line": 704 + "line": 707 }, { "id": "RELEASE-NOTES.SOURCE.commits", "klass": "RELEASE-NOTES", "value": "git log .. --pretty=format:'%s%n%n%b' --no-merges", - "line": 703 + "line": 706 }, { "id": "RELEASE-NOTES.SOURCE.pr-bodies", "klass": "RELEASE-NOTES", "value": "gh pr view --json title,body for fixes lacking a changeset", - "line": 705 + "line": 708 }, { "id": "RELEASE-NOTES.SOURCE.precedence", "klass": "RELEASE-NOTES", "value": "changeset body > commit body > PR body > commit subject (prefer authored content over auto-generated)", - "line": 706 + "line": 709 }, { "id": "RELEASE-NOTES.STANDARD.bullet-shape", "klass": "RELEASE-NOTES", "value": "**Bold user-visible change** — explanation of what was broken or what's new, leading with symptom not implementation. Trailing (#NNN) PR ref.", - "line": 696 + "line": 699 }, { "id": "RELEASE-NOTES.STANDARD.footer.full-changelog", "klass": "RELEASE-NOTES", "value": "**Full Changelog**: https://github.com/open-gsd/gsd-core/compare/...", - "line": 700 + "line": 703 }, { "id": "RELEASE-NOTES.STANDARD.footer.hotfix", "klass": "RELEASE-NOTES", "value": "Install/upgrade: \\`npx @opengsd/gsd-core@latest\\`", - "line": 698 + "line": 701 }, { "id": "RELEASE-NOTES.STANDARD.footer.rc", "klass": "RELEASE-NOTES", "value": "Install for testing: \\`npx @opengsd/gsd-core@next\\` (per branch->dist-tag policy)", - "line": 699 + "line": 702 }, { "id": "RELEASE-NOTES.STANDARD.heading-level", "klass": "RELEASE-NOTES", "value": "## for category, ### for subgroup (area), - for bullet", - "line": 695 + "line": 698 }, { "id": "RELEASE-NOTES.STANDARD.intro", "klass": "RELEASE-NOTES", "value": "optional one-paragraph framing for RC/feature releases; omit for pure-fix hotfixes", - "line": 701 + "line": 704 }, { "id": "RELEASE-NOTES.STANDARD.subgroups", "klass": "RELEASE-NOTES", "value": "phase-planning-state | workstream | query-dispatch-cli | code-review | install | capture | docs | architecture | security", - "line": 697 + "line": 700 }, { "id": "RELEASE-NOTES.STANDARD.taxonomy", "klass": "RELEASE-NOTES", "value": "Keep-a-Changelog 1.1.0: Added | Changed | Deprecated | Removed | Fixed | Security | Documentation", - "line": 694 + "line": 697 }, { "id": "RELEASE-NOTES.TEMPLATE.hotfix", "klass": "RELEASE-NOTES", "value": "## Fixed\\n\\n### \\n- **** — . (#)\\n\\n---\\n\\nInstall/upgrade: \\`npx @opengsd/gsd-core@latest\\`\\n\\n**Full Changelog**: ", - "line": 721 + "line": 724 }, { "id": "RELEASE-NOTES.TEMPLATE.rc", "klass": "RELEASE-NOTES", "value": "\\n\\n## Added\\n### \\n- **** — . (#)\\n\\n## Changed\\n### Architecture\\n- **** — . (#)\\n\\n## Fixed\\n### \\n- **** — . (#)\\n\\n## Documentation\\n- **** — . (#)\\n\\n---\\n\\nThis is a release candidate. Install for testing:\\n\\`\\`\\`bash\\nnpx @opengsd/gsd-core@next\\n\\`\\`\\`\\n\\n**Full Changelog**: ", - "line": 722 + "line": 725 }, { "id": "RELEASE-NOTES.WORKFLOW.edit", "klass": "RELEASE-NOTES", "value": "gh release edit --notes-file ", - "line": 708 + "line": 711 }, { "id": "RELEASE-NOTES.WORKFLOW.idempotency", "klass": "RELEASE-NOTES", "value": "gh release edit overwrites body wholesale; safe to re-run after refining", - "line": 711 + "line": 714 }, { "id": "RELEASE-NOTES.WORKFLOW.token", "klass": "RELEASE-NOTES", "value": "must use .envrc GITHUB_TOKEN per RULESET.GH.AUTH.DEFAULT (this doc); never ambient gh auth", - "line": 710 + "line": 713 }, { "id": "RELEASE-NOTES.WORKFLOW.view", "klass": "RELEASE-NOTES", "value": "gh release view --json body --jq .body", - "line": 709 + "line": 712 }, { "id": "RULESET.ADR-HEADER", "klass": "RULESET", "value": "every docs/adr/NNNN-*.md must open with - **Status:** Accepted|Proposed|Superseded (by [ADR-NNNN](file.md))|Legacy + - **Date:** YYYY-MM-DD immediately after title", - "line": 613 + "line": 616 }, { "id": "RULESET.AGENT_SIZE_BUDGET", "klass": "RULESET", "value": "agent-size-budget (#1074; sibling of WORKFLOW_SIZE_BUDGET; BYTES not lines per #717/#683, rebased from lines in PR 3/3) = differential attribution size ratchet (PRIMARY anti-creep since #2724/ADR-2719 §4, same mechanism and same ack fragments (tests/emitted-drift-acks/, #2914; legacy tests/emitted-drift-ack.json still honored) as WORKFLOW_SIZE_BUDGET, scoped to agents/gsd-*.md) + loose tier hard caps (red lines, never raised on approach: XL<=57344 / LARGE<=49152 / DEFAULT<=24576); net-new agents are DEFAULT-tier (no separate new-file cap). Sizes are measured via the shared scripts/workflow-size.cjs measureMdFiles(dir,predicate) counter (tests/helpers/emitted-runtime.cjs's currentSizes() and the guard's own tier-cap checks both import it). A grown agent fails the differential guard — ack + justify, or extract LAZILY to gsd-core/references/. DISTINCT from DEFECT.AGENT-FILE-SIZE-CAP-BREACH (a separate 45K-CHAR extraction-evidence threshold on gsd-planner via planner-decomposition/reachability tests): that guard proves mode-sections were extracted; this one bounds total agent bytes. Two guards, two units (chars vs bytes), two purposes. The prior per-file baseline (tests/agent-size-baseline.json, `npm run size:baseline`) is REMOVED by #2724", - "line": 602 + "line": 605 }, { "id": "RULESET.ALLOWED-TOOLS-FRONTMATTER", "klass": "RULESET", "value": "command's allowed-tools must cover every tool the workflow calls (including Write for file creation); thin-wrapper pattern makes this easy to miss", - "line": 609 + "line": 612 }, { "id": "RULESET.ARGUMENTS-SANITIZE", "klass": "RULESET", "value": "any workflow step constructing .planning/.../{SLUG}.md path from user input ($ARGUMENTS, parsed remainder) must sanitize inline ([a-z0-9-] only, reject ..//\\\\, max-length) — \"(already sanitized)\" must trace back to explicit guard; RESUME/fallback modes need own guards", - "line": 610 + "line": 613 }, { "id": "RULESET.AUDIT.search-source-not-generated", "klass": "RULESET", "value": "verify an invariant/validation EXISTS by searching the AUTHORED source (src/*.cts OR the scripts/gen-*.cjs generator), never the generated bin/lib/*.cjs (gitignored, ADR-457); gen-time checks live in gen-*.cjs not the .cts it consumes → search BOTH before declaring absent; read generated .cjs only for output drift. Repro: grep src/*.cts for VALID_CONVERTER_NAMES → false \"5e ConverterName unenforced\"; actually enforced in gen-capability-registry.cjs. cf RULESET.TESTS.no-source-grep", - "line": 598 + "line": 601 }, { "id": "RULESET.CAPABILITY.cutover-self-gating", "klass": "RULESET", "value": "a phase-6 per-feature cutover moves the host's phase-context detection + mode/flag logic INTO the skill (self-gating, per ADR-894); the loop hook is intentionally COARSE — \"invoke skill X at point Y when config Z\" — and carries no detection/mode. WORKED EXAMPLE: plan-phase.md §5.6 UI gate (frontend-detection via ui-safety-gate.cjs + --auto/manual branch + --skip-ui bypass) must move into gsd-ui-phase before its plan:pre hook can replace the inline call without behavior loss. Spike #1018 finding.", - "line": 379 + "line": 382 }, { "id": "RULESET.CAPABILITY.off-means-off", "klass": "RULESET", "value": "the host derives shared outputs from the ACTIVE hook set (via loop.render-hooks); a hook may ADD a labeled block or be COUNTED into a host-computed aggregate (e.g. a score denominator), but NEVER mutates host source — so a disabled capability yields the base output by construction, not by authoring discipline. Ratify in ADR-894; proven by spike #1018.", - "line": 377 + "line": 380 }, { "id": "RULESET.CAPABILITY.precedence-engine-single-owner", "klass": "RULESET", "value": "the config-key four-level precedence walk (loadConfig result → workstream config.json → root config.json → registry.configSchema default → absent) is owned solely by src/capability-activation.cts: raw-value primitive resolveConfigKey(dotKey, {config,cwd,registry}) and boolean wrapper _resolveActivationValue(dotKey,config,cwd,registry); loop-resolver.cts imports the engine (no duplicate); resolveConfigValues in loop-resolver.cts delegates to resolveConfigKey; resolveCapabilityRuntimeState does NOT return registry/config — callers import capability-registry.cjs and call loadConfig(cwd) directly.", - "line": 383 + "line": 386 }, { "id": "RULESET.CAPABILITY.step-additive-gate-blocks", "klass": "RULESET", "value": "a `step` hook is purely additive (invoke skill + produce artifacts, NEVER halts the host); host-blocking preconditions are `gate`s (blocking:true, onError:halt); runtime/mode context (auto/chain vs manual) self-gates IN THE SKILL, not via `when` (config-only). §5.6 = plan:pre step (ui-phase; skill self-gates on frontend+pipeline, auto-fires only in pipelines) + a NEW plan:pre gate (frontend-and-no-UI-SPEC → halt, when:workflow.ui_safety_gate); the loop.render-hooks dispatch template handles steps AND gates. Resolves #1022.", - "line": 381 + "line": 384 }, { "id": "RULESET.CODERABBIT.GUARD.COMPLETE", "klass": "RULESET", "value": "required_checks_green && coderabbit_check_pass && graphQL(reviewThreads.unresolved_count)==0", - "line": 635 + "line": 638 }, { "id": "RULESET.CODERABBIT.GUARD.GRAPHQL", "klass": "RULESET", "value": "reviewThreads(first:100){nodes{id isResolved comments{nodes{author body path line originalLine url}}}}; use unresolved threads as authoritative, not badge text alone", - "line": 636 + "line": 639 }, { "id": "RULESET.CODERABBIT.GUARD.OPEN_PRS", "klass": "RULESET", "value": "gh pr list --repo open-gsd/gsd-core --author @me --state open; repeat near end because open PR set can change mid-run", - "line": 634 + "line": 637 }, { "id": "RULESET.CODERABBIT.GUARD.RERUN", "klass": "RULESET", "value": "after every push wait for CodeRabbit completion, then re-query unresolved threads; CodeRabbit can add new findings after earlier threads were resolved", - "line": 637 + "line": 640 }, { "id": "RULESET.CODERABBIT.GUARD.RESOLVE", "klass": "RULESET", "value": "fix validated finding -> focused tests -> commit/push -> resolveReviewThread(threadId) -> wait CI/CodeRabbit -> final unresolved_count query", - "line": 638 + "line": 641 }, { "id": "RULESET.CODERABBIT.GUARD.SCOPE", "klass": "RULESET", "value": "if a new @me open PR appears during final list, include it in the same guard pass before declaring all-open-PRs complete", - "line": 639 + "line": 642 }, { "id": "RULESET.CONTENT-PATH-NORMALIZATION", "klass": "RULESET", "value": "filesystem paths substituted into markdown body text (@-references, workflow .md, agent .md, generated docs, command bodies) MUST be normalized to POSIX forward slashes via .replace(/\\\\/g,'/') at the production source BEFORE substitution; never push normalization to tests; cross-platform content is POSIX-only; applies to: computePathPrefix output, install-path rewrites, generated shim paths emitted into .md bodies; idempotent on POSIX so unconditional; mechanically enforced by local/normalize-path-in-content (eslint, src/**/*.cts; #1733)", - "line": 840 + "line": 843 }, { "id": "RULESET.CONTRIB.CLASSIFY.enhancement", "klass": "RULESET", "value": "requires approved-enhancement before implementation", - "line": 628 + "line": 631 }, { "id": "RULESET.CONTRIB.CLASSIFY.feature", "klass": "RULESET", "value": "requires approved-feature before implementation", - "line": 629 + "line": 632 }, { "id": "RULESET.CONTRIB.CLASSIFY.fix", "klass": "RULESET", "value": "requires confirmed-bug before implementation (legacy 'confirmed' label is back-compat only for duplicate-sweep exemption, not a valid implementation gate)", - "line": 627 + "line": 630 }, { "id": "RULESET.CONTRIB.GATE.ORDER", "klass": "RULESET", "value": "issue-first -> approval-label -> code -> PR-link -> changeset/no-changelog", - "line": 626 + "line": 629 }, { "id": "RULESET.CR-THREAD-RESOLVE", "klass": "RULESET", "value": "after adding // allow-test-rule: to silence lint, resolve existing inline CR threads via graphql resolveReviewThread mutation before merge — open threads mislead future reviewers; pattern: gh api graphql -f query='mutation { resolveReviewThread(input:{threadId:\"PRRT_...\"}) { thread { isResolved } } }'", - "line": 620 + "line": 623 }, { "id": "RULESET.EMITTED_ATTRIBUTION", "klass": "RULESET", - "value": "the emitted-artifact family (ADR-2719, epic #2719) — POST-CUTOVER (#2724, Phase 4). Historically tests/fixtures/golden-install-parity/*.json (19 path→hash manifests) + tests/workflow-size-baseline.json + tests/agent-size-baseline.json were all committed, PURE FUNCTIONS of the source tree whose correct merge was ALWAYS \"recompute\" — 140 of 143 conflicted-file instances across the open PR queue were these files. #2724 DELETES all three, the golden test (tests/golden-install-parity.test.cjs), the generator (scripts/gen-golden-install-parity-zcode.cjs), `npm run gen:golden`, `UPDATE_GOLDEN`, the merge-driver bridge (scripts/git-merge-regen-driver.cjs, `npm run setup:merge-driver`, the .gitattributes merge=gsd-regen block), and scripts/update-size-baseline.cjs (`npm run size:baseline`). The differential attribution check (tests/emitted-attribution.test.cjs + tests/emitted-provenance.test.cjs) is now the SOLE gate for emitted-artifact propagation AND size growth — no committed artifact, nothing to hand-merge, nothing to regenerate. `npm run regen:derived` still exists for what remains committed and derived: build, registry, ADR index, capability matrix, inventory manifest, manifest versions, and `tests/fixtures/install-tree/*.json` (now `npm run gen:install-tree`, folded into `regen:derived`). tests/fixtures/install-tree/*.json is DELIBERATELY EXCLUDED from the cutover (ADR-2719 §7): it conflicts on 0 of 7, its diffs are readable, and it preserves \"the installer stopped shipping X\" as a hard absolute failure — capturing it would convert that absolute into an attribution-free auto-resolve. The baseline the differential compares against is now published by `scripts/gen-emitted-baseline.cjs` on every push to `next` (cached, keyed on sha) and restored in PR lanes via `GSD_EMITTED_BASELINE`/`resolveBaseline()` (tests/helpers/emitted-baseline.cjs); a cache miss falls back to an in-job build via a throwaway `git worktree` (tests/helpers/emitted-runtime.cjs's `buildBaselineAtRef`). REMEDIATION IS PART OF THE GATE (#2778): the failure output names its own remedy, because a gate that states a requirement and withholds the means of satisfying it is a maintainer round-trip, not a gate — ADR-2719 §3's \"conspicuous declaration\" only works if the contributor can discover how to make it. Both failing branches name a NEW fragment to create under `tests/emitted-drift-acks/` (#2914; pick a name nobody else is using), say it may not exist yet (absence is the healthy steady state), print a minimal valid document, and repeat \"do NOT regenerate anything\" — post-#2724 there is nothing left to regenerate, and hunting for a deleted baseline is the predictable wrong guess. The two branches key on DIFFERENT spaces and each says which: the hash pass keys on the EMITTED PATH (always contains a `/`), the size ratchet keys on the BARE FILENAME (`currentSizes` writes `sizes[entry.name]` from readdirSync over `gsd-core/workflows/` + `agents/`). A stale-ack failure additionally says to delete the FILE when removing its last entry, since an empty-but-present ack parses fine yet signals nothing; post-#2789 it also offers CORRECTING the entry to name the ripple actually made, which is the other honest resolution and the one a contributor usually wants. NOT ack-able and deliberately given no ack text: the `NEW_FILE_CAP` branch, whose remedy is extraction. Text is sourced from one frozen `REMEDIATION` export in tests/helpers/emitted-diff.cjs whose example document is rendered from `ACK_VERSION` via `JSON.stringify`, so the taught schema cannot drift from the accepted one (a round-trip test feeds the printed document back through `parseAck`); the message teaches ONE canonical shape even though `parseAck` also accepts a bare-string reason and a missing `version` — liberal in what it accepts, conservative in what it sends. Note the ADR's Consequences originally called the #2724 migration \"terminal\"; #2778 corrected that — it is terminal only for a PR that grows no shipped file. #2914 replaced the single shared ack file with per-PR fragments under `tests/emitted-drift-acks/` — exactly the shape `.changeset/` already uses for the identical \"every PR rewrites one shared document\" conflict problem — so two PRs needing an ack can no longer collide with each other, and a fragment left on `next` after merge is inert rather than a shared cell; the legacy file is still read and unioned in for branches that predate the split, and a duplicate path key across two sources is a hard, loudly-reported error, never silent last-wins. `tests/emitted-drift-ack.json` (the LEGACY file specifically, NOT the fragment directory) must NEVER persist on `next` (#2914): every entry is scoped to the diff that introduced it, so once merged it is by definition already at the base — spent and inert regardless of shape — and a persistent copy makes that ONE file a shared merge-conflict cell across every open PR that also carries an ack, exactly the \"140 of 143\" cost this whole cutover exists to remove; a persisting FRAGMENT is harmless by construction and is deliberately not what this guard checks. This is enforced on `next` itself only, never as a PR-lane check: the `guard-no-ack-on-next` job in `.github/workflows/test.yml` (push-to-`next` trigger) runs `scripts/lint-emitted-drift-ack.cjs --guard-next` (`assertAbsentOnNext`), which fails on the LEGACY file's PRESENCE alone, valid or not — a PR-lane \"base ack must be absent\" check would red every open PR the instant a spent ack merged, which is the #2768 shape #2789 already ended. cf `RULESET.WORKFLOW_SIZE_BUDGET`, `RULESET.AGENT_SIZE_BUDGET`; see `### Emitted Artifact Provenance`", - "line": 603 + "value": "the emitted-artifact family (ADR-2719, epic #2719) — POST-CUTOVER (#2724, Phase 4). Historically tests/fixtures/golden-install-parity/*.json (19 path→hash manifests) + tests/workflow-size-baseline.json + tests/agent-size-baseline.json were all committed, PURE FUNCTIONS of the source tree whose correct merge was ALWAYS \"recompute\" — 140 of 143 conflicted-file instances across the open PR queue were these files. #2724 DELETES all three, the golden test (tests/golden-install-parity.test.cjs), the generator (scripts/gen-golden-install-parity-zcode.cjs), `npm run gen:golden`, `UPDATE_GOLDEN`, the merge-driver bridge (scripts/git-merge-regen-driver.cjs, `npm run setup:merge-driver`, the .gitattributes merge=gsd-regen block), and scripts/update-size-baseline.cjs (`npm run size:baseline`). The differential attribution check (tests/emitted-attribution.test.cjs + tests/emitted-provenance.test.cjs) is now the SOLE gate for emitted-artifact propagation AND size growth — no committed artifact, nothing to hand-merge, nothing to regenerate. `npm run regen:derived` still exists for what remains committed and derived: build, registry, ADR index, capability matrix, inventory manifest, manifest versions, and `tests/fixtures/install-tree/*.json` (now `npm run gen:install-tree`, folded into `regen:derived`). tests/fixtures/install-tree/*.json is DELIBERATELY EXCLUDED from the cutover (ADR-2719 §7): it conflicts on 0 of 7, its diffs are readable, and it preserves \"the installer stopped shipping X\" as a hard absolute failure — capturing it would convert that absolute into an attribution-free auto-resolve. The baseline the differential compares against is now published by `scripts/gen-emitted-baseline.cjs` on every push to `next` (cached, keyed on sha) and restored in PR lanes via `GSD_EMITTED_BASELINE`/`resolveBaseline()` (tests/helpers/emitted-baseline.cjs); a cache miss falls back to an in-job build via a throwaway `git worktree` (tests/helpers/emitted-runtime.cjs's `buildBaselineAtRef`). REMEDIATION IS PART OF THE GATE (#2778): the failure output names its own remedy, because a gate that states a requirement and withholds the means of satisfying it is a maintainer round-trip, not a gate — ADR-2719 §3's \"conspicuous declaration\" only works if the contributor can discover how to make it. Both failing branches name a NEW fragment to create under `tests/emitted-drift-acks/` (#2914; pick a name nobody else is using), say it may not exist yet (absence is the healthy steady state), print a minimal valid document, and repeat \"do NOT regenerate anything\" — post-#2724 there is nothing left to regenerate, and hunting for a deleted baseline is the predictable wrong guess. The two branches key on DIFFERENT spaces and each says which: the hash pass keys on the EMITTED PATH (always contains a `/`), the size ratchet keys on the BARE FILENAME (`currentSizes` writes `sizes[entry.name]` from readdirSync over `gsd-core/workflows/` + `agents/`). A stale-ack failure additionally says to delete the FILE when removing its last entry, since an empty-but-present ack parses fine yet signals nothing; post-#2789 it also offers CORRECTING the entry to name the ripple actually made, which is the other honest resolution and the one a contributor usually wants. NOT ack-able and deliberately given no ack text: the `NEW_FILE_CAP` branch, whose remedy is extraction. Text is sourced from one frozen `REMEDIATION` export in tests/helpers/emitted-diff.cjs whose example document is rendered from `ACK_VERSION` via `JSON.stringify`, so the taught schema cannot drift from the accepted one (a round-trip test feeds the printed document back through `parseAck`); the message teaches ONE canonical shape even though `parseAck` also accepts a bare-string reason and a missing `version` — liberal in what it accepts, conservative in what it sends. Note the ADR's Consequences originally called the #2724 migration \"terminal\"; #2778 corrected that — it is terminal only for a PR that grows no shipped file. #2914 replaced the single shared ack file with per-PR fragments under `tests/emitted-drift-acks/` — exactly the shape `.changeset/` already uses for the identical \"every PR rewrites one shared document\" conflict problem — so two PRs needing an ack can no longer collide with each other on the FILE; the legacy file is still read and unioned in for branches that predate the split, and a duplicate path key across two sources is a hard, loudly-reported error, never silent last-wins — #3078 made that error name its two resolutions (git rm an already-merged, spent owner; APPEND prose to a still-live one, which re-arms it), because the guard runs post-merge and cannot stop the colliding PR. `tests/emitted-drift-ack.json` (the LEGACY file specifically) must NEVER persist on `next` (#2914): every entry is scoped to the diff that introduced it, so once merged it is by definition already at the base — spent and inert regardless of shape — and a persistent copy makes that ONE file a shared merge-conflict cell across every open PR that also carries an ack, exactly the \"140 of 143\" cost this whole cutover exists to remove; #2914 asserted a persisting FRAGMENT was harmless by construction and deliberately exempted the directory; #3078 REVERSED that — fragments do not share a FILE but they DO share a PATH KEY SPACE, so a fully-spent fragment on `next` owns keys it can no longer gate and the next PR growing one of those paths can declare it neither there (spent) nor in its own (duplicate), which is the #2914 wall one level down (measured at the sweep: 45 fragments owning 403 paths, up from 13/272 at triage 19 days earlier). A fragment is judged on INERTNESS, not presence: swept once EVERY entry is spent, left alone while PARTIALLY spent — the asymmetry is what keeps the re-arm-by-appending route (#2639, #2993) working, and the `0000` legacy-migration bucket #2923 created for the old shared file's 35 entries was NOT permanent (the issue's own open question resolved to NO) and went with the rest. This is enforced on `next` itself only, never as a PR-lane check: the `guard-no-ack-on-next` job in `.github/workflows/test.yml` (push-to-`next` trigger) runs `scripts/lint-emitted-drift-ack.cjs --guard-next`, which is now BOTH halves — `assertAbsentOnNext` (legacy file, fails on PRESENCE alone, valid or not) and `assertNoAllSpentFragments` (fragments, fails on all-entries-spent vs the copy at the PRE-PUSH TIP of next — CI passes `github.event.before` via `--base-ref`, because the default branch allows REBASE merges so one push can carry N commits and a bare `HEAD^` would flag a fragment the same push introduced; `HEAD^` remains only the local/manual fallback, using the SAME zero-width/whitespace-stripping prose comparison as `isSpent` so an invisible reword cannot fake a re-arm; duplicated across the scripts-ship/tests-do-not line and held by a parity test). The job's checkout REQUIRES `fetch-depth: 2` plus an explicit `git fetch --depth=1 origin $BEFORE` — at depth 1 no base commit exists locally, every fragment reads as brand-new, and the guard passes vacuously, which is exactly how the legacy half went blind after #2914 removed the file it was watching. The gate's `INVISIBLE`/`normalizeAckReason` are EXPORTED from tests/helpers/emitted-diff.cjs for the sole purpose of letting the parity test compare them against the script's duplicate; before #3078 neither was exported, so the \"parity test\" the comments promised was a tautology checking the script against itself. A PR-lane \"base ack must be absent\" check would red every open PR the instant a spent ack merged, which is the #2768 shape #2789 already ended — so this alerts AFTER the merge by design and never stops the offending PR. cf `RULESET.WORKFLOW_SIZE_BUDGET`, `RULESET.AGENT_SIZE_BUDGET`; see `### Emitted Artifact Provenance`", + "line": 606 }, { "id": "RULESET.GENERATIVE-FIX", "klass": "RULESET", "value": "parallel implementations diverge silently when no parity test enforces equality at the test layer; for any new constant/array/parser shared between two parallel surfaces (two workflow surfaces, or a generated artifact and its hand-authored source), the same commit MUST add a parity assertion that fails when the two diverge; exemplar: tests/runtime-launcher-parity.test.cjs (asserts every workflow bash block uses the canonical gsd_run launcher)", - "line": 838 + "line": 841 }, { "id": "RULESET.GH.AUTH.DEFAULT", "klass": "RULESET", "value": "source .envrc GITHUB_TOKEN before gh; exception=ambient allowed only when user explicitly says machine-only fallback", - "line": 633 + "line": 636 }, { "id": "RULESET.HARNESS.test-memory-guard", "klass": "RULESET", "value": "~/.claude/hooks/test-memory-guard.sh fires on every Bash PreToolUse; if argv[0]∈{node|vitest|jest|mocha|tsx|ts-node|tap|ava|playwright|cypress} OR matches (npm|pnpm|yarn|bun) (run )?(t|test|tests|vitest|jest); blocks via hookSpecificOutput.permissionDecision=deny when sum(RSS of running matching procs, excluding tsserver|*-mcp|claude|Electron|...) ≥ 4 GiB OR when argv[0] basename matches a running process's argv[0]. Exception: node --version|-v|--help|-h|-p|-e are trivial probes and skip the check. Designed for a 24 GB Mac where prior accidental fan-out exhausted RAM", - "line": 879 + "line": 882 }, { "id": "RULESET.MANIFEST-CANONICAL-KEY", "klass": "RULESET", "value": "docs/INVENTORY-MANIFEST.json has a single top-level key: families; ALL EIGHT families.* arrays (agents/commands/workflows/references/cli_modules/hooks flat, plus workflow_modes/workflow_steps nested — #2996, epic #1671 Phase 6.5) are canonical, consumed by test suites — tests/inventory-manifest-sync.test.cjs reads all eight, edit-phase/enh-2380/enh-2430 tests read commands+workflows; the six flat families are keyed by BARE BASENAME while the two nested families are keyed by // path, deliberately, because two workflows may each own a same-named step file and a basename key would silently drop one under a JSON-equality comparison; recursion is bounded at exactly one named subdirectory, never a general walk; the family tables live ONCE in scripts/gen-inventory-manifest.cjs and are IMPORTED by the test (the test formerly redeclared them, a DEFECT.GENERATIVE-FIX divergence that let a new family be verified by nobody while still reporting green); the old generated date field and the stale top-level workflows key are both gone; regen via node scripts/gen-inventory-manifest.cjs --write, AFTER build:lib; #3762 added the ROSTER half — tests/inventory-manifest-sync.test.cjs now also asserts every manifest entry has a hand-written row in docs/INVENTORY.md, via the pure matcher in tests/helpers/inventory-roster.cjs. Scope is the SIX FLAT families only, each searched inside its own `## ` section; workflow_steps/workflow_modes are DELIBERATELY exempt because docs/INVENTORY.md §\"Workflow Sub-Files\" is a shipped decision that they carry no hand-written per-file rows. Matching is whole-CELL-exact (never substring — the rostered host-integration-adapters/imperative-hook-bus.cjs must not satisfy the separate top-level hook-bus.cjs) and section-scoped (smart-entry.md and smart-entry.cjs are different families), EXCEPT commands, which match on the row's Source-column link to ../commands/gsd/.md because the six ns-* namespace routers deliberately RENDER a name that is not their file stem (/gsd-workflow ← ns-workflow.md) — DEFECT.DISPLAY-VALUE-AS-IDENTITY. Landing the gate required backfilling 32 pre-existing unrostered surfaces on next", - "line": 614 + "line": 617 }, { "id": "RULESET.PR-FLOW.docker-before-push", "klass": "RULESET", "value": "before ANY git push of any fix to any PR, run gsd-test (docker on the remote, mirrors ubuntu CI) and confirm exit 0. macOS-local node --test is NOT a substitute — many failures are platform-specific (path separators, case sensitivity, locale, fs semantics). Watchdog with Monitor on the output log; never set a sleep/timer and walk away. Source: user feedback 2026-05-16 — \"we don't set a timer we actively watch and record results in real time as possible\". SUPERSEDED 2026-07-17: 'confirm exit 0' is a false-green trap — piping/backgrounding can report exit 0 on a failed suite; gate on the verdict-line outcome:\"passed\" for the exact HEAD sha instead. See CLAUDE.md's gsd-test rule and the gsd-test-is-ref-based-commit-first predicate for the current, correct gating contract.", - "line": 881 + "line": 884 }, { "id": "RULESET.PR-FLOW.templates-mandatory", "klass": "RULESET", "value": "every gh pr create|edit|gh issue create|edit MUST first invoke the gh-templates-first skill and Read (Read tool, not Bash cat — k321 read-tracking) the matching template in .github/. Apply ALL required sections; never write freeform bodies. Repo enforces this via gsd-pr-template-policy GitHub Action which flags any non-templated body — the bot allows the PR to stay open only because authors are contributors-or-higher, but the warning is a real complaint that must be cured. Source: user feedback 2026-05-16 (multi-message escalation) — \"the whole reason i have that github action is because you fucking blow through and ignore using the templates\"", - "line": 883 + "line": 886 }, { "id": "RULESET.PR-SCOPE.one-concern-per-pr", "klass": "RULESET", "value": "split unrelated changes into separate PRs; cherry-pick doc changes to dedicated docs/ branch immediately, then force-push original to remove the commit", - "line": 616 + "line": 619 }, { "id": "RULESET.SHARED-HELPERS-LINT-VS-TEST", "klass": "RULESET", "value": "when a lint script and test suite both implement same constant (CANONICAL_TOOLS) or parser (parseFrontmatter, executionContextRefs), extract to scripts/*-helpers.cjs required by both — silent divergence otherwise", - "line": 611 + "line": 614 }, { "id": "RULESET.TESTS.CODERABBIT_FIX", "klass": "RULESET", "value": "prefer exported-function behavioral tests over source-grep; lint-no-source-grep rejects readFileSync source assertions without allow-test-rule", - "line": 640 + "line": 643 }, { "id": "RULESET.TESTS.boundary-coverage", "klass": "RULESET", "value": "tests MUST exercise inputs at and near the threshold/limit, not only trivial-fit and trivial-overflow; pick inputs where N ∈ {limit-1, limit, limit+1} and where pre-trim/pre-check accumulators ≈ effective limit; \"very small\" and \"very large\" inputs alone do not constitute edge-case coverage and routinely miss off-by-one + reservation-accounting bugs", - "line": 585 + "line": 588 }, { "id": "RULESET.TESTS.boundary-coverage.anti-pattern", "klass": "RULESET", "value": "test suites that pair budget:1_000_000 (trivially fits) with budget:1 (trivially overflows) and skip the boundary region; failure mode that shipped PR #3708 UNNEEDED_TRIM + FALSE_HARDFAIL regressions (commit 2df566ed, fixed bde1ae8f)", - "line": 588 + "line": 591 }, { "id": "RULESET.TESTS.boundary-coverage.fixtures", "klass": "RULESET", "value": "for any code with budget/limit/quota/threshold parameter, test suite MUST include: (a) input where SUT estimate == limit exactly, (b) input where estimate == limit - 1, (c) input where estimate == limit + 1, (d) input where any internal reserve/safety constant pushes baseline within reserve-distance of limit (catches early-pressure firing)", - "line": 587 + "line": 590 }, { "id": "RULESET.TESTS.clock-seam", "klass": "RULESET", "value": "concurrency logic must accept an optional {clock=Date} parameter; tests control time via t.mock.timers.enable(['Date']) + t.mock.timers.setTime(0) + t.mock.timers.tick(N); real OS scheduler races are not a permitted test pattern after ADR 456 (2026-05-28); real-race tests are deleted once deterministic seam tests cover the same logical path; clock.cjs realClock adds nowIso() (→ new Date(this.now()).toISOString()) and today() (→ nowIso().split('T')[0]) so all date-stamping in state.cjs routes through the seam; subprocess time-pin adapter: set GSD_TEST_MODE=1 + GSD_NOW_MS= in runGsdTools env to pin the date written by the SUT without touching real wall-clock (issue #474)", - "line": 592 + "line": 595 }, { "id": "RULESET.TESTS.coderabbit-fix-prefer", "klass": "RULESET", "value": "behavioral tests (call exported fn, capture JSON, assert typed fields) over source-grep", - "line": 583 + "line": 586 }, { "id": "RULESET.TESTS.delete-bad-tests", "klass": "RULESET", "value": "pass-always / vacuous-truth / source-grep / elapsed-time / real-race / permanent-allow-test-rule tests are DELETED and replaced with compliant tests in the same PR; not skipped, not commented out, not permanently exempted; replacement must cover the same logical path via typed-surface assertion or clock-seam pattern", - "line": 595 + "line": 598 }, { "id": "RULESET.TESTS.diagnostics", "klass": "RULESET", "value": "after JSON.parse, assert output shape (Array.isArray(output.phases)) with raw-output-prefix diagnostics before .map() — prevents opaque TypeErrors when CLI output shape changes", - "line": 584 + "line": 587 }, { "id": "RULESET.TESTS.escape-regex", "klass": "RULESET", "value": "new RegExp(\"prefix${var}\") must escapeRegex(var); phase-id.cjs exports escapeRegex (core.cjs re-export spine retired in epic #1267); phase IDs like 5.1 contain . which is metacharacter", - "line": 580 + "line": 583 }, { "id": "RULESET.TESTS.eslint-harness", "klass": "RULESET", "value": "ADR 452 (2026-05-28): ESLint flat config + typescript-eslint + eslint-plugin-n + eslint-plugin-no-only-tests + local plugin at eslint-rules/ (repo root, NOT scripts/eslint-rules/); replaces scripts/lint-*.cjs regex scanners (fully removed in #632); all three test-rigor rules now ship at error in tests/**/*.test.cjs scope: local/no-source-grep and local/no-magic-sleep-in-tests promoted by #3313, local/no-elapsed-assertion promoted by #3331 once #3314 delivered its ADR-456 §(a) precondition (epic #1885 was subsumed into epic #3053 and closed stale before this promotion landed)", - "line": 596 + "line": 599 }, { "id": "RULESET.TESTS.feedback-loop-convergence", "klass": "RULESET", "value": "when a feature's OUTPUT feeds back into its own INPUT (calibration, retry backoff, adaptive budgets, ratchets, any self-correcting signal), step-wise tests are NOT sufficient evidence of correctness: they assert `given X return Y` while the defect lives in the TRAJECTORY across iterations. Required: a closed-loop test that (a) drives the REAL end-to-end surface — not the pure core alone, since composition bugs live between surfaces — for N >= 2x the loop's window, (b) asserts convergence on the known-true value, (c) asserts the fixed point (an already-correct history must produce NO correction), and (d) asserts boundedness under an adversarial/oscillating history. Two defects shipped past a green ~26,800-test suite in epic #1952 for want of exactly this: calibration applied twice across two surfaces (factor^2, #2631) and calibration measured against its own corrected output so it oscillated to ~1.41 instead of converging on 2.0 (#2632). Every unit, boundary, property and round-trip test passed for both. HOW TO SPOT ONE (the detection tell, not a judgment call): the feature's own acceptance criterion carries a TEMPORAL QUANTIFIER — \"after N phases\", \"subsequent\", \"over time\", \"improves\", \"learns\", \"adapts\". That phrasing means the claim is about a TRAJECTORY, so a step-wise `given X return Y` test does not test the claim that was made. #1952's AC4 read \"After N phases, the error is computed and applied as a correction to SUBSEQUENT estimates\" — the tell was in plain sight and was still tested as a point. Survey of this repo (2026-07): estimation calibration is the ONLY true instance; size/mutation ratchets are exempt because they fail on both growth AND shrinkage (cannot self-satisfy), and retry ladders (node_repair_budget, plan_bounce_passes, provider_escalation) terminate rather than feed back. Test anchor: tests/estimate-loop-convergence.test.cjs", - "line": 586 + "line": 589 }, { "id": "RULESET.TESTS.guard-toplevel-readFileSync", "klass": "RULESET", "value": "module-level const src = readFileSync(...) throws before any test() registers — wrap in try/catch in test() or use lazy load", - "line": 582 + "line": 585 }, { "id": "RULESET.TESTS.mutation-score", "klass": "RULESET", "value": "Stryker runs incremental (--since origin/next) on ubuntu-latest/Node24 CI leg; default threshold 80% killed/total; surviving mutants in scope block merge unless path is listed in stryker.config.mjs with documented reason; treat surviving mutant as a failing test specification", - "line": 594 + "line": 597 }, { "id": "RULESET.TESTS.no-dead-regex-in-includes", "klass": "RULESET", "value": "src.includes(\"foo.*bar\") is always false — .* is regex metacharacter not wildcard; use new RegExp(...).test(src) or delete", - "line": 581 + "line": 584 }, { "id": "RULESET.TESTS.no-duplicate-fold-marker", "klass": "RULESET", "value": "local/no-duplicate-fold-marker ESLint AST rule (eslint-rules/no-duplicate-fold-marker.cjs, #3271) reports the 2nd and every later __foldDescribe(\"folded: ...\") call carrying a marker already seen in the SAME file, naming the first occurrence's line; error in tests/**/*.cjs. The key is the WHITESPACE-delimited token after folded:, NOT a [a-z0-9-]* slice — a slice truncates at \".\" and collides feat-443-effort-fast-mode.integration with feat-443-effort-fast-mode (two distinct suites coexisting in tests/model-resolver.test.cjs), and NOT the whole title, so a re-fold under a different batch label (\"B1 #1970\" vs \"B5 #1975\") is still caught. Deliberately silent on: a __foldDescribe title with no folded: prefix (the alias is reused for one ordinary describe in tests/review-default-reviewers-workflow.test.cjs), a plain describe(), a non-literal title, and the same marker in two DIFFERENT files (the defect class is intra-file).", - "line": 577 + "line": 580 }, { "id": "RULESET.TESTS.no-duplicate-fold-marker.why", "klass": "RULESET", "value": "consolidation epic #1969 folds are self-contained blocks, so a second verbatim copy parses, registers and PASSES twice — nothing reports it; #3271 found 25 such copies (~5,800 lines) in tests/install.test.cjs (18), tests/install-minimal-hooks.test.cjs (5) and tests/install-write-confinement.test.cjs (2), all from one stale-base re-application in 6d072435d (#1975 re-applying #1970's hunks, 2026-07-03). Ref DEFECT.GENERATIVE-FIX: the two copies drift apart silently when a contributor fixes one and leaves the other asserting the old behavior, with the suite still green.", - "line": 578 + "line": 581 }, { "id": "RULESET.TESTS.no-source-grep", "klass": "RULESET", "value": "local/no-source-grep ESLint AST rule (eslint-rules/no-source-grep.cjs) rejects readFileSync of a source .cjs/.js/.ts path bound to a var later hit with .includes()/.match()/.startsWith()/.endsWith()/.indexOf()/.search(); error in tests/**/*.test.cjs, warn in gsd-core/bin/**/*.cjs + scripts/**/*.cjs (ADR 452 retired the old regex script, removed for good in #632)", - "line": 574 + "line": 577 }, { "id": "RULESET.TESTS.no-source-grep.exemption", "klass": "RULESET", "value": "// allow-test-rule: with one-line justification; reserved for tests where the file content IS the product surface (STATE.md, config.toml, hooks.json, agent .md). Migration to typed-IR parser tracked in #2974.", - "line": 575 + "line": 578 }, { "id": "RULESET.TESTS.no-source-grep.tmp-file-traps", "klass": "RULESET", "value": "reading tmp files written by the SUT in tests still trips lint; round-trip through CLI (e.g. frontmatter get) instead of readFileSync+.includes()", - "line": 576 + "line": 579 }, { "id": "RULESET.TESTS.no-timing-assertion", "klass": "RULESET", "value": "do not assert on wall-clock elapsed time (Date.now() delta, performance.now(), process.hrtime() comparison); such assertions test the host machine not the SUT and flake on loaded CI runners; enforcement: local/no-elapsed-assertion ESLint rule, error (promoted by #3331 once #3314 delivered the ADR-456 §(a) reachability rule + deterministic backfill precondition); canonical replacement: clock-seam pattern with node:test mock.timers", - "line": 591 + "line": 594 }, { "id": "RULESET.TESTS.property-based-testing", "klass": "RULESET", "value": "modules implementing parsing / transformation / budget-limit / bijective contracts must include at least one fast-check (fc) property test asserting a domain invariant; invariant categories: round-trip, monotonicity, boundary-containment, idempotency; property tests live in *.test.cjs alongside unit tests; CI signal: Stryker mutation score below 80% blocks merge", - "line": 593 + "line": 596 }, { "id": "RULESET.TRIAGE-EXISTING-WORK", "klass": "RULESET", "value": "before writing agent brief for confirmed bug, check (1) local branches git branch -a | grep , (2) untracked/modified files on that branch, (3) stash, (4) open PRs with matching head branch — recover existing work rather than re-implement", - "line": 618 + "line": 621 }, { "id": "RULESET.WORKFLOW.COVERAGE-METADATA", "klass": "RULESET", "value": "#1602 SUMMARY frontmatter `coverage:` block (list of {id,description,requirement?,verification:[{kind∈unit|integration|e2e|automated_ui|manual_procedural|other, ref, status∈pass|fail|unknown}],human_judgment:bool,rationale?}) is the per-deliverable RTM consumed DETERMINISTICALLY by verify-work extract_tests via `gsd-tools uat classify-coverage --summary ` (src/coverage.cts → bin/lib/coverage.cjs). AUTHORING: execute-plan create_summary populates it from task results; every deliverable MUST be classified; fail-safe default = human_judgment:true + rationale. CLASSIFY CONTRACT: auto-pass (skip human) ONLY when human_judgment===false (strict boolean) AND verification non-empty AND every status==='pass' AND zero validation errors — else PRESENT to human. mode:legacy (no block) ⇒ byte-identical prose `## Accomplishments` fall-through; `coverage: []` ⇒ mode:coverage, zero entries (single-confirmation). Frozen IR: MODE/PRESENT_REASON/ERROR_CODE enums locked by tests/coverage-metadata-parser.test.cjs. extractFrontmatter CANNOT parse it (scalars-only `-` items) → dedicated parser, sibling of parseMustHavesBlock. Asymmetry by design: false-negative=redundant prompt (status quo); false-positive=shipped bug UAT existed to catch", - "line": 607 + "line": 610 }, { "id": "RULESET.WORKFLOW_EXECUTE_END_TO_END", "klass": "RULESET", "value": "standard for single-workflow commands is \"Execute end-to-end.\" (no bolded **Follow the X workflow** fragments); flag-dispatch routing uses \"execute the X workflow end-to-end.\" in routing bullets — convention verified live across ~20 commands/gsd/*.md files; no ADR currently documents this specific phrasing rule (ADR-0002 covers the adjacent but distinct command-contract/@-ref-resolution seam, not this convention)", - "line": 606 + "line": 609 }, { "id": "RULESET.WORKFLOW_EXECUTION_CONTEXT", "klass": "RULESET", "value": "@-ref in commands/gsd/*.md must resolve to an existing file on disk; regression test in tests/docs-update.test.cjs (folds former \\`bug-3135-capture-backlog-workflow\\`, consolidation epic #1969); INVENTORY.md row + INVENTORY-MANIFEST.json families.workflows must stay in sync; \"Invoked by\" attribution must move when a flag absorbs a micro-skill", - "line": 605 + "line": 608 }, { "id": "RULESET.WORKFLOW_FILE_NAMES", "klass": "RULESET", "value": "workflow files use hyphens; XML attributes must match (extract-learnings not extract_learnings); tests should pin exact hyphenated name", - "line": 604 + "line": 607 }, { "id": "RULESET.WORKFLOW_MARKDOWN.FENCES", "klass": "RULESET", "value": "preserve opening language fence when editing shell snippets in workflow markdown; malformed fence creates fresh CR threads (MD040)", - "line": 600 + "line": 603 }, { "id": "RULESET.WORKFLOW_SIZE_BUDGET", "klass": "RULESET", "value": "workflow size enforcement (#1074; BYTES not lines per #717; LF-normalized per #683) = differential attribution size ratchet (PRIMARY anti-creep since #2724/ADR-2719 §4: tests/emitted-attribution.test.cjs's real-tree test reports growth in any gsd-core/workflows/*.md with its exact byte delta vs `next`, no committed snapshot, requires an ack entry — a fragment under tests/emitted-drift-acks/, #2914; the legacy tests/emitted-drift-ack.json is still honored and unioned in) + loose tier hard caps (outer red lines, NEVER raised on approach: XL<=98304 / LARGE<=61440 / DEFAULT<=40960) + discuss-phase<32000; a file that grew fails the differential guard — add an ack entry naming the file and reason, justify the growth in the PR (or extract LAZILY-loaded content; eager @-imports don't reduce loaded context); crossing a hard cap means EXTRACT, not bump. The prior per-file baseline (tests/workflow-size-baseline.json, `npm run size:baseline`) is REMOVED by #2724. Its new-file cap (ADR-1610 Decision point 3, un-baselined files <=32768, the Codex anchor) is REVIVED inside the differential's size ratchet itself (`NEW_FILE_CAP` in tests/helpers/emitted-diff.cjs) rather than lost: \"not yet baselined\" is exactly \"present in sizeCurrent, absent from sizeBaseline\", a signal the ratchet already computes for its own reasons. NOT ack-able — same as the tier hard caps, the fix is extraction. Narrower than the original: this check cannot see XL/LARGE tiering (tests/workflow-size-budget.test.cjs's classification, invisible to the pure differential module), so a legitimately large NEW file must extract rather than tier in, one release earlier than an existing file would need to — a disclosed, deliberate simplification", - "line": 601 + "line": 604 }, { "id": "SESSION.2026-05-05", "klass": "SESSION", "value": "[PRED.k320..k331 introduced; DEFECT.SOURCE-GREP-IN-NEW-TESTS, DEFECT.CHANGESET-PR-FIELD-DRIFT, DEFECT.PHASE-DIR-PREFIX-DRIFT, DEFECT.PROMPT-INJECTION-SCAN-COLLISION; ADR-0002 thin-wrapper pattern findings folded into RULESET.WORKFLOW_*]", - "line": 869 + "line": 872 }, { "id": "SESSION.2026-05-05.sdk-bridge", "klass": "SESSION", "value": "PR #3158 SDK Runtime Bridge — observability isolation rule; strict-mode dispatchMode reporting invariant; transport decision ordering (guard before event emission); folded into Dispatch Policy Module glossary", - "line": 870 + "line": 873 }, { "id": "SESSION.2026-05-09", "klass": "SESSION", "value": "[8-PR triage wave, 7 merged + 1 subsumed; META.RULE.* introduced; WAVE.LESSON.* captured; k320/k322/k323/k326/k331 evidence; AI Ops Memory predicate format established]", - "line": 871 + "line": 874 }, { "id": "SESSION.2026-05-10", "klass": "SESSION", "value": "[ai-ops memory consolidation; release-notes standard taxonomy + templates; RELEASE-NOTES.* predicates introduced]", - "line": 872 + "line": 875 }, { "id": "SESSION.2026-05-13", "klass": "SESSION", "value": "[Shell Command Projection Module expansion (#3465-#3468); ADR-0009 superseded; new exports for subprocess dispatch and platform file I/O; phase-gated migration plan; PR #3464 three-gate invariant CI+CR+unresolved=0; PR #3470 stash-include-untracked rebase pattern]", - "line": 873 + "line": 876 }, { "id": "SESSION.2026-05-14", "klass": "SESSION", "value": "[#3095/PR #3490 EXEC.CLASSIFY.* introduced (Anthropic/Copilot/Codex/Gemini [runtime removed #1928] cross-runtime rate-limit sentinel coverage); #3489/PR #3499 DEFECT.STATE-TRAMPLE.idempotency-oracle (STATE.md current_phase field is oracle for state.complete-phase); #3488/PR #3501 DAG resolver same-phase short-form depends_on (shortFormToId index added to sdk/src/query/phase.ts); #3491/PR #3502 DEFECT.NESTED-GIT-INIT (gitWorktreeInfoInternal helper); #3493/PR #3500 extractCurrentMilestone generic Phase Details continuation past planned-milestone siblings; #3503/PR #3504 DEFECT.PATH-SUBSTRING-CHECK (trailing-slash anchor for homedir checks); #3346/PR #3505 codex AoT TOML leaf-key via extractFlatHookEventName; #3506/PR #3507 label-scoped stale-bot sub-job pattern; multi-PR triage operational lessons folded into PROC.TRIAGE.*; #3508 DEFECT.AGENT-ISOLATION-SILENT-FAIL; gsd-test image-missing auto-build (locally-built image via embedded heredoc Dockerfile); refined PRED.k322 threshold to 3 PRs/<10min]", - "line": 874 + "line": 877 }, { "id": "SESSION.2026-05-15", "klass": "SESSION", "value": "[#3537/PR #3538 DEFECT.PHASE-REGEX-FANOUT — phaseMarkdownRegexSource promoted to core.cjs and wired to 7 sites; parity-style regression test established as DEFECT.GENERATIVE-FIX exemplar; trek-e/gsd-test-runner#1 filed for DEFECT.GSD-TEST-MIRROR-POISONED — chown-back-before-exec legacy gap (poisoned holodeck mirror unstuck via authorized docker chown to remote 1000:1000); RULESET.PR-FLOW.* codified from project CLAUDE.md load-bearing rule; first dispatch under run-tests-before-create held cleanly (PR #3520 worker stopped on Docker exit 12 infra failure, orchestrator opened PR after unblock); CONTEXT.md refactored from 882 lines of mixed prose+predicates into ~500 lines of pure-predicate format with chronological session log]", - "line": 875 + "line": 878 }, { "id": "SESSION.2026-05-15.parallel-fix-dispatch", "klass": "SESSION", "value": "[#3542/PR #3546 prohibit git stash family in executor agents (shared refs/stash across worktrees); #3541/PR #3547 non-TTY resolution for installer prompt-user actions (default remove for SDK build artifacts, keep for skills/gsd-*/SKILL.md); #3545 filed for gsd-test-summary concurrent /tmp output collision; new predicates DEFECT.HOOK-OVER-ENFORCEMENT.read-tool-tracking, DEFECT.GSD-TEST-CONCURRENT-OUTPUT-COLLISION, DEFECT.SUBAGENT-LONG-RUNNING-BG-STALL, DEFECT.AGENT-RETIRED-SLASH-SYNTAX-DRIFT, PROC.PARALLEL-FIX-DISPATCH; agent-trust-but-verify caught /gsd-update retired-syntax comment slip in #3541 implementation before PR open]", - "line": 876 + "line": 879 }, { "id": "SESSION.2026-05-16", "klass": "SESSION", "value": "[multi-PR triage wave (#3577/3581/3640/3641/3642/3648/3649/3637/3639). Established global PreToolUse hook ~/.claude/hooks/test-memory-guard.sh denying new node/test spawns when sum(RSS of node|vitest|jest|...) >= 4 GiB on the 24 GB Mac OR when a same-runner process is already in argv[0] — hard deny via hookSpecificOutput.permissionDecision=deny. PR #3577 fix: revert config-ensure-section dispatch to CJS cmdConfigEnsureSection (SDK author wrote single-section semantics under a name whose legacy callers expect full-default config init); plus 3 SDK parity carve-outs (configNewProject defaults align with sdk/shared/config-defaults.manifest.json, return relative .planning/config.json path, drop quotes from Unknown config key, lead malformed-JSON error with \"Failed to read config.json:\"). PR #3649 fix: chunk node --test spawn at 28K argv ceiling (Windows CreateProcess lpCommandLine cap 32,767 was instantly aborting unchunked spawn of 546 paths). Chunking fix surfaced 14 pre-existing Windows-only test bugs (4010 pass / 14 fail; vs 0/0 before — entire suite was un-runnable on Windows). PRs #3639 + #3637 confirmed unable to stand alone (legitimately depend on Phase 6 scaffolding only present on feat/3575-enforcement-hardening) — user decision: cherry-pick into #3577 and close. Five other PRs each had ≤1 unresolved CR thread of the changeset-pr-number / null-vs-throw / implicit-Claude-runtime / docs-stale-guidance / hardcoded-tests-path family — all quick wins. New predicates: DEFECT.SDK-PORT-NAME-COLLISION, DEFECT.WINDOWS-ARGV-OVERFLOW, DEFECT.STACKED-PR-CANNOT-STAND-ALONE, DEFECT.CANARY-VERSION-LEAK, DEFECT.GSD-TEST-HOST-MID-RUN-DEATH, RULESET.HARNESS.test-memory-guard, RULESET.PR-FLOW.docker-before-push, RULESET.PR-FLOW.templates-mandatory]", - "line": 877 + "line": 880 }, { "id": "WAVE.LESSON.agent-narrative-unreliable", "klass": "WAVE", "value": "k095/k324 confirmed at scale: 5 of 8 agents terminated mid-monitor with stale claims requiring direct verification", - "line": 831 + "line": 834 }, { "id": "WAVE.LESSON.changelog-policy-violation-multiplier", "klass": "WAVE", "value": "brief contradicting CONTRIBUTING.md's changelog-fragment policy (\"CHANGELOG Entries — Drop a Fragment\" section) produced violations on 5 of 8 PRs (#3300, #3302, #3304, #3305, #3308); k326 + k320 capture", - "line": 828 + "line": 831 }, { "id": "WAVE.LESSON.cr-throttle-burst-correlation", "klass": "WAVE", "value": "8 PRs in <15min triggered k322 sustained-throttle on multiple PRs (#3306 worst case)", - "line": 829 + "line": 832 }, { "id": "WAVE.LESSON.k101-still-trips", "klass": "WAVE", "value": "even after CONTEXT.md k101 reinforcement, agent of record posted self-PR comment on close; k331 adds explicit close-time literal-instruction guard", - "line": 832 + "line": 835 }, { "id": "WAVE.LESSON.sibling-audit-overlap", "klass": "WAVE", "value": "k015-family parallel dispatch on #3297 + #3298 produced k323 add-backlog.md cross-PR overlap", - "line": 830 + "line": 833 }, { "id": "WORKSTREAM.INVARIANT.migrate-name", "klass": "WORKSTREAM", "value": "must normalize through canonical slug policy", - "line": 654 + "line": 657 }, { "id": "WORKSTREAM.INVARIANT.slug-contract", "klass": "WORKSTREAM", "value": "all .planning/workstreams/ must be addressable by set/get/status/complete", - "line": 655 + "line": 658 }, { "id": "WORKSTREAM.NAME.POLICY.cjs-module", "klass": "WORKSTREAM", "value": "gsd-core/bin/lib/workstream-name-policy.cjs owns toWorkstreamSlug + active-name/path-segment validation", - "line": 670 + "line": 673 }, { "id": "WORKSTREAM.POINTER.SEAM.cjs-module", "klass": "WORKSTREAM", "value": "gsd-core/bin/lib/active-workstream-store.cjs owns read/write self-heal for .planning/active-workstream", - "line": 671 + "line": 674 }, { "id": "WORKSTREAM.REGRESSION.test-anchor", "klass": "WORKSTREAM", "value": "tests/workstream.test.cjs::normalizes --migrate-name to a valid workstream slug", - "line": 656 + "line": 659 }, { "id": "WORKTREE.SEAM.caller-rule", "klass": "WORKTREE", "value": "verify.cjs must consume inspectWorktreeHealth for W017 classification; no ad-hoc porcelain parsing in callers", - "line": 664 + "line": 667 }, { "id": "WORKTREE.SEAM.current", "klass": "WORKTREE", "value": "Worktree Safety Policy Module", - "line": 648 + "line": 651 }, { "id": "WORKTREE.SEAM.decision-1", "klass": "WORKTREE", "value": "retain non-destructive default; destructive path only as explicit future opt-in scaffold", - "line": 652 + "line": 655 }, { "id": "WORKTREE.SEAM.default-prune-policy", "klass": "WORKTREE", "value": "metadata_prune_only (non-destructive)", - "line": 651 + "line": 654 }, { "id": "WORKTREE.SEAM.files", "klass": "WORKTREE", "value": "[gsd-core/bin/lib/worktree-safety.cjs]", - "line": 649 + "line": 652 }, { "id": "WORKTREE.SEAM.interface", "klass": "WORKTREE", "value": "[resolveWorktreeContext, parseWorktreePorcelain, planWorktreePrune, executeWorktreePrunePlan, planWorktreeRecordAgent, cmdWorktreeRecordAgent]", - "line": 650 + "line": 653 }, { "id": "WORKTREE.SEAM.invariant", "klass": "WORKTREE", "value": "parser failure must degrade to metadata_prune_only and never escalate to destructive removal", - "line": 662 + "line": 665 }, { "id": "WORKTREE.SEAM.inventory-interface", "klass": "WORKTREE", "value": "[listLinkedWorktreePaths, inspectWorktreeHealth]", - "line": 663 + "line": 666 }, { "id": "WORKTREE.SEAM.inventory-snapshot", "klass": "WORKTREE", "value": "snapshotWorktreeInventory(repoRoot,{staleAfterMs,nowMs}) is canonical linked-worktree health snapshot for callers", - "line": 666 + "line": 669 }, { "id": "WORKTREE.SEAM.test-anchor-w017", "klass": "WORKTREE", "value": "tests/orphan-worktree-detection.test.cjs + tests/worktree-safety.test.cjs", - "line": 665 + "line": 668 }, { "id": "WORKTREE.SEAM.test-anchors", "klass": "WORKTREE", "value": "[resolveWorktreeContext:has_local_planning|linked_worktree|not_git_repo|main_worktree, planWorktreePrune:git_list_failed|worktrees_present|no_worktrees|parser_throw_fallback, executeWorktreePrunePlan:missing_plan|skip_passthrough|unsupported_action|metadata_prune_only]", - "line": 661 + "line": 664 }, { "id": "WORKTREE.SEAM.test-policy", "klass": "WORKTREE", "value": "cover all decision branches in policy module before changing prune behavior", - "line": 660 + "line": 663 } ], "duplicates": [] diff --git a/scripts/lint-emitted-drift-ack.cjs b/scripts/lint-emitted-drift-ack.cjs index 6c88aedf0..88bc20d97 100644 --- a/scripts/lint-emitted-drift-ack.cjs +++ b/scripts/lint-emitted-drift-ack.cjs @@ -32,12 +32,46 @@ const fs = require('node:fs'); const path = require('node:path'); +const { execFileSync } = require('node:child_process'); const ACK_VERSION = 1; const ACK_REPO_PATH = 'tests/emitted-drift-ack.json'; const ACK_DIR_REPO_PATH = 'tests/emitted-drift-acks'; const REPO_ROOT = path.join(__dirname, '..'); +/** + * Upper bound on any one git call made by the `--guard-next` lane (#3078). + * + * Every subprocess this repo spawns is bounded (CLAUDE.md -> KNOWN DEFECTS, "Unbounded + * Subprocesses": 5-30s for git). The guard reads one directory listing plus one blob per + * surviving fragment, all against local objects, so 15s is generous — but an unbounded + * `execFileSync` on a wedged git is an indefinite hang in a job whose whole timeout + * budget is one minute. + */ +const GIT_TIMEOUT_MS = 15_000; + +/** + * Characters that render as nothing: soft hyphen, the zero-width family, word joiner, + * BOM. Stripped before ack reasons are compared, so an invisible edit cannot make a spent + * acknowledgment look re-armed. + * + * DUPLICATED from `INVISIBLE` in `tests/helpers/emitted-diff.cjs`, for the same reason + * every other constant here is duplicated rather than required: `scripts/` ships in the + * npm package and `tests/` does not, so the require would be MODULE_NOT_FOUND once + * published (see this file's top-of-file comment). The two are held together by the + * prose-parity test in `tests/emitted-attribution.test.cjs`, which enumerates the gate's + * own codepoints and fails if this list stops covering them. + * + * Spelled as codepoints on purpose — a literal character class here would itself be + * invisible in review, which is the exact failure being defended against. + */ +const ACK_INVISIBLE = new RegExp( + `[${[0x00AD, 0x200B, 0x200C, 0x200D, 0x2060, 0xFEFF] + .map((c) => `\\u${c.toString(16).toUpperCase().padStart(4, '0')}`) + .join('')}]`, + 'g', +); + /** * Upper bound on how many fragment files `listFragmentFiles` may return in one * `readdirSync` pass. Mirrors `MAX_ACK_FRAGMENTS` in `tests/helpers/emitted-diff.cjs` — @@ -215,6 +249,132 @@ function declaredKeys(raw) { return Object.keys(paths).filter((k) => !RESERVED_ACK_KEYS.has(k)); } +/** + * Normalize an ack reason to the prose a reviewer actually reads. + * + * Mirrors the gate's own `prose()` inside `diffEmitted` (`tests/helpers/emitted-diff.cjs`) + * exactly: strip invisibles, collapse internal whitespace, trim. Re-arming a spent ack is + * legitimate — it is how a contributor says "this is a NEW ripple, and here is why" — but + * it must cost an ACTUAL explanation, so a doubled space, a CRLF, or a U+200B may never + * make a spent entry look live. Bounded by the parity test named on ACK_INVISIBLE. + */ +function ackProse(reason) { + return reason.replace(ACK_INVISIBLE, '').replace(/\s+/g, ' ').trim(); +} + +/** + * The declared entries of one ack document as `path key -> normalized prose`, or `null` + * when the document cannot be trusted to answer the question. + * + * `null` is NOT "no entries" — it is "do not draw a conclusion from this file". A document + * that will not parse, is not an object, has a non-object `paths`, carries a reasonless or + * non-string entry, or names a RESERVED_ACK_KEY cannot be shown to be spent, and the + * conservative direction here is to leave it alone: `validateAckText` (run by `lint:ci`, + * pre-merge) owns SHAPE and already blocks such a document from reaching the base, while + * this guard owns LIFECYCLE. Sweeping a file we could not read would delete an + * acknowledgment on the strength of a parse failure. + * + * An ABSENT document (`raw === null`) is a genuine empty entry set — the fragment simply + * did not exist at that ref, so nothing it declares now has a counterpart there. + */ +function ackEntries(raw) { + if (raw === null) return new Map(); + let doc; + try { + doc = JSON.parse(raw); + } catch { + return null; + } + if (!isPlainObject(doc)) return null; + const paths = doc.paths; + if (paths === undefined) return new Map(); + if (!isPlainObject(paths)) return null; + + const entries = new Map(); + for (const [rel, value] of Object.entries(paths)) { + if (RESERVED_ACK_KEYS.has(rel)) return null; + const reason = isPlainObject(value) ? value.reason : value; + if (typeof reason !== 'string') return null; + entries.set(rel, ackProse(reason)); + } + return entries; +} + +/** + * assertNoAllSpentFragments — the fragment half of the `next`-lane guard (#3078). + * + * #2914 split the single shared ack file into per-PR fragments and deliberately exempted + * the fragment directory from `assertAbsentOnNext`, on the premise that a persistent + * fragment "cannot conflict with any other PR". That premise does not hold. Fragments do + * not share a FILE, but they do share a PATH KEY SPACE, and `main()` below treats a path + * claimed by two sources as a hard failure. So a merged fragment is not harmless: every + * entry it leaves on `next` is spent by definition — its prose is already at the base, so + * it gates nothing — while still owning its key, and the next PR that grows the same + * workflow can declare it neither in the owning fragment (spent) nor in its own + * (duplicate). That is the exact failure #2914 fixed for the legacy file, reintroduced one + * level down. Measured on `next` when this landed: 45 fragments owning 403 paths. + * + * Unlike the legacy file, PRESENCE alone is not the failure — a fragment landed by the + * very push being guarded is the healthy case for every ack-carrying PR. The failure is + * INERTNESS: every surviving entry's prose already matches the copy at the base ref, so + * the fragment can no longer clear a delta for anyone. A PARTIALLY spent fragment is left + * alone; only an entirely inert one is cruft. That distinction is why the base side is + * required, and it is what keeps the re-arm-by-appending route working (#2639, #2993). + * + * An ENTRYLESS document is vacuously all-spent and swept for the same reason + * `validateAckText` refuses to let one be committed: it acknowledges nothing. + * + * Pure — no fs, no git, no clock. `main()` does the reading. + * + * @param {Array<{name: string, currentRaw: string|null, baseRaw: string|null}>} fragments + * @returns {{ ok: boolean, message: string, sweepable: string[] }} + */ +function assertNoAllSpentFragments(fragments) { + const sweepable = []; + + for (const { name, currentRaw, baseRaw } of fragments) { + const current = ackEntries(currentRaw); + if (current === null) continue; // unreadable — `validateAckText` owns that verdict + const base = ackEntries(baseRaw); + if (base === null) continue; + + const allSpent = [...current].every(([rel, prose]) => base.get(rel) === prose); + if (allSpent) sweepable.push({ name, entries: current.size }); + } + + if (sweepable.length === 0) { + return { + ok: true, + message: `ok guard-no-ack-on-next: no all-spent fragment survives in ${ACK_DIR_REPO_PATH}/`, + sweepable: [], + }; + } + + const lines = sweepable.map( + ({ name, entries }) => ` - ${ACK_DIR_REPO_PATH}/${name} (${entries} entr${entries === 1 ? 'y' : 'ies'}, all spent)` + + `\n remedy: git rm ${ACK_DIR_REPO_PATH}/${name}`, + ); + + return { + ok: false, + sweepable: sweepable.map(({ name }) => name), + message: [ + `guard-no-ack-on-next: ${sweepable.length} fully-spent ack fragment(s) survive on next.`, + '', + ...lines, + '', + 'Every entry in these fragments is already at the base, so each is spent and gates ' + + 'nothing (#2789) — but it still OWNS its path keys. The next PR that grows one of ' + + 'those paths can declare it neither here (spent) nor in its own fragment (a ' + + 'duplicate ack is a hard failure), so a spent fragment left behind is a wall, not ' + + 'harmless cruft (#3078).', + '', + 'A partially spent fragment is deliberately NOT reported: only an entirely inert one ' + + 'is swept, so appending prose to a live entry to re-arm it keeps working.', + ].join('\n'), + }; +} + /** * assertAbsentOnNext — the `next`-lane guard (#2914), invoked only by the * `guard-no-ack-on-next` workflow job on push to `next`, never in `lint:ci`. @@ -231,6 +391,12 @@ function declaredKeys(raw) { * instant one landed). It is safe only because it runs on `next` itself, asserting a fact * about `next`'s own tree, never about any PR's diff against it. * + * Scoped to the LEGACY FILE ONLY — the fragment directory is guarded by + * `assertNoAllSpentFragments` above, on a stricter-to-state but weaker-to-apply rule + * (inertness, not presence). #3078 corrected the original premise that a persisting + * fragment "cannot conflict with any other PR": fragments share a path key space even + * though they do not share a file. + * * @param {boolean} present whether ACK_REPO_PATH exists in the tree being checked * @returns {{ ok: boolean, message: string }} */ @@ -248,9 +414,10 @@ function assertAbsentOnNext(present) { + 'whether it is otherwise well-formed.', '', '#2914: acks now go in per-PR fragments under tests/emitted-drift-acks/, one file per ' - + 'PR, never this single shared file -- a persistent fragment there is harmless (every ' - + 'fragment is independently named, so it cannot conflict with any other PR), which is ' - + 'why only THIS legacy file is guarded here, never the fragment directory.', + + 'PR, never this single shared file. A fragment cannot MERGE-CONFLICT with another ' + + "PR's fragment, but it does own its path keys, so a fully-spent one left on next " + + 'still blocks the next PR that grows the same path (#3078) -- fragments are guarded ' + + 'separately, on inertness rather than on presence.', '', 'CONTRIBUTING.md: "When you remove the last entry from tests/emitted-drift-ack.json, ' + 'delete the file too -- its presence is the alarm."', @@ -260,13 +427,123 @@ function assertAbsentOnNext(present) { }; } +/** + * Every git call declares the SPECIFIC directory it operates on as safe, mirroring + * `safeDirArgs` in `tests/helpers/emitted-runtime.cjs` (#2767): a checkout mounted at a + * path owned by a different uid makes git refuse EVERY operation there with "detected + * dubious ownership", and this guard's whole value is that it fails LOUDLY on a real + * fault rather than degrading to "no base, nothing spent". Never the `*` wildcard, which + * would mark every repository on the machine safe. Duplicated rather than imported for + * the reason stated at the top of this file: `scripts/` ships in the npm package and + * `tests/` does not. + */ +function git(args, { cwd = REPO_ROOT } = {}) { + return execFileSync('git', ['-c', `safe.directory=${path.resolve(cwd)}`, ...args], { + cwd, + encoding: 'utf8', + timeout: GIT_TIMEOUT_MS, + maxBuffer: 16 * 1024 * 1024, + stdio: ['ignore', 'pipe', 'pipe'], + }); +} + +/** + * The LOCAL/MANUAL fallback for "the commit `next` was at BEFORE this push" — `HEAD^`, + * or `null` on a root commit. Correct only when the push it is standing in for carries + * exactly one commit. + * + * CI never relies on this: it passes the authoritative pre-push tip explicitly via + * `--base-ref` (`github.event.before`, wired in `.github/workflows/test.yml`), because + * the default branch's ruleset allows REBASE merges + * (`.github/rulesets/main-protection.json`, `allowed_merge_methods`), so a single push + * event can land N commits at once. `HEAD^` steps back exactly one commit — for a + * 2-commit rebase-merge whose first commit adds a fragment and whose second is + * unrelated, `HEAD^` would land on the first commit, read the fragment as already + * present there, and demand `git rm` on the very push that introduced it (#3078). This + * function exists purely as the manual-run / single-commit-push fallback. + * + * Two steps on purpose. `HEAD` is resolved first, which proves git runs and the working + * directory is a readable repository; only then is a failure to resolve `HEAD^` read as + * "this commit has no parent". A single blanket try/catch would collapse "git is broken" + * into "there is no base", and a guard with no base sweeps nothing — it would pass + * vacuously, which is precisely how the legacy-file job spent months guarding a file that + * had not existed since #2914 (#3078). + */ +function resolveBaseRef({ cwd = REPO_ROOT, run = git } = {}) { + run(['rev-parse', '--verify', 'HEAD'], { cwd }); + try { + return run(['rev-parse', '--verify', 'HEAD^'], { cwd }).trim(); + } catch { + return null; // root commit — nothing can be spent against it + } +} + +/** + * Raw text of one ack fragment AT `base`, or `null` when it is simply not there. + * + * Mirrors `readAckFileAtRef` in `tests/helpers/emitted-runtime.cjs` (duplicated across the + * ships/does-not-ship line, as everything else here is): `git show` alone cannot tell a + * bogus ref from an absent path — both say "does not exist in" — so absence is established + * with `ls-tree`, which exits 0 with empty output when the path is not there and non-zero + * on a real fault. A genuine git failure THROWS rather than degrading to `null`, because + * "could not read the base" read as "absent at the base" would make every fragment look + * brand-new and silently disarm the sweep. + */ +function readFragmentAtRef(base, name, { cwd = REPO_ROOT, run = git } = {}) { + const repoPath = `${ACK_DIR_REPO_PATH}/${name}`; + const listing = run(['ls-tree', '--name-only', base, '--', repoPath], { cwd }); + if (listing.trim() === '') return null; + return run(['show', `${base}:${repoPath}`], { cwd }); +} + +/** + * A base ref must not begin with `-`: `execFileSync`'s array form stops shell + * metacharacters but not git's own option parsing, and `git show` honors diff options + * including `--output=`, which WRITES. Same guard, same reason, as + * `readAckFileAtRef`'s. + */ +function assertUsableBaseRef(ref) { + if (typeof ref !== 'string' || ref === '' || ref.startsWith('-')) { + throw new Error( + `lint-emitted-drift-ack: refusing to read fragments at ${JSON.stringify(ref)} — a base ` + + 'ref must be a non-empty string that does not begin with "-", which git would parse ' + + 'as an option.', + ); + } + return ref; +} + function main() { const legacyFile = path.join(REPO_ROOT, ...ACK_REPO_PATH.split('/')); if (process.argv.includes('--guard-next')) { - const result = assertAbsentOnNext(fs.existsSync(legacyFile)); - console.log(result.message); - if (!result.ok) process.exitCode = 1; + const legacy = assertAbsentOnNext(fs.existsSync(legacyFile)); + console.log(legacy.message); + + // The fragment half (#3078). CI always passes `--base-ref` (the pre-push tip of + // `next`, `github.event.before`), because the default branch allows REBASE merges + // and one push can carry N commits — `HEAD^` alone is not "the state of next before + // this push" in that case. `resolveBaseRef()`'s `HEAD^` is only the fallback for a + // manual run or a single-commit push, where the two agree. NOTE the job's checkout + // must fetch at least depth 2 for the `HEAD^` fallback to resolve at all, and must + // separately fetch the `--base-ref` commit itself, or every fragment reads as + // brand-new. + const baseFlag = process.argv.indexOf('--base-ref'); + const baseRef = baseFlag === -1 + ? resolveBaseRef() + : assertUsableBaseRef(process.argv[baseFlag + 1]); + + const dir = path.join(REPO_ROOT, ...ACK_DIR_REPO_PATH.split('/')); + const fragments = listFragmentFiles(dir).map((name) => ({ + name, + currentRaw: readIfPresent(path.join(dir, name)), + baseRaw: baseRef === null ? null : readFragmentAtRef(baseRef, name), + })); + + const sweep = assertNoAllSpentFragments(fragments); + console.log(sweep.message); + + if (!legacy.ok || !sweep.ok) process.exitCode = 1; return; } @@ -301,7 +578,12 @@ function main() { problems.push( `duplicate ack for "${key}": declared in both ${owner.get(key)} and ${label}. ` + 'Two ack sources (fragments, or a fragment and the legacy file) may never ' - + 'name the same path — rename or merge them.', + + 'name the same path. Resolve it one of two ways, depending on the owner: if ' + + `${owner.get(key)} is already merged, its entry is SPENT and gates nothing — ` + + `delete it (git rm ${owner.get(key)}) and keep your own. If it is still live ` + + 'on this branch, APPEND your explanation to its existing entry instead, which ' + + 're-arms it — re-arming deliberately costs actual new prose. Do not rename ' + + 'the path to dodge this.', ); continue; } @@ -335,10 +617,19 @@ if (require.main === module) main(); module.exports = { validateAckText, assertAbsentOnNext, + assertNoAllSpentFragments, + ackProse, + ackEntries, declaredKeys, listFragmentFiles, ACK_VERSION, ACK_REPO_PATH, ACK_DIR_REPO_PATH, + ACK_INVISIBLE, MAX_ACK_FRAGMENTS, + git, + resolveBaseRef, + readFragmentAtRef, + assertUsableBaseRef, + GIT_TIMEOUT_MS, }; diff --git a/tests/agent-tracked-source-rule.test.cjs b/tests/agent-tracked-source-rule.test.cjs index 7a92dd8ee..2b6a8cf33 100644 --- a/tests/agent-tracked-source-rule.test.cjs +++ b/tests/agent-tracked-source-rule.test.cjs @@ -79,20 +79,15 @@ describe('#3645 — agents write only git-tracked source paths', () => { 'the rule belongs in the spawn contract, not the frozen agent file (#3645)'); }); - test('growth ack recorded for the grown shipped files (#3645)', () => { - const acksDir = path.join(__dirname, 'emitted-drift-acks'); - const readAck = (name) => fs.readFileSync(path.join(acksDir, name), 'utf8'); - const mine = fs.readdirSync(acksDir).find((f) => f.includes('3645')); - assert.ok(mine, 'an emitted-drift-acks fragment for #3645 must exist'); - const mineSrc = readAck(mine); - assert.ok(mineSrc.includes('"gsd-pattern-mapper.md"'), - 'the #3645 fragment must acknowledge the grown mapper by its ack key'); - assert.ok(!mineSrc.includes('"gsd-planner.md"'), - 'gsd-planner.md is unchanged — no ack entry for it'); - // plan-phase.md's growth ack lives in the 3409 fragment (two ack sources - // may never name the same path — the merge precedent). - const planPhaseAck = readAck('3409-unreachable-guard-arms.json'); - assert.ok(planPhaseAck.includes('"plan-phase.md"') && planPhaseAck.includes('#3645 append'), - 'the plan-phase.md growth reason must be appended to the 3409 fragment (#3645)'); - }); + // A prior version of this suite pinned the EXISTENCE and CONTENTS of the `3645` and + // `3409` emitted-drift-acks fragments. An ack is scoped to the diff that introduced + // it (#2789's ack-lifecycle law): once #3645 merged and its growth is in `next`'s + // baseline, neither fragment gates anything — both are spent, and #3078's + // `guard-no-ack-on-next` sweeps them. A test may therefore never pin a fragment's + // existence or its prose; a fragment that is correctly swept would fail the pinning + // test for a reason that has nothing to do with the behavior it was meant to protect. + // #3645's actual protection is the two behavioral tests above — the mapper emitting + // only tracked analog paths, and the frozen planner file staying untouched — which + // this change leaves exactly as they were. The growth itself is protected by `next`'s + // emitted baseline (the differential attribution check), not by the spent fragment. }); diff --git a/tests/emitted-attribution.test.cjs b/tests/emitted-attribution.test.cjs index b1f5ab96d..97a327ea4 100644 --- a/tests/emitted-attribution.test.cjs +++ b/tests/emitted-attribution.test.cjs @@ -78,7 +78,14 @@ const { EXPECTED_MANIFEST_COUNT, loadManifests } = require('./helpers/emitted-pr const { validateAckText, assertAbsentOnNext, + assertNoAllSpentFragments, + ackProse, + ackEntries, + ACK_INVISIBLE, MAX_ACK_FRAGMENTS: MAX_ACK_FRAGMENTS_LINT, + resolveBaseRef, + readFragmentAtRef, + assertUsableBaseRef, } = require('../scripts/lint-emitted-drift-ack.cjs'); const { ACK_VERSION, @@ -87,6 +94,8 @@ const { NEW_FILE_CAP, MAX_ACK_FRAGMENTS, REMEDIATION, + INVISIBLE, + normalizeAckReason, sourceSatisfiedBy, parseAck, mergeAckSources, @@ -934,9 +943,18 @@ test('assertAbsentOnNext fails when the file is present with entries, naming the assert.match(r.message, /git rm tests\/emitted-drift-ack\.json/, 'the remedy must be named, not just the problem'); assert.match( r.message, /tests\/emitted-drift-acks\//, - '#2914: the message must explain that acks now go in per-PR fragments, and that a ' - + 'persisting fragment (unlike this legacy file) is harmless', + '#2914: the message must explain that acks now go in per-PR fragments', ); + // #3078 removed the premise this used to assert: a persisting fragment is NOT harmless + // just because it does not share a FILE with another PR — fragments share a PATH KEY + // SPACE, and a duplicate path across two sources is a hard failure in main(), so a + // fully-spent fragment left behind still owns keys it can no longer gate. + assert.doesNotMatch( + r.message, /cannot conflict with any other PR/, + '#3078: this premise was false and has been removed — a fragment cannot MERGE-CONFLICT ' + + 'with another PR\'s fragment, but it can still collide on a path key', + ); + assert.match(r.message, /#3078/); }); test('assertAbsentOnNext fed from a real next-like tree: absent passes, present fails (regression, #2914)', () => { @@ -958,6 +976,803 @@ test('assertAbsentOnNext fed from a real next-like tree: absent passes, present assert.match(r.message, /exists on next/); }); +// ─── guard-no-ack-on-next: fully-spent FRAGMENTS are guarded on inertness (#3078) ── +// +// #2914 exempted the fragment directory from `assertAbsentOnNext` on the premise that +// a persistent fragment "cannot conflict with any other PR". That premise is false: +// fragments do not share a FILE, but they do share a PATH KEY SPACE, and a duplicate +// path declared by two sources is a hard failure in `main()` below. So a merged, +// fully-spent fragment still OWNS its path keys, and the next PR that grows one of +// those paths can declare it neither there (spent — `parseAck`'s `isSpent`/`prose` +// contract) nor in its own fragment (a cross-source duplicate). Unlike the legacy +// file, PRESENCE alone is not the failure here — a fragment landed by the very push +// being guarded is the healthy case for every ack-carrying PR. The failure is +// INERTNESS: every surviving entry's prose already matches the base ref's copy, so +// the fragment can no longer clear a delta for anyone. + +const doc = (paths) => JSON.stringify({ version: ACK_VERSION, paths }); +const frag = (name, current, base) => ({ name, currentRaw: current, baseRaw: base }); + +// ── A. assertNoAllSpentFragments — the pure lifecycle rule ────────────────── + +test('ackEntries: a literal "null" document is unreadable, distinct from an entryless object document', () => { + // The distinction assertNoAllSpentFragments's "literal null" test above depends on: + // an entryless OBJECT document is a genuine empty entry set (`new Map()`), while the + // TEXT "null" parses to the JS value `null`, fails isPlainObject, and returns the + // "cannot trust this document" sentinel instead. + assert.deepEqual(ackEntries('{}'), new Map()); + assert.deepEqual(ackEntries('{"version":1,"paths":{}}'), new Map()); + assert.equal(ackEntries('null'), null); + assert.equal(ackEntries(null) instanceof Map, true, 'an ABSENT fragment (raw === null) is a genuine empty entry set'); + assert.deepEqual(ackEntries(null), new Map()); +}); + +test('assertNoAllSpentFragments: zero fragments is ok, vacuously', () => { + const r = assertNoAllSpentFragments([]); + assert.ok(r.ok); + assert.match(r.message, /no all-spent fragment survives/); + assert.deepEqual(r.sweepable, []); +}); + +test('assertNoAllSpentFragments: a fragment absent at the base is live — the healthy shape of every fresh PR', () => { + // A fragment landed by the very push being guarded is the NORMAL case for every + // ack-carrying PR. Reporting it would red `next` on every such merge. + const r = assertNoAllSpentFragments([ + frag('a.json', doc({ 'x.md': { reason: 'why' } }), null), + ]); + assert.ok(r.ok); + assert.deepEqual(r.sweepable, []); +}); + +test('assertNoAllSpentFragments: a fully-spent fragment is reported, naming the file, "all spent", and the exact git rm remedy (n=1, n=3)', () => { + for (const entries of [ + { 'x.md': { reason: 'why' } }, + { 'a.md': { reason: 'a' }, 'b.md': { reason: 'b' }, 'c.md': { reason: 'c' } }, + ]) { + const raw = doc(entries); + const r = assertNoAllSpentFragments([frag('spent.json', raw, raw)]); + assert.ok(!r.ok, `${Object.keys(entries).length} entr(y/ies) must still be reported all-spent`); + assert.match(r.message, /spent\.json/); + assert.match(r.message, /all spent/); + assert.match(r.message, new RegExp(escapeRegex(`git rm ${ACK_DIR_REPO_PATH}/spent.json`))); + assert.deepEqual(r.sweepable, ['spent.json']); + } +}); + +test('assertNoAllSpentFragments: a partially spent fragment (one entry new) is left alone', () => { + // Named explicitly as a must-not-change: appending a new entry beside an + // already-spent one must not get swept out from under the live one. + const r = assertNoAllSpentFragments([ + frag( + 'mixed.json', + doc({ x: { reason: 'x-reason' }, y: { reason: 'y-reason' } }), + doc({ x: { reason: 'x-reason' } }), + ), + ]); + assert.ok(r.ok); + assert.deepEqual(r.sweepable, []); +}); + +test('assertNoAllSpentFragments: re-arming by appending genuinely new prose keeps working (#2639, #2993)', () => { + const r = assertNoAllSpentFragments([ + frag('r.json', doc({ x: { reason: 'a brand new explanation' } }), doc({ x: { reason: 'the original explanation' } })), + ]); + assert.ok(r.ok, 're-arming by appending genuinely new prose must not be swept'); +}); + +test('assertNoAllSpentFragments: a zero-information reword never looks like a re-arm — doubled whitespace, leading/trailing whitespace, CRLF vs LF', () => { + const cases = [ + ['doubled internal whitespace', 'the original explanation', 'the original explanation'], + ['leading/trailing whitespace', ' the original explanation ', 'the original explanation'], + ['CRLF vs LF inside the reason', 'the original\r\nexplanation', 'the original\nexplanation'], + ]; + for (const [label, current, base] of cases) { + const r = assertNoAllSpentFragments([ + frag('r.json', doc({ x: { reason: current } }), doc({ x: { reason: base } })), + ]); + assert.ok(!r.ok, `${label}: a zero-information reword must still read as spent`); + assert.deepEqual(r.sweepable, ['r.json'], label); + } +}); + +test('assertNoAllSpentFragments: each invisible codepoint alone must not re-arm a spent entry', () => { + const codepoints = [0x00AD, 0x200B, 0x200C, 0x200D, 0x2060, 0xFEFF]; + for (const cp of codepoints) { + const base = 'the original explanation'; + const current = `the${String.fromCodePoint(cp)} original explanation`; + const r = assertNoAllSpentFragments([ + frag('r.json', doc({ x: { reason: current } }), doc({ x: { reason: base } })), + ]); + assert.ok(!r.ok, `U+${cp.toString(16).toUpperCase()} alone must not re-arm a spent entry`); + } +}); + +test('assertNoAllSpentFragments: every SURVIVING entry is spent, even after one entry is dropped', () => { + const base = doc({ x: { reason: 'x-reason' }, y: { reason: 'y-reason' } }); + const current = doc({ x: { reason: 'x-reason' } }); + const r = assertNoAllSpentFragments([frag('drop.json', current, base)]); + assert.ok(!r.ok); + assert.deepEqual(r.sweepable, ['drop.json']); +}); + +test('assertNoAllSpentFragments: disjoint key sets between current and base is not spent', () => { + const r = assertNoAllSpentFragments([ + frag('disjoint.json', doc({ x: { reason: 'x' } }), doc({ y: { reason: 'y' } })), + ]); + assert.ok(r.ok); +}); + +test('assertNoAllSpentFragments: an entryless-but-parseable-object document is vacuously all-spent (n=0 boundary)', () => { + // Vacuously all-spent: `[...new Map()].every(...)` is true, so an object document + // declaring zero entries is swept for the same reason validateAckText refuses to let + // one be committed — it acknowledges nothing. + for (const raw of ['{}', '{"version":1}', '{"version":1,"paths":{}}']) { + const r = assertNoAllSpentFragments([frag('empty.json', raw, raw)]); + assert.ok(!r.ok, `${raw} must be swept`); + assert.deepEqual(r.sweepable, ['empty.json'], raw); + } +}); + +test('assertNoAllSpentFragments: a document of literally "null" is UNREADABLE here, not entryless, so it is left alone', () => { + // Distinct from the object-shaped entryless case above: `ackEntries` parses the text + // "null" to the JS value `null`, which fails `isPlainObject` and returns `null` + // (its "cannot trust this document" sentinel) — NOT `new Map()`. So this fragment is + // skipped by assertNoAllSpentFragments entirely, same as any other unreadable + // document (see the next test) — it is never counted as vacuously spent, unlike + // validateAckText's POLICY layer, which does reject "null" (present-but-entryless). + // The two surfaces are allowed to differ here: this guard owns LIFECYCLE, not shape. + const r = assertNoAllSpentFragments([frag('literal-null.json', 'null', 'null')]); + assert.ok(r.ok, 'a literal "null" document must not be swept by this guard'); + assert.deepEqual(r.sweepable, []); +}); + +test('assertNoAllSpentFragments: a document it cannot trust to answer the question is left alone, never swept', () => { + // validateAckText / lint:ci owns SHAPE; this guard owns LIFECYCLE. Sweeping a + // fragment on the strength of a parse failure would delete an acknowledgment for + // the wrong reason. + const validBase = doc({ x: { reason: 'x' } }); + for (const currentRaw of ['{ not json', '[]', '42', '{"paths":[]}', '{"paths":{"x":42}}']) { + const r = assertNoAllSpentFragments([frag('bad.json', currentRaw, validBase)]); + assert.ok(r.ok, `unreadable current ${currentRaw} must not be swept`); + assert.deepEqual(r.sweepable, []); + } + + const r2 = assertNoAllSpentFragments([frag('bad-base.json', validBase, '{ not json')]); + assert.ok(r2.ok, 'an unreadable base must not be swept either'); + assert.deepEqual(r2.sweepable, []); +}); + +test('assertNoAllSpentFragments: naming ONLY the inert fragments makes the remedy safe to apply blindly', () => { + const spentA = doc({ a: { reason: 'a' } }); + const spentB = doc({ b: { reason: 'b' } }); + const live = doc({ c: { reason: 'c-new' } }); + const liveBase = doc({ c: { reason: 'c-old' } }); + const r = assertNoAllSpentFragments([ + frag('spent-a.json', spentA, spentA), + frag('spent-b.json', spentB, spentB), + frag('live-c.json', live, liveBase), + ]); + assert.ok(!r.ok); + assert.deepEqual([...r.sweepable].sort(), ['spent-a.json', 'spent-b.json']); + assert.doesNotMatch(r.message, /live-c\.json/); +}); + +test('assertNoAllSpentFragments: a __proto__ key is never mistaken for a spent entry, and Object.prototype stays untouched', () => { + // A `[key]` computed property, never a literal `{ __proto__: ... }` — the literal + // form is special-cased by JS to SET THE PROTOTYPE rather than create an own + // property, which would make `paths` serialize as `{}` and this test vacuous. + const key = '__proto__'; + const raw = JSON.stringify({ version: ACK_VERSION, paths: { [key]: { reason: 'hostile' } } }); + const parsedPaths = JSON.parse(raw).paths; + assert.deepEqual(Object.keys(parsedPaths), ['__proto__'], 'JSON.parse must create a genuine own key'); + const r = assertNoAllSpentFragments([frag('proto.json', raw, raw)]); + assert.ok(r.ok, '__proto__ makes the document unreadable to ackEntries, so it must never be swept'); + assert.deepEqual(r.sweepable, []); + assert.equal(({}).reason, undefined, 'Object.prototype must stay untouched throughout'); +}); + +test('assertNoAllSpentFragments: a bare-string reason and an object-shaped reason are compared on prose alone', () => { + const bare = doc({ x: 'why' }); + const bareR = assertNoAllSpentFragments([frag('bare.json', bare, bare)]); + assert.ok(!bareR.ok, 'a bare-string reason on both sides must still be recognized as spent'); + + const objCurrent = doc({ x: { reason: 'why' } }); + const bareBase = doc({ x: 'why' }); + const shapeR = assertNoAllSpentFragments([frag('shape.json', objCurrent, bareBase)]); + assert.ok(!shapeR.ok, 'a shape change alone must not re-arm — parseAck accepts both shapes'); +}); + +test('assertNoAllSpentFragments: a decorative "runtime" field beside an unchanged reason does not re-arm (runtime is deliberately not compared)', () => { + const base = doc({ x: { reason: 'why' } }); + const current = doc({ x: { reason: 'why', runtime: 'claude' } }); + const r = assertNoAllSpentFragments([frag('runtime.json', current, base)]); + assert.ok(!r.ok, 'runtime carries no explanation and must not re-arm a byte-identical reason'); +}); + +// ── B. prose parity with the gate (the generative-fix-divergence gate) ────── +// +// `scripts/` ships in the npm package and `tests/` does not, so `ackProse` MUST +// duplicate `emitted-diff.cjs`'s `prose()`. This section is the parity assertion +// that fails when they diverge. + +test('ACK_INVISIBLE and the gate\'s own INVISIBLE are the identical regex, not a second hand-typed copy (#3078)', () => { + // scripts/ ships in the npm package and tests/ does not, so ACK_INVISIBLE is a literal + // duplicate of INVISIBLE (tests/helpers/emitted-diff.cjs) rather than a require across + // that line — see both files' top-of-file comments. A divergence here means an + // invisible reword can re-arm a spent ack on the real `--guard-next` gate and NOT on + // this suite (or vice versa), which is exactly the class of drift a parity test that + // compares a hardcoded literal against its own definition can never catch. + assert.equal( + ACK_INVISIBLE.source, INVISIBLE.source, + 'scripts/lint-emitted-drift-ack.cjs\'s ACK_INVISIBLE and tests/helpers/emitted-diff.cjs\'s ' + + 'INVISIBLE must declare the identical character class — they are duplicated only because ' + + 'scripts/ ships in the npm package and tests/ does not', + ); + assert.equal( + ACK_INVISIBLE.flags, INVISIBLE.flags, + 'both are duplicated for the same reason and must agree on flags too (both carry "g", ' + + 'which is exactly what makes .test() below stateful and in need of a lastIndex reset)', + ); + + // Derive the codepoint list from the REAL gate regex rather than hardcoding one here. + // A hardcoded list would pass even if someone added a codepoint to only one side — the + // exact divergence this test exists to catch. `.test()` on a `g`-flagged regex advances + // `lastIndex` as a side effect, so it is reset before and after every call. + for (let cp = 0x0000; cp <= 0xFFFF; cp++) { + const ch = String.fromCodePoint(cp); + + INVISIBLE.lastIndex = 0; + const gateStrips = INVISIBLE.test(ch); + INVISIBLE.lastIndex = 0; + + ACK_INVISIBLE.lastIndex = 0; + const ackStrips = ACK_INVISIBLE.test(ch); + ACK_INVISIBLE.lastIndex = 0; + + const hex = cp.toString(16).toUpperCase().padStart(4, '0'); + assert.equal(ackStrips, gateStrips, `U+${hex}: ACK_INVISIBLE and INVISIBLE disagree on whether it is invisible`); + + if (gateStrips) { + assert.equal(ackProse(`a${ch}b`), 'ab', `ackProse must strip U+${hex}`); + assert.equal(normalizeAckReason(`a${ch}b`), 'ab', `normalizeAckReason must strip U+${hex}`); + } else if (!/\s/.test(ch)) { + // Not invisible, and not plain whitespace either (whitespace-collapse behavior + // depends on adjacency, so it is exercised separately below) — must survive as-is. + assert.equal(ackProse(`a${ch}b`), `a${ch}b`, `ackProse must NOT strip U+${hex}`); + assert.equal(normalizeAckReason(`a${ch}b`), `a${ch}b`, `normalizeAckReason must NOT strip U+${hex}`); + } + } +}); + +test('normalizeAckReason (the gate\'s own prose normalizer) and ackProse agree byte-for-byte over a behavioral corpus (#3078)', () => { + const invisibleCps = [0x00AD, 0x200B, 0x200C, 0x200D, 0x2060, 0xFEFF]; + const corpus = [ + 'identical text', + 'doubled internal space', + ' leading and trailing space ', + 'crlf\r\nline', + 'lf\nline', + 'tab\ttab', + 'nbsp nbsp', // U+00A0 NBSP + 'ideographic space', // U+3000 IDEOGRAPHIC SPACE + 'em space', // U+2003 EM SPACE + ...invisibleCps.map((cp) => `invisible${String.fromCodePoint(cp)}codepoint`), + '', + ' \t\n ', + ]; + for (const reason of corpus) { + assert.equal( + normalizeAckReason(reason), ackProse(reason), + `normalizeAckReason and ackProse diverge on ${JSON.stringify(reason)}`, + ); + } +}); + +test('ackProse and the sweep guard agree on what counts as "the same explanation, reworded"', () => { + const corpus = [ + ['identical', 'same words', 'same words', true], + ['doubled space', 'same words', 'same words', true], + ['leading/trailing space', ' same words ', 'same words', true], + ['CRLF vs LF', 'same\r\nwords', 'same\nwords', true], + ['NBSP vs space', 'same words', 'same words', true], + ['ideographic space vs space', 'same words', 'same words', true], + ['EM SPACE vs space', 'same words', 'same words', true], + ['zero-width inserted', 'same​words', 'samewords', true], + ['genuinely different words', 'same words', 'different words', false], + ]; + + for (const [label, a, b, expectedSameProse] of corpus) { + assert.equal( + ackProse(a) === ackProse(b), expectedSameProse, + `${label}: ackProse must agree it is${expectedSameProse ? '' : ' NOT'} the same explanation`, + ); + + const gate = assertNoAllSpentFragments([ + frag('r.json', doc({ x: { reason: a } }), doc({ x: { reason: b } })), + ]); + assert.equal( + !gate.ok, expectedSameProse, + `${label}: a divergence here means the sweep guard and the gate disagree about what ` + + 're-arms an ack', + ); + } +}); + +test('property: normalizeAckReason (the gate) and ackProse agree for arbitrary strings, seeded (#3078)', () => { + // Two-sided over unconstrained input, not just the constructed corpus above — a + // divergence anywhere in fc.string()'s space fails here, not just at the handful of + // codepoints the corpus test happens to name. + fc.assert( + fc.property( + fc.string(), + (s) => { + assert.equal(normalizeAckReason(s), ackProse(s)); + }, + ), + { seed: 3078, numRuns: 200 }, + ); +}); + +test('property: ackProse and normalizeAckReason agree, and both are invariant under expanding existing whitespace and inserting invisible codepoints, seeded (#3078)', () => { + const invisibleChars = [0x00AD, 0x200B, 0x200C, 0x200D, 0x2060, 0xFEFF].map((cp) => String.fromCodePoint(cp)); + const word = fc.string({ minLength: 1, maxLength: 8 }).filter((s) => !/\s/.test(s)); + + fc.assert( + fc.property( + fc.array(word, { minLength: 1, maxLength: 6 }), + fc.constantFrom(' ', '\t', '\n', '\r\n', ' ', ' ', ' '), + fc.array(fc.constantFrom(...invisibleChars), { minLength: 0, maxLength: 6 }), + (parts, whitespaceRun, invisibles) => { + const base = parts.join(' '); + // Every existing single space widened to a longer whitespace RUN — never + // introduces whitespace where none existed, so the collapsed result cannot + // change. + const expanded = parts.join(whitespaceRun); + // Invisible codepoints inserted anywhere are stripped outright, never + // collapsed to a space, so they never introduce a new word boundary. + const withInvisibles = invisibles.reduce((s, ch) => ch + s, base); + const padded = ` ${base} `; + + // Two-sided on every one of these forms, not just the base string. + for (const candidate of [base, expanded, withInvisibles, padded]) { + assert.equal(normalizeAckReason(candidate), ackProse(candidate)); + } + + assert.equal(ackProse(expanded), ackProse(base)); + assert.equal(ackProse(withInvisibles), ackProse(base)); + assert.equal(ackProse(padded), ackProse(base)); + }, + ), + { seed: 3078, numRuns: 200 }, + ); +}); + +// ── C. --guard-next wiring against a REAL git repository ──────────────────── +// +// The pure function above is fed hand-built strings; this section proves the +// WIRING — real commits, real `git show` reads — because a guard that resolves no +// base passes vacuously, and that is exactly how the legacy half went blind. + +// #2767: the remote runner mounts the repo at a path owned by another uid, and git then +// refuses every operation there with "detected dubious ownership". Names the SPECIFIC +// directory, never the `*` wildcard. Mirrors `safeDirArgs` in helpers/emitted-runtime.cjs. +const gitIn = (dir, args) => execFileSync( + 'git', ['-c', `safe.directory=${path.resolve(dir)}`, ...args], + { cwd: dir, encoding: 'utf8', timeout: 15000 }, +); + +function makeGuardNextRepo() { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-guard-next-repo-')); + const runGit = (args) => gitIn(dir, args); + runGit(['init', '-q', '-b', 'next']); + function commit(msg) { + runGit(['-c', 'user.email=test@example.com', '-c', 'user.name=Test', 'add', '-A']); + runGit(['-c', 'user.email=test@example.com', '-c', 'user.name=Test', 'commit', '-q', '-m', msg]); + return runGit(['rev-parse', 'HEAD']).trim(); + } + function writeFrag(name, obj) { + const fragDir = path.join(dir, ...ACK_DIR_REPO_PATH.split('/')); + fs.mkdirSync(fragDir, { recursive: true }); + fs.writeFileSync(path.join(fragDir, name), JSON.stringify(obj)); + } + return { dir, commit, writeFrag }; +} + +// Section C used to reimplement git-reading in a local `readFragAtCommit` helper and +// feed only the PURE `assertNoAllSpentFragments`, so the real wiring — the +// `ls-tree`-then-`show` absence-vs-fault discrimination, the `HEAD`-then-`HEAD^` +// two-step, the root-commit fallback, and the option-injection guard — was never +// executed by any test (#3078 review). C1-C4 below now call the REAL +// `readFragmentAtRef`, exported from `scripts/lint-emitted-drift-ack.cjs` for exactly +// this purpose. + +test('C1: real repo — a fragment added in commit 2 vs commit 1 (root, no fragment) is live', () => { + const repo = makeGuardNextRepo(); + try { + fs.writeFileSync(path.join(repo.dir, 'README.md'), 'root\n'); + const c1 = repo.commit('root'); + repo.writeFrag('a.json', { version: ACK_VERSION, paths: { 'x.md': { reason: 'why' } } }); + repo.commit('add fragment'); + + const r = assertNoAllSpentFragments([ + frag( + 'a.json', + readFragmentAtRef('HEAD', 'a.json', { cwd: repo.dir }), + readFragmentAtRef(c1, 'a.json', { cwd: repo.dir }), + ), + ]); + assert.ok(r.ok, 'a fragment absent at the base is live'); + } finally { + cleanup(repo.dir); + } +}); + +test('C2: real repo — an unrelated file change leaves an unchanged fragment fully spent', () => { + const repo = makeGuardNextRepo(); + try { + repo.writeFrag('a.json', { version: ACK_VERSION, paths: { 'x.md': { reason: 'why' } } }); + const c2 = repo.commit('add fragment'); + fs.writeFileSync(path.join(repo.dir, 'README.md'), 'unrelated\n'); + repo.commit('unrelated change'); + + const r = assertNoAllSpentFragments([ + frag( + 'a.json', + readFragmentAtRef('HEAD', 'a.json', { cwd: repo.dir }), + readFragmentAtRef(c2, 'a.json', { cwd: repo.dir }), + ), + ]); + assert.ok(!r.ok, 'unchanged fragment against its own prior commit is fully spent'); + } finally { + cleanup(repo.dir); + } +}); + +test('C3: real repo — appending prose to the owning entry re-arms it', () => { + const repo = makeGuardNextRepo(); + try { + repo.writeFrag('a.json', { version: ACK_VERSION, paths: { 'x.md': { reason: 'the original explanation' } } }); + const c2 = repo.commit('add fragment'); + repo.writeFrag('a.json', { + version: ACK_VERSION, + paths: { 'x.md': { reason: 'the original explanation, now covering a new ripple too' } }, + }); + repo.commit('reword'); + + const r = assertNoAllSpentFragments([ + frag( + 'a.json', + readFragmentAtRef('HEAD', 'a.json', { cwd: repo.dir }), + readFragmentAtRef(c2, 'a.json', { cwd: repo.dir }), + ), + ]); + assert.ok(r.ok, 'genuinely new prose re-arms the entry'); + } finally { + cleanup(repo.dir); + } +}); + +test('C4: real repo — a fragment at the ROOT commit compared against a null base is live', () => { + const repo = makeGuardNextRepo(); + try { + repo.writeFrag('a.json', { version: ACK_VERSION, paths: { 'x.md': { reason: 'why' } } }); + repo.commit('root with fragment'); + + // The base is passed as a literal `null`, not a call to readFragmentAtRef(null, …) + // — this mirrors main()'s own `baseRef === null ? null : readFragmentAtRef(...)` + // branch for a root commit, where resolveBaseRef() has already returned null. + const r = assertNoAllSpentFragments([ + frag('a.json', readFragmentAtRef('HEAD', 'a.json', { cwd: repo.dir }), null), + ]); + assert.ok(r.ok, 'a root commit has no base — resolveBaseRef returns null and nothing can be spent against it'); + } finally { + cleanup(repo.dir); + } +}); + +// ── C2 (direct). Direct coverage of the real git seam (#3078 review) ──────── +// +// C1-C4 above exercise `readFragmentAtRef` only through `assertNoAllSpentFragments`'s +// verdict, which cannot distinguish "read the wrong thing" from "read nothing" if both +// happen to produce the same pure-function outcome. The tests below assert on +// `readFragmentAtRef`, `resolveBaseRef`, and `assertUsableBaseRef` DIRECTLY, plus one +// end-to-end run of the real script as a subprocess — the only thing that exercises +// `main()`'s own argv parsing. `makeGuardNextRepo()` above already satisfies the "one +// makeRepo() helper" shape this needs (mkdtempSync, `git init -q -b next`, per-commit +// identity via `-c user.email=... -c user.name=...`, never mutating global git config), +// so it is reused rather than duplicated. + +test('readFragmentAtRef: returns null for a fragment genuinely absent at that ref — distinguished via ls-tree, not a git-show error message (healthy steady state)', () => { + const repo = makeGuardNextRepo(); + try { + fs.writeFileSync(path.join(repo.dir, 'README.md'), 'root\n'); + const c1 = repo.commit('root, no fragment yet'); + repo.writeFrag('a.json', { version: ACK_VERSION, paths: { 'x.md': { reason: 'why' } } }); + repo.commit('add fragment'); + + assert.equal( + readFragmentAtRef(c1, 'a.json', { cwd: repo.dir }), null, + 'a fragment not yet added at that ref must read as null, not throw', + ); + } finally { + cleanup(repo.dir); + } +}); + +test('readFragmentAtRef: returns the exact bytes committed at that ref, which differ from an uncommitted working-tree edit — proves it reads the REF, not the tree', () => { + const repo = makeGuardNextRepo(); + try { + repo.writeFrag('a.json', { version: ACK_VERSION, paths: { 'x.md': { reason: 'original' } } }); + const c1 = repo.commit('add fragment'); + // Deliberately left UNCOMMITTED — only the working tree carries this edit. + repo.writeFrag('a.json', { version: ACK_VERSION, paths: { 'x.md': { reason: 'edited after the commit' } } }); + + const atRef = readFragmentAtRef(c1, 'a.json', { cwd: repo.dir }); + const onDisk = fs.readFileSync(path.join(repo.dir, ...ACK_DIR_REPO_PATH.split('/'), 'a.json'), 'utf8'); + assert.equal(JSON.parse(atRef).paths['x.md'].reason, 'original'); + assert.notEqual(atRef, onDisk, 'the ref read must not pick up the uncommitted working-tree edit'); + } finally { + cleanup(repo.dir); + } +}); + +test('readFragmentAtRef: throws on a ref that does not exist — a failed base read must be an error, never a silent null', () => { + const repo = makeGuardNextRepo(); + try { + repo.writeFrag('a.json', { version: ACK_VERSION, paths: { 'x.md': { reason: 'why' } } }); + repo.commit('add fragment'); + + // "could not read the base" read as "absent at the base" would make every fragment + // look brand-new and disarm the sweep — this must throw, not return null. + assert.throws( + () => readFragmentAtRef('not-a-real-ref-3078', 'a.json', { cwd: repo.dir }), + /not-a-real-ref-3078/, + 'a bad ref must throw, naming itself', + ); + } finally { + cleanup(repo.dir); + } +}); + +test('resolveBaseRef: returns the parent sha on a two-commit repo, matching `git rev-parse HEAD^` as a 40-hex string', () => { + const repo = makeGuardNextRepo(); + try { + fs.writeFileSync(path.join(repo.dir, 'README.md'), 'one\n'); + const c1 = repo.commit('first'); + fs.writeFileSync(path.join(repo.dir, 'README.md'), 'two\n'); + repo.commit('second'); + + const expected = gitIn(repo.dir, ['rev-parse', 'HEAD^']).trim(); + const actual = resolveBaseRef({ cwd: repo.dir }); + assert.match(actual, /^[0-9a-f]{40}$/, 'must be a 40-hex sha'); + assert.equal(actual, expected); + assert.equal(actual, c1); + } finally { + cleanup(repo.dir); + } +}); + +test('resolveBaseRef: returns null on a root commit (the ONLY way null is reached) and THROWS when pointed at a directory that is not a git repository at all', () => { + const repo = makeGuardNextRepo(); + try { + fs.writeFileSync(path.join(repo.dir, 'README.md'), 'root\n'); + repo.commit('root, no parent'); + assert.equal(resolveBaseRef({ cwd: repo.dir }), null, 'a root commit has no parent'); + } finally { + cleanup(repo.dir); + } + + // The pair this matters for: a blanket try/catch around BOTH the HEAD and HEAD^ + // resolutions would silently collapse "git is broken here" into "there is no base", + // and a guard with no base sweeps nothing — it would pass vacuously, unnoticed by any + // test, which is precisely how the legacy-file job spent months guarding a file that + // had not existed since #2914 (#3078). resolveBaseRef resolves HEAD FIRST, unguarded, + // specifically so a broken repository throws instead of returning null. + const notARepo = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-not-a-repo-')); + try { + assert.throws(() => resolveBaseRef({ cwd: notARepo })); + } finally { + cleanup(notARepo); + } +}); + +test('assertUsableBaseRef: rejects an option-shaped ref, an empty string, null, and a non-string; returns a normal sha unchanged', () => { + // `git show` honors diff options including `--output=`, which WRITES a file — + // an option-shaped ref is a real hazard, not a hypothetical one. + for (const bad of ['-x', '--output=/tmp/gsd-3078-pwned', '', null, 42, {}]) { + assert.throws( + () => assertUsableBaseRef(bad), + /would parse|option/, + `${JSON.stringify(bad)} must be rejected as unusable`, + ); + } + const sha = 'a'.repeat(40); + assert.equal(assertUsableBaseRef(sha), sha, 'a normal 40-hex sha must be returned unchanged'); +}); + +test('E2E: --guard-next --base-ref runs the real script as a subprocess against this checkout, deriving its expected outcome from the live fragment inventory', () => { + // Passing THIS checkout's own HEAD as --base-ref is the deliberate degenerate case: with + // a clean tree, the working-tree copy of every fragment in tests/emitted-drift-acks/ + // equals its committed copy at HEAD, so "spent" is trivially true for every fragment + // that happens to exist right now. That degeneracy is exactly what makes the expected + // exit code and output DERIVABLE from the inventory at runtime rather than a number + // hand-picked when the directory happened to be empty (#3078 regression: it was empty + // when this test was written, then a fragment was restored and the hardcoded + // exit-0/count-2 expectation went stale). Whether the directory holds zero fragments or + // N, the assertion below is the correct one either way. + const scriptPath = path.join(REPO_ROOT, 'scripts', 'lint-emitted-drift-ack.cjs'); + const head = gitIn(REPO_ROOT, ['rev-parse', 'HEAD']).trim(); + const { listFragmentFiles } = require('../scripts/lint-emitted-drift-ack.cjs'); + const fragDir = path.join(REPO_ROOT, 'tests', 'emitted-drift-acks'); + const fragments = listFragmentFiles(fragDir); + + let status = 0; + let out = ''; + try { + out = execFileSync( + process.execPath, + [scriptPath, '--guard-next', '--base-ref', head], + { cwd: REPO_ROOT, encoding: 'utf8', timeout: 30000 }, + ); + } catch (err) { + status = err.status; + out = (err.stdout || '') + (err.stderr || ''); + } + + // The legacy-file half is independent of the fragment inventory and always reports ok + // on this checkout (the legacy file was deleted by #2914) — this is the "legacy-file + // line" half of --guard-next's two halves. + assert.equal( + out.split('\n').filter((l) => l.startsWith('ok guard-no-ack-on-next:')).length >= 1, true, + 'the legacy-file guard must print its own ok line regardless of fragment state', + ); + + if (fragments.length === 0) { + assert.equal(status, 0, 'zero fragments: the guard must exit 0'); + assert.match(out, /no all-spent fragment survives/, 'the fragment-sweep half must also print its ok line'); + } else { + assert.notEqual(status, 0, `${fragments.length} fragment(s) at HEAD are trivially all-spent against themselves`); + assert.match(out, new RegExp(`${fragments.length} fully-spent ack fragment\\(s\\) survive on next`)); + for (const name of fragments) { + assert.ok(out.includes(name), `sweep output must name ${name}`); + assert.match( + out, new RegExp(`git rm[^\\n]*${escapeRegex(name)}`), + `sweep output must print a git rm remedy line for ${name}`, + ); + } + } +}); + +test('E2E: --guard-next rejects an option-shaped --base-ref', () => { + // main()'s argv parsing is the only thing this exercises that the unit tests above + // cannot: an option-shaped --base-ref must be rejected, not silently accepted. + const scriptPath = path.join(REPO_ROOT, 'scripts', 'lint-emitted-drift-ack.cjs'); + let threw = false; + let status; + let stderr = ''; + try { + execFileSync( + process.execPath, + [scriptPath, '--guard-next', '--base-ref', '-x'], + { cwd: REPO_ROOT, encoding: 'utf8', timeout: 30000 }, + ); + } catch (err) { + threw = true; + status = err.status; + stderr = err.stderr || ''; + } + assert.ok(threw, 'an option-shaped --base-ref must make the guard exit non-zero'); + assert.notEqual(status, 0); + assert.match(stderr, /would parse|option/); +}); + +// ── D. the collision shape end-to-end — the issue's own regression criterion ─ +// +// #3078 "Done when": a merged, fully-spent fragment must not silently occupy a path +// key it can no longer gate. All three arms below must hold together: (1) a +// cross-source duplicate is a hard failure naming the remedy, (2) leaving the +// owning entry untouched is spent and gates nothing, (3) appending prose re-arms +// it, and (4) once the spent owner is swept, a new fragment can declare the path +// cleanly. + +test('D1/D2: two fragments naming the same path is a hard failure, naming both files, git rm, re-arms, and APPEND', () => { + const scriptPath = path.join(REPO_ROOT, 'scripts', 'lint-emitted-drift-ack.cjs'); + const fragDir = path.join(REPO_ROOT, ...ACK_DIR_REPO_PATH.split('/')); + const nameA = 'zzz-3078-test-collision-a.json'; + const nameB = 'zzz-3078-test-collision-b.json'; + const pathA = path.join(fragDir, nameA); + const pathB = path.join(fragDir, nameB); + const collisionKey = 'zzz-3078-test-collision-path.md'; + + try { + // The fragment directory does not necessarily exist yet — #2914's directory is + // created on first use, and `listFragmentFiles` treats an absent one as zero + // fragments, so a clean checkout may not have it. + fs.mkdirSync(fragDir, { recursive: true }); + fs.writeFileSync(pathA, JSON.stringify({ version: ACK_VERSION, paths: { [collisionKey]: { reason: 'a' } } })); + fs.writeFileSync(pathB, JSON.stringify({ version: ACK_VERSION, paths: { [collisionKey]: { reason: 'b' } } })); + + let stderr = ''; + let threw = false; + try { + execFileSync(process.execPath, [scriptPath], { cwd: REPO_ROOT, encoding: 'utf8', timeout: 15000 }); + } catch (err) { + threw = true; + stderr = err.stderr || ''; + } + assert.ok(threw, 'a cross-source duplicate must exit non-zero'); + assert.match(stderr, new RegExp(`duplicate ack for "${escapeRegex(collisionKey)}"`)); + assert.match(stderr, new RegExp(escapeRegex(nameA))); + assert.match(stderr, new RegExp(escapeRegex(nameB))); + assert.match(stderr, /git rm /); + assert.match(stderr, /re-arms/); + assert.match(stderr, /APPEND/); + } finally { + // Must NOT survive the test — a real duplicate left behind would red the repo's + // own lint:ci the next time anyone runs it. cleanup() cannot be used here: it + // refuses any path outside the known OS temp roots, and these fixtures are + // deliberately created inside the real repo's tests/emitted-drift-acks/. + // eslint-disable-next-line local/no-raw-rmsync-in-tests -- not a temp dir; cleaning up files created inside the real repo tree + fs.rmSync(pathA, { force: true }); + // eslint-disable-next-line local/no-raw-rmsync-in-tests -- not a temp dir; cleaning up files created inside the real repo tree + fs.rmSync(pathB, { force: true }); + } +}); + +test('D3: leaving the owning fragment entry untouched is spent and gates nothing', () => { + const r = diffEmitted({ + baseline: mf({ [WORKFLOW_KEY]: 'aaa' }), + current: mf({ [WORKFLOW_KEY]: 'bbb' }), + changedPaths: [], + ack: { version: ACK_VERSION, paths: { [WORKFLOW_KEY]: { reason: 'already explained' } } }, + baseAck: { version: ACK_VERSION, paths: { [WORKFLOW_KEY]: { reason: 'already explained' } } }, + }); + assert.deepEqual(r.spentAcks, [WORKFLOW_KEY]); + assert.equal(r.acked.length, 0, 'a spent entry must not clear the new delta'); + assert.deepEqual(r.unattributable.map((u) => u.rel), [WORKFLOW_KEY]); +}); + +test('D4: appending prose to the owning fragment entry re-arms it — #2639/#2993 ship depending on this route', () => { + const r = diffEmitted({ + baseline: mf({ [WORKFLOW_KEY]: 'aaa' }), + current: mf({ [WORKFLOW_KEY]: 'bbb' }), + changedPaths: [], + ack: { + version: ACK_VERSION, + paths: { [WORKFLOW_KEY]: { reason: 'already explained, and now this NEW ripple too' } }, + }, + baseAck: { version: ACK_VERSION, paths: { [WORKFLOW_KEY]: { reason: 'already explained' } } }, + }); + assert.deepEqual(r.spentAcks, []); + assert.equal(r.acked.length, 1, '#2639/#2993 ship depending on this re-arm route continuing to work'); + assert.equal(r.unattributable.length, 0); +}); + +test('D5: once the spent owner is swept, declaring the same path in a new fragment is clean', () => { + // Before the sweep: the owning fragment is fully spent (assertNoAllSpentFragments + // names it) but still present — this is the #3078 hazard. + const spentDoc = doc({ [WORKFLOW_KEY]: { reason: 'already explained' } }); + const before = assertNoAllSpentFragments([frag('owner.json', spentDoc, spentDoc)]); + assert.ok(!before.ok); + assert.deepEqual(before.sweepable, ['owner.json']); + + // After the sweep (fragment deleted, per the remedy): a NEW fragment can declare + // the same path with zero collision, exercised through the real duplicate-detection + // path (mergeAckSources), not just the pure sweep guard above. + const { merged, errors } = mergeAckSources([ + { + source: 'tests/emitted-drift-acks/new.json', + doc: { version: ACK_VERSION, paths: { [WORKFLOW_KEY]: { reason: 'a fresh explanation' } } }, + }, + ]); + assert.deepEqual(errors, [], 'no duplicate arises once the spent owner is gone'); + assert.ok(merged.paths[WORKFLOW_KEY]); + + const parsed = parseAck(merged); + assert.equal(parsed.entries.size, 1); + assert.deepEqual(parsed.errors, []); +}); + // ─── Per-PR ack fragments: mergeAckSources + readAckSources (#2914) ────────── // // #2914 replaces the single shared tests/emitted-drift-ack.json with per-PR fragments @@ -1273,66 +2088,50 @@ test('listAckFragmentFilesAtRef: exactly MAX_ACK_FRAGMENTS entries passes, one o ); }); -test('the migrated fragment physically lives at ACK_DIR/0000-legacy-migration.json on this checkout', () => { - // `includes`, not `deepEqual`, on purpose: other PRs merging their own fragments over - // time must not make this permanent regression test fail — it only pins THIS - // fragment's continued existence at the real, non-injected path. - const fragmentPath = path.join(ACK_DIR, '0000-legacy-migration.json'); - assert.ok(fs.existsSync(fragmentPath), 'the migration must have landed at the real fragment directory path'); - assert.ok(listAckFragmentFiles(ACK_DIR).includes('0000-legacy-migration.json')); -}); +// ─── The migration pin is gone: #3078 emptied the directory it protected (#3078) ── +// #2914 migrated the legacy shared file's 35 entries into the legacy-migration bucket +// fragment and this file pinned that fragment's continued existence, on the premise +// that a fragment left on `next` is inert-but-harmless. #3078 removed that premise: +// fragments don't share a file, but they DO share a path key space, so a fully-spent +// fragment owns keys it can no longer gate, and the next PR to grow one of those paths +// is hard-blocked. `--guard-next` now sweeps all-spent fragments, and the whole +// directory was emptied — the legacy-migration bucket included. The maintainer's +// decision on #3078 states plainly it was never permanent. +// +// The three tests that pinned it are gone rather than adapted: each asserted the +// presence of a specific merged fragment, which is exactly the state the guard now +// forbids. What they protected is covered instead by the fragment-lifecycle section +// (`assertNoAllSpentFragments`) added by #3078, and #2733's own control-flow +// protection lives in `tests/spec-phase-probe-reachability.test.cjs`, which never +// depended on the ack. The #2733 spec-phase.md byte-delta entry (31987 -> 31997) is +// spent by construction: `next`'s published emitted baseline has carried the +// post-#2733 bytes since that PR merged, so `sizeBaseline === sizeCurrent` and there +// is no growth left for it to clear. -test('the migrated legacy fragment still clears the real #2733 spec-phase.md growth (#2914 migration)', () => { - // The whole point of MIGRATING rather than deleting: no acknowledgment is lost, only - // relocated. This pins the migrated fragment's spec-phase.md entry to the exact - // growth #2733 introduced (31987 -> 31997 bytes), so a regression here would have - // reproduced the ratchet failure a real verification run already caught once. - const fragmentPath = path.join(REPO_ROOT, 'tests', 'emitted-drift-acks', '0000-legacy-migration.json'); - const doc = JSON.parse(fs.readFileSync(fragmentPath, 'utf8')); - const entry = doc.paths['spec-phase.md']; - assert.ok(entry, 'the migrated fragment must still carry the spec-phase.md entry from #2733'); - assert.match(entry.reason, /31987 -> 31997/, 'the exact byte delta must survive the migration'); +test('an empty fragment directory is the healthy steady state, end to end (#3078)', () => { + // Absent directory is zero fragments, not a fault. + const missingDir = path.join(REPO_ROOT, 'tests', 'no-such-emitted-drift-acks-dir'); + assert.deepEqual(listAckFragmentFiles(missingDir), []); - // Isolated to JUST this one entry, not the whole 35-entry document: the migrated - // fragment carries 34 OTHER already-spent entries for OTHER paths, which this - // minimal repro's baseline/current never touches — including the full document here - // would report those 34 as freshly-stale (nothing in THIS synthetic diff consumes - // them), which is a fact about this test's narrow fixture, not about the migration. + // On THIS checkout, ACK_DIR may or may not exist — other PRs merge fragments over + // time, so this must not become a new pin either way. Assert the invariant that + // holds regardless. + assert.ok(Array.isArray(listAckFragmentFiles(ACK_DIR))); + assert.ok(assertNoAllSpentFragments([]).ok); + + // The deadlock-avoidance path the empty state now takes every day: a tree carrying + // no ack never consults the base, which is what keeps a repair PR landable — and is + // now the steady state rather than the exception. const r = diffEmitted({ baseline: mf({}), current: mf({}), changedPaths: [], - sizeBaseline: { 'spec-phase.md': 31987 }, - sizeCurrent: { 'spec-phase.md': 31997 }, - ack: { version: ACK_VERSION, paths: { 'spec-phase.md': entry } }, + ack: null, baseAck: null, }); - assert.equal(r.grown.length, 1); - assert.equal(r.grown[0].acked, true, 'the migrated entry must still clear the growth it was written for'); + assert.ok(r.ok); assert.deepEqual(r.staleAcks, []); - assert.ok(r.ok); -}); - -test('the full migrated document is safe to land: every entry is SPENT against the legacy file at base, none stale', () => { - // The actual migration PR's real shape: `next` still carries the legacy file with - // these SAME 35 entries (until this PR removes it), so every entry in the new - // fragment is already "spent" (base already explains it) rather than "live" — this - // PR introduces no NEW ripple of its own, it only relocates old acknowledgments. - // Getting this wrong (e.g. losing a reason's exact text in the move) would turn a - // spent entry into a freshly-unexplained "stale" one and red this very migration PR. - const fragmentPath = path.join(REPO_ROOT, 'tests', 'emitted-drift-acks', '0000-legacy-migration.json'); - const doc = JSON.parse(fs.readFileSync(fragmentPath, 'utf8')); - - const r = diffEmitted({ - baseline: mf({}), - current: mf({}), - changedPaths: [], - ack: doc, - baseAck: doc, // the legacy file at `next` HEAD, byte-identical, before this PR removes it - }); - assert.deepEqual(r.staleAcks, [], 'nothing in the migrated document should read as freshly unexplained'); - assert.equal(r.spentAcks.length, Object.keys(doc.paths).length, 'every migrated entry must be recognized as already spent'); - assert.ok(r.ok); + assert.deepEqual(r.spentAcks, []); }); // ─── readAckFileAtRef: the base-side reader (#2789) ────────────────────────── diff --git a/tests/emitted-drift-acks/0000-legacy-migration.json b/tests/emitted-drift-acks/0000-legacy-migration.json deleted file mode 100644 index 041730733..000000000 --- a/tests/emitted-drift-acks/0000-legacy-migration.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "version": 1, - "paths": { - "agents/gsd-advisor-researcher.toml": "#2834: Codex agent TOML now carries model-routing fields on first install.", - "agents/gsd-ai-researcher.toml": "#2834: Codex agent TOML now carries model-routing fields on first install.", - "agents/gsd-assumptions-analyzer.toml": "#2834: Codex agent TOML now carries model-routing fields on first install.", - "agents/gsd-code-reviewer.toml": "#2834: Codex agent TOML now carries model-routing fields on first install.", - "agents/gsd-codebase-mapper.toml": "#2834: Codex agent TOML now carries model-routing fields on first install.", - "agents/gsd-debug-session-manager.toml": "#2834: Codex agent TOML now carries model-routing fields on first install.", - "agents/gsd-debugger.toml": "#2834: Codex agent TOML now carries model-routing fields on first install.", - "agents/gsd-doc-classifier.toml": "#2834: Codex agent TOML now carries model-routing fields on first install.", - "agents/gsd-doc-synthesizer.toml": "#2834: Codex agent TOML now carries model-routing fields on first install.", - "agents/gsd-doc-verifier.toml": "#2834: Codex agent TOML now carries model-routing fields on first install.", - "agents/gsd-doc-writer.toml": "#2834: Codex agent TOML now carries model-routing fields on first install.", - "agents/gsd-domain-researcher.toml": "#2834: Codex agent TOML now carries model-routing fields on first install.", - "agents/gsd-eval-auditor.toml": "#2834: Codex agent TOML now carries model-routing fields on first install.", - "agents/gsd-eval-planner.toml": "#2834: Codex agent TOML now carries model-routing fields on first install.", - "agents/gsd-executor.toml": "#2834: Codex agent TOML now carries model-routing fields on first install.", - "agents/gsd-framework-selector.toml": "#2834: Codex agent TOML now carries model-routing fields on first install.", - "agents/gsd-integration-checker.toml": "#2834: Codex agent TOML now carries model-routing fields on first install.", - "agents/gsd-intel-updater.toml": "#2834: Codex agent TOML now carries model-routing fields on first install.", - "agents/gsd-mempalace-curator.toml": "#2834: Codex agent TOML now carries model-routing fields on first install.", - "agents/gsd-nyquist-auditor.toml": "#2834: Codex agent TOML now carries model-routing fields on first install.", - "agents/gsd-pattern-mapper.toml": "#2834: Codex agent TOML now carries model-routing fields on first install.", - "agents/gsd-phase-researcher.toml": "#2834: Codex agent TOML now carries model-routing fields on first install.", - "agents/gsd-plan-checker.toml": "#2834: Codex agent TOML now carries model-routing fields on first install.", - "agents/gsd-planner.toml": "#2834: Codex agent TOML now carries model-routing fields on first install.", - "agents/gsd-project-researcher.toml": "#2834: Codex agent TOML now carries model-routing fields on first install.", - "agents/gsd-research-synthesizer.toml": "#2834: Codex agent TOML now carries model-routing fields on first install.", - "agents/gsd-roadmapper.toml": "#2834: Codex agent TOML now carries model-routing fields on first install.", - "agents/gsd-security-auditor.toml": "#2834: Codex agent TOML now carries model-routing fields on first install.", - "agents/gsd-ui-auditor.toml": "#2834: Codex agent TOML now carries model-routing fields on first install.", - "agents/gsd-ui-checker.toml": "#2834: Codex agent TOML now carries model-routing fields on first install.", - "agents/gsd-ui-researcher.toml": "#2834: Codex agent TOML now carries model-routing fields on first install.", - "agents/gsd-user-profiler.toml": "#2834: Codex agent TOML now carries model-routing fields on first install.", - "agents/gsd-verifier.toml": "#2834: Codex agent TOML now carries model-routing fields on first install.", - "gsd-code-fixer.md": "#2647: the three worktree-path sites (setup_worktree bash, concrete-steps prose, critical_rules) replaced the hardcoded /tmp/sv- mktemp path with a repo-relative .claude/worktrees/rf--- path, and added a defense-in-depth padded_phase validation at the sink (the agent prompt is a literal bash contract any caller can spawn; the orchestrator validates upstream but the sink now self-defends against path-traversal/branch-name injection). On Windows/Git Bash the /tmp path landed outside the project tree (outside the session permission allowlist, prompting on every read) and mktemp's MAX_PATH substitute was un-removable; .claude/worktrees/ is the same dir the harness-managed executor worktrees use (gitignored via .claude/, inside the permission scope). Growth is the path-resolution bash (main_repo via `git worktree list --porcelain | awk`) + the $$-PID/epoch uniqueness replacing mktemp's XXXXXX + the padded_phase guard + the #2647 rationale comments at each site. Supersedes the prior #2825 attribution, whose gated-bash + guardrail growth is already in next.", - "spec-phase.md": { - "reason": "#2733: five transitions in gsd-core/workflows/spec-phase.md were re-pointed so control reaches the mandatory Step 5.5 edge-completeness and Step 5.6 prohibition-completeness probes, which no path could reach before. Four upstream gate-passed jumps went from 'Jump to Step 6' to 'Jump to Step 5.5', and Step 5.5's own terminal soft gate at :305 went from 'proceed to Step 6' to 'proceed to Step 5.6' so the common all-edges-resolved path stops skipping the prohibition probe. The +10 bytes is exactly those five targets growing by 2 bytes each ('Step 6' -> 'Step 5.5' / 'Step 5.6'); it is the literal fix, not incidental prose growth, and cannot be avoided without leaving a probe unreachable. Verified: 31987 -> 31997 bytes, DEFAULT tier, cap 40960. #3132: realigned retired covered/backstop-as-status vocab to resolved+verification. #3102: Step 5.5 now RENDERS the edge-probe coverage report into the model's context (a raw printf of $COVERAGE after the well-formedness guard) and binds those rows in the resolution loop and --auto as a deterministic FLOOR, plus the corrected block comment; load-bearing workflow instruction that makes ADR-550 D7b (deterministic propose + LLM resolve) real at runtime and honors ADR-857 sec98's recall gap (floor, not ceiling). 32238 -> 34056 bytes (+1818), DEFAULT tier, cap 40960. #2773: Step 5.5 now tells a `response_language` project that the edge-probe `$REQS_JSON` payload is engine input rather than user-facing output, so each requirement's `text` carries a faithful English translation while the SPEC keeps its original language and requirement ids stay unchanged. The shape cues in src/edge-probe.cts are English word-boundary regexes, so prose in another language matched nothing, classified to zero shapes, and put every row in the `unclassified` sentinel (#1110) — the taxonomy contributed nothing. Growth is that instruction plus the pointer to the authored `shapes` override for prose that classifies to zero even in English (measured: the issue's own repro sentence returns [] in English too, so translation is necessary but not sufficient), and a one-line fix an isolated security review surfaced in the same block — the empty/placeholder guard exited without `rm -f \"$REQS_JSON\"` while its engine-failure sibling below it did, stranding the SPEC requirement text in TMPDIR on every failed run. The instruction sits BEFORE the heredoc deliberately: the `$APPLICABLE = 0` warning fires only when every requirement is unclassified, so a partly-classified non-English spec would otherwise slip through with no signal. Load-bearing runtime instruction; the compiled engine is deliberately untouched (the `lang`-hint / per-language cue-set fix is out of scope per the #2773 triage). 34020 -> 35730 bytes (+1710), DEFAULT tier, cap 40960." - } - } -} diff --git a/tests/emitted-drift-acks/1700-spike-manifest-idea-scoping.json b/tests/emitted-drift-acks/1700-spike-manifest-idea-scoping.json deleted file mode 100644 index 3f6481ffc..000000000 --- a/tests/emitted-drift-acks/1700-spike-manifest-idea-scoping.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "version": 1, - "paths": { - "spike.md": "#1700: create_manifest step now derives an explicit idea key and restructures the .planning/spikes/MANIFEST.md template from one flat top-level `## Idea` / `## Requirements` pair (single-idea only) to `## Ideas` > `### {idea-key}` subsections, each carrying its own scoped Requirements list — the fix for the leak where one idea's requirements bled into another idea's build via spike-wrap-up.md. Growth is: the idea-key derivation instructions and the expanded MANIFEST.md template (new/existing-idea/pre-#1700-flat-shape-migration branches) in create_manifest; an Idea column added to the `## Spikes` table header everywhere it's referenced; an `idea:` field added to the build_spikes README frontmatter template; the reground step scoping its established-requirements check to the current idea only; the frontier_mode load step and 'Get Alignment and Execute' step describing per-idea sections instead of one flat idea; and two success_criteria bullets updated to call out idea-key scoping. This is the write side of the fix, not incidental prose.", - "spike-wrap-up.md": "#1700: gather/synthesize/write_skill steps now resolve each spike's idea key (from its README `idea:` frontmatter, falling back to the `## Spikes` table's Idea column for pre-#1700 spikes) and scope which Requirements they read and re-emit, so a feature-area reference or the generated spike-findings SKILL.md never pulls in an unrelated idea's requirements — the read/re-emit side of the #1700 fix. Growth is: the idea-key resolution instructions added to the gather step; the synthesize step's Requirements block rewritten to pull ONLY from the owning idea key(s) with an explicit 'never include a requirement from an idea key that has no spike in this group' rule; the write_skill step's and blocks scoped to the wrapped idea key(s) instead of 'the whole MANIFEST.md'; and one new success_criteria bullet forbidding blended requirements across ideas.", - "sketch.md": "#1700: sketch.md's build_sketches step (b) reads spikes/MANIFEST.md's Requirements for non-negotiable design constraints; once spike.md restructures that file from one flat Requirements section to per-idea `### {idea-key}` sections (same PR), sketch.md's single-flat-section instruction would silently go stale — collateral of the seam this fix touches, required so sketch.md keeps reading the file correctly rather than a defect introduced by this change. Growth is the one clause distinguishing per-idea sections and instructing the reader to check the Requirements list of the relevant idea key (or all of them if none clearly matches)." - } -} diff --git a/tests/emitted-drift-acks/1762-verification-routing-wording.json b/tests/emitted-drift-acks/1762-verification-routing-wording.json deleted file mode 100644 index 0d199358b..000000000 --- a/tests/emitted-drift-acks/1762-verification-routing-wording.json +++ /dev/null @@ -1 +0,0 @@ -{"version":1,"paths":{"progress.md":{"reason":"#1762: Route V.missing and Route V.unknown now consume the dynamic ${VERIFICATION_NEXT_ACTION} block (matching the existing V.gaps/V.human format) instead of a hardcoded duplicate string, so the routing text stays reassuring and in sync with src/verification.cts's reworded next_action — deliberate growth, not drift."}}} diff --git a/tests/emitted-drift-acks/1954-plan-checker-undeclared-coupling.json b/tests/emitted-drift-acks/1954-plan-checker-undeclared-coupling.json deleted file mode 100644 index 71bfa2dc1..000000000 --- a/tests/emitted-drift-acks/1954-plan-checker-undeclared-coupling.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "version": 1, - "paths": { - "gsd-plan-checker.md": "#1954: Dimension 3 gains sub-dimension 3b (undeclared / temporal coupling), plus one success-criteria line. Growth is the new sub-dimension only — no existing text was rewritten. The addition is deliberately dense rather than extracted: ADR-1610 Decision 4 names eager `@`-import relocation as proxy-gaming (it shrinks the measured file while leaving loaded context unchanged or larger), legitimate extraction is Read-at-step lazy, and this agent has no lazy-read seam; issue #1954's approved scope is also explicitly 'No new files'. After the change the file sits at 48810 bytes against the LARGE tier hard cap of 49152 (tests/agent-size-budget.test.cjs), i.e. 342 bytes of headroom. That is deliberate and disclosed: the cap is not crossed and is not raised, but the next contributor who needs room in this agent must do a lazy extraction rather than add prose. Content justification: Dimension 3 proves declared dependency edges resolve and are acyclic, and execute-phase's intra-wave guard proves same-wave plans do not overlap in files_modified — neither axis sees an undeclared edge, so two same-wave plans coupled through a shared config key, table, migration, env var, singleton or cache (or through one plan's produced state) pass plan-check and fail intermittently under parallel execution. 3b is advisory only (WARNING, never blocker, per the issue's rejected-alternatives list) and reuses the existing dependency_correctness finding key so no consumer sees a new dimension. — #2401 append (merged into this fragment because two ack sources may never name the same path): the Verify Command Path Resolvability dimension is now a short stub pointing at the extracted gsd-core/references/verify-command-path-resolvability.md (the full dimension body — including the {VERIFY_PATHS} probe-consumption rules and severity/reason table — moved out to stay under the LARGE cap). The file is now 49064 bytes, 88 bytes of headroom under the same 49152 LARGE cap. — #3003 append (merged into this fragment because two ack sources may never name the same path): Dimension 3b's description of what the wave guard already covers now names both declared-scope channels (`files_modified`/`files_deleted`), so a delete-vs-modify pair between same-wave plans is attributed once on the file axis rather than re-reported as undeclared coupling. +43 bytes, landing at 49107 with 45 bytes of headroom under the same cap — the 88-byte warning above still stands and is now tighter." - } -} diff --git a/tests/emitted-drift-acks/2142-quick-task-archival.json b/tests/emitted-drift-acks/2142-quick-task-archival.json deleted file mode 100644 index 856a2690d..000000000 --- a/tests/emitted-drift-acks/2142-quick-task-archival.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "version": 1, - "paths": { - "cleanup.md": "#2142: adds retroactive quick-task archival — identify_quick_tasks step (target-milestone selection), dry-run summary + AskUserQuestion, archive_quick_tasks step using the narrow `milestone.archive-quick` command (not `milestone.complete --archive-quick`, which cannot be safely re-run against an already-completed milestone — see the step's own note), and success_criteria updates; the command was renamed from `quick.archive` to `milestone.archive-quick` (code-review FIX 1: fold under the existing `milestone` namespace, no new top-level command). 10319 -> 14941 bytes. (complete-milestone.md's #2142 growth is acknowledged in tests/emitted-drift-acks/3409-unreachable-guard-arms.json — two ack sources may never name the same path.)" - } -} diff --git a/tests/emitted-drift-acks/2229-explore-claim-disposition.json b/tests/emitted-drift-acks/2229-explore-claim-disposition.json deleted file mode 100644 index 6d3b7589a..000000000 --- a/tests/emitted-drift-acks/2229-explore-claim-disposition.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "version": 1, - "paths": { - "explore.md": "#2229 adds the three-way claim disposition (admit / refute / abstain) plus the Unresolved ledger to the /gsd-explore Step-3 research pass, so a surfaced research claim carries its grounding instead of being folded into confident prose unverified. The growth is the contract itself, written inline in the workflow body: the refute-then-ground-or-abstain spawn instruction, an authority-based decision procedure separating refute from abstain (without it the two arms describe the same event), the Unresolved-ledger output template and its five-value reason enum, a defined destination for an untagged finding, the two guards (conflict-abstention and the tier floor), and the Step 4-5 rule carrying the disposition into crystallized artifacts so an abstain is not laundered into flat prose downstream. That guidance is deliberately NOT relocated into an eagerly @-imported reference, which ADR-1610 Decision 4 names as gaming the size proxy (only lazy, Read-at-step extraction counts). Round 11 REDUCED the file: the tier floor now reads one signal (query resolve-model --pick tier) instead of two proxies plus a both-empty fail-safe, which retired the two-signal rationale, the resolveModelInternal precedence explanation, and two of the three disclosed residuals; the canonical gsd_run preamble also moved to an unconditional Step-1 block so declining the research offer cannot leave Step 5's commit unbootstrapped (still exactly one preamble per file, per runtime-launcher-parity). 11127 -> 21297 bytes (+10170 vs next base), DEFAULT tier, cap 40960 - 52% of budget." - } -} diff --git a/tests/emitted-drift-acks/2295-resolved-model.json b/tests/emitted-drift-acks/2295-resolved-model.json deleted file mode 100644 index 2c4b38a47..000000000 --- a/tests/emitted-drift-acks/2295-resolved-model.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "version": 1, - "paths": { - "review.md": { - "reason": "#2295: the write_reviews step gained a models:/model_sources: render instruction plus a frontmatter example, teaching it to surface the per-lane resolved model recorded by resolveSpawnModel/runOpenAiCompatible (src/review-lane-runner.cts). Deliberate growth in runtime-loaded workflow text for the new feature, not converter drift." - } - } -} diff --git a/tests/emitted-drift-acks/2301-roadmapper-write-guard-sentinel.json b/tests/emitted-drift-acks/2301-roadmapper-write-guard-sentinel.json deleted file mode 100644 index 5ef47754e..000000000 --- a/tests/emitted-drift-acks/2301-roadmapper-write-guard-sentinel.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "version": 1, - "paths": { - "gsd-roadmapper.md": "#2255 round 10 Blocker 1: Step 7 now arms the gsd-write-guard single-use sentinel before each curated write. The roadmapper Writes both .planning/ROADMAP.md and .planning/STATE.md wholesale, and /gsd:new-milestone spawns it against the OUTGOING milestone's files — new-milestone's phases.clear archives phase DIRECTORIES, never ROADMAP.md, so nothing compacts it first and no ordering rule forces /gsd:complete-milestone to run beforehand. Measured against the shipped hook at the #973 file size (292 lines), a new 4-phase roadmap lands at 18.2% and an 8-phase one at 31.8%: both hard-blocked. The +1130 bytes are that wiring plus the rationale a future editor needs to keep it — the arming is per-target because the sentinel is path-bound and single-use, and each is gated on [ -f ] so the /gsd:new-project path (ENOENT-exempt) strands no unconsumed token. This is the escape-hatch binding the guard must carry to avoid blocking a first-party flow, not incidental prose growth." - } -} diff --git a/tests/emitted-drift-acks/2398-consensus-gate-cycle-summary.json b/tests/emitted-drift-acks/2398-consensus-gate-cycle-summary.json deleted file mode 100644 index 58b0e4413..000000000 --- a/tests/emitted-drift-acks/2398-consensus-gate-cycle-summary.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "$comment": "Growth ack (#2914 fragment). Reason: #2398 adds the consensus gate to the CYCLE_SUMMARY contract in plan-review-convergence.md step 5a — a claim-class split (existence-class needs source-grounding or corroboration; judgment-class counts unless its raiser opens with an evidence-quality discount marker), plus the all-marked fail-open and the leading-marker requirement. 30889 -> 33566 LF bytes (+2677). Marker literals are pinned to what gsd-core/bin/lib/review-lane-runner.cjs actually emits by a parity test in tests/plan-review-convergence.test.cjs, so the gate cannot key on a signal nothing produces. gsd-core/references/reviewer-instances.md also grew (+1823B) but is deliberately NOT acked here: currentSizes() (tests/helpers/emitted-runtime.cjs:916-929) scans only gsd-core/workflows/ and agents/, so references/ is outside the growth ratchet entirely and an entry for it would be inert. Its emitted-hash ripple is attributable to this diff and needs no acknowledgment.", - "version": 1, - "paths": { - "plan-review-convergence.md": "gsd-core/workflows/plan-review-convergence.md +2677B: consensus gate on newly-raised HIGHs, split by claim class, resolving B2 from closed PR #2417 (#2398)" - } -} diff --git a/tests/emitted-drift-acks/2486-settings-worktrees-runtime.json b/tests/emitted-drift-acks/2486-settings-worktrees-runtime.json deleted file mode 100644 index 1a402a524..000000000 --- a/tests/emitted-drift-acks/2486-settings-worktrees-runtime.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "version": 1, - "paths": { - "settings.md": "#2486 \u2014 the Worktrees question now branches on the runtime's declared dispatch.isolation capability (resolved via the sentinel-free inspect-dispatch-isolation query) instead of offering Claude-only worktree isolation everywhere. Growth is the isolation-none branch, its explanatory notice, and the tri-state pre-selection rule for the broken-inheritance repair." - } -} diff --git a/tests/emitted-drift-acks/2554-code-review-depth-overrides.json b/tests/emitted-drift-acks/2554-code-review-depth-overrides.json deleted file mode 100644 index 563a83829..000000000 --- a/tests/emitted-drift-acks/2554-code-review-depth-overrides.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "version": 1, - "paths": { - "code-review.md": { - "reason": "#2554: +5376 bytes vs next. resolve_depth was rewritten to call the new path-scoped depth resolver (src/code-review-depth.cts) — the two config reads, the JSON payload construction that passes config via env vars and the changed-file list via stdin, an executable misconfiguration guard that halts the review instead of a prose instruction, the provenance/warning/downgrade output, and the large-scope downgrade relocated out of compute_file_scope so one step owns depth. Replaces the spent 3503-diff-base-scope-anchor.json fragment (its code-review.md entry was consumed when #3503 merged at 49b60070a and can no longer clear anything — the base file is exactly the 34435-byte baseline this growth is measured against), mirroring how #3503 itself replaced the spent 3191-unanchored-grep-sites.json." - } - } -} diff --git a/tests/emitted-drift-acks/2658-trae-instruction-file-path.json b/tests/emitted-drift-acks/2658-trae-instruction-file-path.json deleted file mode 100644 index f65f9bcb3..000000000 --- a/tests/emitted-drift-acks/2658-trae-instruction-file-path.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "version": 1, - "paths": { - "gsd-core/references/checkpoints.md": "#2658: mentions CLAUDE.md in prose. convertClaudeToTraeMarkdown's CLAUDE.md replacement target changed from the bare directory '.trae/rules/' to the concrete file '.trae/rules/rules.md' for every CLAUDE.md mention (bare, ./-prefixed, backtick-wrapped, and the buggy .claude/-prefixed form that previously produced a malformed doubled path) \u2014 so trae-emitted output differs for every file that mentions CLAUDE.md, not only the ones that hit the reported bug. Content is otherwise byte-identical.", - "gsd-core/references/debugger-bug-taxonomy.md": "#2658: same CLAUDE.md replacement-target change as checkpoints.md above (bare directory '.trae/rules/' -> concrete file '.trae/rules/rules.md').", - "gsd-core/references/debugger-fix-acceptance.md": "#2658: same CLAUDE.md replacement-target change as checkpoints.md above (bare directory '.trae/rules/' -> concrete file '.trae/rules/rules.md').", - "gsd-core/references/debugger-repro-hardening.md": "#2658: same CLAUDE.md replacement-target change as checkpoints.md above (bare directory '.trae/rules/' -> concrete file '.trae/rules/rules.md').", - "gsd-core/references/debugger-sbfl.md": "#2658: same CLAUDE.md replacement-target change as checkpoints.md above (bare directory '.trae/rules/' -> concrete file '.trae/rules/rules.md').", - "gsd-core/references/git-integration.md": "#2658: same CLAUDE.md replacement-target change as checkpoints.md above (bare directory '.trae/rules/' -> concrete file '.trae/rules/rules.md').", - "gsd-core/references/planner-human-verify-mode.md": "#2658: same CLAUDE.md replacement-target change as checkpoints.md above (bare directory '.trae/rules/' -> concrete file '.trae/rules/rules.md').", - "gsd-core/templates/README.md": "#2658: same CLAUDE.md replacement-target change as checkpoints.md above (bare directory '.trae/rules/' -> concrete file '.trae/rules/rules.md').", - "gsd-core/templates/claude-md.md": "#2658: same CLAUDE.md replacement-target change as checkpoints.md above (bare directory '.trae/rules/' -> concrete file '.trae/rules/rules.md').", - "gsd-core/workflows/help/modes/full.md": "#2658: same CLAUDE.md replacement-target change as checkpoints.md above (bare directory '.trae/rules/' -> concrete file '.trae/rules/rules.md').", - "gsd-core/workflows/milestone-summary.md": "#2658: same CLAUDE.md replacement-target change as checkpoints.md above (bare directory '.trae/rules/' -> concrete file '.trae/rules/rules.md').", - "gsd-core/workflows/progress.md": "#2658: same CLAUDE.md replacement-target change as checkpoints.md above (bare directory '.trae/rules/' -> concrete file '.trae/rules/rules.md').", - "gsd-core/workflows/quick.md": "#2658: same CLAUDE.md replacement-target change as checkpoints.md above (bare directory '.trae/rules/' -> concrete file '.trae/rules/rules.md').", - "gsd-core/workflows/sketch-wrap-up.md": "#2658: same CLAUDE.md replacement-target change as checkpoints.md above (bare directory '.trae/rules/' -> concrete file '.trae/rules/rules.md').", - "gsd-core/workflows/spike-wrap-up.md": "#2658: same CLAUDE.md replacement-target change as checkpoints.md above (bare directory '.trae/rules/' -> concrete file '.trae/rules/rules.md').", - "gsd-core/workflows/update.md": "#2658: same CLAUDE.md replacement-target change as checkpoints.md above (bare directory '.trae/rules/' -> concrete file '.trae/rules/rules.md').", - "skills/gsd-ns-project/skills/profile-user/SKILL.md": "#2658: derived (via commands/gsd/profile-user.md, which mentions CLAUDE.md) through convertClaudeToTraeMarkdown; same replacement-target change as checkpoints.md above.", - "skills/gsd-ns-review/skills/code-review/SKILL.md": "#2658: derived (via commands/gsd/code-review.md, which mentions CLAUDE.md) through convertClaudeToTraeMarkdown; same replacement-target change as checkpoints.md above.", - "ingest-docs.md": "#2658: runtime-detection block gained a `/.trae/` path-based line and a `TRAE_CONFIG_DIR` env-var fallback line (the same trae-detection gap found in new-project.md, fixed here too since it is the identical defect in a sibling workflow). \u2014 #3423 append (epic #1891 F8): also grew with the -> tag rename (15 chars/block, tag-token-only delta). \u2014 #3576 append: bare `references/.md` cites repaired to the canonical `gsd-core/references/.md` form (+36 bytes, 4 cite(s) \u00d7 9). Dead-pointer fix; no content change. \u2014 #3602 append: gains CLASSIFIER_MODEL/SYNTHESIZER_MODEL/ROADMAPPER_MODEL resolve-model bindings, a #2517 model-omit-on-inherit marker + rule block, and model= on all three subagent dispatch shapes (classifier prose fan-out, synthesizer Agent block, roadmapper Agent block) so dynamic_routing/model_profile tiers apply instead of the caller's session model.", - "new-project.md": "#2658: runtime-detection block gained a `/.trae/` path-based line and a `TRAE_CONFIG_DIR` env-var fallback line, so trae resolves to RUNTIME=trae instead of falling through to the claude default. \u2014 #3423 append (epic #1891 F8): also grew with the -> tag rename (15 chars/block, tag-token-only delta)." - } -} \ No newline at end of file diff --git a/tests/emitted-drift-acks/2755-kimi-code-hooks-root.json b/tests/emitted-drift-acks/2755-kimi-code-hooks-root.json deleted file mode 100644 index 9d0af04fa..000000000 --- a/tests/emitted-drift-acks/2755-kimi-code-hooks-root.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "version": 1, - "paths": { - ".kimi-code/hooks/gsd-check-update-worker.js": "#2755: kimi-code installs its hook bundle into Kimi Code own root (~/.kimi-code) instead of Kimi CLI ~/.kimi. The script bytes are unchanged - identical to what ~/.kimi previously received - so no source file under hooks/ moved and the diff cannot attribute the ripple. Only the install destination changed, at resolveKimiHooksTomlDir in src/runtime-homes.cts.", - ".kimi-code/hooks/gsd-check-update.js": "#2755: kimi-code installs its hook bundle into Kimi Code own root (~/.kimi-code) instead of Kimi CLI ~/.kimi. The script bytes are unchanged - identical to what ~/.kimi previously received - so no source file under hooks/ moved and the diff cannot attribute the ripple. Only the install destination changed, at resolveKimiHooksTomlDir in src/runtime-homes.cts.", - ".kimi-code/hooks/gsd-config-reload.js": "#2755: kimi-code installs its hook bundle into Kimi Code own root (~/.kimi-code) instead of Kimi CLI ~/.kimi. The script bytes are unchanged - identical to what ~/.kimi previously received - so no source file under hooks/ moved and the diff cannot attribute the ripple. Only the install destination changed, at resolveKimiHooksTomlDir in src/runtime-homes.cts.", - ".kimi-code/hooks/gsd-context-monitor.js": "#2755: kimi-code installs its hook bundle into Kimi Code own root (~/.kimi-code) instead of Kimi CLI ~/.kimi. The script bytes are unchanged - identical to what ~/.kimi previously received - so no source file under hooks/ moved and the diff cannot attribute the ripple. Only the install destination changed, at resolveKimiHooksTomlDir in src/runtime-homes.cts.", - ".kimi-code/hooks/gsd-cursor-post-tool.js": "#2755: kimi-code installs its hook bundle into Kimi Code own root (~/.kimi-code) instead of Kimi CLI ~/.kimi. The script bytes are unchanged - identical to what ~/.kimi previously received - so no source file under hooks/ moved and the diff cannot attribute the ripple. Only the install destination changed, at resolveKimiHooksTomlDir in src/runtime-homes.cts.", - ".kimi-code/hooks/gsd-cursor-pre-tool.js": "#2755: kimi-code installs its hook bundle into Kimi Code own root (~/.kimi-code) instead of Kimi CLI ~/.kimi. The script bytes are unchanged - identical to what ~/.kimi previously received - so no source file under hooks/ moved and the diff cannot attribute the ripple. Only the install destination changed, at resolveKimiHooksTomlDir in src/runtime-homes.cts.", - ".kimi-code/hooks/gsd-cursor-session-start.js": "#2755: kimi-code installs its hook bundle into Kimi Code own root (~/.kimi-code) instead of Kimi CLI ~/.kimi. The script bytes are unchanged - identical to what ~/.kimi previously received - so no source file under hooks/ moved and the diff cannot attribute the ripple. Only the install destination changed, at resolveKimiHooksTomlDir in src/runtime-homes.cts.", - ".kimi-code/hooks/gsd-cursor-stop.js": "#2755: kimi-code installs its hook bundle into Kimi Code own root (~/.kimi-code) instead of Kimi CLI ~/.kimi. The script bytes are unchanged - identical to what ~/.kimi previously received - so no source file under hooks/ moved and the diff cannot attribute the ripple. Only the install destination changed, at resolveKimiHooksTomlDir in src/runtime-homes.cts.", - ".kimi-code/hooks/gsd-cursor-subagent-start.js": "#2755: kimi-code installs its hook bundle into Kimi Code own root (~/.kimi-code) instead of Kimi CLI ~/.kimi. The script bytes are unchanged - identical to what ~/.kimi previously received - so no source file under hooks/ moved and the diff cannot attribute the ripple. Only the install destination changed, at resolveKimiHooksTomlDir in src/runtime-homes.cts.", - ".kimi-code/hooks/gsd-cursor-subagent-stop.js": "#2755: kimi-code installs its hook bundle into Kimi Code own root (~/.kimi-code) instead of Kimi CLI ~/.kimi. The script bytes are unchanged - identical to what ~/.kimi previously received - so no source file under hooks/ moved and the diff cannot attribute the ripple. Only the install destination changed, at resolveKimiHooksTomlDir in src/runtime-homes.cts.", - ".kimi-code/hooks/gsd-ensure-canonical-path.js": "#2755: kimi-code installs its hook bundle into Kimi Code own root (~/.kimi-code) instead of Kimi CLI ~/.kimi. The script bytes are unchanged - identical to what ~/.kimi previously received - so no source file under hooks/ moved and the diff cannot attribute the ripple. Only the install destination changed, at resolveKimiHooksTomlDir in src/runtime-homes.cts.", - ".kimi-code/hooks/gsd-graphify-update.sh": "#2755: kimi-code installs its hook bundle into Kimi Code own root (~/.kimi-code) instead of Kimi CLI ~/.kimi. The script bytes are unchanged - identical to what ~/.kimi previously received - so no source file under hooks/ moved and the diff cannot attribute the ripple. Only the install destination changed, at resolveKimiHooksTomlDir in src/runtime-homes.cts.", - ".kimi-code/hooks/gsd-phase-boundary.sh": "#2755: kimi-code installs its hook bundle into Kimi Code own root (~/.kimi-code) instead of Kimi CLI ~/.kimi. The script bytes are unchanged - identical to what ~/.kimi previously received - so no source file under hooks/ moved and the diff cannot attribute the ripple. Only the install destination changed, at resolveKimiHooksTomlDir in src/runtime-homes.cts.", - ".kimi-code/hooks/gsd-prompt-guard.js": "#2755: kimi-code installs its hook bundle into Kimi Code own root (~/.kimi-code) instead of Kimi CLI ~/.kimi. The script bytes are unchanged - identical to what ~/.kimi previously received - so no source file under hooks/ moved and the diff cannot attribute the ripple. Only the install destination changed, at resolveKimiHooksTomlDir in src/runtime-homes.cts.", - ".kimi-code/hooks/gsd-read-guard.js": "#2755: kimi-code installs its hook bundle into Kimi Code own root (~/.kimi-code) instead of Kimi CLI ~/.kimi. The script bytes are unchanged - identical to what ~/.kimi previously received - so no source file under hooks/ moved and the diff cannot attribute the ripple. Only the install destination changed, at resolveKimiHooksTomlDir in src/runtime-homes.cts.", - ".kimi-code/hooks/gsd-read-injection-scanner.js": "#2755: kimi-code installs its hook bundle into Kimi Code own root (~/.kimi-code) instead of Kimi CLI ~/.kimi. The script bytes are unchanged - identical to what ~/.kimi previously received - so no source file under hooks/ moved and the diff cannot attribute the ripple. Only the install destination changed, at resolveKimiHooksTomlDir in src/runtime-homes.cts.", - ".kimi-code/hooks/gsd-session-state.sh": "#2755: kimi-code installs its hook bundle into Kimi Code own root (~/.kimi-code) instead of Kimi CLI ~/.kimi. The script bytes are unchanged - identical to what ~/.kimi previously received - so no source file under hooks/ moved and the diff cannot attribute the ripple. Only the install destination changed, at resolveKimiHooksTomlDir in src/runtime-homes.cts.", - ".kimi-code/hooks/gsd-statusline.js": "#2755: kimi-code installs its hook bundle into Kimi Code own root (~/.kimi-code) instead of Kimi CLI ~/.kimi. The script bytes are unchanged - identical to what ~/.kimi previously received - so no source file under hooks/ moved and the diff cannot attribute the ripple. Only the install destination changed, at resolveKimiHooksTomlDir in src/runtime-homes.cts.", - ".kimi-code/hooks/gsd-update-banner.js": "#2755: kimi-code installs its hook bundle into Kimi Code own root (~/.kimi-code) instead of Kimi CLI ~/.kimi. The script bytes are unchanged - identical to what ~/.kimi previously received - so no source file under hooks/ moved and the diff cannot attribute the ripple. Only the install destination changed, at resolveKimiHooksTomlDir in src/runtime-homes.cts.", - ".kimi-code/hooks/gsd-validate-commit.sh": "#2755: kimi-code installs its hook bundle into Kimi Code own root (~/.kimi-code) instead of Kimi CLI ~/.kimi. The script bytes are unchanged - identical to what ~/.kimi previously received - so no source file under hooks/ moved and the diff cannot attribute the ripple. Only the install destination changed, at resolveKimiHooksTomlDir in src/runtime-homes.cts.", - ".kimi-code/hooks/gsd-windsurf-pre-command.js": "#2755: kimi-code installs its hook bundle into Kimi Code own root (~/.kimi-code) instead of Kimi CLI ~/.kimi. The script bytes are unchanged - identical to what ~/.kimi previously received - so no source file under hooks/ moved and the diff cannot attribute the ripple. Only the install destination changed, at resolveKimiHooksTomlDir in src/runtime-homes.cts.", - ".kimi-code/hooks/gsd-windsurf-pre-write.js": "#2755: kimi-code installs its hook bundle into Kimi Code own root (~/.kimi-code) instead of Kimi CLI ~/.kimi. The script bytes are unchanged - identical to what ~/.kimi previously received - so no source file under hooks/ moved and the diff cannot attribute the ripple. Only the install destination changed, at resolveKimiHooksTomlDir in src/runtime-homes.cts.", - ".kimi-code/hooks/gsd-workflow-guard.js": "#2755: kimi-code installs its hook bundle into Kimi Code own root (~/.kimi-code) instead of Kimi CLI ~/.kimi. The script bytes are unchanged - identical to what ~/.kimi previously received - so no source file under hooks/ moved and the diff cannot attribute the ripple. Only the install destination changed, at resolveKimiHooksTomlDir in src/runtime-homes.cts.", - ".kimi-code/hooks/gsd-worktree-path-guard.js": "#2755: kimi-code installs its hook bundle into Kimi Code own root (~/.kimi-code) instead of Kimi CLI ~/.kimi. The script bytes are unchanged - identical to what ~/.kimi previously received - so no source file under hooks/ moved and the diff cannot attribute the ripple. Only the install destination changed, at resolveKimiHooksTomlDir in src/runtime-homes.cts.", - ".kimi-code/hooks/gsd-write-guard.js": "#2755: kimi-code installs its hook bundle into Kimi Code own root (~/.kimi-code) instead of Kimi CLI ~/.kimi. The script bytes are unchanged - identical to what ~/.kimi previously received - so no source file under hooks/ moved and the diff cannot attribute the ripple. Only the install destination changed, at resolveKimiHooksTomlDir in src/runtime-homes.cts.", - ".kimi-code/hooks/lib/cursor-workspace.js": "#2755: kimi-code installs its hook bundle into Kimi Code own root (~/.kimi-code) instead of Kimi CLI ~/.kimi. The script bytes are unchanged - identical to what ~/.kimi previously received - so no source file under hooks/ moved and the diff cannot attribute the ripple. Only the install destination changed, at resolveKimiHooksTomlDir in src/runtime-homes.cts.", - ".kimi-code/hooks/lib/git-cmd.js": "#2755: kimi-code installs its hook bundle into Kimi Code own root (~/.kimi-code) instead of Kimi CLI ~/.kimi. The script bytes are unchanged - identical to what ~/.kimi previously received - so no source file under hooks/ moved and the diff cannot attribute the ripple. Only the install destination changed, at resolveKimiHooksTomlDir in src/runtime-homes.cts.", - ".kimi-code/hooks/lib/gsd-graphify-rebuild.sh": "#2755: kimi-code installs its hook bundle into Kimi Code own root (~/.kimi-code) instead of Kimi CLI ~/.kimi. The script bytes are unchanged - identical to what ~/.kimi previously received - so no source file under hooks/ moved and the diff cannot attribute the ripple. Only the install destination changed, at resolveKimiHooksTomlDir in src/runtime-homes.cts.", - ".kimi-code/hooks/managed-hooks-registry.cjs": "#2755: kimi-code installs its hook bundle into Kimi Code own root (~/.kimi-code) instead of Kimi CLI ~/.kimi. The script bytes are unchanged - identical to what ~/.kimi previously received - so no source file under hooks/ moved and the diff cannot attribute the ripple. Only the install destination changed, at resolveKimiHooksTomlDir in src/runtime-homes.cts.", - ".kimi/hooks/gsd-check-update-worker.js": "#2755: the mirror of the .kimi-code entries above. kimi-code no longer emits into Kimi CLI native root at all, so this path disappears from the kimi-code manifest. Content unchanged; kimi own manifest still emits the same path and is untouched by this PR.", - ".kimi/hooks/gsd-check-update.js": "#2755: the mirror of the .kimi-code entries above. kimi-code no longer emits into Kimi CLI native root at all, so this path disappears from the kimi-code manifest. Content unchanged; kimi own manifest still emits the same path and is untouched by this PR.", - ".kimi/hooks/gsd-config-reload.js": "#2755: the mirror of the .kimi-code entries above. kimi-code no longer emits into Kimi CLI native root at all, so this path disappears from the kimi-code manifest. Content unchanged; kimi own manifest still emits the same path and is untouched by this PR.", - ".kimi/hooks/gsd-context-monitor.js": "#2755: the mirror of the .kimi-code entries above. kimi-code no longer emits into Kimi CLI native root at all, so this path disappears from the kimi-code manifest. Content unchanged; kimi own manifest still emits the same path and is untouched by this PR.", - ".kimi/hooks/gsd-cursor-post-tool.js": "#2755: the mirror of the .kimi-code entries above. kimi-code no longer emits into Kimi CLI native root at all, so this path disappears from the kimi-code manifest. Content unchanged; kimi own manifest still emits the same path and is untouched by this PR.", - ".kimi/hooks/gsd-cursor-pre-tool.js": "#2755: the mirror of the .kimi-code entries above. kimi-code no longer emits into Kimi CLI native root at all, so this path disappears from the kimi-code manifest. Content unchanged; kimi own manifest still emits the same path and is untouched by this PR.", - ".kimi/hooks/gsd-cursor-session-start.js": "#2755: the mirror of the .kimi-code entries above. kimi-code no longer emits into Kimi CLI native root at all, so this path disappears from the kimi-code manifest. Content unchanged; kimi own manifest still emits the same path and is untouched by this PR.", - ".kimi/hooks/gsd-cursor-stop.js": "#2755: the mirror of the .kimi-code entries above. kimi-code no longer emits into Kimi CLI native root at all, so this path disappears from the kimi-code manifest. Content unchanged; kimi own manifest still emits the same path and is untouched by this PR.", - ".kimi/hooks/gsd-cursor-subagent-start.js": "#2755: the mirror of the .kimi-code entries above. kimi-code no longer emits into Kimi CLI native root at all, so this path disappears from the kimi-code manifest. Content unchanged; kimi own manifest still emits the same path and is untouched by this PR.", - ".kimi/hooks/gsd-cursor-subagent-stop.js": "#2755: the mirror of the .kimi-code entries above. kimi-code no longer emits into Kimi CLI native root at all, so this path disappears from the kimi-code manifest. Content unchanged; kimi own manifest still emits the same path and is untouched by this PR.", - ".kimi/hooks/gsd-ensure-canonical-path.js": "#2755: the mirror of the .kimi-code entries above. kimi-code no longer emits into Kimi CLI native root at all, so this path disappears from the kimi-code manifest. Content unchanged; kimi own manifest still emits the same path and is untouched by this PR.", - ".kimi/hooks/gsd-graphify-update.sh": "#2755: the mirror of the .kimi-code entries above. kimi-code no longer emits into Kimi CLI native root at all, so this path disappears from the kimi-code manifest. Content unchanged; kimi own manifest still emits the same path and is untouched by this PR.", - ".kimi/hooks/gsd-phase-boundary.sh": "#2755: the mirror of the .kimi-code entries above. kimi-code no longer emits into Kimi CLI native root at all, so this path disappears from the kimi-code manifest. Content unchanged; kimi own manifest still emits the same path and is untouched by this PR.", - ".kimi/hooks/gsd-prompt-guard.js": "#2755: the mirror of the .kimi-code entries above. kimi-code no longer emits into Kimi CLI native root at all, so this path disappears from the kimi-code manifest. Content unchanged; kimi own manifest still emits the same path and is untouched by this PR.", - ".kimi/hooks/gsd-read-guard.js": "#2755: the mirror of the .kimi-code entries above. kimi-code no longer emits into Kimi CLI native root at all, so this path disappears from the kimi-code manifest. Content unchanged; kimi own manifest still emits the same path and is untouched by this PR.", - ".kimi/hooks/gsd-read-injection-scanner.js": "#2755: the mirror of the .kimi-code entries above. kimi-code no longer emits into Kimi CLI native root at all, so this path disappears from the kimi-code manifest. Content unchanged; kimi own manifest still emits the same path and is untouched by this PR.", - ".kimi/hooks/gsd-session-state.sh": "#2755: the mirror of the .kimi-code entries above. kimi-code no longer emits into Kimi CLI native root at all, so this path disappears from the kimi-code manifest. Content unchanged; kimi own manifest still emits the same path and is untouched by this PR.", - ".kimi/hooks/gsd-statusline.js": "#2755: the mirror of the .kimi-code entries above. kimi-code no longer emits into Kimi CLI native root at all, so this path disappears from the kimi-code manifest. Content unchanged; kimi own manifest still emits the same path and is untouched by this PR.", - ".kimi/hooks/gsd-update-banner.js": "#2755: the mirror of the .kimi-code entries above. kimi-code no longer emits into Kimi CLI native root at all, so this path disappears from the kimi-code manifest. Content unchanged; kimi own manifest still emits the same path and is untouched by this PR.", - ".kimi/hooks/gsd-validate-commit.sh": "#2755: the mirror of the .kimi-code entries above. kimi-code no longer emits into Kimi CLI native root at all, so this path disappears from the kimi-code manifest. Content unchanged; kimi own manifest still emits the same path and is untouched by this PR.", - ".kimi/hooks/gsd-windsurf-pre-command.js": "#2755: the mirror of the .kimi-code entries above. kimi-code no longer emits into Kimi CLI native root at all, so this path disappears from the kimi-code manifest. Content unchanged; kimi own manifest still emits the same path and is untouched by this PR.", - ".kimi/hooks/gsd-windsurf-pre-write.js": "#2755: the mirror of the .kimi-code entries above. kimi-code no longer emits into Kimi CLI native root at all, so this path disappears from the kimi-code manifest. Content unchanged; kimi own manifest still emits the same path and is untouched by this PR.", - ".kimi/hooks/gsd-workflow-guard.js": "#2755: the mirror of the .kimi-code entries above. kimi-code no longer emits into Kimi CLI native root at all, so this path disappears from the kimi-code manifest. Content unchanged; kimi own manifest still emits the same path and is untouched by this PR.", - ".kimi/hooks/gsd-worktree-path-guard.js": "#2755: the mirror of the .kimi-code entries above. kimi-code no longer emits into Kimi CLI native root at all, so this path disappears from the kimi-code manifest. Content unchanged; kimi own manifest still emits the same path and is untouched by this PR.", - ".kimi/hooks/gsd-write-guard.js": "#2755: the mirror of the .kimi-code entries above. kimi-code no longer emits into Kimi CLI native root at all, so this path disappears from the kimi-code manifest. Content unchanged; kimi own manifest still emits the same path and is untouched by this PR.", - ".kimi/hooks/lib/cursor-workspace.js": "#2755: the mirror of the .kimi-code entries above. kimi-code no longer emits into Kimi CLI native root at all, so this path disappears from the kimi-code manifest. Content unchanged; kimi own manifest still emits the same path and is untouched by this PR.", - ".kimi/hooks/lib/git-cmd.js": "#2755: the mirror of the .kimi-code entries above. kimi-code no longer emits into Kimi CLI native root at all, so this path disappears from the kimi-code manifest. Content unchanged; kimi own manifest still emits the same path and is untouched by this PR.", - ".kimi/hooks/lib/gsd-graphify-rebuild.sh": "#2755: the mirror of the .kimi-code entries above. kimi-code no longer emits into Kimi CLI native root at all, so this path disappears from the kimi-code manifest. Content unchanged; kimi own manifest still emits the same path and is untouched by this PR.", - ".kimi/hooks/managed-hooks-registry.cjs": "#2755: the mirror of the .kimi-code entries above. kimi-code no longer emits into Kimi CLI native root at all, so this path disappears from the kimi-code manifest. Content unchanged; kimi own manifest still emits the same path and is untouched by this PR." - } -} diff --git a/tests/emitted-drift-acks/2775-planner-package-legitimacy-gate.json b/tests/emitted-drift-acks/2775-planner-package-legitimacy-gate.json deleted file mode 100644 index e530aa1ab..000000000 --- a/tests/emitted-drift-acks/2775-planner-package-legitimacy-gate.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "version": 1, - "paths": { - "gsd-planner.md": { - "reason": "#2775: the STRIDE supply-chain row for npm/pip/cargo installs was rewritten from 'slopcheck + blocking human checkpoint for [ASSUMED]/[SUS]' to 'package-legitimacy gate + blocking human checkpoint for [ASSUMED]/[SUS]', matching ADR-0656 (registry-API verdicts are the gate; slopcheck is an optional escalate-only adapter no shipped configuration wires). The +14 bytes is the corrected mitigation description agents read at plan time, not incidental prose growth. #3565 appends the fenced `## Return Markers` section (~1.2 KB) enumerating the six exact stall-watch dispatch markers plan-phase.md matches on — the new registry lint requires every declared marker to be emitted in-fence by its producer, and the planner previously documented none of them anywhere. #2401 append (merged into this fragment because two ack sources may never name the same path): the 'Inherit the command that already worked' paragraph is now a short pointer to the extracted gsd-core/references/planner-verify-command-grounding.md (prior_verify_commands inheritance, npm --prefix grounding rules, and the guessing prohibition all moved to the reference). The file was 49274 bytes (49077 chars) immediately before the #3003 and #3299 arms below, both of which supersede that figure, well under the XL tier's 57344-byte cap, and 49077 chars (LF-normalized) under the separate 49152-char cap asserted by tests/planner-decomposition.test.cjs, tests/precondition-element.test.cjs, tests/reversibility-tagging.test.cjs, and tests/security.test.cjs. #3003 append (merged into this fragment because two ack sources may never name the same path): the implicit-dependency wave rule now computes same-wave overlap over files_modified PLUS files_deleted, so a plan deleting a file another same-wave plan edits is pushed to a later wave instead of racing it -- one branch removing what the other is writing is the sharpest conflict there is, and reading files_modified alone scored that pair conflict-free. Deliberately terse (+56 LF chars, landing at 49133 against the 49152-char cap named above, 19 chars of headroom). The `# Implicit dependency: files_modified overlap forces a later wave.` comment is left VERBATIM: tests/parallel-dependent-plans.test.cjs matches that exact unbackticked substring, so rewording it to name the new channel reds the suite -- the files_deleted addition rides in the pseudocode and the Rule sentence instead: the rationale is carried in docs/reference/plan-md.md, which has no cap. The next contributor who needs room in this agent must extract, not add prose. — #3299 append: the tracer task-shape template's now wraps its command in . The Nyquist Rule earlier in the file already required every to carry , but this tracer-specific template still showed the legacy bare-text form, so a planner following its own most specific template emitted tracers the #3299 feedback gate can never auto-continue on (the gate requires a carrying only ) — making the fix unreachable on the default tracer-first path. Caught in peer review. Written on ONE line, matching the one-line example under docs/reference/plan-md.md's \"## Reversibility\" heading (cited by section, not line: an earlier \":207\" citation was accurate when written and drifted with a later merge of next). Sizes re-measured against this merge of next, which landed the #3003 arm above: the file is 49146 chars after this change (49343 bytes), +13 chars on next's 49133, leaving 6 chars under the 49152-CHAR cap asserted independently by tests/planner-decomposition.test.cjs, tests/precondition-element.test.cjs, tests/reversibility-tagging.test.cjs and tests/security.test.cjs (all four assert `< 49152`, so 49146 passes). The 49090 / 62-chars-of-headroom figures this arm carried in earlier rounds were measured before #3003 and are superseded. Note the cap counts CHARACTERS, not bytes, and `wc -m` silently reports bytes under LC_ALL=C, so measure in a UTF-8 locale. The three-line form crossed the cap; the placeholder lost the word 'runnable' to buy that room — 'a real END-TO-END check' still carries the operative requirement, and the neighbouring prose already says the tracer carries the same and validation as any auto task. With 6 chars left this file is effectively full: the next contributor needing room here must extract to a reference file, not bump the cap." - } - } -} diff --git a/tests/emitted-drift-acks/2818-ship-note-wedged-pr.json b/tests/emitted-drift-acks/2818-ship-note-wedged-pr.json deleted file mode 100644 index 056bd09a2..000000000 --- a/tests/emitted-drift-acks/2818-ship-note-wedged-pr.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "version": 1, - "paths": { - "ship.md": "#2783: the ship-note protocol adds a bounded merge-state poll and an empty recovery commit when a skip token leaves a PR blocked with no checks. #3559: the ship:pre preflight replaced two hardcoded capId arms (security, broken-windows) with a single generic loop over every active kind==\"gate\" entry, dispatching by check shape through the generic predicate evaluator (ADR-2008/#2008) and honoring each gate's own blocking/onError — the same dispatch contract execute:wave:post, execute:post and plan:post already implement and references/loop-hook-dispatch.md already specifies. Before this, a third-party capability's blocking ship:pre gate was resolved and evaluable and then silently dropped, so a phase shipped past its own declared gate with no warning. Growth is the generic two-step contract (command-failure routed per onError, then the block decision routed per blocking), the every-other-capId evaluator arm, and the empty-activeHooks and unsupported-check-shape arms; the two named branches are retained verbatim inside the loop so their fail-closed semantics are unchanged and no gate is enforced twice. The generic arm also carries the in-context validation contract loop-hook-dispatch.md mandates for ref.command: #3559 is the first time a THIRD-PARTY manifest string reaches a shell at ship:pre, and gates[].check is not one of the four executable surfaces the install consent prompt discloses, so an unvalidated check.query would be a consent-model bypass. 33726 -> 36784 bytes (+3058), DEFAULT tier, cap 40960." - } -} diff --git a/tests/emitted-drift-acks/2845-ui-spec-inventory-provenance.json b/tests/emitted-drift-acks/2845-ui-spec-inventory-provenance.json deleted file mode 100644 index fd184012a..000000000 --- a/tests/emitted-drift-acks/2845-ui-spec-inventory-provenance.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "$comment": "Growth ack (#2914 fragment). Reason: #2845 makes a UI-SPEC component inventory falsifiable. gsd-ui-checker.md gains Dimension 7 (Inventory Provenance) — BLOCK/FLAG/PASS criteria, the two-shape provenance grammar it keys on, an unfilled-placeholder BLOCK rule, an example issue, and the allowlist-downgrade rule; 14118 -> 18180 LF bytes (+4062, DEFAULT cap 24576). gsd-ui-researcher.md gains the enumeration ladder and the identical grammar line it must record; 19557 -> 21474 (+1917, DEFAULT cap 24576). gsd-core/workflows/ui-phase.md is byte-unchanged (6->7 is same-width). The grammar line is byte-identical across template, checker and researcher, and the dimension roster is pinned across thirteen surfaces, by tests/ui-spec-inventory-provenance.test.cjs.", - "version": 1, - "paths": { - "gsd-ui-checker.md": "agents/gsd-ui-checker.md +4062B: Dimension 7 Inventory Provenance — an unsourced component inventory is a defect and is downgraded from a closed allowlist to a non-exhaustive list (#2845)", - "gsd-ui-researcher.md": "agents/gsd-ui-researcher.md +1917B: component-inventory enumeration gate — enumerate from the installed package and record command, count, resolved package@version and date (#2845)" - } -} diff --git a/tests/emitted-drift-acks/2943-context7-tool-name.json b/tests/emitted-drift-acks/2943-context7-tool-name.json deleted file mode 100644 index a8d9ac96b..000000000 --- a/tests/emitted-drift-acks/2943-context7-tool-name.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "version": 1, - "paths": { - "gsd-executor.md": "#2943: the context7 doc-lookup block's ctx7 CLI-fallback rationale was rewritten to describe the real mechanism (custom subagents cannot see project-scoped .mcp.json; they inherit only user-scoped ~/.claude/mcp.json), replacing the wrong anthropics/claude-code#13898 'tools: frontmatter restriction' attribution. The tool name was also corrected (get-library-docs -> query-docs) and the params renamed (context7CompatibleLibraryId/topic -> libraryId/query). The +95 bytes is the longer-but-accurate mechanism description; it is the literal fix for the mis-attribution, not incidental prose growth, and the rationale must be correct because agents read it to decide when to fall back to the CLI. #3021 amendment: branch allow-list regex widened to accept worktree-wf_* (Workflow backend naming) alongside agent-*/worktree-agent-*. \u2014 #3210 append: the unmet- branch now reports the returned checkpoint with **Gate:** blocking-human (both auto-mode bypass layers key on that gate; without it auto-mode silently auto-approved the checkpoint with a synthetic 'approved'), the auto-mode human-verify rule names precondition-unmet checkpoints as exempt, and checkpoint_return_format's Gate line notes precondition-unmet checkpoints report blocking-human; +134 bytes, still under the 49152 cap. \u2014 #3576 append: bare `references/.md` cites repaired to the canonical `gsd-core/references/.md` form (+18 bytes, 2 cite(s) \u00d7 9). Dead-pointer fix; no content change." - } -} \ No newline at end of file diff --git a/tests/emitted-drift-acks/2962-zsh-nomatch-for-glob-portability.json b/tests/emitted-drift-acks/2962-zsh-nomatch-for-glob-portability.json deleted file mode 100644 index a41a39949..000000000 --- a/tests/emitted-drift-acks/2962-zsh-nomatch-for-glob-portability.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "version": 1, - "paths": { - "gsd-integration-checker.md": "#2962: 1 bash block (line ~98 SUMMARY iteration) gained the nullglob shim for zsh portability of the for-glob loop.", - "resume-project.md": "#2962: 1 bash block (line ~66 plans-without-summaries scan) gained the nullglob shim for zsh portability of the for-glob loop.", - "audit-milestone.md": "#2962: 1 bash block (line ~126 requirements_completed extraction) gained the nullglob shim for zsh portability of the for-glob loop." - } -} diff --git a/tests/emitted-drift-acks/2971-pr-branch-strict-planning-filter.json b/tests/emitted-drift-acks/2971-pr-branch-strict-planning-filter.json deleted file mode 100644 index 41fd6db84..000000000 --- a/tests/emitted-drift-acks/2971-pr-branch-strict-planning-filter.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "version": 1, - "paths": { - "pr-branch.md": { - "reason": "#3679 re-arm: +14 lines vs the #2971-acknowledged size. The #2971 rewrite already fixed the deletion class this issue reported (rm -f --ignore-unmatch + checkout HEAD restore — pre-existing target-tracked planning files survive); #3679 adds the piece the brief still required of verify: a PLANNING_DELETIONS count over git diff --name-status with a second must-be-0 gate beside $FORBIDDEN (name-only counting cannot see status, so a deleted allowed/structural planning path verified clean), plus its display line. Prior reason kept below for the audit trail. — #2971: +5729 bytes vs next. Adds the planning.pr_strict filter mode and repairs two verified defects in the same cherry-pick loop. The growth is four things, none of them prose padding: (1) the canonical TRANSIENT_DIRS/STRUCTURAL_RE declarations plus their per-mode FILTER_PATHS/FORBIDDEN_RE projections, which replace two duplicated hardcoded lists so create_pr_branch and verify can no longer disagree about what the filter promised; (2) a rewritten create_pr_branch loop — the old `git rm -r --cached` staged a DELETION of any .planning/ path the target branch already tracked, and left the picked file untracked on disk so a later commit touching that path aborted with \"untracked working tree files would be overwritten\" and every remaining commit was dropped, both reproduced against real git before the fix; the replacement forces filtered paths back to HEAD in index and worktree, halts on a conflict outside the filter instead of improvising, and skips a commit left empty by filtering; (3) a mode-derived verify step plus the advisory line naming the .planning/ paths default mode deliberately keeps, so correcting the assertion does not trade a permanently-wrong signal for silence; (4) a clean-working-tree precondition, required because the corrected filter now removes files from the working tree. Comments carry the why for each, because every one of them is a place a future edit would otherwise reintroduce the defect." - } - } -} diff --git a/tests/emitted-drift-acks/2994-fragmentize-final-slice.json b/tests/emitted-drift-acks/2994-fragmentize-final-slice.json deleted file mode 100644 index 9ee3b4309..000000000 --- a/tests/emitted-drift-acks/2994-fragmentize-final-slice.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "version": 1, - "paths": { - "update.md": "#2994 (epic #1671 Phase 6.3, FINAL slice): fragmentizes update.md onto the marker grammar, admitting `state:next-channel` (channel-banner section) and a new dedicated `init.update` entry point (cmdInitUpdate). update.md previously carried NO `gsd_run query init.*` call at all; the new INIT_UPDATE fetch (reusing the already-resolved $GSD_TOOLS from get_installed_version, dual .cjs/PATH-shim invocation style, matching the pre-existing update-context call) plus its surrounding comment outweighs the single `` marker-pair stub that replaces the extracted channel-banner prose (now living in gsd-core/workflows/update/steps/channel-banner.md). Net SOURCE growth is +703 bytes (26,024 -> 26,727). The pre-existing TAG=\"next\"/TAG=\"latest\" case-statement in parse_update_channel is deliberately left byte-identical — issue #815's regression test (tests/issue-815-update-next-channel.test.cjs) asserts that literal text stays in the workflow, since the npm dist-tag selection must run in the workflow's own shell before any gsd_run round-trip. The EMITTED artifact composeWorkflow produces at install time still includes the extracted prose verbatim (markers strip, gap+section bodies re-join byte-for-byte) when the atom is unresolved (section_manifest null -> read-everything fallback), so installed behavior is unchanged; only the SOURCE file's on-disk byte count moves." - } -} diff --git a/tests/emitted-drift-acks/3003-declared-deletions.json b/tests/emitted-drift-acks/3003-declared-deletions.json deleted file mode 100644 index fc2884c3e..000000000 --- a/tests/emitted-drift-acks/3003-declared-deletions.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "$comment": "Growth ack (#2914 fragment). RE-ARMED for #3683 (+238B, gated extract-learnings wiring in auto_copy_learnings); prior #3003 reason retained after the em-dash. Reason: #3003 threads a plan-declared deletion list from plan frontmatter to the cleanup-wave deletions guard. execute-phase.md gains --deletions \"$PLAN_DELETIONS\" on the record-agent call; 92326 -> 92356 LF bytes (+30). Supersedes the spent #2856 fragment entry (tests/emitted-drift-acks/2856-live-dom-uat.json, merged into next so its ripple is already absorbed at the base and it can no longer clear anything), which also named execute-phase.md and would otherwise double-ack the same path — the same supersede that entry itself performed on the spent #3370 fragment, which had performed it on #3324. Only this path needs an entry: currentSizes() (tests/helpers/emitted-runtime.cjs:916-929) reads gsd-core/workflows/ and agents/ with a NON-recursive readdirSync that skips directories, so the two other grown shipped files are outside the growth ratchet — gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md (+885) sits in a subdirectory and gsd-core/templates/phase-prompt.md (+285) is under templates/. Their emitted-hash ripples are attributable to this diff and need no acknowledgment.", - "version": 1, - "paths": { - "execute-phase.md": "gsd-core/workflows/execute-phase.md +1786B: #3684 wires condition 3 of discover_and_group_plans to roadmap.analyze's roadmap_complete (bash read + three-way branch) — the verified-but-never-marked-complete shape now resumes at update_roadmap instead of exiting clean forever. Supersedes the spent #3683 entry (gated extract-learnings in auto_copy_learnings), same chain #3683 ← #3003 ← #2856 ← #3370 ← #3324." - } -} diff --git a/tests/emitted-drift-acks/3004-codex-adapter-collaboration-vocab.json b/tests/emitted-drift-acks/3004-codex-adapter-collaboration-vocab.json deleted file mode 100644 index ae3217cc0..000000000 --- a/tests/emitted-drift-acks/3004-codex-adapter-collaboration-vocab.json +++ /dev/null @@ -1,76 +0,0 @@ -{ - "version": 1, - "paths": { - ".agents/skills/gsd-add-tests/SKILL.md": "#3004: Codex adapter builder (getCodexSkillAdapterHeader in bin/install.js) changed collaboration-tool vocabulary \u2014 wait(ids)\u2192wait_agent, gated close_agent, added task_name+fork_turns. Every Codex SKILL.md embeds this adapter block, so all ripple.#3423 append (epic #1891 F8): body also moved with the -> rename in its embedded workflow/command/capability sources (the skills-from-commands rule expects a commands/gsd change this diff does not carry) \u2014 deliberate, tag-token-only delta.", - ".agents/skills/gsd-ai-integration-phase/SKILL.md": "#3004: Codex adapter builder (getCodexSkillAdapterHeader in bin/install.js) changed collaboration-tool vocabulary \u2014 wait(ids)\u2192wait_agent, gated close_agent, added task_name+fork_turns. Every Codex SKILL.md embeds this adapter block, so all ripple.#3423 append (epic #1891 F8): body also moved with the -> rename in its embedded workflow/command/capability sources (the skills-from-commands rule expects a commands/gsd change this diff does not carry) \u2014 deliberate, tag-token-only delta.", - ".agents/skills/gsd-audit-fix/SKILL.md": "#3004: Codex adapter builder (getCodexSkillAdapterHeader in bin/install.js) changed collaboration-tool vocabulary \u2014 wait(ids)\u2192wait_agent, gated close_agent, added task_name+fork_turns. Every Codex SKILL.md embeds this adapter block, so all ripple.#3423 append (epic #1891 F8): body also moved with the -> rename in its embedded workflow/command/capability sources (the skills-from-commands rule expects a commands/gsd change this diff does not carry) \u2014 deliberate, tag-token-only delta.", - ".agents/skills/gsd-audit-milestone/SKILL.md": "#3004: Codex adapter builder (getCodexSkillAdapterHeader in bin/install.js) changed collaboration-tool vocabulary \u2014 wait(ids)\u2192wait_agent, gated close_agent, added task_name+fork_turns. Every Codex SKILL.md embeds this adapter block, so all ripple.#3423 append (epic #1891 F8): body also moved with the -> rename in its embedded workflow/command/capability sources (the skills-from-commands rule expects a commands/gsd change this diff does not carry) \u2014 deliberate, tag-token-only delta.", - ".agents/skills/gsd-audit-uat/SKILL.md": "#3004: Codex adapter builder (getCodexSkillAdapterHeader in bin/install.js) changed collaboration-tool vocabulary \u2014 wait(ids)\u2192wait_agent, gated close_agent, added task_name+fork_turns. Every Codex SKILL.md embeds this adapter block, so all ripple.#3423 append (epic #1891 F8): body also moved with the -> rename in its embedded workflow/command/capability sources (the skills-from-commands rule expects a commands/gsd change this diff does not carry) \u2014 deliberate, tag-token-only delta.", - ".agents/skills/gsd-autonomous/SKILL.md": "#3004: Codex adapter builder (getCodexSkillAdapterHeader in bin/install.js) changed collaboration-tool vocabulary \u2014 wait(ids)\u2192wait_agent, gated close_agent, added task_name+fork_turns. Every Codex SKILL.md embeds this adapter block, so all ripple.#3423 append (epic #1891 F8): body also moved with the -> rename in its embedded workflow/command/capability sources (the skills-from-commands rule expects a commands/gsd change this diff does not carry) \u2014 deliberate, tag-token-only delta.", - ".agents/skills/gsd-capture/SKILL.md": "#3004: Codex adapter builder (getCodexSkillAdapterHeader in bin/install.js) changed collaboration-tool vocabulary \u2014 wait(ids)\u2192wait_agent, gated close_agent, added task_name+fork_turns. Every Codex SKILL.md embeds this adapter block, so all ripple.#3423 append (epic #1891 F8): body also moved with the -> rename in its embedded workflow/command/capability sources (the skills-from-commands rule expects a commands/gsd change this diff does not carry) \u2014 deliberate, tag-token-only delta.", - ".agents/skills/gsd-cleanup/SKILL.md": "#3004: Codex adapter builder (getCodexSkillAdapterHeader in bin/install.js) changed collaboration-tool vocabulary \u2014 wait(ids)\u2192wait_agent, gated close_agent, added task_name+fork_turns. Every Codex SKILL.md embeds this adapter block, so all ripple.#3423 append (epic #1891 F8): body also moved with the -> rename in its embedded workflow/command/capability sources (the skills-from-commands rule expects a commands/gsd change this diff does not carry) \u2014 deliberate, tag-token-only delta.", - ".agents/skills/gsd-code-review/SKILL.md": "#3004: Codex adapter builder (getCodexSkillAdapterHeader in bin/install.js) changed collaboration-tool vocabulary \u2014 wait(ids)\u2192wait_agent, gated close_agent, added task_name+fork_turns. Every Codex SKILL.md embeds this adapter block, so all ripple.", - ".agents/skills/gsd-complete-milestone/SKILL.md": "#3004: Codex adapter builder (getCodexSkillAdapterHeader in bin/install.js) changed collaboration-tool vocabulary \u2014 wait(ids)\u2192wait_agent, gated close_agent, added task_name+fork_turns. Every Codex SKILL.md embeds this adapter block, so all ripple.#3423 append (epic #1891 F8): body also moved with the -> rename in its embedded workflow/command/capability sources (the skills-from-commands rule expects a commands/gsd change this diff does not carry) \u2014 deliberate, tag-token-only delta.", - ".agents/skills/gsd-config/SKILL.md": "#3004: Codex adapter builder (getCodexSkillAdapterHeader in bin/install.js) changed collaboration-tool vocabulary \u2014 wait(ids)\u2192wait_agent, gated close_agent, added task_name+fork_turns. Every Codex SKILL.md embeds this adapter block, so all ripple.#3423 append (epic #1891 F8): body also moved with the -> rename in its embedded workflow/command/capability sources (the skills-from-commands rule expects a commands/gsd change this diff does not carry) \u2014 deliberate, tag-token-only delta.", - ".agents/skills/gsd-debug/SKILL.md": "#3004: Codex adapter builder (getCodexSkillAdapterHeader in bin/install.js) changed collaboration-tool vocabulary \u2014 wait(ids)\u2192wait_agent, gated close_agent, added task_name+fork_turns. Every Codex SKILL.md embeds this adapter block, so all ripple.#3423 append (epic #1891 F8): body also moved with the -> rename in its embedded workflow/command/capability sources (the skills-from-commands rule expects a commands/gsd change this diff does not carry) \u2014 deliberate, tag-token-only delta.", - ".agents/skills/gsd-discuss-phase/SKILL.md": "#3004: Codex adapter builder (getCodexSkillAdapterHeader in bin/install.js) changed collaboration-tool vocabulary \u2014 wait(ids)\u2192wait_agent, gated close_agent, added task_name+fork_turns. Every Codex SKILL.md embeds this adapter block, so all ripple.#3423 append (epic #1891 F8): body also moved with the -> rename in its embedded workflow/command/capability sources (the skills-from-commands rule expects a commands/gsd change this diff does not carry) \u2014 deliberate, tag-token-only delta.", - ".agents/skills/gsd-docs-update/SKILL.md": "#3004: Codex adapter builder (getCodexSkillAdapterHeader in bin/install.js) changed collaboration-tool vocabulary \u2014 wait(ids)\u2192wait_agent, gated close_agent, added task_name+fork_turns. Every Codex SKILL.md embeds this adapter block, so all ripple.#3423 append (epic #1891 F8): body also moved with the -> rename in its embedded workflow/command/capability sources (the skills-from-commands rule expects a commands/gsd change this diff does not carry) \u2014 deliberate, tag-token-only delta.", - ".agents/skills/gsd-eval-review/SKILL.md": "#3004: Codex adapter builder (getCodexSkillAdapterHeader in bin/install.js) changed collaboration-tool vocabulary \u2014 wait(ids)\u2192wait_agent, gated close_agent, added task_name+fork_turns. Every Codex SKILL.md embeds this adapter block, so all ripple.#3423 append (epic #1891 F8): body also moved with the -> rename in its embedded workflow/command/capability sources (the skills-from-commands rule expects a commands/gsd change this diff does not carry) \u2014 deliberate, tag-token-only delta.", - ".agents/skills/gsd-execute-phase/SKILL.md": "#3004: Codex adapter builder (getCodexSkillAdapterHeader in bin/install.js) changed collaboration-tool vocabulary \u2014 wait(ids)\u2192wait_agent, gated close_agent, added task_name+fork_turns. Every Codex SKILL.md embeds this adapter block, so all ripple.", - ".agents/skills/gsd-explore/SKILL.md": "#3004: Codex adapter builder (getCodexSkillAdapterHeader in bin/install.js) changed collaboration-tool vocabulary \u2014 wait(ids)\u2192wait_agent, gated close_agent, added task_name+fork_turns. Every Codex SKILL.md embeds this adapter block, so all ripple.#3423 append (epic #1891 F8): body also moved with the -> rename in its embedded workflow/command/capability sources (the skills-from-commands rule expects a commands/gsd change this diff does not carry) \u2014 deliberate, tag-token-only delta.", - ".agents/skills/gsd-extract-learnings/SKILL.md": "#3004: Codex adapter builder (getCodexSkillAdapterHeader in bin/install.js) changed collaboration-tool vocabulary \u2014 wait(ids)\u2192wait_agent, gated close_agent, added task_name+fork_turns. Every Codex SKILL.md embeds this adapter block, so all ripple.#3423 append (epic #1891 F8): body also moved with the -> rename in its embedded workflow/command/capability sources (the skills-from-commands rule expects a commands/gsd change this diff does not carry) \u2014 deliberate, tag-token-only delta.", - ".agents/skills/gsd-fast/SKILL.md": "#3004: Codex adapter builder (getCodexSkillAdapterHeader in bin/install.js) changed collaboration-tool vocabulary \u2014 wait(ids)\u2192wait_agent, gated close_agent, added task_name+fork_turns. Every Codex SKILL.md embeds this adapter block, so all ripple.#3423 append (epic #1891 F8): body also moved with the -> rename in its embedded workflow/command/capability sources (the skills-from-commands rule expects a commands/gsd change this diff does not carry) \u2014 deliberate, tag-token-only delta.", - ".agents/skills/gsd-forensics/SKILL.md": "#3004: Codex adapter builder (getCodexSkillAdapterHeader in bin/install.js) changed collaboration-tool vocabulary \u2014 wait(ids)\u2192wait_agent, gated close_agent, added task_name+fork_turns. Every Codex SKILL.md embeds this adapter block, so all ripple.#3423 append (epic #1891 F8): body also moved with the -> rename in its embedded workflow/command/capability sources (the skills-from-commands rule expects a commands/gsd change this diff does not carry) \u2014 deliberate, tag-token-only delta.", - ".agents/skills/gsd-graphify/SKILL.md": "#3004: Codex adapter builder (getCodexSkillAdapterHeader in bin/install.js) changed collaboration-tool vocabulary \u2014 wait(ids)\u2192wait_agent, gated close_agent, added task_name+fork_turns. Every Codex SKILL.md embeds this adapter block, so all ripple.#3423 append (epic #1891 F8): body also moved with the -> rename in its embedded workflow/command/capability sources (the skills-from-commands rule expects a commands/gsd change this diff does not carry) \u2014 deliberate, tag-token-only delta.", - ".agents/skills/gsd-health/SKILL.md": "#3004: Codex adapter builder (getCodexSkillAdapterHeader in bin/install.js) changed collaboration-tool vocabulary \u2014 wait(ids)\u2192wait_agent, gated close_agent, added task_name+fork_turns. Every Codex SKILL.md embeds this adapter block, so all ripple.#3423 append (epic #1891 F8): body also moved with the -> rename in its embedded workflow/command/capability sources (the skills-from-commands rule expects a commands/gsd change this diff does not carry) \u2014 deliberate, tag-token-only delta.", - ".agents/skills/gsd-help/SKILL.md": "#3004: Codex adapter builder (getCodexSkillAdapterHeader in bin/install.js) changed collaboration-tool vocabulary \u2014 wait(ids)\u2192wait_agent, gated close_agent, added task_name+fork_turns. Every Codex SKILL.md embeds this adapter block, so all ripple.#3423 append (epic #1891 F8): body also moved with the -> rename in its embedded workflow/command/capability sources (the skills-from-commands rule expects a commands/gsd change this diff does not carry) \u2014 deliberate, tag-token-only delta.", - ".agents/skills/gsd-import/SKILL.md": "#3004: Codex adapter builder (getCodexSkillAdapterHeader in bin/install.js) changed collaboration-tool vocabulary \u2014 wait(ids)\u2192wait_agent, gated close_agent, added task_name+fork_turns. Every Codex SKILL.md embeds this adapter block, so all ripple.#3423 append (epic #1891 F8): body also moved with the -> rename in its embedded workflow/command/capability sources (the skills-from-commands rule expects a commands/gsd change this diff does not carry) \u2014 deliberate, tag-token-only delta.", - ".agents/skills/gsd-inbox/SKILL.md": "#3004: Codex adapter builder (getCodexSkillAdapterHeader in bin/install.js) changed collaboration-tool vocabulary \u2014 wait(ids)\u2192wait_agent, gated close_agent, added task_name+fork_turns. Every Codex SKILL.md embeds this adapter block, so all ripple.#3423 append (epic #1891 F8): body also moved with the -> rename in its embedded workflow/command/capability sources (the skills-from-commands rule expects a commands/gsd change this diff does not carry) \u2014 deliberate, tag-token-only delta.", - ".agents/skills/gsd-ingest-docs/SKILL.md": "#3004: Codex adapter builder (getCodexSkillAdapterHeader in bin/install.js) changed collaboration-tool vocabulary \u2014 wait(ids)\u2192wait_agent, gated close_agent, added task_name+fork_turns. Every Codex SKILL.md embeds this adapter block, so all ripple.#3423 append (epic #1891 F8): body also moved with the -> rename in its embedded workflow/command/capability sources (the skills-from-commands rule expects a commands/gsd change this diff does not carry) \u2014 deliberate, tag-token-only delta.", - ".agents/skills/gsd-manager/SKILL.md": "#3004: Codex adapter builder (getCodexSkillAdapterHeader in bin/install.js) changed collaboration-tool vocabulary \u2014 wait(ids)\u2192wait_agent, gated close_agent, added task_name+fork_turns. Every Codex SKILL.md embeds this adapter block, so all ripple.#3423 append (epic #1891 F8): body also moved with the -> rename in its embedded workflow/command/capability sources (the skills-from-commands rule expects a commands/gsd change this diff does not carry) \u2014 deliberate, tag-token-only delta.", - ".agents/skills/gsd-map-codebase/SKILL.md": "#3004: Codex adapter builder (getCodexSkillAdapterHeader in bin/install.js) changed collaboration-tool vocabulary \u2014 wait(ids)\u2192wait_agent, gated close_agent, added task_name+fork_turns. Every Codex SKILL.md embeds this adapter block, so all ripple.#3423 append (epic #1891 F8): body also moved with the -> rename in its embedded workflow/command/capability sources (the skills-from-commands rule expects a commands/gsd change this diff does not carry) \u2014 deliberate, tag-token-only delta.", - ".agents/skills/gsd-mempalace-capture/SKILL.md": "#3004: Codex adapter builder (getCodexSkillAdapterHeader in bin/install.js) changed collaboration-tool vocabulary \u2014 wait(ids)\u2192wait_agent, gated close_agent, added task_name+fork_turns. Every Codex SKILL.md embeds this adapter block, so all ripple.#3423 append (epic #1891 F8): body also moved with the -> rename in its embedded workflow/command/capability sources (the skills-from-commands rule expects a commands/gsd change this diff does not carry) \u2014 deliberate, tag-token-only delta.", - ".agents/skills/gsd-mempalace-recall/SKILL.md": "#3004: Codex adapter builder (getCodexSkillAdapterHeader in bin/install.js) changed collaboration-tool vocabulary \u2014 wait(ids)\u2192wait_agent, gated close_agent, added task_name+fork_turns. Every Codex SKILL.md embeds this adapter block, so all ripple.#3423 append (epic #1891 F8): body also moved with the -> rename in its embedded workflow/command/capability sources (the skills-from-commands rule expects a commands/gsd change this diff does not carry) \u2014 deliberate, tag-token-only delta.", - ".agents/skills/gsd-milestone-summary/SKILL.md": "#3004: Codex adapter builder (getCodexSkillAdapterHeader in bin/install.js) changed collaboration-tool vocabulary \u2014 wait(ids)\u2192wait_agent, gated close_agent, added task_name+fork_turns. Every Codex SKILL.md embeds this adapter block, so all ripple.#3423 append (epic #1891 F8): body also moved with the -> rename in its embedded workflow/command/capability sources (the skills-from-commands rule expects a commands/gsd change this diff does not carry) \u2014 deliberate, tag-token-only delta.", - ".agents/skills/gsd-mvp-phase/SKILL.md": "#3004: Codex adapter builder (getCodexSkillAdapterHeader in bin/install.js) changed collaboration-tool vocabulary \u2014 wait(ids)\u2192wait_agent, gated close_agent, added task_name+fork_turns. Every Codex SKILL.md embeds this adapter block, so all ripple.#3423 append (epic #1891 F8): body also moved with the -> rename in its embedded workflow/command/capability sources (the skills-from-commands rule expects a commands/gsd change this diff does not carry) \u2014 deliberate, tag-token-only delta.", - ".agents/skills/gsd-new-milestone/SKILL.md": "#3004: Codex adapter builder (getCodexSkillAdapterHeader in bin/install.js) changed collaboration-tool vocabulary \u2014 wait(ids)\u2192wait_agent, gated close_agent, added task_name+fork_turns. Every Codex SKILL.md embeds this adapter block, so all ripple.", - ".agents/skills/gsd-new-project/SKILL.md": "#3004: Codex adapter builder (getCodexSkillAdapterHeader in bin/install.js) changed collaboration-tool vocabulary \u2014 wait(ids)\u2192wait_agent, gated close_agent, added task_name+fork_turns. Every Codex SKILL.md embeds this adapter block, so all ripple.#3423 append (epic #1891 F8): body also moved with the -> rename in its embedded workflow/command/capability sources (the skills-from-commands rule expects a commands/gsd change this diff does not carry) \u2014 deliberate, tag-token-only delta.", - ".agents/skills/gsd-next/SKILL.md": "#3004: Codex adapter builder (getCodexSkillAdapterHeader in bin/install.js) changed collaboration-tool vocabulary \u2014 wait(ids)\u2192wait_agent, gated close_agent, added task_name+fork_turns. Every Codex SKILL.md embeds this adapter block, so all ripple.#3423 append (epic #1891 F8): body also moved with the -> rename in its embedded workflow/command/capability sources (the skills-from-commands rule expects a commands/gsd change this diff does not carry) \u2014 deliberate, tag-token-only delta.", - ".agents/skills/gsd-ns-context/SKILL.md": "#3004: Codex adapter builder (getCodexSkillAdapterHeader in bin/install.js) changed collaboration-tool vocabulary \u2014 wait(ids)\u2192wait_agent, gated close_agent, added task_name+fork_turns. Every Codex SKILL.md embeds this adapter block, so all ripple.#3423 append (epic #1891 F8): body also moved with the -> rename in its embedded workflow/command/capability sources (the skills-from-commands rule expects a commands/gsd change this diff does not carry) \u2014 deliberate, tag-token-only delta.", - ".agents/skills/gsd-ns-ideate/SKILL.md": "#3004: Codex adapter builder (getCodexSkillAdapterHeader in bin/install.js) changed collaboration-tool vocabulary \u2014 wait(ids)\u2192wait_agent, gated close_agent, added task_name+fork_turns. Every Codex SKILL.md embeds this adapter block, so all ripple.#3423 append (epic #1891 F8): body also moved with the -> rename in its embedded workflow/command/capability sources (the skills-from-commands rule expects a commands/gsd change this diff does not carry) \u2014 deliberate, tag-token-only delta.", - ".agents/skills/gsd-ns-manage/SKILL.md": "#3004: Codex adapter builder (getCodexSkillAdapterHeader in bin/install.js) changed collaboration-tool vocabulary \u2014 wait(ids)\u2192wait_agent, gated close_agent, added task_name+fork_turns. Every Codex SKILL.md embeds this adapter block, so all ripple.#3423 append (epic #1891 F8): body also moved with the -> rename in its embedded workflow/command/capability sources (the skills-from-commands rule expects a commands/gsd change this diff does not carry) \u2014 deliberate, tag-token-only delta.", - ".agents/skills/gsd-ns-project/SKILL.md": "#3004: Codex adapter builder (getCodexSkillAdapterHeader in bin/install.js) changed collaboration-tool vocabulary \u2014 wait(ids)\u2192wait_agent, gated close_agent, added task_name+fork_turns. Every Codex SKILL.md embeds this adapter block, so all ripple.#3423 append (epic #1891 F8): body also moved with the -> rename in its embedded workflow/command/capability sources (the skills-from-commands rule expects a commands/gsd change this diff does not carry) \u2014 deliberate, tag-token-only delta.", - ".agents/skills/gsd-ns-review/SKILL.md": "#3004: Codex adapter builder (getCodexSkillAdapterHeader in bin/install.js) changed collaboration-tool vocabulary \u2014 wait(ids)\u2192wait_agent, gated close_agent, added task_name+fork_turns. Every Codex SKILL.md embeds this adapter block, so all ripple.#3423 append (epic #1891 F8): body also moved with the -> rename in its embedded workflow/command/capability sources (the skills-from-commands rule expects a commands/gsd change this diff does not carry) \u2014 deliberate, tag-token-only delta.", - ".agents/skills/gsd-ns-workflow/SKILL.md": "#3004: Codex adapter builder (getCodexSkillAdapterHeader in bin/install.js) changed collaboration-tool vocabulary \u2014 wait(ids)\u2192wait_agent, gated close_agent, added task_name+fork_turns. Every Codex SKILL.md embeds this adapter block, so all ripple.#3423 append (epic #1891 F8): body also moved with the -> rename in its embedded workflow/command/capability sources (the skills-from-commands rule expects a commands/gsd change this diff does not carry) \u2014 deliberate, tag-token-only delta.", - ".agents/skills/gsd-onboard/SKILL.md": "#3004: Codex adapter builder (getCodexSkillAdapterHeader in bin/install.js) changed collaboration-tool vocabulary \u2014 wait(ids)\u2192wait_agent, gated close_agent, added task_name+fork_turns. Every Codex SKILL.md embeds this adapter block, so all ripple.#3423 append (epic #1891 F8): body also moved with the -> rename in its embedded workflow/command/capability sources (the skills-from-commands rule expects a commands/gsd change this diff does not carry) \u2014 deliberate, tag-token-only delta.", - ".agents/skills/gsd-pause-work/SKILL.md": "#3004: Codex adapter builder (getCodexSkillAdapterHeader in bin/install.js) changed collaboration-tool vocabulary \u2014 wait(ids)\u2192wait_agent, gated close_agent, added task_name+fork_turns. Every Codex SKILL.md embeds this adapter block, so all ripple.#3423 append (epic #1891 F8): body also moved with the -> rename in its embedded workflow/command/capability sources (the skills-from-commands rule expects a commands/gsd change this diff does not carry) \u2014 deliberate, tag-token-only delta.", - ".agents/skills/gsd-phase/SKILL.md": "#3004: Codex adapter builder (getCodexSkillAdapterHeader in bin/install.js) changed collaboration-tool vocabulary \u2014 wait(ids)\u2192wait_agent, gated close_agent, added task_name+fork_turns. Every Codex SKILL.md embeds this adapter block, so all ripple.#3423 append (epic #1891 F8): body also moved with the -> rename in its embedded workflow/command/capability sources (the skills-from-commands rule expects a commands/gsd change this diff does not carry) \u2014 deliberate, tag-token-only delta.", - ".agents/skills/gsd-plan-phase/SKILL.md": "#3004: Codex adapter builder (getCodexSkillAdapterHeader in bin/install.js) changed collaboration-tool vocabulary \u2014 wait(ids)\u2192wait_agent, gated close_agent, added task_name+fork_turns. Every Codex SKILL.md embeds this adapter block, so all ripple.#3423 append (epic #1891 F8): body also moved with the -> rename in its embedded workflow/command/capability sources (the skills-from-commands rule expects a commands/gsd change this diff does not carry) \u2014 deliberate, tag-token-only delta.", - ".agents/skills/gsd-plan-review-convergence/SKILL.md": "#3004: Codex adapter builder (getCodexSkillAdapterHeader in bin/install.js) changed collaboration-tool vocabulary \u2014 wait(ids)\u2192wait_agent, gated close_agent, added task_name+fork_turns. Every Codex SKILL.md embeds this adapter block, so all ripple.#3423 append (epic #1891 F8): body also moved with the -> rename in its embedded workflow/command/capability sources (the skills-from-commands rule expects a commands/gsd change this diff does not carry) \u2014 deliberate, tag-token-only delta.", - ".agents/skills/gsd-pr-branch/SKILL.md": "#3004: Codex adapter builder (getCodexSkillAdapterHeader in bin/install.js) changed collaboration-tool vocabulary \u2014 wait(ids)\u2192wait_agent, gated close_agent, added task_name+fork_turns. Every Codex SKILL.md embeds this adapter block, so all ripple.#3423 append (epic #1891 F8): body also moved with the -> rename in its embedded workflow/command/capability sources (the skills-from-commands rule expects a commands/gsd change this diff does not carry) \u2014 deliberate, tag-token-only delta.", - ".agents/skills/gsd-profile-user/SKILL.md": "#3004: Codex adapter builder (getCodexSkillAdapterHeader in bin/install.js) changed collaboration-tool vocabulary \u2014 wait(ids)\u2192wait_agent, gated close_agent, added task_name+fork_turns. Every Codex SKILL.md embeds this adapter block, so all ripple.#3423 append (epic #1891 F8): body also moved with the -> rename in its embedded workflow/command/capability sources (the skills-from-commands rule expects a commands/gsd change this diff does not carry) \u2014 deliberate, tag-token-only delta.", - ".agents/skills/gsd-progress/SKILL.md": "#3004: Codex adapter builder (getCodexSkillAdapterHeader in bin/install.js) changed collaboration-tool vocabulary \u2014 wait(ids)\u2192wait_agent, gated close_agent, added task_name+fork_turns. Every Codex SKILL.md embeds this adapter block, so all ripple.#3423 append (epic #1891 F8): body also moved with the -> rename in its embedded workflow/command/capability sources (the skills-from-commands rule expects a commands/gsd change this diff does not carry) \u2014 deliberate, tag-token-only delta.", - ".agents/skills/gsd-quick/SKILL.md": "#3004: Codex adapter builder (getCodexSkillAdapterHeader in bin/install.js) changed collaboration-tool vocabulary \u2014 wait(ids)\u2192wait_agent, gated close_agent, added task_name+fork_turns. Every Codex SKILL.md embeds this adapter block, so all ripple.", - ".agents/skills/gsd-resume-work/SKILL.md": "#3004: Codex adapter builder (getCodexSkillAdapterHeader in bin/install.js) changed collaboration-tool vocabulary \u2014 wait(ids)\u2192wait_agent, gated close_agent, added task_name+fork_turns. Every Codex SKILL.md embeds this adapter block, so all ripple.#3423 append (epic #1891 F8): body also moved with the -> rename in its embedded workflow/command/capability sources (the skills-from-commands rule expects a commands/gsd change this diff does not carry) \u2014 deliberate, tag-token-only delta.", - ".agents/skills/gsd-review/SKILL.md": "#3004: Codex adapter builder (getCodexSkillAdapterHeader in bin/install.js) changed collaboration-tool vocabulary \u2014 wait(ids)\u2192wait_agent, gated close_agent, added task_name+fork_turns. Every Codex SKILL.md embeds this adapter block, so all ripple.#3423 append (epic #1891 F8): body also moved with the -> rename in its embedded workflow/command/capability sources (the skills-from-commands rule expects a commands/gsd change this diff does not carry) \u2014 deliberate, tag-token-only delta.", - ".agents/skills/gsd-review-backlog/SKILL.md": "#3004: Codex adapter builder (getCodexSkillAdapterHeader in bin/install.js) changed collaboration-tool vocabulary \u2014 wait(ids)\u2192wait_agent, gated close_agent, added task_name+fork_turns. Every Codex SKILL.md embeds this adapter block, so all ripple.#3423 append (epic #1891 F8): body also moved with the -> rename in its embedded workflow/command/capability sources (the skills-from-commands rule expects a commands/gsd change this diff does not carry) \u2014 deliberate, tag-token-only delta.", - ".agents/skills/gsd-secure-phase/SKILL.md": "#3004: Codex adapter builder (getCodexSkillAdapterHeader in bin/install.js) changed collaboration-tool vocabulary \u2014 wait(ids)\u2192wait_agent, gated close_agent, added task_name+fork_turns. Every Codex SKILL.md embeds this adapter block, so all ripple.#3423 append (epic #1891 F8): body also moved with the -> rename in its embedded workflow/command/capability sources (the skills-from-commands rule expects a commands/gsd change this diff does not carry) \u2014 deliberate, tag-token-only delta.", - ".agents/skills/gsd-settings/SKILL.md": "#3004: Codex adapter builder (getCodexSkillAdapterHeader in bin/install.js) changed collaboration-tool vocabulary \u2014 wait(ids)\u2192wait_agent, gated close_agent, added task_name+fork_turns. Every Codex SKILL.md embeds this adapter block, so all ripple.#3423 append (epic #1891 F8): body also moved with the -> rename in its embedded workflow/command/capability sources (the skills-from-commands rule expects a commands/gsd change this diff does not carry) \u2014 deliberate, tag-token-only delta.", - ".agents/skills/gsd-ship/SKILL.md": "#3004: Codex adapter builder (getCodexSkillAdapterHeader in bin/install.js) changed collaboration-tool vocabulary \u2014 wait(ids)\u2192wait_agent, gated close_agent, added task_name+fork_turns. Every Codex SKILL.md embeds this adapter block, so all ripple.#3423 append (epic #1891 F8): body also moved with the -> rename in its embedded workflow/command/capability sources (the skills-from-commands rule expects a commands/gsd change this diff does not carry) \u2014 deliberate, tag-token-only delta.", - ".agents/skills/gsd-sketch/SKILL.md": "#3004: Codex adapter builder (getCodexSkillAdapterHeader in bin/install.js) changed collaboration-tool vocabulary \u2014 wait(ids)\u2192wait_agent, gated close_agent, added task_name+fork_turns. Every Codex SKILL.md embeds this adapter block, so all ripple.#3423 append (epic #1891 F8): body also moved with the -> rename in its embedded workflow/command/capability sources (the skills-from-commands rule expects a commands/gsd change this diff does not carry) \u2014 deliberate, tag-token-only delta.", - ".agents/skills/gsd-spec-phase/SKILL.md": "#3004: Codex adapter builder (getCodexSkillAdapterHeader in bin/install.js) changed collaboration-tool vocabulary \u2014 wait(ids)\u2192wait_agent, gated close_agent, added task_name+fork_turns. Every Codex SKILL.md embeds this adapter block, so all ripple.#3423 append (epic #1891 F8): body also moved with the -> rename in its embedded workflow/command/capability sources (the skills-from-commands rule expects a commands/gsd change this diff does not carry) \u2014 deliberate, tag-token-only delta.", - ".agents/skills/gsd-spike/SKILL.md": "#3004: Codex adapter builder (getCodexSkillAdapterHeader in bin/install.js) changed collaboration-tool vocabulary \u2014 wait(ids)\u2192wait_agent, gated close_agent, added task_name+fork_turns. Every Codex SKILL.md embeds this adapter block, so all ripple.#3423 append (epic #1891 F8): body also moved with the -> rename in its embedded workflow/command/capability sources (the skills-from-commands rule expects a commands/gsd change this diff does not carry) \u2014 deliberate, tag-token-only delta.", - ".agents/skills/gsd-stats/SKILL.md": "#3004: Codex adapter builder (getCodexSkillAdapterHeader in bin/install.js) changed collaboration-tool vocabulary \u2014 wait(ids)\u2192wait_agent, gated close_agent, added task_name+fork_turns. Every Codex SKILL.md embeds this adapter block, so all ripple.#3423 append (epic #1891 F8): body also moved with the -> rename in its embedded workflow/command/capability sources (the skills-from-commands rule expects a commands/gsd change this diff does not carry) \u2014 deliberate, tag-token-only delta.", - ".agents/skills/gsd-surface/SKILL.md": "#3004: Codex adapter builder (getCodexSkillAdapterHeader in bin/install.js) changed collaboration-tool vocabulary \u2014 wait(ids)\u2192wait_agent, gated close_agent, added task_name+fork_turns. Every Codex SKILL.md embeds this adapter block, so all ripple.#3423 append (epic #1891 F8): body also moved with the -> rename in its embedded workflow/command/capability sources (the skills-from-commands rule expects a commands/gsd change this diff does not carry) \u2014 deliberate, tag-token-only delta.", - ".agents/skills/gsd-thread/SKILL.md": "#3004: Codex adapter builder (getCodexSkillAdapterHeader in bin/install.js) changed collaboration-tool vocabulary \u2014 wait(ids)\u2192wait_agent, gated close_agent, added task_name+fork_turns. Every Codex SKILL.md embeds this adapter block, so all ripple.#3423 append (epic #1891 F8): body also moved with the -> rename in its embedded workflow/command/capability sources (the skills-from-commands rule expects a commands/gsd change this diff does not carry) \u2014 deliberate, tag-token-only delta.", - ".agents/skills/gsd-ui-phase/SKILL.md": "#3004: Codex adapter builder (getCodexSkillAdapterHeader in bin/install.js) changed collaboration-tool vocabulary \u2014 wait(ids)\u2192wait_agent, gated close_agent, added task_name+fork_turns. Every Codex SKILL.md embeds this adapter block, so all ripple.#3423 append (epic #1891 F8): body also moved with the -> rename in its embedded workflow/command/capability sources (the skills-from-commands rule expects a commands/gsd change this diff does not carry) \u2014 deliberate, tag-token-only delta.", - ".agents/skills/gsd-ui-review/SKILL.md": "#3004: Codex adapter builder (getCodexSkillAdapterHeader in bin/install.js) changed collaboration-tool vocabulary \u2014 wait(ids)\u2192wait_agent, gated close_agent, added task_name+fork_turns. Every Codex SKILL.md embeds this adapter block, so all ripple.#3423 append (epic #1891 F8): body also moved with the -> rename in its embedded workflow/command/capability sources (the skills-from-commands rule expects a commands/gsd change this diff does not carry) \u2014 deliberate, tag-token-only delta.", - ".agents/skills/gsd-ultraplan-phase/SKILL.md": "#3004: Codex adapter builder (getCodexSkillAdapterHeader in bin/install.js) changed collaboration-tool vocabulary \u2014 wait(ids)\u2192wait_agent, gated close_agent, added task_name+fork_turns. Every Codex SKILL.md embeds this adapter block, so all ripple.#3423 append (epic #1891 F8): body also moved with the -> rename in its embedded workflow/command/capability sources (the skills-from-commands rule expects a commands/gsd change this diff does not carry) \u2014 deliberate, tag-token-only delta.", - ".agents/skills/gsd-undo/SKILL.md": "#3004: Codex adapter builder (getCodexSkillAdapterHeader in bin/install.js) changed collaboration-tool vocabulary \u2014 wait(ids)\u2192wait_agent, gated close_agent, added task_name+fork_turns. Every Codex SKILL.md embeds this adapter block, so all ripple.#3423 append (epic #1891 F8): body also moved with the -> rename in its embedded workflow/command/capability sources (the skills-from-commands rule expects a commands/gsd change this diff does not carry) \u2014 deliberate, tag-token-only delta.", - ".agents/skills/gsd-update/SKILL.md": "#3004: Codex adapter builder (getCodexSkillAdapterHeader in bin/install.js) changed collaboration-tool vocabulary \u2014 wait(ids)\u2192wait_agent, gated close_agent, added task_name+fork_turns. Every Codex SKILL.md embeds this adapter block, so all ripple.#3423 append (epic #1891 F8): body also moved with the -> rename in its embedded workflow/command/capability sources (the skills-from-commands rule expects a commands/gsd change this diff does not carry) \u2014 deliberate, tag-token-only delta.", - ".agents/skills/gsd-validate-phase/SKILL.md": "#3004: Codex adapter builder (getCodexSkillAdapterHeader in bin/install.js) changed collaboration-tool vocabulary \u2014 wait(ids)\u2192wait_agent, gated close_agent, added task_name+fork_turns. Every Codex SKILL.md embeds this adapter block, so all ripple.#3423 append (epic #1891 F8): body also moved with the -> rename in its embedded workflow/command/capability sources (the skills-from-commands rule expects a commands/gsd change this diff does not carry) \u2014 deliberate, tag-token-only delta.", - ".agents/skills/gsd-verify-work/SKILL.md": "#3004: Codex adapter builder (getCodexSkillAdapterHeader in bin/install.js) changed collaboration-tool vocabulary \u2014 wait(ids)\u2192wait_agent, gated close_agent, added task_name+fork_turns. Every Codex SKILL.md embeds this adapter block, so all ripple.", - ".agents/skills/gsd-workspace/SKILL.md": "#3004: Codex adapter builder (getCodexSkillAdapterHeader in bin/install.js) changed collaboration-tool vocabulary \u2014 wait(ids)\u2192wait_agent, gated close_agent, added task_name+fork_turns. Every Codex SKILL.md embeds this adapter block, so all ripple.#3423 append (epic #1891 F8): body also moved with the -> rename in its embedded workflow/command/capability sources (the skills-from-commands rule expects a commands/gsd change this diff does not carry) \u2014 deliberate, tag-token-only delta.", - ".agents/skills/gsd-workstreams/SKILL.md": "#3004: Codex adapter builder (getCodexSkillAdapterHeader in bin/install.js) changed collaboration-tool vocabulary \u2014 wait(ids)\u2192wait_agent, gated close_agent, added task_name+fork_turns. Every Codex SKILL.md embeds this adapter block, so all ripple.#3423 append (epic #1891 F8): body also moved with the -> rename in its embedded workflow/command/capability sources (the skills-from-commands rule expects a commands/gsd change this diff does not carry) \u2014 deliberate, tag-token-only delta." - } -} diff --git a/tests/emitted-drift-acks/3023-pi-shared-hooks-rename.json b/tests/emitted-drift-acks/3023-pi-shared-hooks-rename.json deleted file mode 100644 index 5c51ab1a5..000000000 --- a/tests/emitted-drift-acks/3023-pi-shared-hooks-rename.json +++ /dev/null @@ -1,179 +0,0 @@ -{ - "version": 1, - "paths": { - "gsd-hooks/gsd-agent-isolation-guard.js": { - "reason": "#3023: pi reserves its own hooks/ directory, so the installer now stages the shared hook bundle under gsd-hooks/ for the pi runtime instead of hooks/. This file's bytes are unchanged from hooks/gsd-agent-isolation-guard.js — only the emitted install path moved." - }, - "gsd-hooks/gsd-check-update.js": { - "reason": "#3023: pi reserves its own hooks/ directory, so the installer now stages the shared hook bundle under gsd-hooks/ for the pi runtime instead of hooks/. This file's bytes are unchanged from hooks/gsd-check-update.js — only the emitted install path moved." - }, - "gsd-hooks/gsd-config-reload.js": { - "reason": "#3023: pi reserves its own hooks/ directory, so the installer now stages the shared hook bundle under gsd-hooks/ for the pi runtime instead of hooks/. This file's bytes are unchanged from hooks/gsd-config-reload.js — only the emitted install path moved." - }, - "gsd-hooks/gsd-context-monitor.js": { - "reason": "#3023: pi reserves its own hooks/ directory, so the installer now stages the shared hook bundle under gsd-hooks/ for the pi runtime instead of hooks/. This file's bytes are unchanged from hooks/gsd-context-monitor.js — only the emitted install path moved." - }, - "gsd-hooks/gsd-cursor-post-tool.js": { - "reason": "#3023: pi reserves its own hooks/ directory, so the installer now stages the shared hook bundle under gsd-hooks/ for the pi runtime instead of hooks/. This file's bytes are unchanged from hooks/gsd-cursor-post-tool.js — only the emitted install path moved." - }, - "gsd-hooks/gsd-cursor-pre-tool.js": { - "reason": "#3023: pi reserves its own hooks/ directory, so the installer now stages the shared hook bundle under gsd-hooks/ for the pi runtime instead of hooks/. This file's bytes are unchanged from hooks/gsd-cursor-pre-tool.js — only the emitted install path moved." - }, - "gsd-hooks/gsd-cursor-session-start.js": { - "reason": "#3023: pi reserves its own hooks/ directory, so the installer now stages the shared hook bundle under gsd-hooks/ for the pi runtime instead of hooks/. This file's bytes are unchanged from hooks/gsd-cursor-session-start.js — only the emitted install path moved." - }, - "gsd-hooks/gsd-cursor-stop.js": { - "reason": "#3023: pi reserves its own hooks/ directory, so the installer now stages the shared hook bundle under gsd-hooks/ for the pi runtime instead of hooks/. This file's bytes are unchanged from hooks/gsd-cursor-stop.js — only the emitted install path moved." - }, - "gsd-hooks/gsd-cursor-subagent-start.js": { - "reason": "#3023: pi reserves its own hooks/ directory, so the installer now stages the shared hook bundle under gsd-hooks/ for the pi runtime instead of hooks/. This file's bytes are unchanged from hooks/gsd-cursor-subagent-start.js — only the emitted install path moved." - }, - "gsd-hooks/gsd-cursor-subagent-stop.js": { - "reason": "#3023: pi reserves its own hooks/ directory, so the installer now stages the shared hook bundle under gsd-hooks/ for the pi runtime instead of hooks/. This file's bytes are unchanged from hooks/gsd-cursor-subagent-stop.js — only the emitted install path moved." - }, - "gsd-hooks/gsd-ensure-canonical-path.js": { - "reason": "#3023: pi reserves its own hooks/ directory, so the installer now stages the shared hook bundle under gsd-hooks/ for the pi runtime instead of hooks/. This file's bytes are unchanged from hooks/gsd-ensure-canonical-path.js — only the emitted install path moved." - }, - "gsd-hooks/gsd-graphify-update.sh": { - "reason": "#3023: pi reserves its own hooks/ directory, so the installer now stages the shared hook bundle under gsd-hooks/ for the pi runtime instead of hooks/. This file's bytes are unchanged from hooks/gsd-graphify-update.sh — only the emitted install path moved." - }, - "gsd-hooks/gsd-phase-boundary.sh": { - "reason": "#3023: pi reserves its own hooks/ directory, so the installer now stages the shared hook bundle under gsd-hooks/ for the pi runtime instead of hooks/. This file's bytes are unchanged from hooks/gsd-phase-boundary.sh — only the emitted install path moved." - }, - "gsd-hooks/gsd-prompt-guard.js": { - "reason": "#3023: pi reserves its own hooks/ directory, so the installer now stages the shared hook bundle under gsd-hooks/ for the pi runtime instead of hooks/. This file's bytes are unchanged from hooks/gsd-prompt-guard.js — only the emitted install path moved." - }, - "gsd-hooks/gsd-read-guard.js": { - "reason": "#3023: pi reserves its own hooks/ directory, so the installer now stages the shared hook bundle under gsd-hooks/ for the pi runtime instead of hooks/. This file's bytes are unchanged from hooks/gsd-read-guard.js — only the emitted install path moved." - }, - "gsd-hooks/gsd-session-state.sh": { - "reason": "#3023: pi reserves its own hooks/ directory, so the installer now stages the shared hook bundle under gsd-hooks/ for the pi runtime instead of hooks/. This file's bytes are unchanged from hooks/gsd-session-state.sh — only the emitted install path moved." - }, - "gsd-hooks/gsd-statusline.js": { - "reason": "#3023: pi reserves its own hooks/ directory, so the installer now stages the shared hook bundle under gsd-hooks/ for the pi runtime instead of hooks/. This file's bytes are unchanged from hooks/gsd-statusline.js — only the emitted install path moved." - }, - "gsd-hooks/gsd-update-banner.js": { - "reason": "#3023: pi reserves its own hooks/ directory, so the installer now stages the shared hook bundle under gsd-hooks/ for the pi runtime instead of hooks/. This file's bytes are unchanged from hooks/gsd-update-banner.js — only the emitted install path moved." - }, - "gsd-hooks/gsd-validate-commit.sh": { - "reason": "#3023: pi reserves its own hooks/ directory, so the installer now stages the shared hook bundle under gsd-hooks/ for the pi runtime instead of hooks/. This file's bytes are unchanged from hooks/gsd-validate-commit.sh — only the emitted install path moved." - }, - "gsd-hooks/gsd-windsurf-pre-command.js": { - "reason": "#3023: pi reserves its own hooks/ directory, so the installer now stages the shared hook bundle under gsd-hooks/ for the pi runtime instead of hooks/. This file's bytes are unchanged from hooks/gsd-windsurf-pre-command.js — only the emitted install path moved." - }, - "gsd-hooks/gsd-windsurf-pre-write.js": { - "reason": "#3023: pi reserves its own hooks/ directory, so the installer now stages the shared hook bundle under gsd-hooks/ for the pi runtime instead of hooks/. This file's bytes are unchanged from hooks/gsd-windsurf-pre-write.js — only the emitted install path moved." - }, - "gsd-hooks/gsd-workflow-guard.js": { - "reason": "#3023: pi reserves its own hooks/ directory, so the installer now stages the shared hook bundle under gsd-hooks/ for the pi runtime instead of hooks/. This file's bytes are unchanged from hooks/gsd-workflow-guard.js — only the emitted install path moved." - }, - "gsd-hooks/gsd-worktree-path-guard.js": { - "reason": "#3023: pi reserves its own hooks/ directory, so the installer now stages the shared hook bundle under gsd-hooks/ for the pi runtime instead of hooks/. This file's bytes are unchanged from hooks/gsd-worktree-path-guard.js — only the emitted install path moved." - }, - "gsd-hooks/gsd-write-guard.js": { - "reason": "#3023: pi reserves its own hooks/ directory, so the installer now stages the shared hook bundle under gsd-hooks/ for the pi runtime instead of hooks/. This file's bytes are unchanged from hooks/gsd-write-guard.js — only the emitted install path moved." - }, - "gsd-hooks/lib/cursor-workspace.js": { - "reason": "#3023: pi reserves its own hooks/ directory, so the installer now stages the shared hook bundle under gsd-hooks/ for the pi runtime instead of hooks/. This file's bytes are unchanged from hooks/lib/cursor-workspace.js — only the emitted install path moved." - }, - "gsd-hooks/lib/git-cmd.js": { - "reason": "#3023: pi reserves its own hooks/ directory, so the installer now stages the shared hook bundle under gsd-hooks/ for the pi runtime instead of hooks/. This file's bytes are unchanged from hooks/lib/git-cmd.js — only the emitted install path moved." - }, - "gsd-hooks/lib/gsd-graphify-rebuild.sh": { - "reason": "#3023: pi reserves its own hooks/ directory, so the installer now stages the shared hook bundle under gsd-hooks/ for the pi runtime instead of hooks/. This file's bytes are unchanged from hooks/lib/gsd-graphify-rebuild.sh — only the emitted install path moved." - }, - "gsd-hooks/lib/isolation-sentinel.js": { - "reason": "#3023: pi reserves its own hooks/ directory, so the installer now stages the shared hook bundle under gsd-hooks/ for the pi runtime instead of hooks/. This file's bytes are unchanged from hooks/lib/isolation-sentinel.js — only the emitted install path moved." - }, - "gsd-hooks/managed-hooks-registry.cjs": { - "reason": "#3023: pi reserves its own hooks/ directory, so the installer now stages the shared hook bundle under gsd-hooks/ for the pi runtime instead of hooks/. This file's bytes are unchanged from hooks/managed-hooks-registry.cjs — only the emitted install path moved." - }, - "hooks/gsd-agent-isolation-guard.js": { - "reason": "#3023: pi no longer emits this file under hooks/ (its reserved directory); the installer now stages the same unchanged bytes under gsd-hooks/gsd-agent-isolation-guard.js for the pi runtime, so this path disappears from pi's manifest without its source changing." - }, - "hooks/gsd-check-update.js": { - "reason": "#3023: pi no longer emits this file under hooks/ (its reserved directory); the installer now stages the same unchanged bytes under gsd-hooks/gsd-check-update.js for the pi runtime, so this path disappears from pi's manifest without its source changing." - }, - "hooks/gsd-config-reload.js": { - "reason": "#3023: pi no longer emits this file under hooks/ (its reserved directory); the installer now stages the same unchanged bytes under gsd-hooks/gsd-config-reload.js for the pi runtime, so this path disappears from pi's manifest without its source changing." - }, - "hooks/gsd-context-monitor.js": { - "reason": "#3023: pi no longer emits this file under hooks/ (its reserved directory); the installer now stages the same unchanged bytes under gsd-hooks/gsd-context-monitor.js for the pi runtime, so this path disappears from pi's manifest without its source changing." - }, - "hooks/gsd-cursor-post-tool.js": { - "reason": "#3023: pi no longer emits this file under hooks/ (its reserved directory); the installer now stages the same unchanged bytes under gsd-hooks/gsd-cursor-post-tool.js for the pi runtime, so this path disappears from pi's manifest without its source changing." - }, - "hooks/gsd-cursor-pre-tool.js": { - "reason": "#3023: pi no longer emits this file under hooks/ (its reserved directory); the installer now stages the same unchanged bytes under gsd-hooks/gsd-cursor-pre-tool.js for the pi runtime, so this path disappears from pi's manifest without its source changing." - }, - "hooks/gsd-cursor-session-start.js": { - "reason": "#3023: pi no longer emits this file under hooks/ (its reserved directory); the installer now stages the same unchanged bytes under gsd-hooks/gsd-cursor-session-start.js for the pi runtime, so this path disappears from pi's manifest without its source changing." - }, - "hooks/gsd-cursor-stop.js": { - "reason": "#3023: pi no longer emits this file under hooks/ (its reserved directory); the installer now stages the same unchanged bytes under gsd-hooks/gsd-cursor-stop.js for the pi runtime, so this path disappears from pi's manifest without its source changing." - }, - "hooks/gsd-cursor-subagent-start.js": { - "reason": "#3023: pi no longer emits this file under hooks/ (its reserved directory); the installer now stages the same unchanged bytes under gsd-hooks/gsd-cursor-subagent-start.js for the pi runtime, so this path disappears from pi's manifest without its source changing." - }, - "hooks/gsd-cursor-subagent-stop.js": { - "reason": "#3023: pi no longer emits this file under hooks/ (its reserved directory); the installer now stages the same unchanged bytes under gsd-hooks/gsd-cursor-subagent-stop.js for the pi runtime, so this path disappears from pi's manifest without its source changing." - }, - "hooks/gsd-ensure-canonical-path.js": { - "reason": "#3023: pi no longer emits this file under hooks/ (its reserved directory); the installer now stages the same unchanged bytes under gsd-hooks/gsd-ensure-canonical-path.js for the pi runtime, so this path disappears from pi's manifest without its source changing." - }, - "hooks/gsd-graphify-update.sh": { - "reason": "#3023: pi no longer emits this file under hooks/ (its reserved directory); the installer now stages the same unchanged bytes under gsd-hooks/gsd-graphify-update.sh for the pi runtime, so this path disappears from pi's manifest without its source changing." - }, - "hooks/gsd-phase-boundary.sh": { - "reason": "#3023: pi no longer emits this file under hooks/ (its reserved directory); the installer now stages the same unchanged bytes under gsd-hooks/gsd-phase-boundary.sh for the pi runtime, so this path disappears from pi's manifest without its source changing." - }, - "hooks/gsd-prompt-guard.js": { - "reason": "#3023: pi no longer emits this file under hooks/ (its reserved directory); the installer now stages the same unchanged bytes under gsd-hooks/gsd-prompt-guard.js for the pi runtime, so this path disappears from pi's manifest without its source changing." - }, - "hooks/gsd-read-guard.js": { - "reason": "#3023: pi no longer emits this file under hooks/ (its reserved directory); the installer now stages the same unchanged bytes under gsd-hooks/gsd-read-guard.js for the pi runtime, so this path disappears from pi's manifest without its source changing." - }, - "hooks/gsd-session-state.sh": { - "reason": "#3023: pi no longer emits this file under hooks/ (its reserved directory); the installer now stages the same unchanged bytes under gsd-hooks/gsd-session-state.sh for the pi runtime, so this path disappears from pi's manifest without its source changing." - }, - "hooks/gsd-statusline.js": { - "reason": "#3023: pi no longer emits this file under hooks/ (its reserved directory); the installer now stages the same unchanged bytes under gsd-hooks/gsd-statusline.js for the pi runtime, so this path disappears from pi's manifest without its source changing." - }, - "hooks/gsd-update-banner.js": { - "reason": "#3023: pi no longer emits this file under hooks/ (its reserved directory); the installer now stages the same unchanged bytes under gsd-hooks/gsd-update-banner.js for the pi runtime, so this path disappears from pi's manifest without its source changing." - }, - "hooks/gsd-validate-commit.sh": { - "reason": "#3023: pi no longer emits this file under hooks/ (its reserved directory); the installer now stages the same unchanged bytes under gsd-hooks/gsd-validate-commit.sh for the pi runtime, so this path disappears from pi's manifest without its source changing." - }, - "hooks/gsd-windsurf-pre-command.js": { - "reason": "#3023: pi no longer emits this file under hooks/ (its reserved directory); the installer now stages the same unchanged bytes under gsd-hooks/gsd-windsurf-pre-command.js for the pi runtime, so this path disappears from pi's manifest without its source changing." - }, - "hooks/gsd-windsurf-pre-write.js": { - "reason": "#3023: pi no longer emits this file under hooks/ (its reserved directory); the installer now stages the same unchanged bytes under gsd-hooks/gsd-windsurf-pre-write.js for the pi runtime, so this path disappears from pi's manifest without its source changing." - }, - "hooks/gsd-workflow-guard.js": { - "reason": "#3023: pi no longer emits this file under hooks/ (its reserved directory); the installer now stages the same unchanged bytes under gsd-hooks/gsd-workflow-guard.js for the pi runtime, so this path disappears from pi's manifest without its source changing." - }, - "hooks/gsd-worktree-path-guard.js": { - "reason": "#3023: pi no longer emits this file under hooks/ (its reserved directory); the installer now stages the same unchanged bytes under gsd-hooks/gsd-worktree-path-guard.js for the pi runtime, so this path disappears from pi's manifest without its source changing." - }, - "hooks/gsd-write-guard.js": { - "reason": "#3023: pi no longer emits this file under hooks/ (its reserved directory); the installer now stages the same unchanged bytes under gsd-hooks/gsd-write-guard.js for the pi runtime, so this path disappears from pi's manifest without its source changing." - }, - "hooks/lib/cursor-workspace.js": { - "reason": "#3023: pi no longer emits this file under hooks/ (its reserved directory); the installer now stages the same unchanged bytes under gsd-hooks/lib/cursor-workspace.js for the pi runtime, so this path disappears from pi's manifest without its source changing." - }, - "hooks/lib/git-cmd.js": { - "reason": "#3023: pi no longer emits this file under hooks/ (its reserved directory); the installer now stages the same unchanged bytes under gsd-hooks/lib/git-cmd.js for the pi runtime, so this path disappears from pi's manifest without its source changing." - }, - "hooks/lib/gsd-graphify-rebuild.sh": { - "reason": "#3023: pi no longer emits this file under hooks/ (its reserved directory); the installer now stages the same unchanged bytes under gsd-hooks/lib/gsd-graphify-rebuild.sh for the pi runtime, so this path disappears from pi's manifest without its source changing." - }, - "hooks/lib/isolation-sentinel.js": { - "reason": "#3023: pi no longer emits this file under hooks/ (its reserved directory); the installer now stages the same unchanged bytes under gsd-hooks/lib/isolation-sentinel.js for the pi runtime, so this path disappears from pi's manifest without its source changing." - }, - "hooks/managed-hooks-registry.cjs": { - "reason": "#3023: pi no longer emits this file under hooks/ (its reserved directory); the installer now stages the same unchanged bytes under gsd-hooks/managed-hooks-registry.cjs for the pi runtime, so this path disappears from pi's manifest without its source changing." - } - } -} diff --git a/tests/emitted-drift-acks/3025-sync-skills-refuse-cross-runtime.json b/tests/emitted-drift-acks/3025-sync-skills-refuse-cross-runtime.json deleted file mode 100644 index 4bf231c9e..000000000 --- a/tests/emitted-drift-acks/3025-sync-skills-refuse-cross-runtime.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "version": 1, - "paths": { - "sync-skills.md": { - "reason": "#3025 (option b, user decision): sync-skills refused cross-runtime sync. Skill content/layout is runtime-specific (the installer applies per-runtime converters/adapter headers/brand swaps/layout rules) and grok/gemini alias another runtime's skills root, so a verbatim cross-runtime cp -r corrupted destination skills and could damage a runtime the user never named; #3024 (closed) un-masked it. This PR adds: (1) a Step 1 runtime-id SHAPE validation guard (^[a-z0-9][a-z0-9-]*$) that rejects shell-metachar --from/--to values before any echo/heredoc/[[ ]] interpolation, hardening a command-substitution injection vector in the new (and pre-existing) error messages; (2) a FUNCTIONAL Step 1 cross-runtime refuse guard (a bash loop over TO_RUNTIMES that exits 1 with an installer pointer when any destination != FROM_RUNTIME), placed before Step 2 resolution and Step 5's rm -rf/cp -r so cross-runtime can never reach the copy; identity sync (--from == --to) stays a no-op. Growth is: the shape-validation guard (is_runtime_id + loop), the cross-runtime refuse guard loop + cat< -> tag rename (15 chars/block, tag-token-only delta)." - } -} \ No newline at end of file diff --git a/tests/emitted-drift-acks/3151-skill-effort-cache-invalidation.json b/tests/emitted-drift-acks/3151-skill-effort-cache-invalidation.json deleted file mode 100644 index f0ad1a888..000000000 --- a/tests/emitted-drift-acks/3151-skill-effort-cache-invalidation.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "version": 1, - "paths": { - "skills/gsd-ns-manage/skills/stats/SKILL.md": { - "reason": "#3151: convertClaudeCommandToClaudeSkill no longer emits effort: into skill frontmatter (a static effort value invalidates the caller's prompt cache at both scope boundaries). The converter (src/runtime-artifact-conversion.cts + duplicate bin/install.js) changed, moving these emitted skill hashes without a source-command change; this ack covers that converter-driven emission drop across all runtime layouts (per ADR-2719/#3039 precedent)." - }, - "skills/gsd-ns-workflow/skills/autonomous/SKILL.md": { - "reason": "#3151: convertClaudeCommandToClaudeSkill no longer emits effort: into skill frontmatter (a static effort value invalidates the caller's prompt cache at both scope boundaries). The converter (src/runtime-artifact-conversion.cts + duplicate bin/install.js) changed, moving these emitted skill hashes without a source-command change; this ack covers that converter-driven emission drop across all runtime layouts (per ADR-2719/#3039 precedent)." - }, - "skills/gsd-ns-workflow/skills/execute-phase/SKILL.md": { - "reason": "#3151: convertClaudeCommandToClaudeSkill no longer emits effort: into skill frontmatter (a static effort value invalidates the caller's prompt cache at both scope boundaries). The converter (src/runtime-artifact-conversion.cts + duplicate bin/install.js) changed, moving these emitted skill hashes without a source-command change; this ack covers that converter-driven emission drop across all runtime layouts (per ADR-2719/#3039 precedent)." - }, - "skills/gsd-ns-workflow/skills/next/SKILL.md": { - "reason": "#3151: convertClaudeCommandToClaudeSkill no longer emits effort: into skill frontmatter (a static effort value invalidates the caller's prompt cache at both scope boundaries). The converter (src/runtime-artifact-conversion.cts + duplicate bin/install.js) changed, moving these emitted skill hashes without a source-command change; this ack covers that converter-driven emission drop across all runtime layouts (per ADR-2719/#3039 precedent)." - }, - "skills/gsd-ns-workflow/skills/plan-phase/SKILL.md": { - "reason": "#3151: convertClaudeCommandToClaudeSkill no longer emits effort: into skill frontmatter (a static effort value invalidates the caller's prompt cache at both scope boundaries). The converter (src/runtime-artifact-conversion.cts + duplicate bin/install.js) changed, moving these emitted skill hashes without a source-command change; this ack covers that converter-driven emission drop across all runtime layouts (per ADR-2719/#3039 precedent)." - }, - "skills/gsd-ns-workflow/skills/progress/SKILL.md": { - "reason": "#3151: convertClaudeCommandToClaudeSkill no longer emits effort: into skill frontmatter (a static effort value invalidates the caller's prompt cache at both scope boundaries). The converter (src/runtime-artifact-conversion.cts + duplicate bin/install.js) changed, moving these emitted skill hashes without a source-command change; this ack covers that converter-driven emission drop across all runtime layouts (per ADR-2719/#3039 precedent)." - }, - "skills/gsd/gsd-ns-manage/skills/stats/SKILL.md": { - "reason": "#3151: convertClaudeCommandToClaudeSkill no longer emits effort: into skill frontmatter (a static effort value invalidates the caller's prompt cache at both scope boundaries). The converter (src/runtime-artifact-conversion.cts + duplicate bin/install.js) changed, moving these emitted skill hashes without a source-command change; this ack covers that converter-driven emission drop across all runtime layouts (per ADR-2719/#3039 precedent)." - }, - "skills/gsd/gsd-ns-workflow/skills/autonomous/SKILL.md": { - "reason": "#3151: convertClaudeCommandToClaudeSkill no longer emits effort: into skill frontmatter (a static effort value invalidates the caller's prompt cache at both scope boundaries). The converter (src/runtime-artifact-conversion.cts + duplicate bin/install.js) changed, moving these emitted skill hashes without a source-command change; this ack covers that converter-driven emission drop across all runtime layouts (per ADR-2719/#3039 precedent)." - }, - "skills/gsd/gsd-ns-workflow/skills/execute-phase/SKILL.md": { - "reason": "#3151: convertClaudeCommandToClaudeSkill no longer emits effort: into skill frontmatter (a static effort value invalidates the caller's prompt cache at both scope boundaries). The converter (src/runtime-artifact-conversion.cts + duplicate bin/install.js) changed, moving these emitted skill hashes without a source-command change; this ack covers that converter-driven emission drop across all runtime layouts (per ADR-2719/#3039 precedent)." - }, - "skills/gsd/gsd-ns-workflow/skills/next/SKILL.md": { - "reason": "#3151: convertClaudeCommandToClaudeSkill no longer emits effort: into skill frontmatter (a static effort value invalidates the caller's prompt cache at both scope boundaries). The converter (src/runtime-artifact-conversion.cts + duplicate bin/install.js) changed, moving these emitted skill hashes without a source-command change; this ack covers that converter-driven emission drop across all runtime layouts (per ADR-2719/#3039 precedent)." - }, - "skills/gsd/gsd-ns-workflow/skills/plan-phase/SKILL.md": { - "reason": "#3151: convertClaudeCommandToClaudeSkill no longer emits effort: into skill frontmatter (a static effort value invalidates the caller's prompt cache at both scope boundaries). The converter (src/runtime-artifact-conversion.cts + duplicate bin/install.js) changed, moving these emitted skill hashes without a source-command change; this ack covers that converter-driven emission drop across all runtime layouts (per ADR-2719/#3039 precedent)." - }, - "skills/gsd/gsd-ns-workflow/skills/progress/SKILL.md": { - "reason": "#3151: convertClaudeCommandToClaudeSkill no longer emits effort: into skill frontmatter (a static effort value invalidates the caller's prompt cache at both scope boundaries). The converter (src/runtime-artifact-conversion.cts + duplicate bin/install.js) changed, moving these emitted skill hashes without a source-command change; this ack covers that converter-driven emission drop across all runtime layouts (per ADR-2719/#3039 precedent)." - } - } -} \ No newline at end of file diff --git a/tests/emitted-drift-acks/3184-next-route0-window-scope.json b/tests/emitted-drift-acks/3184-next-route0-window-scope.json deleted file mode 100644 index 96b0fb7ed..000000000 --- a/tests/emitted-drift-acks/3184-next-route0-window-scope.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "version": 1, - "paths": { - "next.md": "#3184 (epic #3180 Phase 2): Route 0 (resume_incomplete_phase) grows by one branch that reads the new `scope` field `roadmap analyze` emits. Before this, Route 0 treated a well-formed `{phases: []}` as a clean scan and fell through to routing as though the incomplete-phase invariant (#160) had been checked and passed — the silent disarm #3165 reports, where a milestone window truncated by a closed-milestone heading yields an empty phase list with exit 0 and no error. The added `elif [ \"$ROADMAP_SCOPE\" != \"complete\" ]` arm routes that case into the SAME warn-and-fall-through branch the existing `roadmap.analyze failed` arm already uses, so a non-answer is no longer indistinguishable from a genuinely empty scan. The growth is the new arm's three warning lines, its explanatory comment, the `ROADMAP_SCOPE` assignment, and one success-criteria bullet. Adding this to the workflow rather than only to the CLI is deliberate: a spec review of this phase found that emitting `scope` without a consumer left #3165's actual symptom reproducing, so the field would have been a diagnostic nobody reads." - } -} diff --git a/tests/emitted-drift-acks/3186-mvp-phase-disk-strict.json b/tests/emitted-drift-acks/3186-mvp-phase-disk-strict.json deleted file mode 100644 index 6c2ccbd88..000000000 --- a/tests/emitted-drift-acks/3186-mvp-phase-disk-strict.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "version": 1, - "paths": { - "mvp-phase.md": "#3186 (epic #3180 Phase 4, ADR-3180 §7.4, disk-strict per #2957 maintainer decision): removes the `PHASE_COMPLETE` ROADMAP-checkbox override this workflow ORed into its completion check (`if [[ \"$DISK_STATUS\" == \"complete\" || \"$PHASE_COMPLETE\" == \"true\" ]]`) — a ticked checkbox is a human annotation with no machine authority, and the prompt layer was one of the (previously undiscovered) sites still trusting it. The `PHASE_COMPLETE=$(...jq -r '.roadmap_complete // false')` assignment line is deleted outright; `DISK_STATUS` alone (already routed through the canonical `isPhaseComplete` owner via `roadmap.analyze`) now decides completion. Growth is a 4-line explanatory comment documenting why the OR was removed and that this is the same predicate the read and write paths share — the deleted assignment line and the shortened `if` condition are smaller than what they replace, so the net +233 bytes is entirely the comment, not new control flow. See ADR-3180 §7.4 Decision 4(d) (the prompt layer is in scope) and the design doc `.gsd/phase/refactor-3186-phase-completion-predicate/40-design.md` row `mvp-phase.md:49`." - } -} diff --git a/tests/emitted-drift-acks/3190-code-review-fix-auto-rewrite-review.json b/tests/emitted-drift-acks/3190-code-review-fix-auto-rewrite-review.json deleted file mode 100644 index ad46f5428..000000000 --- a/tests/emitted-drift-acks/3190-code-review-fix-auto-rewrite-review.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "version": 1, - "paths": { - "code-review-fix.md": "#3190: the --auto convergence loop rewrote REVIEW.md every re-review iteration but never recommitted it (committed REVIEW.md stayed at iteration 1, contradicting the committed REVIEW-FIX.md), and the commit-fix step's two inline frontmatter validators exported REVIEW_PATH into a node -e body that reads process.env.FIX_REPORT_PATH — so HAS_STATUS/FIX_FRONTMATTER were always empty and REVIEW-FIX.md was never committed at all. The fix renames the exported env var to FIX_REPORT_PATH at both validator sites, stages the converged REVIEW.md alongside REVIEW-FIX.md in the single --auto docs commit (guarded on AUTO_MODE so non-auto single-pass runs are untouched), and removes the spent .iterN.md backups on successful convergence (retained on degradation for post-mortem). Growth is the deployed contract for those three coupled fixes plus inline rationale comments; the regression test is tests/code-review-fix-pipeline-regression.test.cjs. — #3423 append (epic #1891 F8): also grew with the -> tag rename (15 chars/block, tag-token-only delta; no prose changed)." - } -} diff --git a/tests/emitted-drift-acks/3210-autonomous-precondition-gate.json b/tests/emitted-drift-acks/3210-autonomous-precondition-gate.json deleted file mode 100644 index 357ee24b3..000000000 --- a/tests/emitted-drift-acks/3210-autonomous-precondition-gate.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "version": 1, - "paths": { - "autonomous.md": "#3210: handle_blocker's 'Fix and retry' path gained a retry ceiling — a per-phase-step RETRY_COUNT that escalates to a terminal needs_human halt (STATE.md '## Needs Human' row + unmet items surfaced) after 3 failed fix attempts, instead of re-presenting the options indefinitely. This is the loop-fixing behavior the issue title names; +704 bytes is the ceiling rule itself, not incidental prose. (The matching gsd-executor.md growth is acknowledged in the 2943 fragment and the execute-phase.md growth in the 3370 fragment, which already name those paths.)" - } -} diff --git a/tests/emitted-drift-acks/3262-editphase-milestone-scope-guard.json b/tests/emitted-drift-acks/3262-editphase-milestone-scope-guard.json deleted file mode 100644 index 74ce0cc88..000000000 --- a/tests/emitted-drift-acks/3262-editphase-milestone-scope-guard.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "version": 1, - "paths": { - "edit-phase.md": "#3262: adds the mechanical milestone-scope guard the issue says this workflow lacks. `write_updated_phase` now captures `gsd_run query roadmap milestone-scope` (the new read-only window-identity probe: scope + declared phase ids) BEFORE the in-place section splice, re-derives it AFTER, and on any `scope`/`phases` difference restores the original section text and exits with an explicit ERROR — the same block-before-write shape as the workflow's existing `validate_depends_on` gate, which is untouched. One success-criteria checkbox and one anti-pattern line accompany it. Net growth is the two small probe invocations plus the rollback/error contract (~1.1 KB over the 12,927-byte base, well inside the DEFAULT 40 KiB hard cap); no prose was extracted or moved. Companion code changes (src/roadmap-parser.cts, src/roadmap.cts, src/phase.cts, src/command-aliases.cts, src/roadmap-command-router.cts) are self-attributing." - } -} diff --git a/tests/emitted-drift-acks/3309-health-docs-generated.json b/tests/emitted-drift-acks/3309-health-docs-generated.json deleted file mode 100644 index 6d46e536e..000000000 --- a/tests/emitted-drift-acks/3309-health-docs-generated.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "version": 1, - "paths": { - "health.md": { - "reason": "#3309 (epic #3180 Phase 11, ADR-3180): the ``/`` tables and their footnote are GENERATED by `scripts/gen-health-docs.cjs` from the live `RULES` table, replacing a hand-maintained 16-code table — growth here is a deliberate, expected consequence of that generator doing its job as `RULES` grows, not accidental bloat. #2873 (epic #2866 Phase 4a) adds health rule W028 (\"A GSD-owned install scope shadows another on this machine\"), growing `RULES` from 31 to 32 entries and the generated `` table from 34 to 35 rows (an incremental 84-byte growth on top of #3309's original generation). #3586 (epic #2292 Phase 2) adds health rule W029 (\"`.planning/` is gitignored but still tracked by git\"), growing `RULES` from 32 to 33 entries and the generated `` table from 35 to 36 rows (a further incremental 141-byte growth). Regeneration is verified deterministic via `node scripts/gen-health-docs.cjs --check` (wired into `npm run lint:generated-sync`). NOTE for the next code added: this file is amended IN PLACE rather than joined by a sibling fragment — `health.md` is generated and grows on every new rule, and two ack sources naming one path is a hard error (`mergeAckSources`), so a per-PR fragment is not an option here. The base-side copy is spent, so the amendment is what makes the ack live for your diff." - } - } -} diff --git a/tests/emitted-drift-acks/3357-verification-resolver.json b/tests/emitted-drift-acks/3357-verification-resolver.json deleted file mode 100644 index 2436adb80..000000000 --- a/tests/emitted-drift-acks/3357-verification-resolver.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "version": 1, - "paths": { - "verify-work.md": { - "reason": "#3357: the `03-VERIFICATION.md` staleness check now resolves the phase's own report via `gsd_run query verification.resolve-file \"$PHASE_DIR\" --raw` instead of `ls \"${PHASE_DIR}\"/*-VERIFICATION.md | head -1`, routing through the same shared resolver seam as transition.md so both call sites agree on which file is canonical. Growth is +13 bytes (38,983 -> 38,996), the delta between the old ls/head-1 pipeline and the gsd_run call. #3559: the generic gate-dispatch arm gained the in-context validation contract for manifest-supplied check values. gates[].check is not one of the four executable surfaces the install consent prompt discloses, so a capability consented to as declarative-only could still reach a shell through an unvalidated check.query interpolated into a command substitution. The reference (references/loop-hook-dispatch.md) stated this requirement for step -> ref.command and omitted it for gate; that omission is the root cause and is now closed at the reference plus all four dispatch sites. Growth is one validation paragraph per site. 38996 -> 39107 bytes (+111). \u2014 #3606 append: verify:post consumer gains generic step dispatch (deferral to loop-hook-dispatch.md) before the secure-phase specialization, so ui/security/mempalace/nyquist steps registered at verify:post actually run instead of being filtered out by the one-skill narrow." - } - } -} diff --git a/tests/emitted-drift-acks/3370-execute-phase-gate-conflation.json b/tests/emitted-drift-acks/3370-execute-phase-gate-conflation.json deleted file mode 100644 index 2a3c7122e..000000000 --- a/tests/emitted-drift-acks/3370-execute-phase-gate-conflation.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "version": 1, - "paths": { - "execute-plan.md": "#3370: the Pattern A dispatch prompt spec gained the gate-semantics clause (gate=\"blocking\" (the default) is auto-approvable in auto-mode per the executor's own checkpoint protocol, gate=\"blocking-human\" always surfaces to a human; add no instruction overriding that protocol), closing the identically-shaped dispatch-time gap on the single-plan path named in the issue. Growth ~248 bytes (38913 -> 39161, still under the DEFAULT 40 KiB ceiling). Supersedes the spent #2652 fragment (merged into next), which also named execute-plan.md and would otherwise double-ack the same path. #3659 appends --mode \"$ISOLATION\" to the #2649 pre-dispatch base-check and corrects the baseRef restore-advice to the orchestrator/harness split (+154B; the harness does not read baseRef, #48). Deliberate growth. — #3299 append: the tracer feedback gate's interactive branch, which read `Interactive: STOP -> return a checkpoint:human-verify` and keyed on auto-mode alone, now branches on HUMAN_VERIFY_MODE — under the documented `end-of-phase` default an automated-only tracer `` is re-run and continues to expansion with no checkpoint. Growth is 796 bytes (39315 -> 40111, still under the DEFAULT_CAP of 40960 LF bytes, 849 bytes of headroom; the earlier 39161 -> 39957 / 1003 figures were measured before the 2026-08-22 merge of next and are superseded): that branch plus a `HUMAN_VERIFY_MODE=` read alongside the existing RUNTIME/USE_WORKTREES reads in parse_segments. The read carries an explicit `--default end-of-phase` because `workflow.human_verify_mode` is absent from SCHEMA_DEFAULTS (src/config.cts): a bare config-get exits non-zero with `Key not found` on any project whose config.json predates #3309, which is every pre-existing project and the reporter's exact config. The three carve-outs (``, `gate=\"blocking-human\"`, `mid-flight`) are stated inline here rather than delegated to gsd-core/references/dispatch-isolation-gate.md, because this file is the inline dispatch path used for step-by-step / non-Claude-Code execution, where agents/gsd-executor.md is never loaded. This entry carries #3370's own reason forward verbatim above rather than replacing it — that growth is in the base and still needs its account. The blocking-human STOP is evaluated BEFORE the auto-mode branch here, matching golden rule 6 in checkpoints.md — the earlier ordering let an autonomous run continue past a blocking-human tracer. — #3028 append: the checkpoint_protocol step's one-line display spec described the old drawn box (`CHECKPOINT: [Type]` box … `YOUR ACTION: [signal]`). Under #3028 a checkpoint is a `### CHECKPOINT: [Type]` heading, a `---` break and a bolded action prompt, so the spec now reads `### CHECKPOINT: [Type]` heading → Progress {X}/{Y} → Task name → type-specific content → `---` → `**YOUR ACTION: [signal]**`. Growth is 22 bytes (40111 -> 40133, still under the DEFAULT_CAP of 40960 LF bytes, 827 bytes of headroom) — the naming of the two new Markdown tokens and the heading marker. Appended here rather than filed as a new fragment because a growth ack keys on the BARE FILENAME and #3370 already declares `execute-plan.md`; two sources naming one path is a hard duplicate-key error, so this follows the same supersede-by-append route #3370 itself took for the spent #2652 fragment. Every other file this change touches SHRANK, which needs no acknowledgment." - } -} diff --git a/tests/emitted-drift-acks/3409-unreachable-guard-arms.json b/tests/emitted-drift-acks/3409-unreachable-guard-arms.json deleted file mode 100644 index f8493dc96..000000000 --- a/tests/emitted-drift-acks/3409-unreachable-guard-arms.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "version": 1, - "paths": { - "gsd-phase-researcher.md": "#3409: guarded `cat \"$phase_dir\"/*-CONTEXT.md` against nullglob wiping the pattern to zero operands when no CONTEXT.md exists — a bare `cat` with no operands blocks reading stdin (hangs the agent) instead of the `2>/dev/null` guard ever firing, since a stalled read is not a failing exit. Now checks `${_CTX[0]}` is a real path before invoking cat. Growth is the array-guard idiom itself (+43 bytes). — #2951 append (merged into this fragment because two ack sources may never name the same path; the #3409 entry above is at the base and therefore spent, and a second fragment naming this path is a hard failure in scripts/lint-emitted-drift-ack.cjs): +2352 bytes, 44250 -> 46602 (LARGE tier, cap 49152, 2550 bytes headroom). Adds the absent-evidence provenance rule to the claim-provenance section — the third member of the family alongside the package-name and in-repo-value rules. A compatibility claim resting on MISSING metadata (no python_requires, no engines field, no per-version classifier, no changelog entry, no matching support-matrix row) no longer earns [VERIFIED] however authoritative the source consulted; the only route from an absence to [VERIFIED] is a positive falsification attempt with its failing output pasted, and everything short of that is [ASSUMED], which the file already routes to \"needs user confirmation before becoming a locked decision\". Growth is inline prose in the agent body, deliberately NOT relocated into an eagerly @-imported reference, which ADR-1610 Decision 4 names as gaming the size proxy.", - "gsd-verifier.md": "#3409: same nullglob-hang fix as gsd-phase-researcher.md, applied to `cat \"$PHASE_DIR\"/*-VERIFICATION.md` in Step 0 — an absent VERIFICATION.md previously left a zero-operand `cat` blocking on stdin instead of falling through to first-verification mode. Growth is the array-guard idiom (+49 bytes). — #3206 append (merged into this fragment because two ack sources may never name the same path): +52 bytes, 49098 -> 49150 (2 under the LARGE cap). The growth is the literal fix for the term 5b used undefined: the compressed explicit-evidence definition inlined at 5b (+34 net on the rewritten line — the trailing honest-verifier cite there is dropped as superseded by the inline definition; honest-verifier.md stays cited at 5c) plus gsd-core/ path-prefix repairs on the two 404ing bare references/ cites at 5c (honest-verifier.md) and the MVP-mode section (verify-mvp-mode.md) (+9 each). Lazy extraction remains untakeable in this change: the large extractable blocks are content-pinned by tests that read the agent file directly (tests/verifier-behavior-unverified.test.cjs, tests/verification-overrides.test.cjs), so extraction is its own coordinated change.", - "complete-milestone.md": "#3409: guarded `cat .planning/phases/*-*/*-SUMMARY.md` — with `shopt -s nullglob` active in this block's preamble (#2962), zero matching phase summaries collapses the glob to nothing and a bare `cat` blocks reading stdin rather than producing empty output, wedging the milestone-completion review. Growth is the array-existence-check idiom (+73 bytes, two glob segments makes this longer than the single-glob sites). — #2142 append (merged into this fragment because two ack sources may never name the same path): +1605 bytes, 40498 -> 42103. The `archive_milestone` step now documents the opt-in `--archive-quick` quick-task archival flag (default OFF, deliberately NOT symmetrical with phase archival's default-ON posture), folds the AskUserQuestion decision for it into the SAME `milestone.complete` invocation (avoiding a redundant second call), and states the known bucket-all provenance limit.", - "discuss-phase-assumptions.md": "#3409: replaced the unreachable `AUTO_MODE=$(gsd_run query check auto-mode --pick active 2>/dev/null || echo \"false\")` — `||` never fires because the query exits 0 with empty stdout when the field is absent, not a failure, so AUTO_MODE silently ended up empty rather than \"false\" — with a two-line capture-then-default (`AUTO_MODE=\"${AUTO_MODE:-false}\"`) that actually reaches the fallback. Growth is the extra default-assignment line (+19 bytes).", - "plan-phase.md": "#3409: three sites. `AUTO_CHAIN` and `PHASE_REQ_IDS` get the same unreachable-`||`-fallback fix as discuss-phase-assumptions.md (empty-but-successful `gsd_run query` output never triggered `|| echo`, now uses `${VAR:-default}`); `PRIOR_SUMMARIES` additionally swapped `gsd_run query phases.list --pick summaries_total` for `--type summaries --pick count` since the old pick key produced the same unreachable-fallback failure mode for the walking-skeleton check. Net growth across the three sites is +39 bytes. — #3576 append: bare `references/.md` cites repaired to the canonical `gsd-core/references/.md` form (+36 bytes, 4 cite(s) × 9). Dead-pointer fix; no content change. #3559: the generic gate-dispatch arm gained the in-context validation contract for manifest-supplied check values. gates[].check is not one of the four executable surfaces the install consent prompt discloses, so a capability consented to as declarative-only could still reach a shell through an unvalidated check.query interpolated into a command substitution. The reference (references/loop-hook-dispatch.md) stated this requirement for step -> ref.command and omitted it for gate; that omission is the root cause and is now closed at the reference plus all four dispatch sites. Growth is one validation paragraph per site. 90516 -> 90627 bytes (+111). — #2401 append (merged into this fragment because two ack sources may never name the same path): plan-phase.md now dispatches the deterministic `gsd_run check verify-command-paths` probe before spawning the plan-check pass and interpolates its result into the verification prompt as {VERIFY_PATHS} inside a new block, plus a block carrying prior_verify_commands into planning context so the planner can reuse a proven path instead of re-deriving one. 90627 -> 92807 bytes (+2180). Deliberate runtime-loaded workflow text for the new feature, not converter drift. — #3606 append: plan:pre gains generic contribution dispatch (deferral covering every `into` target, not just planner); plan:post gains generic step + contribution dispatch and its skip condition no longer keys on the gap-analysis gate’s absence (that skip silently dropped every other registered hook at the point). #3645 append (merged here because two ack sources may never name the same path): plan-phase.md also grew ~+1040 for the block in the gsd-planner spawn prompt — files_modified/must_haves/artifact/action/PATTERNS-inherited paths must be git-tracked source (git ls-files verification, tracked-origin fallback, re-verify inherited paths).", - "session-report.md": "#3409: guarded `ls -la .planning/reports/SESSION_REPORT*.md 2>/dev/null || echo \"No previous reports\"` — with nullglob active, zero prior reports collapses the pattern to nothing and `ls -la` with no operands lists the current directory (a successful exit, wrong output) instead of failing into the `|| echo` fallback, so the report-existence check silently printed a directory listing. Replaced with an array-existence check that only lists when a real report file is present. Growth is the guard idiom (+58 bytes).", - "transition.md": "#3409: guarded `cat .planning/phases/XX-current/*-SUMMARY.md` — same nullglob-hang defect as complete-milestone.md's phase-summary read: zero summaries left a bare `cat` blocking on stdin instead of proceeding with no summary content during PROJECT.md evolution. Growth is the array-existence-check idiom (+73 bytes)." - } -} diff --git a/tests/emitted-drift-acks/3448-debug-autoresume-next-action.json b/tests/emitted-drift-acks/3448-debug-autoresume-next-action.json deleted file mode 100644 index aba0b1223..000000000 --- a/tests/emitted-drift-acks/3448-debug-autoresume-next-action.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "version": 1, - "paths": { - "debug.md": "#3448: both auto-resume paragraphs (Section 1c continue-path return handling and Section 4's non-terminal branch — textual duplicates, both fixed for symmetry) grow to append the resume parameters to the re-spawn: `resume: true`, `resume_status`, `resume_next_action`, both sourced from `.planning/debug/{slug}.md`, plus the explicit not-parameter-identical-to-a-cold-start rationale. Before this, the respawn carried identical session_params, so the recorded next action and the prior-checkpoints-already-answered disposition never reached the respawned manager — the auto-resume stall behind the no-progress guard. Growth is +963 bytes (21,315 -> 22,278), far under the 40,960 DEFAULT tier cap. The anti-loop guard paragraphs (next_action-only heuristic + absolute 3-resume hard cap) are untouched.", - "gsd-debug-session-manager.md": "#3448: documents the new `resume`/`resume_status`/`resume_next_action` trio, and the Step 2 gsd-debugger prompt template gains a conditional block carrying the recorded next action and the prior-checkpoints-already-answered disposition, DATA_START/DATA_END-bounded per the existing Step 3d checkpoint-response shape — the seam that lets the manager consume what the orchestrator now forwards. Growth is +822 bytes (19,751 -> 20,573). Terminal paths, CHECKPOINT REACHED handling, and the commit step are untouched." - } -} diff --git a/tests/emitted-drift-acks/3479-mempalace-default-true-gates.json b/tests/emitted-drift-acks/3479-mempalace-default-true-gates.json deleted file mode 100644 index af172bff2..000000000 --- a/tests/emitted-drift-acks/3479-mempalace-default-true-gates.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "version": 1, - "paths": { - "gsd-mempalace-curator.md": { - "reason": "#3479: the diary_journal and mirror_kg task gates changed from positive presence ('when is true') to disabled-only-on-explicit-false ('unless !== false — registry default is true, an absent key means enabled'), matching the registry-declared defaults. The ~124-byte growth is the two inline absence-semantics clarifications. #3565 appends the standard MUST-Read gate clause (~200 B): ship.md emits a required-reading block to an agent whose instructions never referenced the gate — the F8 defect one layer up, caught by the new read-tag arm." - } - } -} diff --git a/tests/emitted-drift-acks/3544-home-expansion-tilde-restore.json b/tests/emitted-drift-acks/3544-home-expansion-tilde-restore.json deleted file mode 100644 index e831f38ef..000000000 --- a/tests/emitted-drift-acks/3544-home-expansion-tilde-restore.json +++ /dev/null @@ -1,287 +0,0 @@ -{ - "paths": { - "gsd-core/references/context-budget.md": { - "reason": "#3544: the gsd-core/ spec-tree emit path now runs restoreClaudeGlobalAtRefTilde over @-file-reference lines, rewriting every @$HOME/gsd-core/... include to the @~/gsd-core/... tilde form Claude Code actually expands (verified: @$HOME references silently resolved to nothing on a live global install, 54 includes across 22 files). This is the same correction #3133 already applies to skill/command bodies via _applyRuntimeRewrites's 'claude' case; this fragment covers the gsd-core/ workflows, references, and templates tree, which never had it. bin/install.js's bare-form '~/.claude'/'$HOME/.claude' substitution regexes were also tightened from a trailing \\b to (?![\\w-]), so a --config-dir extending '.claude' (e.g. '.claude-work') no longer gets its own emitted prefix re-mangled; that fix does not itself change any byte in this fragment's paths under the default global install measured here, but is part of the same commit and is named for completeness." - }, - "gsd-core/references/edge-probe.md": { - "reason": "#3544: the gsd-core/ spec-tree emit path now runs restoreClaudeGlobalAtRefTilde over @-file-reference lines, rewriting every @$HOME/gsd-core/... include to the @~/gsd-core/... tilde form Claude Code actually expands (verified: @$HOME references silently resolved to nothing on a live global install, 54 includes across 22 files). This is the same correction #3133 already applies to skill/command bodies via _applyRuntimeRewrites's 'claude' case; this fragment covers the gsd-core/ workflows, references, and templates tree, which never had it. bin/install.js's bare-form '~/.claude'/'$HOME/.claude' substitution regexes were also tightened from a trailing \\b to (?![\\w-]), so a --config-dir extending '.claude' (e.g. '.claude-work') no longer gets its own emitted prefix re-mangled; that fix does not itself change any byte in this fragment's paths under the default global install measured here, but is part of the same commit and is named for completeness." - }, - "gsd-core/references/honest-verifier.md": { - "reason": "#3544: the gsd-core/ spec-tree emit path now runs restoreClaudeGlobalAtRefTilde over @-file-reference lines, rewriting every @$HOME/gsd-core/... include to the @~/gsd-core/... tilde form Claude Code actually expands (verified: @$HOME references silently resolved to nothing on a live global install, 54 includes across 22 files). This is the same correction #3133 already applies to skill/command bodies via _applyRuntimeRewrites's 'claude' case; this fragment covers the gsd-core/ workflows, references, and templates tree, which never had it. bin/install.js's bare-form '~/.claude'/'$HOME/.claude' substitution regexes were also tightened from a trailing \\b to (?![\\w-]), so a --config-dir extending '.claude' (e.g. '.claude-work') no longer gets its own emitted prefix re-mangled; that fix does not itself change any byte in this fragment's paths under the default global install measured here, but is part of the same commit and is named for completeness." - }, - "gsd-core/references/model-profile-resolution.md": { - "reason": "#3544: the gsd-core/ spec-tree emit path now runs restoreClaudeGlobalAtRefTilde over @-file-reference lines, rewriting every @$HOME/gsd-core/... include to the @~/gsd-core/... tilde form Claude Code actually expands (verified: @$HOME references silently resolved to nothing on a live global install, 54 includes across 22 files). This is the same correction #3133 already applies to skill/command bodies via _applyRuntimeRewrites's 'claude' case; this fragment covers the gsd-core/ workflows, references, and templates tree, which never had it. bin/install.js's bare-form '~/.claude'/'$HOME/.claude' substitution regexes were also tightened from a trailing \\b to (?![\\w-]), so a --config-dir extending '.claude' (e.g. '.claude-work') no longer gets its own emitted prefix re-mangled; that fix does not itself change any byte in this fragment's paths under the default global install measured here, but is part of the same commit and is named for completeness." - }, - "gsd-core/references/planner-guidance.md": { - "reason": "#3544: the gsd-core/ spec-tree emit path now runs restoreClaudeGlobalAtRefTilde over @-file-reference lines, rewriting every @$HOME/gsd-core/... include to the @~/gsd-core/... tilde form Claude Code actually expands (verified: @$HOME references silently resolved to nothing on a live global install, 54 includes across 22 files). This is the same correction #3133 already applies to skill/command bodies via _applyRuntimeRewrites's 'claude' case; this fragment covers the gsd-core/ workflows, references, and templates tree, which never had it. bin/install.js's bare-form '~/.claude'/'$HOME/.claude' substitution regexes were also tightened from a trailing \\b to (?![\\w-]), so a --config-dir extending '.claude' (e.g. '.claude-work') no longer gets its own emitted prefix re-mangled; that fix does not itself change any byte in this fragment's paths under the default global install measured here, but is part of the same commit and is named for completeness." - }, - "gsd-core/references/planner-mvp-mode.md": { - "reason": "#3544: the gsd-core/ spec-tree emit path now runs restoreClaudeGlobalAtRefTilde over @-file-reference lines, rewriting every @$HOME/gsd-core/... include to the @~/gsd-core/... tilde form Claude Code actually expands (verified: @$HOME references silently resolved to nothing on a live global install, 54 includes across 22 files). This is the same correction #3133 already applies to skill/command bodies via _applyRuntimeRewrites's 'claude' case; this fragment covers the gsd-core/ workflows, references, and templates tree, which never had it. bin/install.js's bare-form '~/.claude'/'$HOME/.claude' substitution regexes were also tightened from a trailing \\b to (?![\\w-]), so a --config-dir extending '.claude' (e.g. '.claude-work') no longer gets its own emitted prefix re-mangled; that fix does not itself change any byte in this fragment's paths under the default global install measured here, but is part of the same commit and is named for completeness." - }, - "gsd-core/references/prohibition-probe.md": { - "reason": "#3544: the gsd-core/ spec-tree emit path now runs restoreClaudeGlobalAtRefTilde over @-file-reference lines, rewriting every @$HOME/gsd-core/... include to the @~/gsd-core/... tilde form Claude Code actually expands (verified: @$HOME references silently resolved to nothing on a live global install, 54 includes across 22 files). This is the same correction #3133 already applies to skill/command bodies via _applyRuntimeRewrites's 'claude' case; this fragment covers the gsd-core/ workflows, references, and templates tree, which never had it. bin/install.js's bare-form '~/.claude'/'$HOME/.claude' substitution regexes were also tightened from a trailing \\b to (?![\\w-]), so a --config-dir extending '.claude' (e.g. '.claude-work') no longer gets its own emitted prefix re-mangled; that fix does not itself change any byte in this fragment's paths under the default global install measured here, but is part of the same commit and is named for completeness." - }, - "gsd-core/references/thinking-models-planning.md": { - "reason": "#3544: the gsd-core/ spec-tree emit path now runs restoreClaudeGlobalAtRefTilde over @-file-reference lines, rewriting every @$HOME/gsd-core/... include to the @~/gsd-core/... tilde form Claude Code actually expands (verified: @$HOME references silently resolved to nothing on a live global install, 54 includes across 22 files). This is the same correction #3133 already applies to skill/command bodies via _applyRuntimeRewrites's 'claude' case; this fragment covers the gsd-core/ workflows, references, and templates tree, which never had it. bin/install.js's bare-form '~/.claude'/'$HOME/.claude' substitution regexes were also tightened from a trailing \\b to (?![\\w-]), so a --config-dir extending '.claude' (e.g. '.claude-work') no longer gets its own emitted prefix re-mangled; that fix does not itself change any byte in this fragment's paths under the default global install measured here, but is part of the same commit and is named for completeness." - }, - "gsd-core/references/verification-patterns.md": { - "reason": "#3544: the gsd-core/ spec-tree emit path now runs restoreClaudeGlobalAtRefTilde over @-file-reference lines, rewriting every @$HOME/gsd-core/... include to the @~/gsd-core/... tilde form Claude Code actually expands (verified: @$HOME references silently resolved to nothing on a live global install, 54 includes across 22 files). This is the same correction #3133 already applies to skill/command bodies via _applyRuntimeRewrites's 'claude' case; this fragment covers the gsd-core/ workflows, references, and templates tree, which never had it. bin/install.js's bare-form '~/.claude'/'$HOME/.claude' substitution regexes were also tightened from a trailing \\b to (?![\\w-]), so a --config-dir extending '.claude' (e.g. '.claude-work') no longer gets its own emitted prefix re-mangled; that fix does not itself change any byte in this fragment's paths under the default global install measured here, but is part of the same commit and is named for completeness." - }, - "gsd-core/references/verifier-phase-gates.md": { - "reason": "#3544: the gsd-core/ spec-tree emit path now runs restoreClaudeGlobalAtRefTilde over @-file-reference lines, rewriting every @$HOME/gsd-core/... include to the @~/gsd-core/... tilde form Claude Code actually expands (verified: @$HOME references silently resolved to nothing on a live global install, 54 includes across 22 files). This is the same correction #3133 already applies to skill/command bodies via _applyRuntimeRewrites's 'claude' case; this fragment covers the gsd-core/ workflows, references, and templates tree, which never had it. bin/install.js's bare-form '~/.claude'/'$HOME/.claude' substitution regexes were also tightened from a trailing \\b to (?![\\w-]), so a --config-dir extending '.claude' (e.g. '.claude-work') no longer gets its own emitted prefix re-mangled; that fix does not itself change any byte in this fragment's paths under the default global install measured here, but is part of the same commit and is named for completeness." - }, - "gsd-core/templates/codebase/structure.md": { - "reason": "#3544: the gsd-core/ spec-tree emit path now runs restoreClaudeGlobalAtRefTilde over @-file-reference lines, rewriting every @$HOME/gsd-core/... include to the @~/gsd-core/... tilde form Claude Code actually expands (verified: @$HOME references silently resolved to nothing on a live global install, 54 includes across 22 files). This is the same correction #3133 already applies to skill/command bodies via _applyRuntimeRewrites's 'claude' case; this fragment covers the gsd-core/ workflows, references, and templates tree, which never had it. bin/install.js's bare-form '~/.claude'/'$HOME/.claude' substitution regexes were also tightened from a trailing \\b to (?![\\w-]), so a --config-dir extending '.claude' (e.g. '.claude-work') no longer gets its own emitted prefix re-mangled; that fix does not itself change any byte in this fragment's paths under the default global install measured here, but is part of the same commit and is named for completeness." - }, - "gsd-core/templates/phase-prompt.md": { - "reason": "#3544: the gsd-core/ spec-tree emit path now runs restoreClaudeGlobalAtRefTilde over @-file-reference lines, rewriting every @$HOME/gsd-core/... include to the @~/gsd-core/... tilde form Claude Code actually expands (verified: @$HOME references silently resolved to nothing on a live global install, 54 includes across 22 files). This is the same correction #3133 already applies to skill/command bodies via _applyRuntimeRewrites's 'claude' case; this fragment covers the gsd-core/ workflows, references, and templates tree, which never had it. bin/install.js's bare-form '~/.claude'/'$HOME/.claude' substitution regexes were also tightened from a trailing \\b to (?![\\w-]), so a --config-dir extending '.claude' (e.g. '.claude-work') no longer gets its own emitted prefix re-mangled; that fix does not itself change any byte in this fragment's paths under the default global install measured here, but is part of the same commit and is named for completeness." - }, - "gsd-core/workflows/ai-integration-phase.md": { - "reason": "#3544: the gsd-core/ spec-tree emit path now runs restoreClaudeGlobalAtRefTilde over @-file-reference lines, rewriting every @$HOME/gsd-core/... include to the @~/gsd-core/... tilde form Claude Code actually expands (verified: @$HOME references silently resolved to nothing on a live global install, 54 includes across 22 files). This is the same correction #3133 already applies to skill/command bodies via _applyRuntimeRewrites's 'claude' case; this fragment covers the gsd-core/ workflows, references, and templates tree, which never had it. bin/install.js's bare-form '~/.claude'/'$HOME/.claude' substitution regexes were also tightened from a trailing \\b to (?![\\w-]), so a --config-dir extending '.claude' (e.g. '.claude-work') no longer gets its own emitted prefix re-mangled; that fix does not itself change any byte in this fragment's paths under the default global install measured here, but is part of the same commit and is named for completeness." - }, - "gsd-core/workflows/autonomous.md": { - "reason": "#3544: the gsd-core/ spec-tree emit path now runs restoreClaudeGlobalAtRefTilde over @-file-reference lines, rewriting every @$HOME/gsd-core/... include to the @~/gsd-core/... tilde form Claude Code actually expands (verified: @$HOME references silently resolved to nothing on a live global install, 54 includes across 22 files). This is the same correction #3133 already applies to skill/command bodies via _applyRuntimeRewrites's 'claude' case; this fragment covers the gsd-core/ workflows, references, and templates tree, which never had it. bin/install.js's bare-form '~/.claude'/'$HOME/.claude' substitution regexes were also tightened from a trailing \\b to (?![\\w-]), so a --config-dir extending '.claude' (e.g. '.claude-work') no longer gets its own emitted prefix re-mangled; that fix does not itself change any byte in this fragment's paths under the default global install measured here, but is part of the same commit and is named for completeness." - }, - "gsd-core/workflows/discuss-phase.md": { - "reason": "#3544: the gsd-core/ spec-tree emit path now runs restoreClaudeGlobalAtRefTilde over @-file-reference lines, rewriting every @$HOME/gsd-core/... include to the @~/gsd-core/... tilde form Claude Code actually expands (verified: @$HOME references silently resolved to nothing on a live global install, 54 includes across 22 files). This is the same correction #3133 already applies to skill/command bodies via _applyRuntimeRewrites's 'claude' case; this fragment covers the gsd-core/ workflows, references, and templates tree, which never had it. bin/install.js's bare-form '~/.claude'/'$HOME/.claude' substitution regexes were also tightened from a trailing \\b to (?![\\w-]), so a --config-dir extending '.claude' (e.g. '.claude-work') no longer gets its own emitted prefix re-mangled; that fix does not itself change any byte in this fragment's paths under the default global install measured here, but is part of the same commit and is named for completeness." - }, - "gsd-core/workflows/discuss-phase/modes/power.md": { - "reason": "#3544: the gsd-core/ spec-tree emit path now runs restoreClaudeGlobalAtRefTilde over @-file-reference lines, rewriting every @$HOME/gsd-core/... include to the @~/gsd-core/... tilde form Claude Code actually expands (verified: @$HOME references silently resolved to nothing on a live global install, 54 includes across 22 files). This is the same correction #3133 already applies to skill/command bodies via _applyRuntimeRewrites's 'claude' case; this fragment covers the gsd-core/ workflows, references, and templates tree, which never had it. bin/install.js's bare-form '~/.claude'/'$HOME/.claude' substitution regexes were also tightened from a trailing \\b to (?![\\w-]), so a --config-dir extending '.claude' (e.g. '.claude-work') no longer gets its own emitted prefix re-mangled; that fix does not itself change any byte in this fragment's paths under the default global install measured here, but is part of the same commit and is named for completeness." - }, - "gsd-core/workflows/eval-review.md": { - "reason": "#3544: the gsd-core/ spec-tree emit path now runs restoreClaudeGlobalAtRefTilde over @-file-reference lines, rewriting every @$HOME/gsd-core/... include to the @~/gsd-core/... tilde form Claude Code actually expands (verified: @$HOME references silently resolved to nothing on a live global install, 54 includes across 22 files). This is the same correction #3133 already applies to skill/command bodies via _applyRuntimeRewrites's 'claude' case; this fragment covers the gsd-core/ workflows, references, and templates tree, which never had it. bin/install.js's bare-form '~/.claude'/'$HOME/.claude' substitution regexes were also tightened from a trailing \\b to (?![\\w-]), so a --config-dir extending '.claude' (e.g. '.claude-work') no longer gets its own emitted prefix re-mangled; that fix does not itself change any byte in this fragment's paths under the default global install measured here, but is part of the same commit and is named for completeness." - }, - "gsd-core/workflows/execute-phase.md": { - "reason": "#3544: the gsd-core/ spec-tree emit path now runs restoreClaudeGlobalAtRefTilde over @-file-reference lines, rewriting every @$HOME/gsd-core/... include to the @~/gsd-core/... tilde form Claude Code actually expands (verified: @$HOME references silently resolved to nothing on a live global install, 54 includes across 22 files). This is the same correction #3133 already applies to skill/command bodies via _applyRuntimeRewrites's 'claude' case; this fragment covers the gsd-core/ workflows, references, and templates tree, which never had it. bin/install.js's bare-form '~/.claude'/'$HOME/.claude' substitution regexes were also tightened from a trailing \\b to (?![\\w-]), so a --config-dir extending '.claude' (e.g. '.claude-work') no longer gets its own emitted prefix re-mangled; that fix does not itself change any byte in this fragment's paths under the default global install measured here, but is part of the same commit and is named for completeness." - }, - "gsd-core/workflows/execute-plan.md": { - "reason": "#3544: the gsd-core/ spec-tree emit path now runs restoreClaudeGlobalAtRefTilde over @-file-reference lines, rewriting every @$HOME/gsd-core/... include to the @~/gsd-core/... tilde form Claude Code actually expands (verified: @$HOME references silently resolved to nothing on a live global install, 54 includes across 22 files). This is the same correction #3133 already applies to skill/command bodies via _applyRuntimeRewrites's 'claude' case; this fragment covers the gsd-core/ workflows, references, and templates tree, which never had it. bin/install.js's bare-form '~/.claude'/'$HOME/.claude' substitution regexes were also tightened from a trailing \\b to (?![\\w-]), so a --config-dir extending '.claude' (e.g. '.claude-work') no longer gets its own emitted prefix re-mangled; that fix does not itself change any byte in this fragment's paths under the default global install measured here, but is part of the same commit and is named for completeness." - }, - "gsd-core/workflows/explore.md": { - "reason": "#3544: the gsd-core/ spec-tree emit path now runs restoreClaudeGlobalAtRefTilde over @-file-reference lines, rewriting every @$HOME/gsd-core/... include to the @~/gsd-core/... tilde form Claude Code actually expands (verified: @$HOME references silently resolved to nothing on a live global install, 54 includes across 22 files). This is the same correction #3133 already applies to skill/command bodies via _applyRuntimeRewrites's 'claude' case; this fragment covers the gsd-core/ workflows, references, and templates tree, which never had it. bin/install.js's bare-form '~/.claude'/'$HOME/.claude' substitution regexes were also tightened from a trailing \\b to (?![\\w-]), so a --config-dir extending '.claude' (e.g. '.claude-work') no longer gets its own emitted prefix re-mangled; that fix does not itself change any byte in this fragment's paths under the default global install measured here, but is part of the same commit and is named for completeness." - }, - "gsd-core/workflows/mvp-phase.md": { - "reason": "#3544: the gsd-core/ spec-tree emit path now runs restoreClaudeGlobalAtRefTilde over @-file-reference lines, rewriting every @$HOME/gsd-core/... include to the @~/gsd-core/... tilde form Claude Code actually expands (verified: @$HOME references silently resolved to nothing on a live global install, 54 includes across 22 files). This is the same correction #3133 already applies to skill/command bodies via _applyRuntimeRewrites's 'claude' case; this fragment covers the gsd-core/ workflows, references, and templates tree, which never had it. bin/install.js's bare-form '~/.claude'/'$HOME/.claude' substitution regexes were also tightened from a trailing \\b to (?![\\w-]), so a --config-dir extending '.claude' (e.g. '.claude-work') no longer gets its own emitted prefix re-mangled; that fix does not itself change any byte in this fragment's paths under the default global install measured here, but is part of the same commit and is named for completeness." - }, - "gsd-core/workflows/onboard.md": { - "reason": "#3544: the gsd-core/ spec-tree emit path now runs restoreClaudeGlobalAtRefTilde over @-file-reference lines, rewriting every @$HOME/gsd-core/... include to the @~/gsd-core/... tilde form Claude Code actually expands (verified: @$HOME references silently resolved to nothing on a live global install, 54 includes across 22 files). This is the same correction #3133 already applies to skill/command bodies via _applyRuntimeRewrites's 'claude' case; this fragment covers the gsd-core/ workflows, references, and templates tree, which never had it. bin/install.js's bare-form '~/.claude'/'$HOME/.claude' substitution regexes were also tightened from a trailing \\b to (?![\\w-]), so a --config-dir extending '.claude' (e.g. '.claude-work') no longer gets its own emitted prefix re-mangled; that fix does not itself change any byte in this fragment's paths under the default global install measured here, but is part of the same commit and is named for completeness." - }, - "gsd-core/workflows/plan-phase.md": { - "reason": "#3544: the gsd-core/ spec-tree emit path now runs restoreClaudeGlobalAtRefTilde over @-file-reference lines, rewriting every @$HOME/gsd-core/... include to the @~/gsd-core/... tilde form Claude Code actually expands (verified: @$HOME references silently resolved to nothing on a live global install, 54 includes across 22 files). This is the same correction #3133 already applies to skill/command bodies via _applyRuntimeRewrites's 'claude' case; this fragment covers the gsd-core/ workflows, references, and templates tree, which never had it. bin/install.js's bare-form '~/.claude'/'$HOME/.claude' substitution regexes were also tightened from a trailing \\b to (?![\\w-]), so a --config-dir extending '.claude' (e.g. '.claude-work') no longer gets its own emitted prefix re-mangled; that fix does not itself change any byte in this fragment's paths under the default global install measured here, but is part of the same commit and is named for completeness." - }, - "gsd-core/workflows/plan-review-convergence.md": { - "reason": "#3544: the gsd-core/ spec-tree emit path now runs restoreClaudeGlobalAtRefTilde over @-file-reference lines, rewriting every @$HOME/gsd-core/... include to the @~/gsd-core/... tilde form Claude Code actually expands (verified: @$HOME references silently resolved to nothing on a live global install, 54 includes across 22 files). This is the same correction #3133 already applies to skill/command bodies via _applyRuntimeRewrites's 'claude' case; this fragment covers the gsd-core/ workflows, references, and templates tree, which never had it. bin/install.js's bare-form '~/.claude'/'$HOME/.claude' substitution regexes were also tightened from a trailing \\b to (?![\\w-]), so a --config-dir extending '.claude' (e.g. '.claude-work') no longer gets its own emitted prefix re-mangled; that fix does not itself change any byte in this fragment's paths under the default global install measured here, but is part of the same commit and is named for completeness." - }, - "gsd-core/workflows/profile-user.md": { - "reason": "#3544: the gsd-core/ spec-tree emit path now runs restoreClaudeGlobalAtRefTilde over @-file-reference lines, rewriting every @$HOME/gsd-core/... include to the @~/gsd-core/... tilde form Claude Code actually expands (verified: @$HOME references silently resolved to nothing on a live global install, 54 includes across 22 files). This is the same correction #3133 already applies to skill/command bodies via _applyRuntimeRewrites's 'claude' case; this fragment covers the gsd-core/ workflows, references, and templates tree, which never had it. bin/install.js's bare-form '~/.claude'/'$HOME/.claude' substitution regexes were also tightened from a trailing \\b to (?![\\w-]), so a --config-dir extending '.claude' (e.g. '.claude-work') no longer gets its own emitted prefix re-mangled; that fix does not itself change any byte in this fragment's paths under the default global install measured here, but is part of the same commit and is named for completeness." - }, - "gsd-core/workflows/resume-project.md": { - "reason": "#3544: the gsd-core/ spec-tree emit path now runs restoreClaudeGlobalAtRefTilde over @-file-reference lines, rewriting every @$HOME/gsd-core/... include to the @~/gsd-core/... tilde form Claude Code actually expands (verified: @$HOME references silently resolved to nothing on a live global install, 54 includes across 22 files). This is the same correction #3133 already applies to skill/command bodies via _applyRuntimeRewrites's 'claude' case; this fragment covers the gsd-core/ workflows, references, and templates tree, which never had it. bin/install.js's bare-form '~/.claude'/'$HOME/.claude' substitution regexes were also tightened from a trailing \\b to (?![\\w-]), so a --config-dir extending '.claude' (e.g. '.claude-work') no longer gets its own emitted prefix re-mangled; that fix does not itself change any byte in this fragment's paths under the default global install measured here, but is part of the same commit and is named for completeness." - }, - "gsd-core/workflows/secure-phase.md": { - "reason": "#3544: the gsd-core/ spec-tree emit path now runs restoreClaudeGlobalAtRefTilde over @-file-reference lines, rewriting every @$HOME/gsd-core/... include to the @~/gsd-core/... tilde form Claude Code actually expands (verified: @$HOME references silently resolved to nothing on a live global install, 54 includes across 22 files). This is the same correction #3133 already applies to skill/command bodies via _applyRuntimeRewrites's 'claude' case; this fragment covers the gsd-core/ workflows, references, and templates tree, which never had it. bin/install.js's bare-form '~/.claude'/'$HOME/.claude' substitution regexes were also tightened from a trailing \\b to (?![\\w-]), so a --config-dir extending '.claude' (e.g. '.claude-work') no longer gets its own emitted prefix re-mangled; that fix does not itself change any byte in this fragment's paths under the default global install measured here, but is part of the same commit and is named for completeness." - }, - "gsd-core/workflows/sketch.md": { - "reason": "#3544: the gsd-core/ spec-tree emit path now runs restoreClaudeGlobalAtRefTilde over @-file-reference lines, rewriting every @$HOME/gsd-core/... include to the @~/gsd-core/... tilde form Claude Code actually expands (verified: @$HOME references silently resolved to nothing on a live global install, 54 includes across 22 files). This is the same correction #3133 already applies to skill/command bodies via _applyRuntimeRewrites's 'claude' case; this fragment covers the gsd-core/ workflows, references, and templates tree, which never had it. bin/install.js's bare-form '~/.claude'/'$HOME/.claude' substitution regexes were also tightened from a trailing \\b to (?![\\w-]), so a --config-dir extending '.claude' (e.g. '.claude-work') no longer gets its own emitted prefix re-mangled; that fix does not itself change any byte in this fragment's paths under the default global install measured here, but is part of the same commit and is named for completeness." - }, - "gsd-core/workflows/spec-phase.md": { - "reason": "#3544: the gsd-core/ spec-tree emit path now runs restoreClaudeGlobalAtRefTilde over @-file-reference lines, rewriting every @$HOME/gsd-core/... include to the @~/gsd-core/... tilde form Claude Code actually expands (verified: @$HOME references silently resolved to nothing on a live global install, 54 includes across 22 files). This is the same correction #3133 already applies to skill/command bodies via _applyRuntimeRewrites's 'claude' case; this fragment covers the gsd-core/ workflows, references, and templates tree, which never had it. bin/install.js's bare-form '~/.claude'/'$HOME/.claude' substitution regexes were also tightened from a trailing \\b to (?![\\w-]), so a --config-dir extending '.claude' (e.g. '.claude-work') no longer gets its own emitted prefix re-mangled; that fix does not itself change any byte in this fragment's paths under the default global install measured here, but is part of the same commit and is named for completeness." - }, - "gsd-core/workflows/transition.md": { - "reason": "#3544: the gsd-core/ spec-tree emit path now runs restoreClaudeGlobalAtRefTilde over @-file-reference lines, rewriting every @$HOME/gsd-core/... include to the @~/gsd-core/... tilde form Claude Code actually expands (verified: @$HOME references silently resolved to nothing on a live global install, 54 includes across 22 files). This is the same correction #3133 already applies to skill/command bodies via _applyRuntimeRewrites's 'claude' case; this fragment covers the gsd-core/ workflows, references, and templates tree, which never had it. bin/install.js's bare-form '~/.claude'/'$HOME/.claude' substitution regexes were also tightened from a trailing \\b to (?![\\w-]), so a --config-dir extending '.claude' (e.g. '.claude-work') no longer gets its own emitted prefix re-mangled; that fix does not itself change any byte in this fragment's paths under the default global install measured here, but is part of the same commit and is named for completeness." - }, - "gsd-core/workflows/ui-phase.md": { - "reason": "#3544: the gsd-core/ spec-tree emit path now runs restoreClaudeGlobalAtRefTilde over @-file-reference lines, rewriting every @$HOME/gsd-core/... include to the @~/gsd-core/... tilde form Claude Code actually expands (verified: @$HOME references silently resolved to nothing on a live global install, 54 includes across 22 files). This is the same correction #3133 already applies to skill/command bodies via _applyRuntimeRewrites's 'claude' case; this fragment covers the gsd-core/ workflows, references, and templates tree, which never had it. bin/install.js's bare-form '~/.claude'/'$HOME/.claude' substitution regexes were also tightened from a trailing \\b to (?![\\w-]), so a --config-dir extending '.claude' (e.g. '.claude-work') no longer gets its own emitted prefix re-mangled; that fix does not itself change any byte in this fragment's paths under the default global install measured here, but is part of the same commit and is named for completeness." - }, - "gsd-core/workflows/ui-review.md": { - "reason": "#3544: the gsd-core/ spec-tree emit path now runs restoreClaudeGlobalAtRefTilde over @-file-reference lines, rewriting every @$HOME/gsd-core/... include to the @~/gsd-core/... tilde form Claude Code actually expands (verified: @$HOME references silently resolved to nothing on a live global install, 54 includes across 22 files). This is the same correction #3133 already applies to skill/command bodies via _applyRuntimeRewrites's 'claude' case; this fragment covers the gsd-core/ workflows, references, and templates tree, which never had it. bin/install.js's bare-form '~/.claude'/'$HOME/.claude' substitution regexes were also tightened from a trailing \\b to (?![\\w-]), so a --config-dir extending '.claude' (e.g. '.claude-work') no longer gets its own emitted prefix re-mangled; that fix does not itself change any byte in this fragment's paths under the default global install measured here, but is part of the same commit and is named for completeness." - }, - "gsd-core/workflows/undo.md": { - "reason": "#3544: the gsd-core/ spec-tree emit path now runs restoreClaudeGlobalAtRefTilde over @-file-reference lines, rewriting every @$HOME/gsd-core/... include to the @~/gsd-core/... tilde form Claude Code actually expands (verified: @$HOME references silently resolved to nothing on a live global install, 54 includes across 22 files). This is the same correction #3133 already applies to skill/command bodies via _applyRuntimeRewrites's 'claude' case; this fragment covers the gsd-core/ workflows, references, and templates tree, which never had it. bin/install.js's bare-form '~/.claude'/'$HOME/.claude' substitution regexes were also tightened from a trailing \\b to (?![\\w-]), so a --config-dir extending '.claude' (e.g. '.claude-work') no longer gets its own emitted prefix re-mangled; that fix does not itself change any byte in this fragment's paths under the default global install measured here, but is part of the same commit and is named for completeness." - }, - "gsd-core/workflows/validate-phase.md": { - "reason": "#3544: the gsd-core/ spec-tree emit path now runs restoreClaudeGlobalAtRefTilde over @-file-reference lines, rewriting every @$HOME/gsd-core/... include to the @~/gsd-core/... tilde form Claude Code actually expands (verified: @$HOME references silently resolved to nothing on a live global install, 54 includes across 22 files). This is the same correction #3133 already applies to skill/command bodies via _applyRuntimeRewrites's 'claude' case; this fragment covers the gsd-core/ workflows, references, and templates tree, which never had it. bin/install.js's bare-form '~/.claude'/'$HOME/.claude' substitution regexes were also tightened from a trailing \\b to (?![\\w-]), so a --config-dir extending '.claude' (e.g. '.claude-work') no longer gets its own emitted prefix re-mangled; that fix does not itself change any byte in this fragment's paths under the default global install measured here, but is part of the same commit and is named for completeness." - }, - "gsd-core/workflows/verify-work.md": { - "reason": "#3544: the gsd-core/ spec-tree emit path now runs restoreClaudeGlobalAtRefTilde over @-file-reference lines, rewriting every @$HOME/gsd-core/... include to the @~/gsd-core/... tilde form Claude Code actually expands (verified: @$HOME references silently resolved to nothing on a live global install, 54 includes across 22 files). This is the same correction #3133 already applies to skill/command bodies via _applyRuntimeRewrites's 'claude' case; this fragment covers the gsd-core/ workflows, references, and templates tree, which never had it. bin/install.js's bare-form '~/.claude'/'$HOME/.claude' substitution regexes were also tightened from a trailing \\b to (?![\\w-]), so a --config-dir extending '.claude' (e.g. '.claude-work') no longer gets its own emitted prefix re-mangled; that fix does not itself change any byte in this fragment's paths under the default global install measured here, but is part of the same commit and is named for completeness." - }, - "gsd-core/workflows/verify-work/steps/mvp-uat-framing.md": { - "reason": "#3544: the gsd-core/ spec-tree emit path now runs restoreClaudeGlobalAtRefTilde over @-file-reference lines, rewriting every @$HOME/gsd-core/... include to the @~/gsd-core/... tilde form Claude Code actually expands (verified: @$HOME references silently resolved to nothing on a live global install, 54 includes across 22 files). This is the same correction #3133 already applies to skill/command bodies via _applyRuntimeRewrites's 'claude' case; this fragment covers the gsd-core/ workflows, references, and templates tree, which never had it. bin/install.js's bare-form '~/.claude'/'$HOME/.claude' substitution regexes were also tightened from a trailing \\b to (?![\\w-]), so a --config-dir extending '.claude' (e.g. '.claude-work') no longer gets its own emitted prefix re-mangled; that fix does not itself change any byte in this fragment's paths under the default global install measured here, but is part of the same commit and is named for completeness." - }, - "skills/gsd-add-tests/SKILL.md": { - "reason": "#3544: the gsd-core/ spec-tree tilde-restore fix (see the workflows/references/templates entries in this same fragment) also applies inside command bodies converted into Claude skills - this SKILL.md embeds a @$HOME/gsd-core/... include from its source command that now emits as @~/gsd-core/... so Claude Code actually expands it, matching the tilde form #3133 already normalized elsewhere in the skill body. The bin/install.js bare-form '~/.claude'/'$HOME/.claude' guard (\\b -> (?![\\w-])) shipped in the same commit but does not itself move this file's bytes under the default global install." - }, - "skills/gsd-ai-integration-phase/SKILL.md": { - "reason": "#3544: the gsd-core/ spec-tree tilde-restore fix (see the workflows/references/templates entries in this same fragment) also applies inside command bodies converted into Claude skills - this SKILL.md embeds a @$HOME/gsd-core/... include from its source command that now emits as @~/gsd-core/... so Claude Code actually expands it, matching the tilde form #3133 already normalized elsewhere in the skill body. The bin/install.js bare-form '~/.claude'/'$HOME/.claude' guard (\\b -> (?![\\w-])) shipped in the same commit but does not itself move this file's bytes under the default global install." - }, - "skills/gsd-audit-fix/SKILL.md": { - "reason": "#3544: the gsd-core/ spec-tree tilde-restore fix (see the workflows/references/templates entries in this same fragment) also applies inside command bodies converted into Claude skills - this SKILL.md embeds a @$HOME/gsd-core/... include from its source command that now emits as @~/gsd-core/... so Claude Code actually expands it, matching the tilde form #3133 already normalized elsewhere in the skill body. The bin/install.js bare-form '~/.claude'/'$HOME/.claude' guard (\\b -> (?![\\w-])) shipped in the same commit but does not itself move this file's bytes under the default global install." - }, - "skills/gsd-audit-milestone/SKILL.md": { - "reason": "#3544: the gsd-core/ spec-tree tilde-restore fix (see the workflows/references/templates entries in this same fragment) also applies inside command bodies converted into Claude skills - this SKILL.md embeds a @$HOME/gsd-core/... include from its source command that now emits as @~/gsd-core/... so Claude Code actually expands it, matching the tilde form #3133 already normalized elsewhere in the skill body. The bin/install.js bare-form '~/.claude'/'$HOME/.claude' guard (\\b -> (?![\\w-])) shipped in the same commit but does not itself move this file's bytes under the default global install." - }, - "skills/gsd-audit-uat/SKILL.md": { - "reason": "#3544: the gsd-core/ spec-tree tilde-restore fix (see the workflows/references/templates entries in this same fragment) also applies inside command bodies converted into Claude skills - this SKILL.md embeds a @$HOME/gsd-core/... include from its source command that now emits as @~/gsd-core/... so Claude Code actually expands it, matching the tilde form #3133 already normalized elsewhere in the skill body. The bin/install.js bare-form '~/.claude'/'$HOME/.claude' guard (\\b -> (?![\\w-])) shipped in the same commit but does not itself move this file's bytes under the default global install." - }, - "skills/gsd-autonomous/SKILL.md": { - "reason": "#3544: the gsd-core/ spec-tree tilde-restore fix (see the workflows/references/templates entries in this same fragment) also applies inside command bodies converted into Claude skills - this SKILL.md embeds a @$HOME/gsd-core/... include from its source command that now emits as @~/gsd-core/... so Claude Code actually expands it, matching the tilde form #3133 already normalized elsewhere in the skill body. The bin/install.js bare-form '~/.claude'/'$HOME/.claude' guard (\\b -> (?![\\w-])) shipped in the same commit but does not itself move this file's bytes under the default global install." - }, - "skills/gsd-capture/SKILL.md": { - "reason": "#3544: the gsd-core/ spec-tree tilde-restore fix (see the workflows/references/templates entries in this same fragment) also applies inside command bodies converted into Claude skills - this SKILL.md embeds a @$HOME/gsd-core/... include from its source command that now emits as @~/gsd-core/... so Claude Code actually expands it, matching the tilde form #3133 already normalized elsewhere in the skill body. The bin/install.js bare-form '~/.claude'/'$HOME/.claude' guard (\\b -> (?![\\w-])) shipped in the same commit but does not itself move this file's bytes under the default global install." - }, - "skills/gsd-cleanup/SKILL.md": { - "reason": "#3544: the gsd-core/ spec-tree tilde-restore fix (see the workflows/references/templates entries in this same fragment) also applies inside command bodies converted into Claude skills - this SKILL.md embeds a @$HOME/gsd-core/... include from its source command that now emits as @~/gsd-core/... so Claude Code actually expands it, matching the tilde form #3133 already normalized elsewhere in the skill body. The bin/install.js bare-form '~/.claude'/'$HOME/.claude' guard (\\b -> (?![\\w-])) shipped in the same commit but does not itself move this file's bytes under the default global install." - }, - "skills/gsd-code-review/SKILL.md": { - "reason": "#3544: the gsd-core/ spec-tree tilde-restore fix (see the workflows/references/templates entries in this same fragment) also applies inside command bodies converted into Claude skills - this SKILL.md embeds a @$HOME/gsd-core/... include from its source command that now emits as @~/gsd-core/... so Claude Code actually expands it, matching the tilde form #3133 already normalized elsewhere in the skill body. The bin/install.js bare-form '~/.claude'/'$HOME/.claude' guard (\\b -> (?![\\w-])) shipped in the same commit but does not itself move this file's bytes under the default global install." - }, - "skills/gsd-complete-milestone/SKILL.md": { - "reason": "#3544: the gsd-core/ spec-tree tilde-restore fix (see the workflows/references/templates entries in this same fragment) also applies inside command bodies converted into Claude skills - this SKILL.md embeds a @$HOME/gsd-core/... include from its source command that now emits as @~/gsd-core/... so Claude Code actually expands it, matching the tilde form #3133 already normalized elsewhere in the skill body. The bin/install.js bare-form '~/.claude'/'$HOME/.claude' guard (\\b -> (?![\\w-])) shipped in the same commit but does not itself move this file's bytes under the default global install." - }, - "skills/gsd-config/SKILL.md": { - "reason": "#3544: the gsd-core/ spec-tree tilde-restore fix (see the workflows/references/templates entries in this same fragment) also applies inside command bodies converted into Claude skills - this SKILL.md embeds a @$HOME/gsd-core/... include from its source command that now emits as @~/gsd-core/... so Claude Code actually expands it, matching the tilde form #3133 already normalized elsewhere in the skill body. The bin/install.js bare-form '~/.claude'/'$HOME/.claude' guard (\\b -> (?![\\w-])) shipped in the same commit but does not itself move this file's bytes under the default global install." - }, - "skills/gsd-debug/SKILL.md": { - "reason": "#3544: the gsd-core/ spec-tree tilde-restore fix (see the workflows/references/templates entries in this same fragment) also applies inside command bodies converted into Claude skills - this SKILL.md embeds a @$HOME/gsd-core/... include from its source command that now emits as @~/gsd-core/... so Claude Code actually expands it, matching the tilde form #3133 already normalized elsewhere in the skill body. The bin/install.js bare-form '~/.claude'/'$HOME/.claude' guard (\\b -> (?![\\w-])) shipped in the same commit but does not itself move this file's bytes under the default global install." - }, - "skills/gsd-docs-update/SKILL.md": { - "reason": "#3544: the gsd-core/ spec-tree tilde-restore fix (see the workflows/references/templates entries in this same fragment) also applies inside command bodies converted into Claude skills - this SKILL.md embeds a @$HOME/gsd-core/... include from its source command that now emits as @~/gsd-core/... so Claude Code actually expands it, matching the tilde form #3133 already normalized elsewhere in the skill body. The bin/install.js bare-form '~/.claude'/'$HOME/.claude' guard (\\b -> (?![\\w-])) shipped in the same commit but does not itself move this file's bytes under the default global install." - }, - "skills/gsd-eval-review/SKILL.md": { - "reason": "#3544: the gsd-core/ spec-tree tilde-restore fix (see the workflows/references/templates entries in this same fragment) also applies inside command bodies converted into Claude skills - this SKILL.md embeds a @$HOME/gsd-core/... include from its source command that now emits as @~/gsd-core/... so Claude Code actually expands it, matching the tilde form #3133 already normalized elsewhere in the skill body. The bin/install.js bare-form '~/.claude'/'$HOME/.claude' guard (\\b -> (?![\\w-])) shipped in the same commit but does not itself move this file's bytes under the default global install." - }, - "skills/gsd-execute-phase/SKILL.md": { - "reason": "#3544: the gsd-core/ spec-tree tilde-restore fix (see the workflows/references/templates entries in this same fragment) also applies inside command bodies converted into Claude skills - this SKILL.md embeds a @$HOME/gsd-core/... include from its source command that now emits as @~/gsd-core/... so Claude Code actually expands it, matching the tilde form #3133 already normalized elsewhere in the skill body. The bin/install.js bare-form '~/.claude'/'$HOME/.claude' guard (\\b -> (?![\\w-])) shipped in the same commit but does not itself move this file's bytes under the default global install." - }, - "skills/gsd-explore/SKILL.md": { - "reason": "#3544: the gsd-core/ spec-tree tilde-restore fix (see the workflows/references/templates entries in this same fragment) also applies inside command bodies converted into Claude skills - this SKILL.md embeds a @$HOME/gsd-core/... include from its source command that now emits as @~/gsd-core/... so Claude Code actually expands it, matching the tilde form #3133 already normalized elsewhere in the skill body. The bin/install.js bare-form '~/.claude'/'$HOME/.claude' guard (\\b -> (?![\\w-])) shipped in the same commit but does not itself move this file's bytes under the default global install." - }, - "skills/gsd-extract-learnings/SKILL.md": { - "reason": "#3544: the gsd-core/ spec-tree tilde-restore fix (see the workflows/references/templates entries in this same fragment) also applies inside command bodies converted into Claude skills - this SKILL.md embeds a @$HOME/gsd-core/... include from its source command that now emits as @~/gsd-core/... so Claude Code actually expands it, matching the tilde form #3133 already normalized elsewhere in the skill body. The bin/install.js bare-form '~/.claude'/'$HOME/.claude' guard (\\b -> (?![\\w-])) shipped in the same commit but does not itself move this file's bytes under the default global install." - }, - "skills/gsd-fast/SKILL.md": { - "reason": "#3544: the gsd-core/ spec-tree tilde-restore fix (see the workflows/references/templates entries in this same fragment) also applies inside command bodies converted into Claude skills - this SKILL.md embeds a @$HOME/gsd-core/... include from its source command that now emits as @~/gsd-core/... so Claude Code actually expands it, matching the tilde form #3133 already normalized elsewhere in the skill body. The bin/install.js bare-form '~/.claude'/'$HOME/.claude' guard (\\b -> (?![\\w-])) shipped in the same commit but does not itself move this file's bytes under the default global install." - }, - "skills/gsd-forensics/SKILL.md": { - "reason": "#3544: the gsd-core/ spec-tree tilde-restore fix (see the workflows/references/templates entries in this same fragment) also applies inside command bodies converted into Claude skills - this SKILL.md embeds a @$HOME/gsd-core/... include from its source command that now emits as @~/gsd-core/... so Claude Code actually expands it, matching the tilde form #3133 already normalized elsewhere in the skill body. The bin/install.js bare-form '~/.claude'/'$HOME/.claude' guard (\\b -> (?![\\w-])) shipped in the same commit but does not itself move this file's bytes under the default global install." - }, - "skills/gsd-health/SKILL.md": { - "reason": "#3544: the gsd-core/ spec-tree tilde-restore fix (see the workflows/references/templates entries in this same fragment) also applies inside command bodies converted into Claude skills - this SKILL.md embeds a @$HOME/gsd-core/... include from its source command that now emits as @~/gsd-core/... so Claude Code actually expands it, matching the tilde form #3133 already normalized elsewhere in the skill body. The bin/install.js bare-form '~/.claude'/'$HOME/.claude' guard (\\b -> (?![\\w-])) shipped in the same commit but does not itself move this file's bytes under the default global install." - }, - "skills/gsd-help/SKILL.md": { - "reason": "#3544: the gsd-core/ spec-tree tilde-restore fix (see the workflows/references/templates entries in this same fragment) also applies inside command bodies converted into Claude skills - this SKILL.md embeds a @$HOME/gsd-core/... include from its source command that now emits as @~/gsd-core/... so Claude Code actually expands it, matching the tilde form #3133 already normalized elsewhere in the skill body. The bin/install.js bare-form '~/.claude'/'$HOME/.claude' guard (\\b -> (?![\\w-])) shipped in the same commit but does not itself move this file's bytes under the default global install." - }, - "skills/gsd-import/SKILL.md": { - "reason": "#3544: the gsd-core/ spec-tree tilde-restore fix (see the workflows/references/templates entries in this same fragment) also applies inside command bodies converted into Claude skills - this SKILL.md embeds a @$HOME/gsd-core/... include from its source command that now emits as @~/gsd-core/... so Claude Code actually expands it, matching the tilde form #3133 already normalized elsewhere in the skill body. The bin/install.js bare-form '~/.claude'/'$HOME/.claude' guard (\\b -> (?![\\w-])) shipped in the same commit but does not itself move this file's bytes under the default global install." - }, - "skills/gsd-inbox/SKILL.md": { - "reason": "#3544: the gsd-core/ spec-tree tilde-restore fix (see the workflows/references/templates entries in this same fragment) also applies inside command bodies converted into Claude skills - this SKILL.md embeds a @$HOME/gsd-core/... include from its source command that now emits as @~/gsd-core/... so Claude Code actually expands it, matching the tilde form #3133 already normalized elsewhere in the skill body. The bin/install.js bare-form '~/.claude'/'$HOME/.claude' guard (\\b -> (?![\\w-])) shipped in the same commit but does not itself move this file's bytes under the default global install." - }, - "skills/gsd-ingest-docs/SKILL.md": { - "reason": "#3544: the gsd-core/ spec-tree tilde-restore fix (see the workflows/references/templates entries in this same fragment) also applies inside command bodies converted into Claude skills - this SKILL.md embeds a @$HOME/gsd-core/... include from its source command that now emits as @~/gsd-core/... so Claude Code actually expands it, matching the tilde form #3133 already normalized elsewhere in the skill body. The bin/install.js bare-form '~/.claude'/'$HOME/.claude' guard (\\b -> (?![\\w-])) shipped in the same commit but does not itself move this file's bytes under the default global install." - }, - "skills/gsd-manager/SKILL.md": { - "reason": "#3544: the gsd-core/ spec-tree tilde-restore fix (see the workflows/references/templates entries in this same fragment) also applies inside command bodies converted into Claude skills - this SKILL.md embeds a @$HOME/gsd-core/... include from its source command that now emits as @~/gsd-core/... so Claude Code actually expands it, matching the tilde form #3133 already normalized elsewhere in the skill body. The bin/install.js bare-form '~/.claude'/'$HOME/.claude' guard (\\b -> (?![\\w-])) shipped in the same commit but does not itself move this file's bytes under the default global install." - }, - "skills/gsd-map-codebase/SKILL.md": { - "reason": "#3544: the gsd-core/ spec-tree tilde-restore fix (see the workflows/references/templates entries in this same fragment) also applies inside command bodies converted into Claude skills - this SKILL.md embeds a @$HOME/gsd-core/... include from its source command that now emits as @~/gsd-core/... so Claude Code actually expands it, matching the tilde form #3133 already normalized elsewhere in the skill body. The bin/install.js bare-form '~/.claude'/'$HOME/.claude' guard (\\b -> (?![\\w-])) shipped in the same commit but does not itself move this file's bytes under the default global install." - }, - "skills/gsd-milestone-summary/SKILL.md": { - "reason": "#3544: the gsd-core/ spec-tree tilde-restore fix (see the workflows/references/templates entries in this same fragment) also applies inside command bodies converted into Claude skills - this SKILL.md embeds a @$HOME/gsd-core/... include from its source command that now emits as @~/gsd-core/... so Claude Code actually expands it, matching the tilde form #3133 already normalized elsewhere in the skill body. The bin/install.js bare-form '~/.claude'/'$HOME/.claude' guard (\\b -> (?![\\w-])) shipped in the same commit but does not itself move this file's bytes under the default global install." - }, - "skills/gsd-mvp-phase/SKILL.md": { - "reason": "#3544: the gsd-core/ spec-tree tilde-restore fix (see the workflows/references/templates entries in this same fragment) also applies inside command bodies converted into Claude skills - this SKILL.md embeds a @$HOME/gsd-core/... include from its source command that now emits as @~/gsd-core/... so Claude Code actually expands it, matching the tilde form #3133 already normalized elsewhere in the skill body. The bin/install.js bare-form '~/.claude'/'$HOME/.claude' guard (\\b -> (?![\\w-])) shipped in the same commit but does not itself move this file's bytes under the default global install." - }, - "skills/gsd-new-milestone/SKILL.md": { - "reason": "#3544: the gsd-core/ spec-tree tilde-restore fix (see the workflows/references/templates entries in this same fragment) also applies inside command bodies converted into Claude skills - this SKILL.md embeds a @$HOME/gsd-core/... include from its source command that now emits as @~/gsd-core/... so Claude Code actually expands it, matching the tilde form #3133 already normalized elsewhere in the skill body. The bin/install.js bare-form '~/.claude'/'$HOME/.claude' guard (\\b -> (?![\\w-])) shipped in the same commit but does not itself move this file's bytes under the default global install." - }, - "skills/gsd-new-project/SKILL.md": { - "reason": "#3544: the gsd-core/ spec-tree tilde-restore fix (see the workflows/references/templates entries in this same fragment) also applies inside command bodies converted into Claude skills - this SKILL.md embeds a @$HOME/gsd-core/... include from its source command that now emits as @~/gsd-core/... so Claude Code actually expands it, matching the tilde form #3133 already normalized elsewhere in the skill body. The bin/install.js bare-form '~/.claude'/'$HOME/.claude' guard (\\b -> (?![\\w-])) shipped in the same commit but does not itself move this file's bytes under the default global install." - }, - "skills/gsd-next/SKILL.md": { - "reason": "#3544: the gsd-core/ spec-tree tilde-restore fix (see the workflows/references/templates entries in this same fragment) also applies inside command bodies converted into Claude skills - this SKILL.md embeds a @$HOME/gsd-core/... include from its source command that now emits as @~/gsd-core/... so Claude Code actually expands it, matching the tilde form #3133 already normalized elsewhere in the skill body. The bin/install.js bare-form '~/.claude'/'$HOME/.claude' guard (\\b -> (?![\\w-])) shipped in the same commit but does not itself move this file's bytes under the default global install." - }, - "skills/gsd-onboard/SKILL.md": { - "reason": "#3544: the gsd-core/ spec-tree tilde-restore fix (see the workflows/references/templates entries in this same fragment) also applies inside command bodies converted into Claude skills - this SKILL.md embeds a @$HOME/gsd-core/... include from its source command that now emits as @~/gsd-core/... so Claude Code actually expands it, matching the tilde form #3133 already normalized elsewhere in the skill body. The bin/install.js bare-form '~/.claude'/'$HOME/.claude' guard (\\b -> (?![\\w-])) shipped in the same commit but does not itself move this file's bytes under the default global install." - }, - "skills/gsd-pause-work/SKILL.md": { - "reason": "#3544: the gsd-core/ spec-tree tilde-restore fix (see the workflows/references/templates entries in this same fragment) also applies inside command bodies converted into Claude skills - this SKILL.md embeds a @$HOME/gsd-core/... include from its source command that now emits as @~/gsd-core/... so Claude Code actually expands it, matching the tilde form #3133 already normalized elsewhere in the skill body. The bin/install.js bare-form '~/.claude'/'$HOME/.claude' guard (\\b -> (?![\\w-])) shipped in the same commit but does not itself move this file's bytes under the default global install." - }, - "skills/gsd-phase/SKILL.md": { - "reason": "#3544: the gsd-core/ spec-tree tilde-restore fix (see the workflows/references/templates entries in this same fragment) also applies inside command bodies converted into Claude skills - this SKILL.md embeds a @$HOME/gsd-core/... include from its source command that now emits as @~/gsd-core/... so Claude Code actually expands it, matching the tilde form #3133 already normalized elsewhere in the skill body. The bin/install.js bare-form '~/.claude'/'$HOME/.claude' guard (\\b -> (?![\\w-])) shipped in the same commit but does not itself move this file's bytes under the default global install." - }, - "skills/gsd-plan-phase/SKILL.md": { - "reason": "#3544: the gsd-core/ spec-tree tilde-restore fix (see the workflows/references/templates entries in this same fragment) also applies inside command bodies converted into Claude skills - this SKILL.md embeds a @$HOME/gsd-core/... include from its source command that now emits as @~/gsd-core/... so Claude Code actually expands it, matching the tilde form #3133 already normalized elsewhere in the skill body. The bin/install.js bare-form '~/.claude'/'$HOME/.claude' guard (\\b -> (?![\\w-])) shipped in the same commit but does not itself move this file's bytes under the default global install." - }, - "skills/gsd-plan-review-convergence/SKILL.md": { - "reason": "#3544: the gsd-core/ spec-tree tilde-restore fix (see the workflows/references/templates entries in this same fragment) also applies inside command bodies converted into Claude skills - this SKILL.md embeds a @$HOME/gsd-core/... include from its source command that now emits as @~/gsd-core/... so Claude Code actually expands it, matching the tilde form #3133 already normalized elsewhere in the skill body. The bin/install.js bare-form '~/.claude'/'$HOME/.claude' guard (\\b -> (?![\\w-])) shipped in the same commit but does not itself move this file's bytes under the default global install." - }, - "skills/gsd-pr-branch/SKILL.md": { - "reason": "#3544: the gsd-core/ spec-tree tilde-restore fix (see the workflows/references/templates entries in this same fragment) also applies inside command bodies converted into Claude skills - this SKILL.md embeds a @$HOME/gsd-core/... include from its source command that now emits as @~/gsd-core/... so Claude Code actually expands it, matching the tilde form #3133 already normalized elsewhere in the skill body. The bin/install.js bare-form '~/.claude'/'$HOME/.claude' guard (\\b -> (?![\\w-])) shipped in the same commit but does not itself move this file's bytes under the default global install." - }, - "skills/gsd-profile-user/SKILL.md": { - "reason": "#3544: the gsd-core/ spec-tree tilde-restore fix (see the workflows/references/templates entries in this same fragment) also applies inside command bodies converted into Claude skills - this SKILL.md embeds a @$HOME/gsd-core/... include from its source command that now emits as @~/gsd-core/... so Claude Code actually expands it, matching the tilde form #3133 already normalized elsewhere in the skill body. The bin/install.js bare-form '~/.claude'/'$HOME/.claude' guard (\\b -> (?![\\w-])) shipped in the same commit but does not itself move this file's bytes under the default global install." - }, - "skills/gsd-progress/SKILL.md": { - "reason": "#3544: the gsd-core/ spec-tree tilde-restore fix (see the workflows/references/templates entries in this same fragment) also applies inside command bodies converted into Claude skills - this SKILL.md embeds a @$HOME/gsd-core/... include from its source command that now emits as @~/gsd-core/... so Claude Code actually expands it, matching the tilde form #3133 already normalized elsewhere in the skill body. The bin/install.js bare-form '~/.claude'/'$HOME/.claude' guard (\\b -> (?![\\w-])) shipped in the same commit but does not itself move this file's bytes under the default global install." - }, - "skills/gsd-quick/SKILL.md": { - "reason": "#3544: the gsd-core/ spec-tree tilde-restore fix (see the workflows/references/templates entries in this same fragment) also applies inside command bodies converted into Claude skills - this SKILL.md embeds a @$HOME/gsd-core/... include from its source command that now emits as @~/gsd-core/... so Claude Code actually expands it, matching the tilde form #3133 already normalized elsewhere in the skill body. The bin/install.js bare-form '~/.claude'/'$HOME/.claude' guard (\\b -> (?![\\w-])) shipped in the same commit but does not itself move this file's bytes under the default global install." - }, - "skills/gsd-resume-work/SKILL.md": { - "reason": "#3544: the gsd-core/ spec-tree tilde-restore fix (see the workflows/references/templates entries in this same fragment) also applies inside command bodies converted into Claude skills - this SKILL.md embeds a @$HOME/gsd-core/... include from its source command that now emits as @~/gsd-core/... so Claude Code actually expands it, matching the tilde form #3133 already normalized elsewhere in the skill body. The bin/install.js bare-form '~/.claude'/'$HOME/.claude' guard (\\b -> (?![\\w-])) shipped in the same commit but does not itself move this file's bytes under the default global install." - }, - "skills/gsd-review/SKILL.md": { - "reason": "#3544: the gsd-core/ spec-tree tilde-restore fix (see the workflows/references/templates entries in this same fragment) also applies inside command bodies converted into Claude skills - this SKILL.md embeds a @$HOME/gsd-core/... include from its source command that now emits as @~/gsd-core/... so Claude Code actually expands it, matching the tilde form #3133 already normalized elsewhere in the skill body. The bin/install.js bare-form '~/.claude'/'$HOME/.claude' guard (\\b -> (?![\\w-])) shipped in the same commit but does not itself move this file's bytes under the default global install." - }, - "skills/gsd-secure-phase/SKILL.md": { - "reason": "#3544: the gsd-core/ spec-tree tilde-restore fix (see the workflows/references/templates entries in this same fragment) also applies inside command bodies converted into Claude skills - this SKILL.md embeds a @$HOME/gsd-core/... include from its source command that now emits as @~/gsd-core/... so Claude Code actually expands it, matching the tilde form #3133 already normalized elsewhere in the skill body. The bin/install.js bare-form '~/.claude'/'$HOME/.claude' guard (\\b -> (?![\\w-])) shipped in the same commit but does not itself move this file's bytes under the default global install." - }, - "skills/gsd-settings/SKILL.md": { - "reason": "#3544: the gsd-core/ spec-tree tilde-restore fix (see the workflows/references/templates entries in this same fragment) also applies inside command bodies converted into Claude skills - this SKILL.md embeds a @$HOME/gsd-core/... include from its source command that now emits as @~/gsd-core/... so Claude Code actually expands it, matching the tilde form #3133 already normalized elsewhere in the skill body. The bin/install.js bare-form '~/.claude'/'$HOME/.claude' guard (\\b -> (?![\\w-])) shipped in the same commit but does not itself move this file's bytes under the default global install." - }, - "skills/gsd-ship/SKILL.md": { - "reason": "#3544: the gsd-core/ spec-tree tilde-restore fix (see the workflows/references/templates entries in this same fragment) also applies inside command bodies converted into Claude skills - this SKILL.md embeds a @$HOME/gsd-core/... include from its source command that now emits as @~/gsd-core/... so Claude Code actually expands it, matching the tilde form #3133 already normalized elsewhere in the skill body. The bin/install.js bare-form '~/.claude'/'$HOME/.claude' guard (\\b -> (?![\\w-])) shipped in the same commit but does not itself move this file's bytes under the default global install." - }, - "skills/gsd-sketch/SKILL.md": { - "reason": "#3544: the gsd-core/ spec-tree tilde-restore fix (see the workflows/references/templates entries in this same fragment) also applies inside command bodies converted into Claude skills - this SKILL.md embeds a @$HOME/gsd-core/... include from its source command that now emits as @~/gsd-core/... so Claude Code actually expands it, matching the tilde form #3133 already normalized elsewhere in the skill body. The bin/install.js bare-form '~/.claude'/'$HOME/.claude' guard (\\b -> (?![\\w-])) shipped in the same commit but does not itself move this file's bytes under the default global install." - }, - "skills/gsd-spec-phase/SKILL.md": { - "reason": "#3544: the gsd-core/ spec-tree tilde-restore fix (see the workflows/references/templates entries in this same fragment) also applies inside command bodies converted into Claude skills - this SKILL.md embeds a @$HOME/gsd-core/... include from its source command that now emits as @~/gsd-core/... so Claude Code actually expands it, matching the tilde form #3133 already normalized elsewhere in the skill body. The bin/install.js bare-form '~/.claude'/'$HOME/.claude' guard (\\b -> (?![\\w-])) shipped in the same commit but does not itself move this file's bytes under the default global install." - }, - "skills/gsd-spike/SKILL.md": { - "reason": "#3544: the gsd-core/ spec-tree tilde-restore fix (see the workflows/references/templates entries in this same fragment) also applies inside command bodies converted into Claude skills - this SKILL.md embeds a @$HOME/gsd-core/... include from its source command that now emits as @~/gsd-core/... so Claude Code actually expands it, matching the tilde form #3133 already normalized elsewhere in the skill body. The bin/install.js bare-form '~/.claude'/'$HOME/.claude' guard (\\b -> (?![\\w-])) shipped in the same commit but does not itself move this file's bytes under the default global install." - }, - "skills/gsd-stats/SKILL.md": { - "reason": "#3544: the gsd-core/ spec-tree tilde-restore fix (see the workflows/references/templates entries in this same fragment) also applies inside command bodies converted into Claude skills - this SKILL.md embeds a @$HOME/gsd-core/... include from its source command that now emits as @~/gsd-core/... so Claude Code actually expands it, matching the tilde form #3133 already normalized elsewhere in the skill body. The bin/install.js bare-form '~/.claude'/'$HOME/.claude' guard (\\b -> (?![\\w-])) shipped in the same commit but does not itself move this file's bytes under the default global install." - }, - "skills/gsd-thread/SKILL.md": { - "reason": "#3544: the gsd-core/ spec-tree tilde-restore fix (see the workflows/references/templates entries in this same fragment) also applies inside command bodies converted into Claude skills - this SKILL.md embeds a @$HOME/gsd-core/... include from its source command that now emits as @~/gsd-core/... so Claude Code actually expands it, matching the tilde form #3133 already normalized elsewhere in the skill body. The bin/install.js bare-form '~/.claude'/'$HOME/.claude' guard (\\b -> (?![\\w-])) shipped in the same commit but does not itself move this file's bytes under the default global install." - }, - "skills/gsd-ui-phase/SKILL.md": { - "reason": "#3544: the gsd-core/ spec-tree tilde-restore fix (see the workflows/references/templates entries in this same fragment) also applies inside command bodies converted into Claude skills - this SKILL.md embeds a @$HOME/gsd-core/... include from its source command that now emits as @~/gsd-core/... so Claude Code actually expands it, matching the tilde form #3133 already normalized elsewhere in the skill body. The bin/install.js bare-form '~/.claude'/'$HOME/.claude' guard (\\b -> (?![\\w-])) shipped in the same commit but does not itself move this file's bytes under the default global install." - }, - "skills/gsd-ui-review/SKILL.md": { - "reason": "#3544: the gsd-core/ spec-tree tilde-restore fix (see the workflows/references/templates entries in this same fragment) also applies inside command bodies converted into Claude skills - this SKILL.md embeds a @$HOME/gsd-core/... include from its source command that now emits as @~/gsd-core/... so Claude Code actually expands it, matching the tilde form #3133 already normalized elsewhere in the skill body. The bin/install.js bare-form '~/.claude'/'$HOME/.claude' guard (\\b -> (?![\\w-])) shipped in the same commit but does not itself move this file's bytes under the default global install." - }, - "skills/gsd-ultraplan-phase/SKILL.md": { - "reason": "#3544: the gsd-core/ spec-tree tilde-restore fix (see the workflows/references/templates entries in this same fragment) also applies inside command bodies converted into Claude skills - this SKILL.md embeds a @$HOME/gsd-core/... include from its source command that now emits as @~/gsd-core/... so Claude Code actually expands it, matching the tilde form #3133 already normalized elsewhere in the skill body. The bin/install.js bare-form '~/.claude'/'$HOME/.claude' guard (\\b -> (?![\\w-])) shipped in the same commit but does not itself move this file's bytes under the default global install." - }, - "skills/gsd-undo/SKILL.md": { - "reason": "#3544: the gsd-core/ spec-tree tilde-restore fix (see the workflows/references/templates entries in this same fragment) also applies inside command bodies converted into Claude skills - this SKILL.md embeds a @$HOME/gsd-core/... include from its source command that now emits as @~/gsd-core/... so Claude Code actually expands it, matching the tilde form #3133 already normalized elsewhere in the skill body. The bin/install.js bare-form '~/.claude'/'$HOME/.claude' guard (\\b -> (?![\\w-])) shipped in the same commit but does not itself move this file's bytes under the default global install." - }, - "skills/gsd-update/SKILL.md": { - "reason": "#3544: the gsd-core/ spec-tree tilde-restore fix (see the workflows/references/templates entries in this same fragment) also applies inside command bodies converted into Claude skills - this SKILL.md embeds a @$HOME/gsd-core/... include from its source command that now emits as @~/gsd-core/... so Claude Code actually expands it, matching the tilde form #3133 already normalized elsewhere in the skill body. The bin/install.js bare-form '~/.claude'/'$HOME/.claude' guard (\\b -> (?![\\w-])) shipped in the same commit but does not itself move this file's bytes under the default global install." - }, - "skills/gsd-validate-phase/SKILL.md": { - "reason": "#3544: the gsd-core/ spec-tree tilde-restore fix (see the workflows/references/templates entries in this same fragment) also applies inside command bodies converted into Claude skills - this SKILL.md embeds a @$HOME/gsd-core/... include from its source command that now emits as @~/gsd-core/... so Claude Code actually expands it, matching the tilde form #3133 already normalized elsewhere in the skill body. The bin/install.js bare-form '~/.claude'/'$HOME/.claude' guard (\\b -> (?![\\w-])) shipped in the same commit but does not itself move this file's bytes under the default global install." - }, - "skills/gsd-verify-work/SKILL.md": { - "reason": "#3544: the gsd-core/ spec-tree tilde-restore fix (see the workflows/references/templates entries in this same fragment) also applies inside command bodies converted into Claude skills - this SKILL.md embeds a @$HOME/gsd-core/... include from its source command that now emits as @~/gsd-core/... so Claude Code actually expands it, matching the tilde form #3133 already normalized elsewhere in the skill body. The bin/install.js bare-form '~/.claude'/'$HOME/.claude' guard (\\b -> (?![\\w-])) shipped in the same commit but does not itself move this file's bytes under the default global install." - }, - "skills/gsd-workspace/SKILL.md": { - "reason": "#3544: the gsd-core/ spec-tree tilde-restore fix (see the workflows/references/templates entries in this same fragment) also applies inside command bodies converted into Claude skills - this SKILL.md embeds a @$HOME/gsd-core/... include from its source command that now emits as @~/gsd-core/... so Claude Code actually expands it, matching the tilde form #3133 already normalized elsewhere in the skill body. The bin/install.js bare-form '~/.claude'/'$HOME/.claude' guard (\\b -> (?![\\w-])) shipped in the same commit but does not itself move this file's bytes under the default global install." - } - }, - "version": 1 -} diff --git a/tests/emitted-drift-acks/3565-contract-drift-registry.json b/tests/emitted-drift-acks/3565-contract-drift-registry.json deleted file mode 100644 index df2945ea8..000000000 --- a/tests/emitted-drift-acks/3565-contract-drift-registry.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "version": 1, - "paths": { - "gsd-user-profiler.md": "#3565: gained the standard MUST-Read gate clause. profile-user.md sends a required-reading block; the agent's instructions never referenced the gate, so the clause could not fire — the F8 defect one layer up, caught by the new read-tag arm. +~200 bytes." - } -} diff --git a/tests/emitted-drift-acks/3576-references-canonical-cites.json b/tests/emitted-drift-acks/3576-references-canonical-cites.json deleted file mode 100644 index 659369532..000000000 --- a/tests/emitted-drift-acks/3576-references-canonical-cites.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "version": 1, - "paths": { - "import.md": "#3576: four bare `references/.md` cites repaired to the canonical `gsd-core/references/.md` form (+36 bytes, 4 \u00d7 9). Dead-pointer fix; no content change. \u2014 #3602 append: gains a CHECKER_MODEL resolve-model binding, the #2517 marker + rule block, and model=\"{CHECKER_MODEL}\" on the gsd-plan-checker Agent block so the checker honors dynamic_routing/model_profile tiers.", - "gsd-doc-synthesizer.md": "#3576: two bare `references/.md` cites repaired to the canonical `gsd-core/references/.md` form (+18 bytes, 2 \u00d7 9). Dead-pointer fix; no content change." - } -} \ No newline at end of file diff --git a/tests/emitted-drift-acks/3585-planning-commit-guard.json b/tests/emitted-drift-acks/3585-planning-commit-guard.json deleted file mode 100644 index 7b525d93e..000000000 --- a/tests/emitted-drift-acks/3585-planning-commit-guard.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "version": 1, - "paths": { - "fast.md": { - "reason": "#3585: the commit step ran `git add -A`, sweeping .planning/ into every /gsd:fast commit regardless of commit_docs. Gating it needs `gsd_run` in that block, so the launcher preamble was MOVED from the later log_to_state step into the commit step (moved, not duplicated \u2014 log_to_state now relies on the one-preamble-per-workflow convention every other workflow uses). Growth is the preamble's new position plus four lines of guard; the commit_docs=true path stays byte-identical to the previous unconditional `git add -A`." - }, - "new-milestone.md": { - "reason": "#3585: `git add .planning/milestones/ .planning/phases/` was ungated, and the correctly-gated `query commit` that follows it skips under commit_docs:false \u2014 leaving those paths in the index for the next commit to absorb. Growth is the executable commit_docs guard wrapping the stage, plus one sentence recording that the unstaged archive move is deliberate rather than a bug. \u2014 #3576 append: bare `references/.md` cites repaired to the canonical `gsd-core/references/.md` form (+18 bytes, 2 cite(s) \u00d7 9). Dead-pointer fix; no content change." - } - } -} \ No newline at end of file diff --git a/tests/emitted-drift-acks/3602-workflow-subagent-model-resolution.json b/tests/emitted-drift-acks/3602-workflow-subagent-model-resolution.json deleted file mode 100644 index 17f33b9bc..000000000 --- a/tests/emitted-drift-acks/3602-workflow-subagent-model-resolution.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "version": 1, - "paths": { - "audit-fix.md": { - "reason": "#3602: the issue's file-level grep audit missed this file \u2014 its gsd-executor spawn is dispatch-shaped. Gains an EXECUTOR_MODEL resolve-model binding, the #2517 marker + rule block, and model= on the executor Agent block. Deliberate growth, not converter drift." - }, - "diagnose-issues.md": { - "reason": "#3602: same class \u2014 gsd-debugger spawned with no model resolution. Gains a DEBUGGER_MODEL resolve-model assignment in the pre-spawn block, the #2517 marker + rule block, and model=\"{DEBUGGER_MODEL}\" on the debugger Agent block. Deliberate growth, not converter drift. #3659 appends --mode \"$ISOLATION\" to both #2649 base-check calls (+40B): the isolation mode now gates the baseref-head suppress (harness ignores the setting, #48). Deliberate growth." - }, - "profile-user.md": { - "reason": "#3602: same class via a Task-tool prose spawn the issue's Agent()-shaped audit could not see. Gains a PROFILER_MODEL resolve-model binding, the #2517 marker + rule block, and a model=\"{PROFILER_MODEL}\" pass/omit instruction on the Task-tool spawn. Deliberate growth, not converter drift." - }, - "docs-update.md": { - "reason": "#3602 (isolated adversarial review finding): the --verify-only step's gsd-doc-verifier spawn had no model resolution \u2014 doc_writer_model covers only the writer spawns, and docs-init emits no verifier field. Gains a DOC_VERIFIER_MODEL resolve-model assignment in the init block, a model=\"{DOC_VERIFIER_MODEL}\" pass/omit instruction at the verifier spawn, and the #2517 blockquote's variable list extended. Deliberate growth, not converter drift." - } - } -} diff --git a/tests/emitted-drift-acks/3606-hook-kind-coverage.json b/tests/emitted-drift-acks/3606-hook-kind-coverage.json deleted file mode 100644 index b6532c5bd..000000000 --- a/tests/emitted-drift-acks/3606-hook-kind-coverage.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "version": 1, - "paths": { - "quick.md": { - "reason": "#3606: the execute:post consumer gains generic step dispatch (deferral to loop-hook-dispatch.md) before the code-review specialization, so refactor-trigger's ref.command step and any other registered execute:post steps actually run on /gsd:quick runs instead of being filtered out by the one-skill narrow. Deliberate growth, not converter drift. #3659 appends --mode \"$ISOLATION\" to both #1941 base-check calls (+40B): the isolation mode now gates the baseref-head suppress \u2014 the runtime harness does not read project-settings baseRef (#48), so harness mode must compare instead of suppressing. Deliberate growth." - } - } -} diff --git a/tests/emitted-drift-acks/3645-agents-tracked-source-rule.json b/tests/emitted-drift-acks/3645-agents-tracked-source-rule.json deleted file mode 100644 index e7f614a4a..000000000 --- a/tests/emitted-drift-acks/3645-agents-tracked-source-rule.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "$comment": "Growth ack (#2914 fragment). Reason: #3645 adds the tracked-source rule (git ls-files verification, gitignored-mirror rejection, tracked-origin fallback, submodule note). gsd-pattern-mapper.md 12487 -> 13326 (+839, DEFAULT cap 24576). The planner-side rule lives in plan-phase.md (ack appended to the 3409 fragment — two ack sources may never name the same path). gsd-planner.md is UNCHANGED (frozen under 49152 LF chars by four suites).", - "version": 1, - "paths": { - "gsd-pattern-mapper.md": "agents/gsd-pattern-mapper.md +839B: Tracked-source gate on analog selection; PATTERNS.md never emits mirror paths (#3645)" - } -} diff --git a/tests/emitted-drift-acks/3651-settings-integrations-prescriptions.json b/tests/emitted-drift-acks/3651-settings-integrations-prescriptions.json deleted file mode 100644 index 94d1ed9d7..000000000 --- a/tests/emitted-drift-acks/3651-settings-integrations-prescriptions.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "$comment": "Growth ack (#2914 fragment). Reason: #3651 corrects the two broken prescriptions in settings-integrations.md — the review.models section states the registry-derived settable rule (per-lane modelConfigKey, nine lanes enumerated once as full review.models.* keys, keyless lanes cursor/qwen/coderabbit named) instead of a nonexistent dynamic-key pattern claim, and the agent_skills section prescribes the JSON array write form plus the split-on-commas instruction (quote-bearing entries rejected). settings-integrations.md 16257 -> 18309 LF bytes (+2052, DEFAULT cap 40960). Pinned by the #3651 rows in tests/settings-integrations.test.cjs (workflow text must match the registry's settable set).", - "version": 1, - "paths": { - "settings-integrations.md": "gsd-core/workflows/settings-integrations.md +2052B: registry-derived review.models settable rule + agent_skills JSON-array prescription (#3651)" - } -} diff --git a/tests/emitted-drift-acks/README.md b/tests/emitted-drift-acks/README.md new file mode 100644 index 000000000..94833de49 --- /dev/null +++ b/tests/emitted-drift-acks/README.md @@ -0,0 +1,64 @@ +# tests/emitted-drift-acks/ + +Per-PR acknowledgment fragments for the differential attribution check +(`tests/emitted-attribution.test.cjs`, ADR-2719 / #2789 / #2914). + +**This directory being empty is the healthy steady state.** A fragment appearing +in a diff *is* the alarm; a fragment sitting here on `next` is spent cruft. +`README.md` is not a fragment — every reader filters on `.json` — and exists so +the directory (which `CONTEXT.md` and `CONTRIBUTING.md` both reference by path) +survives the sweep that empties it. + +## The lifecycle, in three steps + +1. **The gate names its own remedy.** When an emitted-artifact hash moves, or a + `gsd-core/workflows/*.md` / `agents/gsd-*.md` file grows, and your diff cannot + explain it, the failure output tells you which key to use and prints a minimal + valid document to paste. Create a NEW fragment named for your issue or PR + (`-.json`) — never reuse someone else's, and never revive the + legacy single `tests/emitted-drift-ack.json`. +2. **Note the two key spaces.** The message says which one applies. An + unattributable **hash** ripple is keyed on the emitted path + (`skills/gsd-add-tests/SKILL.md`); **growth** is keyed on the bare filename as + it appears under `gsd-core/workflows/` or `agents/` (`explore.md`). +3. **Delete the fragment once it has merged (#3078).** Every entry is scoped to + the diff that introduced it, so the moment it lands on `next` its prose is + already at the base — it is spent and can no longer clear anything, while + still owning its path keys. The `guard-no-ack-on-next` job reds `next` and + prints the exact `git rm`. Run it. + +## Why the sweep exists + +Fragments end the *file* conflict the single shared document caused. They do not +end the *key* conflict: two ack sources may never name the same path, and that is +a hard, loudly-reported error. So a fully-spent fragment left here walls off every +path it owns — the next PR to grow one of them can declare it neither in the +owning fragment (spent, gates nothing) nor in its own (duplicate). #2914 assumed a +persisting fragment was harmless; #3078 measured the cost at 45 fragments owning +403 paths and made the guard sweep them. + +A **partially** spent fragment is deliberately left alone. That asymmetry is what +keeps the re-arm route working: appending prose to a live entry re-arms it, and +re-arming deliberately costs an actual new sentence — the comparison strips +zero-width characters and collapses whitespace precisely so a zero-information +edit cannot fake one. + +## Never pin a fragment in a test + +A fragment is deleted the moment it has merged (see step 3 above), so any test +that asserts one exists, or asserts its contents, will fail the instant +`guard-no-ack-on-next` sweeps it — and that failure has nothing to do with the +behavior the fragment once explained. This has already cost two suites: +`tests/emitted-attribution.test.cjs`'s three `#2914` migration pins, and +`tests/agent-tracked-source-rule.test.cjs`'s `#3645` growth-ack pin. What a test +may legitimately assert is the BEHAVIOR the ack explains, or the guard's own +verdict (`assertNoAllSpentFragments`, `assertAbsentOnNext`) — never the +paperwork. + +## Do not regenerate anything + +There is no baseline file to re-run a generator over; #2724 deleted it. If you +find yourself hunting for one, that is the predictable wrong guess. + +See `CONTRIBUTING.md` → "Editing shipped content", `docs/TESTING-SUITES.md`, and +`CONTEXT.md`'s `RULESET.EMITTED_ATTRIBUTION` for the full model. diff --git a/tests/helpers/emitted-diff.cjs b/tests/helpers/emitted-diff.cjs index b3ec11a86..efe7d413d 100644 --- a/tests/helpers/emitted-diff.cjs +++ b/tests/helpers/emitted-diff.cjs @@ -96,6 +96,26 @@ const INVISIBLE = new RegExp( 'g', ); +/** + * The single definition of "the prose a reviewer actually reads" for an ack reason. + * + * It is exported for exactly one reason: `scripts/lint-emitted-drift-ack.cjs` carries + * its own duplicate (`ackProse`) rather than requiring this file, because `scripts/` + * ships in the published npm package and `tests/` does not — a `require('../tests/...')` + * from a shipped script would work in this repo and break for every installed user + * (#3078). That duplication is unavoidable, but leaving both copies unexported meant + * nothing could ever compare them: the "parity test" this module's comments promised + * was checking the script against itself. Exporting this lets a real test hold the + * script's copy to this one. + * + * The invisible-stripping (`INVISIBLE`) and whitespace-collapse below are anti-gaming + * defences, not incidental normalization — see the comments at the two call sites in + * `diffEmitted`. Do not weaken either side of the duplicate without weakening both. + */ +function normalizeAckReason(reason) { + return reason.replace(INVISIBLE, '').replace(/\s+/g, ' ').trim(); +} + /** * A brand-new workflow/agent file — absent from the baseline, present now — must * still stay under the Codex `project_doc_max_bytes` anchor (ADR-1610 Decision @@ -490,7 +510,7 @@ function diffEmitted({ // while being literally INVISIBLE in the diff — the purest form of "no new // explanation". Built from codepoints rather than literal characters, because a // literal class would itself be unreviewable in this file. - const prose = (reason) => reason.replace(INVISIBLE, '').replace(/\s+/g, ' ').trim(); + const prose = normalizeAckReason; const isSpent = (rel, entry) => { const prior = baseAcks.get(rel); return prior !== undefined && prose(prior.reason) === prose(entry.reason); @@ -841,6 +861,8 @@ module.exports = { NEW_FILE_CAP, MAX_ACK_FRAGMENTS, REMEDIATION, + INVISIBLE, + normalizeAckReason, sourceSatisfiedBy, parseAck, mergeAckSources,