Commit Graph

400 Commits

Author SHA1 Message Date
Tom Boucher
c07734216f fix(#2603): document kimi-code in the host-integration capability matrix (#2687)
* fix(#2603): document kimi-code in the host-integration matrix; correct 3 inherited axes

The matrix — ADR-1239's deployment source-of-truth — had a section for 18 of 19
installed runtimes but none for `kimi-code`, so its `hostIntegration` axes shipped
with no citation and no evidence quote.

Sourcing every axis independently against Kimi Code CLI's own docs (the issue's
explicit requirement — `kimi` and `kimi-code` are distinct products) showed three
values had been inherited from the Python `kimi` descriptor rather than sourced:

- `embeddingMode` imperative -> declarative. Kimi Code plugins are a
  `kimi.plugin.json` manifest plus markdown Skills with no in-process programmatic
  API (docs/en/customization/plugins.md) — the same shape as `codex`.
- `dispatch.nested` false -> true. The `coder` built-in "can dispatch its own
  nested sub-agents when a task decomposes naturally" (docs/en/customization/agents.md).
  The Python `kimi` CLI genuinely prohibits nesting; Kimi Code does not.
- `dispatch.maxDepth` 1 -> "undocumented". Nesting is documented but no depth
  bound is published, so the fail-closed sentinel applies over a guessed integer.

`dispatch.namedDispatch` deliberately stays `false`: GSD's kimi-code artifact
layout installs Agent Skills only (no `agents` kind), so no named GSD subagent is
registered with the host and `resolveDispatchType` maps every role onto
coder/explore/plan. Flipping it would reintroduce the dispatch failure recorded in
docs/migration/kimi-to-kimi-code.md. The matrix records the host-capability nuance
under Documentation gaps instead.

Behaviourally inert: `namedDispatch:false` already caps nested/maxDepth/background/
backgroundDispatch to false/0 in the effective axes (host-integration.cts:493-499),
and the install adapter is not selected by `embeddingMode` (install.js:543 always
uses the imperative adapter). The one visible effect is the curated profile pin,
which moves programmatic-cli -> declarative-cli.

Also fixes the axes legend, which omitted the `built-in-only` subagentToolkit
member that has been in the closed vocabulary since kimi-code shipped.

Same defect class and countermeasure as #2598: pin the corrected values and require
the matrix to agree with the descriptor, because a descriptor/matrix disagreement is
how the gap survived.

Closes #2603

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QT3ibz5qJuDuGqpTGRYVGf

* fix(#2603): report the maxDepth `undocumented` sentinel as a sentinel, not as malformed

Surfaced by the orthogonal review of this change. `negotiateHostCapabilities`
emits a sentinel-specific warning for every dispatch sub-axis carrying the
documented `undocumented` value — namedDispatch, nested, background,
subagentToolkit, backgroundDispatch, isolation — except `maxDepth`, which fell
through to the numeric guard and reported `host dispatch.maxDepth is missing or
not a number — treating as 0`.

That message is indistinguishable from a genuinely malformed descriptor, so a
correctly fail-closed descriptor reads as broken. Six shipped runtimes carry the
sentinel here (antigravity, augment, opencode, trae, windsurf, zcode) and this
PR's kimi-code correction adds a seventh, which is why it is fixed here rather
than left in place.

The numeric guard keeps firing for genuinely malformed values; both paths still
degrade `effective.dispatch.maxDepth` closed to 0. Covered by three tests,
including the boundary case that the sentinel carve-out must not swallow a real
malformed value.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QT3ibz5qJuDuGqpTGRYVGf

* chore(#2603): backfill changeset PR number (#2687)

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 22:52:57 -04:00
Tom Boucher
a3853472de fix(#2598): declare OpenCode subagent dispatch synchronous, not background (#2682)
* fix(#2598): declare OpenCode subagent dispatch synchronous, not background

capabilities/opencode/capability.json advertised dispatch.background: true and
dispatch.backgroundDispatch: true. negotiateHostCapabilities and every
degradationFor / shouldFlattenDispatch consumer trusts these per-field values, so
declaring a capability the host lacks OVERSTATES it — the opposite of the
fail-closed posture the negotiation is built for.

The issue's own citations needed checking before acting: the host-integration
matrix (ADR-1239's designated deployment source-of-truth) documented `true` with
NEWER evidence than the issue cited, and explicitly marked the issue's
sst/opencode#5887 reference as a stale snapshot superseded by #2087. git log
confirms #2087 deliberately flipped these from false to true, citing OpenCode
v1.15.0/v1.17 as "background subagents enabled by default in all modes". Applying
the issue as filed would, on that evidence, have REGRESSED a deliberate update.

So the claim was verified against current upstream rather than either document.
`packages/opencode/src/effect/runtime-flags.ts` on `dev` today reads:

    experimentalBackgroundSubagents: enabledByExperimental("OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS")

`enabledByExperimental` falls back to the `experimental` flag and `bool()`
defaults to false — the parameter is hidden from the model unless an operator
opts in by env var. Upstream #29638 is still OPEN and confirms the session loop
`tasks.pop()`s one subtask at a time. #2087's "default-on in all modes" reading
does not hold against current dev.

The issue's CONCLUSION is therefore right even though part of its evidence was
superseded: concurrent dispatch cannot be relied on, so both fields are false.

The matrix rows are corrected with the verified citation rather than reverted to
the old #5887 quote, so the record shows why the value is false TODAY rather than
re-asserting evidence that was legitimately superseded. Neighbouring sub-fields
are untouched and pinned by test: namedDispatch, subagentToolkit, and
isolation:'orchestrator-worktree' (which works via `opencode run --dir` at the OS
process level and is unaffected — #2584 does not depend on this value either way).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015TCwhbMuY37DzRMCfzTABJ

* fix(#2598): re-pin the dispatch contract tests to synchronous OpenCode dispatch

gsd-test on the descriptor change came back FAILED (5 unique, both node
versions). The failures were not incidental — they were deliberate contract-pin
tests encoding #2087's decision, one named literally "background UPGRADE":

  tests/host-integration-descriptors.test.cjs
    - EXPECTED_FLATTEN[opencode] === false (background-eligible)
    - the derived background-eligible set pin
  tests/opencode-imperative-reference.test.cjs
    - "descriptor declares background dispatch true/true (v1.15/v1.17 upgrade)"
    - "background UPGRADE changes shouldFlattenDispatch: false now"

So this is a recorded decision being reversed, not drift being corrected, and it
is reversed on evidence: current upstream `dev` gates the capability behind
OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS (default false) and upstream #29638
(OPEN) confirms the session loop still handles one subtask at a time. The issue
is filed by the maintainer and explicitly directs "update golden-parity /
validator fixtures as needed", which sanctions re-pinning.

Behavioral consequence, verified: shouldFlattenDispatch(opencode) now returns
TRUE, so GSD serializes opencode dispatch instead of trusting concurrency it
cannot get. That is the correct fail-closed direction and is safe today — no
shipped GSD flow drives OpenCode background waves (per the issue), and
isolation:'orchestrator-worktree' is unaffected because it works at the OS
process level via `opencode run --dir`, not via the native subagent.

Each re-pinned test now asserts the retracted contract in the opposite
direction — feeding the #2087 axes back in must still yield "would not flatten" —
so a silent re-flip of either field is caught rather than merely un-asserted.

lint:ci exit 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015TCwhbMuY37DzRMCfzTABJ

* chore(#2598): backfill changeset pr number (#2682)

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 21:47:54 -04:00
Tom Boucher
0d08c32048 fix(#2590): emit Workflow scripts the Workflow tool accepts; make the backend reachable (#2681)
* fix(#2590): emit Workflow scripts the Workflow tool accepts; make the backend reachable

Every emitted script was rejected. Four invalid constructs, the first fatal on
its own, so the Workflow backend could never dispatch a wave:

  1. no `export const meta = {…}` first statement -> whole script rejected
  2. resumeFromRunId("<id>")  -> "resumeFromRunId is not defined". It is a
     Workflow TOOL INPUT parameter, not a script function. The run id still
     reaches the caller via summary.resumeRunId, to pass as that input.
  3. budget(<n>)              -> "budget is not a function". `budget` is a
     read-only object { total, spent(), remaining() } fed by the caller's token
     directive; a script cannot set it. Recorded as intent in a comment.
  4. parallel(agent(…), agent(…)) -> "parallel() expects an array of functions".
     Now parallel([() => agent(…), …]) — passing agent() results directly also
     started every agent eagerly, before parallel() could bound concurrency.

The single-plan stage had its own branch with the same parallel() defect; both
branches are now one array-emitting path. Waves also emit phase() calls whose
titles match meta.phases exactly, so progress groups correctly.

Two secondary defects kept the script from ever being REACHED — which is why
this shipped undetected:

  5. NOTHING resolved the Agent SDK version. The fragment claimed there was "no
     scriptable way" to introspect it and told callers to omit the flag, so
     gate 5 returned agent_sdk_version_unknown on every automated run while
     `capability state` still reported active:true. True for bash, false for
     Node: the router now reads the installed @anthropic-ai/claude-agent-sdk
     version, walking node_modules up the tree and reading package.json
     directly — require.resolve throws ERR_PACKAGE_PATH_NOT_EXPORTED because the
     SDK's exports map does not expose ./package.json. Precedence: explicit flag
     > GSD_AGENT_SDK_VERSION > installed. Fail-closed is preserved; an
     unresolvable version still declines to inline. A too-old SDK now reports
     the truthful agent_sdk_version_below_floor instead of unknown.
  6. The runtime fallback was `--runtime > GSD_RUNTIME > 'unknown'`, diverging
     from the canonical `GSD_RUNTIME > config.runtime > 'claude'`, so any
     invocation without --runtime reported runtime_not_claude on an ordinary
     Claude project. Now delegates to runtime-slash.resolveRuntime.

The fragment's `${AGENT_SDK_VERSION:+--agent-sdk-version "$AGENT_SDK_VERSION"}`
snippet is removed rather than repaired: it was also shell-dependent — zsh does
not word-split unquoted parameter expansions, so it collapsed to a single argv
element, argValue() never matched, and the run failed into the same
agent_sdk_version_unknown, indistinguishable from genuinely unknown. Auto-
resolution removes the need for the construct entirely.

Verified with the issue's own repro: no flags now reaches the version gate; an
SDK above the floor yields backend:"workflow" with a script that parses as a
real ES module.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015TCwhbMuY37DzRMCfzTABJ

* fix(#2590): sync generated registry, repair sibling tests, reject duplicate wave ids

Findings from the isolated review, all fixed.

HIGH — gsd-core/bin/lib/capability-registry.cjs was stale, and `lint:ci` was
already RED because of it. The registry embeds the fragment text INLINE, so the
shipped/installed copy still taught the exact broken contract this PR fixes:
the old `${AGENT_SDK_VERSION:+…}` bash line and the "OMIT the flag when unknown"
guidance. Regenerated. (I had read `lint:ci` by grepping its output instead of
checking its exit code, so I recorded a red chain as green — checking $? now.)

HIGH — three existing tests asserted the OLD broken shape and would have failed
CI; none was touched by the first commit:
  tests/fix-2285-claude-orchestration-wiring.test.cjs — matched resumeFromRunId("…")
  tests/claude-orchestration.test.cjs                 — .includes('budget(')
  tests/claude-orchestration-command-router.test.cjs  — .includes('budget(')
Each now asserts the corrected contract: the id/pool reaches the caller via
summary, and neither construct is ever CALLED. Two sibling assertions had also
gone vacuous — `.includes('resumeFromRunId')` still passed, but only because the
new explanatory COMMENT contains that substring, not because anything is wired.
Rewritten to assert the real property.

MEDIUM — duplicate wave ids were never rejected. Plan-id uniqueness was checked
within a wave, but nothing checked wave ids across waves. That was harmless
before; it is not now, because each wave emits a `phase("Wave <id>")` call plus a
matching meta.phases entry and the tool matches titles by exact string — two
waves sharing an id would collapse into one progress group and misattribute the
second wave's agents to the first. Rejected at validation, with tests either
side of the boundary.

MEDIUM — the fragment contradicted itself (its "Manifest construction" header
still listed $AGENT_SDK_VERSION as orchestrator-built) and, more seriously, never
told the orchestrator to pass summary.resumeRunId as the Workflow tool's
resumeFromRunId INPUT. Since this PR moves resume from a broken in-script call to
a tool-invocation input, an implementer following only the fragment would have
silently regressed phase-resume to a no-op. Both fixed.

MEDIUM — docs/how-to/enable-claude-orchestration-workflow-backend.md and
docs/explanation/claude-orchestration-capability.md documented
`resumeFromRunId("<id>")` and `budget(<tokens>)` as current correct output —
teaching the bug as the feature. Updated to the real contract, including the
required meta block and the thunk-array parallel() form. (The changeset is
`Fixed`, so the docs gate exempts this; it is corrected because it is wrong,
not because a gate demanded it.)

LOW — the router's top-of-file comment still described the divergent
`--runtime > GSD_RUNTIME > 'unknown'` chain as current, ninety lines above the
fix; and inserting resolveInstalledAgentSdkVersion had orphaned
resolveDetectionArgs' JSDoc above the wrong function. Both repaired.

lint:ci now exits 0 (verified by exit code, not by reading output).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015TCwhbMuY37DzRMCfzTABJ

* chore(#2590): backfill changeset pr number (#2681)

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 20:49:38 -04:00
Tom Boucher
c87f6f358e enhance(#1854): offer restore for user-added files backed up on update (#2679)
* test(#1854): failing-first coverage for user-files-backup restore

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(#1854): offer restore for user-added files backed up on update

Adds a restore-custom-files gsd-tools verb and wires it into update.md as a
restore_custom_files step: plan, compatibility-check against the newly
installed release, then restore only on explicit opt-in. The backup is never
deleted, a shipped path is never overwritten, and a single unwritable entry
does not abort the rest.

Also drops the jq pipe from update-context field extraction (#2589 class,
missed by that sweep) and repairs a broken code fence in docs/CLI-TOOLS.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(#1854): reject symlinked restore destinations and backup roots

Self-review of the restore path found two write-through holes: copyFileSync
follows a symlinked destination, so a link planted at the restore target wrote
outside the config dir with every ancestor still a real directory; and statSync
on the backup root followed a link, letting the walk read arbitrary files and
present them as the user's own backup. Both now lstat.

Also marks the report's path/detail strings as untrusted data in update.md so
the rendered step cannot carry instructions into the runtime model.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(#1854): move the update-context jq guard into the #2589 sweep

update.md joins the AUDITED list rather than carrying a duplicate assertion in
the backup-restore suite, and the guard gains a negative-proof companion so
'no jq pipe' cannot pass by the fields simply no longer being read.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(#1854): validate manifest files map shape before trusting it

Security review flagged that Object.keys on a non-plain-object files field
yields numeric-index keys matching nothing, so the managed-path check dies
silently while manifest_found still reports true. Shape, not just type
(ADR-227): an array or scalar files map is now an unusable manifest.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(#1854): size the restore prompt by eligible_count

Spec review found the prompt was driven by entries.length, so a backup holding
only blocked entries asked "Restore 1 file(s)?" when accepting would restore
zero. The question now reads eligible_count, and an all-blocked backup reports
its reasons instead of offering a choice that cannot be honored. The decline
path names the resolved backup_dir rather than the bare directory name.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(#1854): use t.skip on hosts without symlink support

A bare return in a node:test body registers as a PASS, so the four symlink
guards silently reported green on unprivileged Windows instead of skipping.
Adds the dangling-link destination case the security review called out, and
moves outside-dir teardown to t.after so a failing assert cannot leak it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(#1854): unfence restore hint, regen goldens, widen install timeout

Three gate failures from the c99d612a5 run, all root-caused:

1. capability-registry (3): update.md's decline message put an instructional
   'gsd-tools ...' line in an UNTAGGED fence, and the guard treats untagged
   fences as shell blocks. Retagged both display blocks as text and switched
   the hint to the resolved 'node <config-dir>/.../gsd-tools.cjs' form users
   can actually paste.

2. golden-install-parity (19): update.md and gsd-tools.cjs ship, so every
   runtime fixture moved. Regenerated; the diff is exactly those two hashes
   per fixture, no other drift.

3. install.test.cjs (5): one real failure, four cascades. The Cursor suite's
   before hook died on 'spawnSync ETIMEDOUT' at the 60s cap while the node22
   lane passed the SAME commit in 12.7s. A full install measures 13-30s idle,
   so 60s was under 2x headroom and shrinks with every file added to the
   payload. Raised to 120s, matching the heavy case already in this file.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore(#1854): backfill changeset pr number to 2679

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 19:50:56 -04:00
Tom Boucher
bd570618d4 feat(#2632): executor actuals and the closed estimate-calibration loop (#2672)
* feat(#2632): record executor actuals and close the estimate calibration loop

* fix(#2632): calibrate against the raw projection so the loop converges

* test(#2632): add closed-loop convergence guard and codify the feedback-loop rule

* fix(#2632): pair calibration samples per plan; atomic write; amend adr

* chore(#2632): backfill changeset pr to 2672

* fix(#2632): retry renameSync on transient windows errnos and clean up the temp
2026-07-26 16:28:56 -04:00
Tom Boucher
920a5f3f06 fix(#2589): use --raw/--pick for config/model/verify lookups, drop jq dep (#2673)
* fix(#2589): use --raw/--pick for config/model/verify lookups, drop jq dep

The reviewer/workflow config lookups resolved scalars and object fields with a
`gsd_run query <cmd> … | jq … 2>/dev/null || <default>` shape. On any machine
without jq (the default on Windows/Git-Bash) the jq stage fails with exit 127,
the failure is swallowed by 2>/dev/null + the trailing || default, and the
variable comes back EMPTY — the configured per-lane model/host/budget is
silently dropped and the lane falls back to CLI defaults with no diagnostic.

gsd-tools ships native flags that do the same job with no external dep:
  config-get <key> --raw          (strips JSON quotes off a scalar)
  resolve-model <id> --pick model (descends an object)
  resolve-execution … --pick <f>  (same)
  verification.status … --pick status

Replaced every jq-piped config/model/verify lookup across review.md (×23),
plan-phase.md, ship.md, debug.md (incl. the redundant boolean coercion — --raw
returns true/false as bare tokens natively), autonomous.md (×2),
ai-integration-phase.md (×4), and eval-review.md. The legitimate structured-JSON
jq sites that parse HTTP curl responses (.choices[0], jq -rs, jq -n --rawfile)
are untouched — only the jq-replaceable lookups moved to the native flags.

Adds tests/fix-2589-config-get-no-jq.test.cjs: a source-invariant guard asserting
no audited workflow pipes config-get/resolve-model/resolve-execution/verification.status
to jq (fails-first on the pre-fix text, passes after).

* test(#2589): update autonomous-converge jq assertion to --pick; regen golden fixtures

Two test consequences of the workflow-doc edits in the prior commit:

1. tests/autonomous-converge.test.cjs pinned the OLD jq-dependent shape
   (`verification.status … | jq -r '.status//empty'`) as the canonical routing
   contract. The test's INTENT is correct (route human validation through
   canonical verification.status) but it over-specified the MECHANISM (the jq
   pipe). Updated the assertion to match the new native --pick status shape;
   the contract being guarded (canonical verification.status read before the
   human_needed branch) is unchanged.

2. The golden-install-parity fixtures (19 runtimes) record a content hash of
   every installed workflow .md; the 7 edited workflows changed those hashes.
   Regenerated via `npm run gen:golden` (the test's own failure message
   instructs this). Only the 7 edited workflow hashes changed in each fixture.

* fix(#2589): declare jq a prerequisite for the lanes that still need it; repair test file

Three defects in the first cut of the #2589 fix:

1. tests/autonomous-converge.test.cjs was a JavaScript syntax error. The regex
   literal /...2>\/dev/null .../ left the second slash unescaped, terminating the
   literal early and parsing `null` as regex flags:
     SyntaxError: Invalid regular expression flags
   The whole file failed to load, so every assertion in it — including the #1522
   and #1526 guards — silently stopped running. Replaced with the string-compare
   form already used at line 202 for the sibling shell-snippet assertion.

2. lint:ci failed. tests/fix-2589-config-get-no-jq.test.cjs buckets into the
   capped `config` production module via its `config-get-...` effective prefix,
   making it a novel offender against the 2-file cap. The test is about workflow
   documents, not the config module, so it is renamed to
   fix-2589-workflow-jq-dependency.test.cjs (free prefix) rather than growing the
   allowlist with a module that does not actually need a 5th test file.

3. The fix deleted the repo's only jq-prerequisite declaration. review.md:244
   ("install jq if missing") was the anchor plan-review-convergence.md cites by
   line number, and it went away with the jq pipes — while the ollama, lm_studio,
   llama_cpp, opencode, and agy lanes still hard-require jq to parse HTTP
   /v1/chat/completions responses, opencode's JSONL event stream, and agy's
   conversation cache. On a jq-less host those five lanes swallow exit 127 into
   empty output: the same silent-degradation class #2589 exists to close.

   detect_clis now probes jq alongside the other prerequisites and emits
   jq:available / jq:missing, and the five dependent lanes are treated as
   undetected when it is absent, with an install hint. The six lanes that do not
   need jq stay selectable. plan-review-convergence.md now cites the section by
   name instead of a line number that moves.

Regression guards added to the renamed test file: review.md must keep the jq
probe and must name all five dependent lanes, and no workflow may cite review.md
by line number. Workflow-size baseline and the 19 install-parity goldens
regenerated for the review.md / plan-review-convergence.md edits.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015TCwhbMuY37DzRMCfzTABJ

* fix(#2589): decide the ship verification gate on a single verification.status read

Isolated review finding (medium). Pre-fix, ship.md captured verification.status
ONCE into $VERIFICATION and picked status / next_action / next_command off that
cached JSON with three jq calls. --pick takes a single dot-path field, so the
mechanical conversion issued three separate queries up front: three node spawns
that each re-read the phase VERIFICATION.md and re-derive the commit-time vs
mtime staleness comparison, on every ship — including the common passing path
that never uses the two message fields. It also meant the gate's verdict and the
message shown to the user were derived from three reads with no guarantee they
observed the same state.

The gate now reads `status` once and decides. The two message-only fields are
read on the blocking path only, after PHASE_VERIFICATION_INCOMPLETE is already
determined — so the passing path costs one query instead of three, and a
concurrent write between reads can no longer make the gate and its message
disagree, because the block/allow decision no longer depends on them.

Adding a multi-field --pick to gsd-tools would have collapsed this to one query,
but that changes the flag's output contract and belongs in its own change.

Regression guard in tests/fix-2589-workflow-jq-dependency.test.cjs: ship.md must
read verification.status exactly three times total, the block decision must
follow the status read, and next_action / next_command must both appear after
the blocking prose so they cannot drift back onto the passing path.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015TCwhbMuY37DzRMCfzTABJ

* docs(#2589): document the jq prerequisite for the five reviewer lanes that need it

/gsd-review's ollama, lm_studio, llama_cpp, opencode, and agy lanes parse JSON
GSD does not produce (OpenAI-compatible /v1/chat/completions responses,
OpenCode's JSONL event stream, Antigravity's conversation cache), so they require
jq on PATH. Nothing in docs/ said so. Records which five lanes need it, which six
do not, that reading configured models/hosts/budgets no longer requires jq at
all, and what /gsd-review now does when jq is absent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015TCwhbMuY37DzRMCfzTABJ

* chore(#2589): backfill changeset pr number (#2673)

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 16:17:02 -04:00
Tom Boucher
89b673e40d feat(#2631): planner emits estimate and plan-checker surfaces the over-budget flag (#2670)
* test(#2631): failing-first planner estimate emission and over-budget surfacing

* feat(#2631): emit plan estimate and surface the over-budget split recommendation

* fix(#2631): extract sizing prose to references to fit planner and plan-phase caps

* fix(#2631): move estimate check to plan-checker; fix template regex and caps

* fix(#2631): restore ALWAYS split literal and keep gsd_run after the launcher preamble

* fix(#2631): invoke estimate-check after the launcher preamble in plan-checker

* fix(#2631): stop double-applying calibration; repair COMMANDS table and stale reference

* chore(#2631): backfill changeset pr to 2670

* chore(#2631): backfill changeset pr to 2670
2026-07-26 14:29:02 -04:00
Tom Boucher
46ba02acde feat(#2630): phase-estimation module, smart-zone config key, and cli verbs (#2661)
* feat(#2630): add phase-estimation module, smart-zone config key, and cli verbs

* fix(#2630): document smart_zone_tokens, refresh golden fixtures, fix null-proto property assertions

* fix(#2630): align smart_zone_tokens write/read validation and harden estimation tests

* chore(#2630): backfill changeset pr to 2661
2026-07-26 01:42:47 -04:00
Tom Boucher
b897070de3 fix(#2653): regenerate stale api-coverage.cjs + add artifact-sync guard (#2656)
* fix(#2653): regenerate stale api-coverage.cjs + add artifact-sync guard

The tracked build artifact gsd-core/bin/lib/api-coverage.cjs had drifted
four days behind src/api-coverage.cts: PR 2551 landed the #2366 fix in
the source without regenerating the compiled output, so the module that
actually ships still carried none of it.

Regenerate the artifact, and add scripts/lint-compiled-artifact-sync.cjs
to lint:generated-sync so a tracked compiled artifact can never again
silently diverge from its source. The check derives its file set from
git ls-files rather than a hand-maintained list, and is regime-agnostic:
if these artifacts are later untracked and gitignored per ADR-457, the
tracked set becomes empty and the check passes trivially.

Verified fail-first: the guard exits 1 against the previously-committed
artifact (37731 bytes vs 38634 expected) and 0 after regeneration.

Also fixes a defect this change surfaced in tests/no-phantom-issue-refs:
its PHANTOM list still banned 2551 and 2361, but GitHub numbers issues
and PRs from one shared counter and this repo has since reached 2654, so
both now resolve to merged PRs. The guard was rejecting accurate
citations of them — it failed this very commit for naming PR 2551 as the
drift's provenance. Verified by replaying the guard's scan: 1 offender
under the old list, 0 under the pruned one. Only 3182 is still a 404.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore(#2653): backfill changeset pr number to 2656

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 23:45:11 -04:00
Behruz Nassre Esfahani
a40ee8a5e7 fix(#2479): drop the codex hook-trust bypass flag and its capability probe (#2536)
* fix(#2479): drop the codex hook-trust bypass flag and its capability probe

The /gsd-review codex lane emitted a hook-trust bypass flag via a
capability-probed variable (#1115). Host-harness safety classifiers deny
commands carrying the flag (23/33 sampled invocations), one denial citing
the probe itself as intent, while flagless retries succeeded 32/32 — the
flag only bypasses persisted hook trust, a first-run condition with no
steady-state value. Remove both the flag and the probe per maintainer
direction (no config key). #1115's diagnosability half — stderr to .err,
folded into the lane on empty output — is untouched; its version-gate
becomes vacuous with no flag to gate. The regression test inverts: the
literal flag is now banned file-wide in review.md (covers continuation
lines, carrier variables, and probes), alongside bans on the carrier
variable and any codex help-grep probe shape.

Fixes #2479

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore(#2479): add changeset for PR #2536

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore(#2479): changeset body in house format (bold lead) + post-rebase fixture regen

Review round 2 Minor: wrap the changeset lead clause in the required
**bold** span (reviewer-supplied text, applied verbatim). Rebased onto
current next; golden-install-parity (19 runtimes) + size baseline
regenerated — delta confined to review.md's hash/size.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 20:15:49 -04:00
Tom Boucher
6ee4349272 fix(#2537): extract offer_next step to references/ (~3.3KB headroom restored) (#2642)
* fix(#2537): extract offer_next step to references/ (~3.3KB headroom restored)

* chore(#2537): backfill changeset pr to 2642
2026-07-25 05:30:20 -04:00
Tom Boucher
6ad30f74b6 feat(#2584): Phase 3 — scheduler consumer + isolation adapters (#2635)
Final phase of #2584 (ADR-1239 Codex-binding amendment). execute-phase now negotiates dispatch.isolation and dispatches through the matching adapter, so a wave's independent plans run concurrently on six runtimes instead of one — with no runtime=== branch in the scheduler.

harness-worktree passes the host's declared isolation flag (claude, cursor); orchestrator-worktree creates the worktree via the Phase-2 verb and spawns the executor into it with the resolved argv/cwd (codex, opencode, kimi, kimi-code); none stays sequential. Undeclared/unknown/unresolvable isolation degrades to none — never an unisolated parallel run.

Fixes two shipped Phase-2 descriptors that per-host research found would fail at spawn: kimi lacked its headless flag (would launch the interactive TUI and hang the orchestrator), and kimi-code named a non-existent binary (Kimi Code installs as 'kimi'). Adds the worktree-path root confinement Phase 2 deferred here, and leading-dash guards on the resolver's prompt/cwd matching the existing git-argument guard.

Closes #2627

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 01:50:20 -04:00
Tom Boucher
b97b3dad21 fix(#2517): plan-phase omits model= when *_model is inherit/empty (port execute-phase fix) (#2634)
* test(#2517): guard plan/execute-phase omit model= when *_model inherit/empty

* fix(#2517): plan-phase omits model= when *_model is inherit/empty (port execute-phase fix)

* chore(#2517): backfill changeset pr to 2634
2026-07-24 23:44:12 -04:00
Tom Boucher
c2b498e452 fix(#2498): pass --no-track when branching off origin/$DEFAULT_BRANCH (#2628)
* test(#2498): guard branch-creation uses --no-track (all workflows)

* fix(#2498): pass --no-track when branching off origin/$DEFAULT_BRANCH

* chore(#2498): backfill changeset pr to 2628
2026-07-24 22:49:29 -04:00
Tom Boucher
57b2bd8368 fix(#2491): finish todos/done -> todos/completed rename (14 stale refs) + guard (#2626)
* test(#2491): add todos/done rename under-sweep guard

* fix(#2491): finish todos/done -> todos/completed rename (14 stale refs)

* chore(#2491): backfill changeset pr to 2626

* test(#2491): fix lint-legacy-dir-name + allow-test-rule-refs (split legacy token, add issue ref)
2026-07-24 21:36:33 -04:00
Tom Boucher
4a66d62d10 feat(#2584): Phase 2 — worktree create verb + orchestrator-exec resolver (#2625)
* feat(#2584): Phase 2 — worktree create verb + orchestrator-exec resolver

Phase 2 of the negotiated executor-isolation feature (ADR-1239 Codex-binding amendment). Two building blocks for `dispatch.isolation: orchestrator-worktree` hosts, both unconsumed — no scheduler wires them yet (that is Phase 3), so no runtime behavior changes.

worktree create verb (planWorktreeCreate / executeWorktreeCreatePlan / cmdWorktreeCreate in worktree-safety.cts, routed via routeWorktree in gsd-tools.cjs): validates the wave base, creates a bounded branch+worktree, records it in the run manifest reusing record-agent 4-field entry shape, returns the executor working directory. Bounded git (10s timeout, degrade-not-throw); all manifest read/parse/validate/dedupe precedes the single git side effect (no unmanifested-orphan on a bad manifest); timeout-only best-effort partial rollback (a clean collision-exit never removes a live peer worktree); fail-closed on bad base, unsafe leading-dash / .. inputs, and malformed/mis-shaped manifest.

resolveOrchestratorExec (host-integration.cts): pure descriptor->argv resolver reading the new runtime.orchestratorExec descriptor field (codex/opencode/kimi/kimi-code), fail-closed on missing/invalid shape. Validator (capability-validator.cjs) + a parity guard asserting every orchestrator-worktree host declares a resolvable orchestratorExec.

Adding the create route edits the installed gsd-core/bin/gsd-tools.cjs, so the golden-install-parity fixtures for all 19 runtimes are regenerated (npm run gen:golden) — the only changed hash is gsd-tools.cjs. CONTEXT.md glossary updated; capability-registry regenerated. Behavioral tests (worktree-safety + host-integration) incl. a fast-check property test and the parity sweep.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore: rebuild tracked state-transition.cjs to match #2400 source

The tracked compiled artifact drifted from src/state-transition.cts: #2400 (commit 2bcfaa2e2) added the progress.total_plans frontmatter sync to source but the tracked bin/lib/state-transition.cjs was never rebuilt, so the fix was not shipping to consumers of the compiled artifact. The mandatory build:lib step for Phase 2 surfaced the drift; recompiling makes the already-merged, already-changelogged #2400 fix effective. Artifact-only resync (no source/test change); drift class tracked by #2591.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 21:23:06 -04:00
Tom Boucher
ec7978c0b4 feat(#2584): add dispatch.isolation sub-field, descriptors, validator + negotiation (#2604) 2026-07-24 12:51:42 -04:00
0xdhx
bd18a593a9 fix(#2494): capture stderr and guard empty output on the claude and gemini reviewer legs (#2592)
* fix(#2494): capture stderr and guard empty output on the claude and gemini reviewer legs

The gemini and claude blocks in gsd-core/workflows/review.md were the only
two of the ten prompt-fed reviewer legs with both `2>/dev/null` and no
empty-output guard. Any failure that wrote no stdout — CLI missing,
unauthenticated, rate-limited, crashed — left a zero-byte review file with
the only diagnostic evidence already discarded.

write_reviews substitutes each file's raw content into a `## <Reviewer>
Review` section with no marker separating "empty/failed" from "ran cleanly,
nothing to report", so a failed lane silently degraded the advertised
N-reviewer consensus to N-1 while present_results reported success. Both
/gsd:review and /gsd:plan-review-convergence share the invoke_reviewers
step, so both were affected.

Both legs now redirect stderr to a `.err` sidecar and write a diagnostic
stub with the captured stderr appended when the review file comes back
empty — the same shape the codex and cursor legs already use.

Scope limit, noted in the block comment: the guard only runs if the block
itself completes. A host Bash-tool timeout that kills the whole block skips
it, the same hard bound the OpenCode block already documents. The existing
timeout guidance (#2194) covers that case and is unchanged.

Regression test extracts the two dispatch blocks verbatim from the workflow
and runs them under bash against a failing CLI stub, asserting the review
file is non-empty and carries a diagnosable message plus the captured
stderr. It fails against pre-fix review.md (4 of 5 cases).

Golden install-parity fixtures and the workflow size baseline are
regenerated: one review.md hash per fixture, one size entry.

Fixes #2494

* fix(#2494): stamp the changeset fragment with the filed PR number

The fragment was authored with the documented `pr: 0` placeholder because
the PR number does not exist until `gh pr create` returns. Now that this
PR is #2592, stamp it — scripts/changeset/parse.cjs requires pr > 0, so
the placeholder would fail the Changeset Required check.
2026-07-24 12:50:34 -04:00
Tom Boucher
3b15a1e3cc fix(#2576): normalize padded vs unpadded resolves_phase in close_phase_todos (#2597)
* test(#2576): add failing regression for padded resolves_phase compare

* fix(#2576): normalize padded vs unpadded resolves_phase in close_phase_todos

* chore(#2576): backfill changeset pr to 2597

* test(#2576): drop fast-check property test (cross-platform-fragile on Windows CI)
2026-07-24 12:10:32 -04:00
Tom Boucher
7d298d6d4d fix(#2474): gate worktree dispatch on project-level USE_WORKTREES too (#2561)
* test(#2474): update dispatch gate test for dual-gate behavior

The #2772 test asserted the gate reads USE_WORKTREES_FOR_PLAN only.
Update to accept the dual-gate (USE_WORKTREES + USE_WORKTREES_FOR_PLAN).

* fix(#2474): gate worktree dispatch on project-level USE_WORKTREES too

The per-plan dispatch condition checked only USE_WORKTREES_FOR_PLAN
(submodule-derived), ignoring the project-level USE_WORKTREES flag.
Add USE_WORKTREES to the gate. Net-negative edit: compress two
nearby prose lines to offset the added shell condition (93353 bytes,
down from 93368).

Closes #2474

* docs(#2474): backfill changeset PR number (2561)

* fix: merge coverage gate into single-process check (#2474)

The test:coverage:unit script chained two c8 invocations with &&:
the first ran tests and wrote coverage data to .nyc_output/, the
second read that data for per-file branch checks. On fast CI runners
(ubuntu/24), the second process started before the filesystem flushed
the first process's writes — a classic TOCTOU race that caused
intermittent coverage gate failures.

Replace the two-process chain with a single c8 invocation that
generates both text and json-summary reports, followed by a Node
script (scripts/check-coverage-gate.cjs) that reads the JSON summary
once and checks both overall and per-file thresholds. No filesystem
race is possible because the JSON report is fully written before the
check script reads it.
2026-07-23 09:14:02 -04:00
Tom Boucher
0ad3c5dd14 fix(#2279): refresh date stamps on map-codebase Update runs (#2550)
* fix(#2279): reword date stamping to overwrite existing dates on Update runs

The map-codebase agent and workflow instructions only said to replace
[YYYY-MM-DD] placeholders, but Update-path files already contain concrete
dates from the prior run. Reword to SET the date stamps unconditionally,
overwriting whatever date is already there.

Closes #2279

* docs(#2279): backfill changeset PR number (2550)
2026-07-23 07:37:37 -04:00
Tom Boucher
200daa456c fix(#2269): add --files to three unscoped query commit call sites (#2549)
* test(#2269): regression test for query commit --files scoping

Verify secure-phase.md, validate-phase.md, and next.md all pass --files
to their query commit calls.

* fix(#2269): add --files to three unscoped query commit call sites

secure-phase.md, validate-phase.md, and next.md were the only 3 of 65
query commit call sites that omitted --files, causing blanket staging
of .planning/ and committing unrelated files. Add --files with the
specific artifact path to each.

Closes #2269

* docs(#2269): backfill changeset PR number (2549)
2026-07-23 07:37:15 -04:00
Tom Boucher
77bf21b3a6 fix(#1995): widen worktree branch regex to accept agent-<id> namespace (#2548)
* test(#1995): regression test for agent-<id> branch namespace

Add failing-first tests proving that normalizeCleanupManifestEntry and
planWorktreeRecordAgent reject Claude Code's current agent-<id> isolation
branches (only worktree-agent-<id> is accepted). Boundary tests cover both
namespaces plus rejection cases.

* fix(#1995): widen worktree branch regex to accept agent-<id> namespace

Claude Code's isolation="worktree" branch naming changed from
worktree-agent-<id> to agent-<id>. Widen the regex in all 7 locations
from ^worktree-agent-[A-Za-z0-9._/-]+$ to ^(worktree-)?agent-[A-Za-z0-9._/-]+$
so both namespaces are accepted. Introduce a shared WORKTREE_AGENT_BRANCH_RE
constant in src/worktree-safety.cts to prevent future drift.

Closes #1995

* fix(#1995): update workflow guards, test assertions, and baselines

Widen the branch-check regex in execute-phase.md and execute-plan.md.
Update all test assertions that checked for ^worktree-agent- to expect
the widened ^(worktree-)?agent- pattern. Regenerate golden-install-parity
fixtures, agent-size-baseline, and workflow-size-baseline.

Closes #1995

* fix(#1995): update extractCwdGuardBash sanity check for widened regex

The e2e test's sanity check verified the extracted bash block contained
'worktree-agent-'. After widening to '(worktree-)?agent-', update the
check to match the new pattern.

* fix(#1995): widen missed workflow-guard branch check + changeset + lint fixes

- hooks/gsd-workflow-guard.js: widen startsWith('worktree-agent-') to
  /^(worktree-)?agent-/ regex — same defect class, was missed in prior commit
- tests/worktree.test.cjs: fix indentation regression from prior edit
- Add .changeset/1995-worktree-agent-branch-namespace.md (pr:0 placeholder)

Found by orthogonal code review (Step 4).

* fix(#1995): regenerate golden + size baselines for workflow-guard change

* docs(#1995): backfill changeset PR number (2548)
2026-07-23 07:36:53 -04:00
Tom Boucher
d579daa3ed docs(#2505): Phase 6 — migration guide + built-in-only subagent-toolkit enum (#2538)
* docs(#2512): Phase 6 — migration guide + built-in-only subagent-toolkit enum

* fix #2512: update CONTRACT-PIN for built-in-only subagentToolkit value

* docs(changeset): backfill PR #2538 for Phase 6 (#2512)
2026-07-22 14:30:26 -04:00
Tom Boucher
f654c24a3e feat(#2505): Phase 4 — runtime-aware subagent dispatch (Option A; resolve-dispatch-type query) (#2525)
* feat(#2508): Phase 4 Option A — runtime-aware subagent dispatch via resolve-dispatch-type query (#2505)

* fix(#2508): prose-variant preamble (avoid scanner-tripping literals) + namedDispatch===false-only mapping

* fix(#2508): remove leftover old-preamble lines (keep prose variant only)

* fix #2508: prose-only reference file

* test #2508: regen golden install parity after workflow preamble additions

* fix #2508: remove preamble from plan-phase.md (Phase 6 capstone ceiling); regen size+golden baselines

* docs(changeset): backfill PR #2525 for Phase 4 (#2508)
2026-07-22 10:22:42 -04:00
Tom Boucher
c2a305c44d feat(#2505): Phase 2 — kimi-code Agent Skills install layout (#2520)
* feat(#2454): PR 2 — kimi-code Agent Skills converter + install layout

PR 1 registered the kimi-code EoS descriptor with empty artifactLayout
(SKIP_INSTALL_CONTRACT excluded it from the end-to-end install test).
PR 2 fills in the install surface:

- src/runtime-artifact-conversion.cts: new convertClaudeCommandToKimiCodeSkill
  function. Today it delegates to convertClaudeCommandToKimiSkill (Python
  kimi-cli) because Kimi Code uses the same Agent Skills format + /skill:
  invocation per official docs. The distinct function name lets a future
  divergence land cleanly if Kimi Code's skill format evolves independently.
- gsd-core/bin/lib/capability-validator.cjs: add to ALLOWED_SKILLS_CONVERTERS.
- capabilities/kimi-code/capability.json: artifactLayout.global now declares
  the skills kind with converter='convertClaudeCommandToKimiCodeSkill' +
  home='.kimi-code' (auto-discovered at ~/.kimi-code/skills/ per Kimi Code
  docs: merge_all_available_skills = true default).
- tests/installer-migration-install.integration.test.cjs: REMOVE the
  SKIP_INSTALL_CONTRACT exclusion — kimi-code now has a full install surface.
- Regenerated capability-registry + capability-matrix + golden install
  parity + install tree fixtures for kimi-code.

* fix(#2454): wire kimi-code converter into SKILLS_CONVERTER_REGISTRY + count bump

- src/install-engine.cts: add convertClaudeCommandToKimiCodeSkill to
  SKILLS_CONVERTER_REGISTRY so the layout-driven skills install path
  can dispatch off the descriptor's converter string.
- tests/capability-registry.test.cjs: bump VALID_CONVERTER_NAMES count
  26 → 27 (added convertClaudeCommandToKimiCodeSkill).

* fix(#2454): remove home override from kimi-code skills (inherit configDir)

The home:'.kimi-code' override made the install plan resolve skills dest
to ~/.kimi-code/skills instead of <configDir>/skills, causing the test's
temp configDir to miss the install. Removing it lets skills inherit
configDir like most runtimes.

* fix(#2454): kimi-code install contract surface is flat-skills (no agents)

Kimi Code has NO custom named subagents (per official docs: 3 built-in
coder/explore/plan only). The kimi-skills-agents surface expects agents/
gsd.yaml + subagents/*.yaml which kimi-code does not produce. Changed
to flat-skills which only checks for skills/gsd-* dirs.

* docs(changeset): Phase 2 kimi-code install layout Added (#2509)

* docs(changeset): backfill PR #2520 for Phase 2 (#2509)
2026-07-22 00:58:58 -04:00
Tom Boucher
bf8f320083 feat(#2505): Phase 1 — EoS descriptor split (kimi-code capability.json + drift-guard registration) (#2519)
* feat(#2454): add kimi-code as an EoS capability (Node Kimi Code CLI)

PR 1 of N for #2454. Establishes the EoS descriptor foundation for splitting
GSD's kimi support into two distinct products per the user's directive:
- kimi       (existing): Moonshot's Python kimi-cli (~/.kimi, runtime: python)
- kimi-code  (new):      Moonshot's Node Kimi Code CLI (~/.kimi-code,
                         runtime: node, KIMI_CODE_HOME env)

Per ADR-1239 EoS, runtime behavior is driven by capabilities/<id>/capability.json
descriptors, not hardcoded branches in install.js. The new descriptor uses
the existing primitives (dot-home configHome, skills artifactLayout, kimi-hooks-toml
hooksSurface — same TOML [[hooks]] format Kimi Code reads per its docs).

Critical Kimi Code constraint reflected in the descriptor:
  hostIntegration.dispatch.namedDispatch: false
  hostIntegration.dispatch.builtInSubagents: ['coder', 'explore', 'plan']
  hostBehaviors.namedSubagentsSupported: false
Kimi Code's official docs confirm only 3 built-in subagents with NO custom-
subagent registration (the [subagent] table only has timeout_ms). The
kimi-agents YAML layout (used by Python kimi-cli) is therefore NOT in
kimi-code's artifactLayout.

Schema adjustments:
- subagentToolkit set to 'undocumented' (the existing escape hatch); the
  schema enum (full/read-only) lacks a 'limited'/'built-in-only' value.
  A follow-up PR can extend the schema enum to add 'built-in-only' as a
  first-class axis value reflecting Kimi Code's documented model.

Registration:
- capabilities/kimi-code/capability.json (new descriptor, modeled on codex)
- bin/install.js: allRuntimes array + --all list + --kimi-code flag
- gsd-core/bin/shared/runtime-aliases.manifest.json: kimi-code aliases
  (kimi-code, kimicode, kimi_code)
- src/runtime-name-policy.cts: FALLBACK_ALIASES map
- gsd-core/bin/lib/capability-registry.cjs: regenerated via
  scripts/gen-capability-registry.cjs --write

Tests:
- tests/multi-runtime-select.test.cjs updated for the new runtime count (18)
  + new --kimi-code flag test + 'All' shortcut renumbered 18 → 19.

Out of scope for PR 1 (follow-up PRs in the sequence):
- Install-time decision logic (kimi vs kimi-code detection / prompt)
- agent-install-check semantics for kimi-code (verify Agent Skills presence)
- cmdAgentSkills fallback returning subagent prompt content
- Workflow template mapping (named agents → built-in coder/explore/plan)
- Migration guidance for users currently on 'kimi' who are actually on Kimi Code
- Schema enum extension for subagentToolkit: 'built-in-only'

Refs #2454, #2095 (EoS/kimi migration epic), ADR-1239 (EoS).

* fix(#2454): complete drift-guard registrations for kimi-code runtime

The drift guards caught every surface that pins runtime enumeration. Each
update is mechanical, driven by the guard's named failure mode:

- src/runtime-name-policy.cts RUNTIME_LABELS: 'Kimi Code' label for kimi-code
- src/runtime-name-policy.cts RUNTIME_FLAG_IDS: add kimi-code to the
  isKimiCode predicate generator
- bin/install.js runtimeMap: option '11' → 'kimi-code', renumber downstream
  entries (11..17 → 12..18), ALL_RUNTIMES_OPTION 18 → 19
- gsd-core/bin/shared/model-catalog.json runtimeTierDefaults: kimi-code entry
  (null/null/null — same as kimi, no model tier defaults until configured)
- docs/reference/capability-matrix.md: regenerated via
  scripts/gen-capability-matrix.cjs --write (kimi-code row added)
- tests/global-config-home-fragment.test.cjs GOLDEN_FRAGMENT_MAP:
  kimi-code → '.kimi-code'
- tests/fixtures/golden-install-parity/*.json: regenerated via npm run gen:golden
  (the runtime-aliases.manifest.json hash changed; all 17 runtime fixtures updated)

The capability-registry is already regenerated from the prior commit.

* test(#2454): update drift-guard tests for kimi-code runtime registration

Multiple drift guards pin runtime enumeration counts and option numbering.
Each update is mechanical, driven by the guard's named failure mode:

- tests/runtime-flags.test.cjs: EXPECTED_FLAGS gains isKimiCode (16 → 17);
  'all 16 flags' → 'all 17 flags' in test names + messages.
- tests/multi-runtime-select.test.cjs: parseRuntimeInput option renumbering
  cascade — kilo moves 11→12, opencode 12→13, pi 13→14, qwen 14→15,
  trae 15→16, windsurf 16→17, zcode 17→18, All 18→19. New single-choice
  test for kimi-code (option 11). Prompt test updated for new numbering.
- tests/host-integration-descriptors.test.cjs: EXPECTED_PROFILES gains
  kimi-code → 'programmatic-cli' (terminal CLI per Kimi Code docs);
  EXPECTED_FLATTEN gains kimi-code → false (backgroundDispatch:true per
  docs, same as Python kimi/opencode).
- tests/global-config-home-fragment.test.cjs: table-count test renamed
  13 → 14 table runtimes (kimi-code added to GOLDEN_FRAGMENT_MAP earlier).

* fix(#2454): empty artifactLayout for kimi-code (PR 1 scope)

The skills kind requires a converter (existing converters are per-runtime
like convertClaudeCommandToKimiSkill). PR 1 of this multi-PR sequence only
registers the descriptor; the actual Agent Skills converter (and a new
'convertClaudeCommandToKimiCodeSkill' function) lands in PR 2 alongside
the install-time decision logic. Empty artifactLayout.global is valid and
means 'nothing to install yet via the layout seam'.

Also: added kimi-code to RUNTIME_META in tests/helpers/install-shared.cjs
(localDir .kimi-code, globalSuffix .kimi-code), and added Kimi Code as
option 11 in install.js's buildRuntimePromptText (renumbered downstream
options 11..17 → 12..18, All 18 → 19).

* fix(#2454): camelCase runtimeFlags for hyphenated ids (kimi-code → isKimiCode)

The runtimeFlags generator previously produced 'isKimi-code' (hyphen preserved)
for the new kimi-code runtime id. Property names with hyphens are awkward for
consumers (flags['isKimi-code'] instead of flags.isKimiCode). The new
runtimeIdToFlagName helper folds -[a-z] boundaries to uppercase, producing
the conventional PascalCase flag name. The 16 prior single-word runtime ids
are unaffected (the regex finds no hyphens).

* fix(#2454): update remaining drift-guard tests + gen kimi-code fixtures

- tests/runtime-flags.test.cjs drift guard: use proper kebab-case
  conversion (isKimiCode → kimi-code, not 'kimicode') so the registry
  comparison doesn't false-positive on hyphenated runtime ids.
- tests/multi-runtime-select.test.cjs: fix kilo/opencode/pi/qwen/trae
  single-choice tests for the renumbered options (kilo 11→12, opencode
  12→13, pi 13→14, qwen 14→15, trae 15→16).
- tests/install.test.cjs: Kilo integration option 11→12, prompt test
  regex updated.
- tests/fixtures/golden-install-parity/kimi-code.json + install-tree/
  kimi-code.json: generated via UPDATE_GOLDEN=1 + UPDATE_INSTALL_TREE=1.
  The kimi-code install produces the standard GSD install layout (skills,
  contexts, references, etc.) — 436 paths, same shape as other runtimes
  that have no custom converter yet.

* fix(#2454): add kimi-code install contract + global config home fragment

- src/runtime-name-policy.cts GLOBAL_CONFIG_HOME_FRAGMENTS: add kimi-code
  → '.kimi-code' so getGlobalConfigHomeFragment returns the correct path
  instead of falling through to the default '.claude'.
- tests/installer-migration-install.integration.test.cjs
  RUNTIME_INSTALL_CONTRACTS: kimi-code entry (same surface as kimi for
  PR 1; PR 2 will specialize once the Agent Skills converter lands).
- tests/multi-runtime-select.test.cjs: fix space-separated-choices test
  for the renumbered kilo option (11 → 12).
- tests/fixtures/golden-install-parity/kimi-code.json + install-tree/
  kimi-code.json: regenerated after rebasing onto current next (new
  planner-reversibility.md from #2471 etc. now included).

* test(#2454): skip kimi-code install contract until PR 2 ships install layout

The end-to-end install test (tests/installer-migration-install.integration
.test.cjs) asserts every allRuntimes entry installs a runtime-specific
artifact surface. PR 1 of #2454 registers kimi-code in allRuntimes + the
capability descriptor + flags + labels, but the install LAYOUT (Agent
Skills converter + global AGENTS.md at $KIMI_CODE_HOME/AGENTS.md) lands
in PR 2. The SKIP_INSTALL_CONTRACT set marks this exclusion explicit and
self-removing — PR 2 removes the entry alongside adding the install
surface, restoring the contract loop to full coverage.

* fix(#2454): restore compact model-catalog.json format (M1 review)

Per code-review M1: my prior 'fix(#2454): complete drift-guard registrations'
commit used python json.dump(indent=2) which inflated the file from 165→607
lines (every nested entry got expanded) and lost the trailing newline. The
semantic change was just a 3-line kimi-code entry. Restored the original
hybrid format (top-level indent=2 + inner entries' one-line style) and
added kimi-code in matching form.

Regenerated golden install parity + install tree fixtures since the
model-catalog.json hash changed.

* fix(#2454): update CONTEXT.md allRuntimes glossary (17 → 18, add kimi-code)

CI lint-tests job failed on the glossary drift guard
(scripts/check-glossary-refs.cjs --check):
  ✗ CONTEXT.md's allRuntimes enum-count sentence claims 17 values but
    bin/install.js's allRuntimes array has 18.
  ✗ CONTEXT.md's allRuntimes member list has drifted from bin/install.js
    (missing from CONTEXT.md's list: kimi-code).

Missed in the prior commits because gsd-test does not run the glossary
check (it's a CI lint-tests-only check). Updating CONTEXT.md's two claims
to 18 values + kimi-code in the member list.

* chore(#2505): regen capability-registry + stamp kimi-code version 1.8.0 (#2511)

* docs(changeset): Phase 1 kimi-code runtime Added (#2511)

* test(#2511): regen kimi-code golden parity fixture after Phase 0 guard normalization lands

* docs(changeset): backfill PR #2519 for Phase 1 (#2511)
2026-07-22 00:27:35 -04:00
Tom Boucher
d39ca0c004 Merge pull request #2490 from open-gsd/docs/2481-adr-1239-effort-axis
feat(#2481): add a negotiated effortSurface axis and wire invocation-time effort
2026-07-21 20:14:27 -04:00
github-actions[bot]
8cf747724f chore: sync next package version to 1.8.0 2026-07-22 00:06:08 +00:00
Tom Boucher
09b535ac00 feat(#2481): add a negotiated effortSurface axis and wire invocation-time effort
ADR-1239 gains a ninth negotiated axis, effortSurface (argv | none), declaring how
a host accepts reasoning effort. ADR-443 is amended in the same change because its
recorded deferral is what the axis resolves: its Unblock condition offered paths
(a) and (b) and stated the choice was 'a maintainer call this file records but does
not make'. Path (a) is selected and satisfied here.

Before this, effort reached a runtime only through install-time channels
(EFFORT_RENDERING's frontmatter/api), so reviewer CLIs spawned as subprocesses
silently inherited whatever effort sat in the user's own global CLI config. The
review lane now resolves one universal effort through the ADR-443 cascade and
renders it per host through the negotiated descriptor.

Every per-host value is documentation-sourced, never inferred:
- claude   argv  -- verified via 'claude --help' (--effort <level>)
- opencode argv  -- verified via 'opencode run --help' (--variant)
- codex    argv  -- codex-rs/exec/src/cli.rs: model_reasoning_effort is NOT a CLI
                    flag (config.toml key only), so the global -c override is the
                    only argv route
- 15 hosts undocumented -- their docs state no reasoning setting; the sentinel
                    fails closed rather than inheriting a profile baseline

No config-file vocabulary member: the only host that ever had one (Gemini CLI's
thinkingConfig) was removed as a sunset runtime by 8f2ebbe9b (#1928, PR #1996),
and neither Antigravity CLI nor ZCode documents a reasoning setting.

Closes #2481

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 19:10:22 -04:00
Tom Boucher
c5e0371775 feat(#1951): reversibility tagging — gate one-way-door decisions (#2471)
* test(#1951): add failing-first tests for reversibility tagging

Red phase for issue #1951 (reversibility tagging: classify decisions by
undo cost, gate one-way doors behind a checkpoint:decision).

Tests assert, per the issue's acceptance criteria:
- discuss-phase CONTEXT.md template records a **Reversibility:** field with
  a rationale on captured decisions, and states it is optional
- gsd-planner @-references planner-reversibility.md and stays under the
  49152-char agent cap (LARGE_CAP, tests/agent-size-budget.test.cjs)
- a one-way rating inserts a checkpoint:decision before the dependent task;
  reversible inserts none; costly is flagged but never blocks
- the taxonomy defaults to reversible when unsure (checkpoint-fatigue guard)
  and inserting a checkpoint implies autonomous: false
- docs/reference/plan-md.md documents <reversibility> as optional with all
  three ratings
- --no-reversibility-gates parses to REVERSIBILITY_GATES=false, is injected
  into the planner prompt, and is advertised in the command argument-hint
  and help full mode (argument-hint parity)
- the override suppresses the gate but still persists the rating
- cmdVerifyPlanStructure accepts every rating and the absent case
  (additive-validator guarantee, behavioral via runGsdTools)
- parity: thinking-models-planning.md #4 adopts the canonical three-level
  taxonomy and the binary REVERSIBLE/IRREVERSIBLE vocabulary is gone
- no content loss from the planner extraction made to fit under the cap

Prose-contract assertions are Red until the implementation lands. The
behavioral validator assertions pass immediately — regression guards
proving the validator already accepts unknown optional tags.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(#1951): reversibility tagging — gate one-way-door decisions

Classify planning decisions by what undoing them would cost, and give a
one-way door a human beat before the agent walks through it (issue #1951,
The Pragmatic Programmer Topic 15 'Reversibility'; Bezos's one-way/two-way
door framing).

Acceptance criteria met:
- discuss-phase records an optional reversibility rating with a rationale
  on <decisions> entries in the phase CONTEXT.md template. Unrated
  decisions are treated as reversible, so existing phases are unaffected.
- a one-way rating makes gsd-planner insert a checkpoint:decision before
  the task that implements the decision, reusing the existing checkpoint
  mechanism -- no new checkpoint machinery.
- reversible ratings trigger no checkpoint; costly ratings are flagged in
  the plan but never block.
- the rating persists on the task as the optional <reversibility rating=>
  element. cmdVerifyPlanStructure accepts every rating and the absent
  case; the structural validator does not reject unknown optional tags.
- --no-reversibility-gates (REVERSIBILITY_GATES=false) suppresses
  checkpoint insertion for intentionally-unattended runs while still
  recording ratings -- the override changes what stops the run, not what
  the plan remembers.

Single taxonomy, not two: references/thinking-models-planning.md #4
already shipped a binary REVERSIBLE/IRREVERSIBLE classification and is
loaded by both gsd-planner and gsd-plan-checker. It is rewritten onto the
canonical three-level vocabulary and now points at planner-reversibility.md
as the taxonomy owner, with a parity test that fails if the surfaces
diverge (DEFECT.GENERATIVE-FIX-DIVERGENCE).

agents/gsd-planner.md sat 47 chars under the 49152 LARGE_CAP, so the
checkpoint DO/DON'T guidance was relocated verbatim into
planner-antipatterns.md -- already @-referenced from the same section for
the same topic, so the planner still loads it and nothing was dropped. A
test guards the relocation against content loss.

Files: gsd-core/references/planner-reversibility.md (NEW, canonical
taxonomy + emission rules + anti-patterns), gsd-planner.md, plan-phase
workflow/command/help (flag wiring + parity), plan-md.md schema,
discuss-phase context template, CONTEXT.md glossary, INVENTORY + manifest,
size baselines, install goldens, plugin skills regen, changeset.

Closes #1951

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(#1951): address orthogonal review findings

Two isolated reviewers (correctness + security), neither of which authored
the change. Every finding fixed:

Security — the rationale is untrusted input (ADR-1577). It originates in
conversation and flows CONTEXT.md -> planner -> PLAN.md -> executor, each
hop an LLM reading the previous hop's output, with no validation on the
path. planner-reversibility.md and the discuss-phase template now state
it is data and never instructions, and name the </reversibility>
early-termination hazard explicitly -- a rationale that closes its own
element injects sibling structure the executor reads as real tasks.
Four tests guard it.

Correctness 1 — nothing machine-enforced the feature's own promise: a task
rated one-way with no preceding checkpoint:decision validated as fully
clean, so a planner error silently reopened the gap this feature exists to
close. cmdVerifyPlanStructure now warns on an ungated one-way rating. A
warning, not an error: <reversibility> stays additive and the plan stays
valid. Four tests cover ungated (warns), gated (silent), still-valid, and
reversible/costly never flagged.

Correctness 2 — pass-always test. The --no-reversibility-gates parse test
substring-matched the whole workflow file, and plan-phase.md prose mentions
both tokens in one sentence, so it passed with the bash conditional
deleted: it was testing the documentation, not the parser. Now scoped to
the fenced bash blocks and matched as one physical line, with a negative
control confirming prose alone cannot satisfy it.

Correctness 3 — costly had no itemized emission rule, only one-way did, so
two agents could diverge on whether to tag costly at all.

Correctness 4 — template convention break: the example ratings were bare
while every sibling field uses [...] to signal substitution, inviting an
LLM to copy one-way/costly forward as boilerplate. Now bracketed.

Correctness 5 — latent false-green: .includes('reversible') also matches
inside irreversible/irreversibility, which appear in anti-pattern
prose, so a surface that dropped the real taxonomy entry would still pass.
Now word-boundary matched.

ADR-857 phase-6 ceiling — the first gsd-test run caught plan-phase.md
1216 bytes over its frozen 94519 ceiling (it had 49 bytes of headroom on
next). The ceiling may only rise for privileged host machinery, and
reversibility gating is optional-feature logic, so the wiring was slimmed
to its minimum and the explanatory prose moved to the reference files the
planner already loads. plan-phase.md is now 94400 bytes -- 119 under the
ceiling and 70 bytes SMALLER than on next, so the host loop shrank while
gaining the feature, which is what phase 6 ratchets toward. The tracer
contract (tests/tracer-bullet.test.cjs) is unchanged.

Lint — fixed an unnecessary non-null assertion in verify.cts and a
CRLF-fragile bare \n regex in the new test (DEFECT.WINDOWS-CRLF-TEST-
PORTABILITY, the #1658/#1668/#2206/#2449/#2450 class).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(#1951): checkpoint fixture must carry the common task elements

The gated-one-way fixture built a checkpoint:decision task from the
abbreviated skeleton in gsd-planner.md, which shows only the
checkpoint-specific elements (<decision>/<context>/<resume-signal>).
cmdVerifyPlanStructure requires <name> and <action> on EVERY task
regardless of type, so the fixture failed validation for reasons that had
nothing to do with reversibility:

  errors: ["Task missing <name> element", "Task 'unnamed' missing <action>"]

Caught by gsd-test on 14d14a39 (2 failures, both this fixture).

The canonical shape is in tests/verify.test.cjs:266 — a checkpoint task
carries <name>/<files>/<action>/<verify> like any other. Fixture corrected
to match. Verified behaviorally against the real gsd-tools CLI across all
four cases: gated one-way (valid, silent), ungated one-way (valid, warns),
costly (valid, silent), absent (valid, silent).

Not a product defect: the validator's every-task contract is intentional
and pre-existing, and docs/reference/plan-md.md scopes its required-element
list to type=auto/tracer only because those are the elements a planner must
author, not because checkpoints are exempt from <name>.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(#1951): backfill changeset pr number to 2471

* fix(#1951): CodeQL incomplete-sanitization + prompt-injection scan collision

Both CI failures were real defects in code this PR added, not false
positives.

CodeQL js/incomplete-sanitization (high), reversibility-tagging.test.cjs:46 —
the namesRating helper built its regex with `rating.replace(/[-]/g, '\\-')`,
which escapes the hyphen but not backslash, so the escape was incomplete.
It was also unnecessary: `-` carries no special meaning outside a character
class. Replaced with a complete metacharacter escape (backslash included).
Word-boundary behavior verified unchanged across all three ratings — notably
that "irreversible" prose still does not satisfy a "reversible" match, which
is the false-green this helper exists to prevent.

Prompt injection scan — the checkpoint fixture used the human-verification
child element inside <verify>. That tag name is a fake-instruction-boundary
pattern in scripts/prompt-injection-scan.sh, and the scan runs over changed
files, so copying the shape from tests/verify.test.cjs (unflagged only
because it is not in this diff) tripped the gate. Switched to the documented
plain-prose <verify> form.

The first attempt at that fix failed the same gate a second time: the
comment explaining the collision quoted the offending tag literally. The
comment now names it in prose instead — the scanner does not care whether a
match is code or commentary, which is the whole point of the
DEFECT.PROMPT-INJECTION-SCAN-COLLISION note in CLAUDE.md.

Verified locally before push: scan reports 0 findings across 57 changed
files, eslint clean, and both fixtures still validate as designed (gated
one-way silent, ungated one-way warns, neither errors).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(#1951): record measured cost and halve gsd-tools spawns

The Windows shard 1/3 job timeout was traced to the sharding layer, not to
this PR's assertions — see #2472. Two contributing factors were this file's
own, and are fixed here.

1. tests/test-timings.json had no entry for reversibility-tagging.test.cjs,
   so scripts/run-tests.cjs weighted it at the table's median fallback
   (~315ms) for LPT chunk packing. It actually measures 5595ms — an 18x
   under-weight. Recorded the measured value from the green gsd-test run
   (max across the node22/node24 lanes, per gen-test-timings.cjs's
   convention). Only this one entry: a full regen churns 634 entries of
   run-to-run drift, and the table is explicitly advisory and un-gated, so
   a 637-line diff does not belong in a feature PR.

2. Each verifyPlan() spawns gsd-tools, which dominates this file's cost.
   Spawns cut from 9 to 6 with no coverage lost:
   - the ungated-one-way warning and its stays-valid assertion now share
     one plan instead of building the same plan twice;
   - the reversible/costly never-flagged-as-ungated test was strictly
     subsumed by the additive suite, which already runs those two ratings
     ungated and asserts no /reversibilit/ warning at all — and the gate
     warning's text contains both "reversibility" and "one-way", so the
     broader assertion catches it. It only re-spawned gsd-tools twice to
     prove the same thing.

Both are symptom fixes. The shard imbalance itself (19/11/10 minutes
against a 20-minute cap, from a cost-blind round-robin partition that also
reshuffles downstream files whenever one is inserted) is tracked in #2472.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(#1951): checkpoint fixture adopts the #2444 type-branched contract

Surfaced by rebasing onto next, which gained #2444 (branch plan-structure
validation on task type=checkpoint:*) while this PR was in review.

cmdVerifyPlanStructure no longer applies one required-element set to every
task. A checkpoint:decision now requires <name> + <resume-signal> +
<decision> + <options>, and is exempt from the <action>/<verify>/<done>/
<files> set that auto and tracer tasks carry. The gated-one-way fixture
predated that split and failed on the new requirement:

  errors: ["Task 'Task 0: Confirm the on-disk format' missing <options>"]

Fixture rewritten to mirror the checkpoint:decision contract exactly — real
<options> with two <option> children — rather than padding it with fields
checkpoints no longer need. That also drops the plain-prose <verify> the
earlier revision carried purely to dodge the prompt-injection scan; a
checkpoint task has no <verify> requirement at all, so the workaround is
moot.

Verified against the real gsd-tools CLI across all four cases: gated one-way
(valid, silent), ungated one-way (valid, warns), costly (valid, silent),
absent (valid, silent).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 10:44:55 -04:00
Tom Boucher
909a3b180b fix(#2470): install pi's extension as gsd.js so pi actually discovers it (#2478)
* test(#2470): failing-first — pi extension must satisfy pi's auto-discovery filter

pi auto-discovers extensions/ entries through isExtensionFile(), which accepts
only .ts and .js. GSD installs its extension as gsd.cjs, so pi silently skips
it: no /gsd command, no error, no log line.

Encodes pi's discovery PREDICATE rather than a literal filename, so the
contract keeps holding across future renames, and adds the migration-006 test
matrix for retiring the stale gsd.cjs left in pre-fix installs.

Red until the fix lands.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(#2470): install pi's extension as gsd.js so pi actually discovers it

pi auto-discovers extensions/ entries via isExtensionFile(), which accepts
only .ts and .js and skips everything else silently. capabilities/pi declared
the dest as gsd.cjs, so the extension installed correctly and was then ignored
forever: no /gsd command, no error, no log line.

Install it as gsd.js. The in-repo source stays pi/gsd.cjs — tests require() it
directly and .cjs is unambiguous CommonJS; only the installed name has to
satisfy pi, and pi loads accepted files through jiti, which handles CJS and ESM
alike. (The reporter's premise that ~/.pi/agent/package.json declares
"type":"commonjs" does not hold — pi never writes that file.)

Renaming an installed artifact requires a migration record, so add 006 to
retire the stale gsd.cjs from pre-fix installs; without it the old path drops
out of the manifest and uninstall can never remove it. The migration plans
nothing for an unmanifested gsd.cjs: emitting remove-managed there would have
the executor downgrade it to preserve-user and mark it blocked, failing the
install for anyone who hand-placed their own file.

Also pins body-parser >=2.3.0 (GHSA-v422-hmwv-36x6). The advisory reaches the
production tree transitively via the Claude Agent SDK and fails the
npm-integrity gate, blocking any PR; pinned via the existing overrides idiom.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(#2470): address orthogonal review findings + register migration checksum

Code review:
- pi/gsd.cjs's install docstring still told readers to copy the file to
  extensions/gsd.cjs — the exact silently-broken state this PR fixes. Anyone
  following it recreated the bug.
- Two stale extensions/gsd.cjs comments in install-minimal-hooks.test.cjs.

Security review:
- _installNativePluginIfDeclared confined nativePlugin.dir but joined
  nativePlugin.file onto the validated directory unchecked, so a descriptor
  whose file carried .., an absolute path, or a NUL byte would have written
  outside configHome. Not reachable in a shipped build (descriptors are
  first-party and compiled into the capability registry), but file is exactly
  the field this PR changes. Confine the full dest path instead; for a
  well-formed descriptor this resolves identically to the previous
  mkdir(dir) + join(dir, file). Covered by four new write-confinement tests.

Also register migration 006 in the #670 EXPECTED_CHECKSUMS baseline — shipped
migration bodies are locked to a committed checksum and a new migration fails
CI until it is listed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(#2470): never dereference a symlinked managed path when snapshotting

fs.copyFileSync follows symlinks, so a managed path replaced by a link had the
REFERENT's bytes copied into the migration journal's rollback and backup trees
— a gsd.cjs symlinked at a private key would land that key's contents under
gsd-migration-journal/. Deletion was already safe (fs.rmSync unlinks the link,
never the target); the copy was not.

Nothing GSD installs is ever a symlink, so the faithful snapshot of a symlinked
managed path is the link itself. copyPreservingSymlink recreates it, which
keeps rollback fidelity (restore re-creates the same link) while never reading
the referent. Scoped the pre-delete to the symlink branch only, so the
regular-file path keeps copyFileSync's overwrite-in-place and a mid-restore
failure cannot destroy the destination. The restore-side existence check moves
to lstat, since existsSync follows a link whose target is gone and would
silently skip the restore.

This lives in the engine all six migrations share, so 000-005 are hardened too.

Also regenerates the pi golden-parity hash: correcting pi/gsd.cjs's own install
docstring changes the extension's content, which the golden suite caught.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(#2470): symlink-preserve the in-apply failure-recovery restore too

The previous commit routed three copy sites through copyPreservingSymlink but
missed a fourth: the catch block inside applyInstallerMigrationPlan, which
replays rollback snapshots taken earlier in the SAME apply attempt. Those
snapshots are symlinks precisely because of that commit, so the raw
copyFileSync there dereferenced them and wrote the referent's bytes to the LIVE
install path — worse than the journal-tree leak it was meant to fix, since it
is user-visible and at a predictable location.

Verified by experiment rather than assertion: with the pre-fix line restored,
the managed path comes back as a REGULAR FILE containing the referent's bytes;
with the fix it comes back as a symlink and the bytes appear nowhere.

The accompanying test injects the failure by letting the delete succeed and
then throwing once, modelling a later step failing after the delete. That
ordering is load-bearing — an earlier draft injected before the delete, which
leaves the live path in place, so the pre-fix copyFileSync hit a same-file
collision and threw instead of leaking. That draft passed against the bug it
was written to catch; this one fails against it.

Adds the missing rollback() coverage as well: a restored symlinked managed path
must come back as a link pointing at its original target.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(#2470): read the backup location from the journal, not the plan

The new backup-content assertion read backupRelPath off result.plan.actions,
where it is always null: the planner reserves the field and apply chooses the
concrete location, recording it in the journal. The assertion therefore failed
on "backup path must be recorded for the user" rather than on anything about
the behavior it was written to check.

Read it from the journal, which is the authoritative record. Verified by
executing all four new test bodies in-process against the built engine — the
backup file exists and holds the locally patched content.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(#2470): backfill changeset pr number to 2478

* chore(#2470): backfill changeset pr number to 2478

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 08:26:47 -04:00
Tom Boucher
a54feb4216 fix(#2440): per-counter progress ratchet — total_plans always takes derived value (#2468)
* fix(#2440): per-counter progress ratchet — total_plans always takes derived value

Two sites fixed (targeted — existing body-only write tests preserved):

Site A — read path: shouldPreserveExistingProgress (state-document.cts:167)
removed total_plans from the all-or-nothing ratchet check. It now joins
total_phases as an always-derived counter. Only completed_phases and
completed_plans keep ratchet behaviour (they are monotonic). This fixes
gsd-tools query state.json reporting stale total_plans when a curated
completed_plans triggers the ratchet.

Site B — write path: applyStatePreservation (state-transition.cts:162)
gained a deriveProgressKeys opt-in flag. When true (passed by
cmdStatePlannedPhase only), total_plans and total_phases take the
derived (post-sync) value instead of the wholesale curated restore.
When false (the default — state.update, state.patch), the existing
#3242 wholesale protection stays fully in force. This fixes the
state planned-phase verb writing a stale total_plans.

The opt-in approach preserves all 8 existing #3242/#1264/#500 body-only
write tests that assert wholesale progress preservation during non-
progress updates.

Tests:
- tests/state.test.cjs: 4 unit tests for shouldPreserveExistingProgress
  (total_plans upward/downward/equality + completed_plans ratchet active).
- tests/state-transition.test.cjs: 2 #2440 regression tests for
  deriveProgressKeys=true (total_plans takes derived; boundary at equality).
  The existing !resync wholesale-restore test stays unchanged (default
  behavior preserved).

References: #2440; #1446 (total_phases read-path fix — same principle);
#3242 Bug A (body-only preservation — protection preserved via the opt-in
gate); ADR-1769 (applyStatePreservation table-driven preservation).

* chore(#2440): backfill pr:2468 in .changeset/mellow-eagles-chatter.md
2026-07-20 19:24:21 -04:00
Tom Boucher
bb97ffb5aa fix(#2431): self-suppress TDD Audit section when all commits are missing (#2467)
* fix(#2431): self-suppress TDD Audit section when all commits are missing

The TDD Audit section in ship.md step 8 was always emitted — but the
execute pipeline only writes gate_status: git trailers when TDD mode is
active. Without TDD mode (the default), every commit's trailer is absent
and the section normalizes to 100% missing, producing a noise table with
no way to disable it.

Fix: add a self-suppress instruction at the point where gate_status
values are normalized. When every commit in the scan normalizes to
'missing', skip both step 8 (TDD Audit section) and step 9 (aggregate
gate_status trailer) entirely. Only emit when at least one commit carries
a real value (skill, fallback, or exempt).

This is data-driven, NOT config-gated. An earlier iteration used inline
'gsd_run query config-get workflow.tdd_mode' — but workflow.tdd_mode is
owned by the tdd capability, and ADR-857 Phase 6 forbids host loop
workflows from reading capability-owned keys via inline config-get. The
self-suppress approach avoids any config-get entirely; it checks the
actual trailer data and skips when there's nothing real to report.

Matches the triage's suggested approach: 'have the audit gracefully
degrade (skip the section)' when there is no real signal.

Tests: tests/workflow-compat.test.cjs gains 3 #2431 assertions:
- documents self-suppress when every commit is missing
- step 9 (aggregate trailer) is also gated on real values existing
- does NOT read workflow.tdd_mode inline (ADR-857 Phase 6 compliant)

The existing feat-41 assertions still pass — the section content is
preserved, only gated by the self-suppress instruction.

References: #2431; PR #585 (consumer shipped, producer never wired);
ADR-857 Phase 6 (capability-owned config keys must not be read inline by
host loop workflows).

* chore(#2431): backfill pr:2467 in .changeset/clever-moles-frolic.md
2026-07-20 19:24:08 -04:00
Tom Boucher
455ad49ae3 feat(#2296): config-gated provider escalation on quota-exceeded (#2458)
* test(#2296): failing-first coverage for provider escalation on quota-exceeded

Covers the provider-escalation ladder layered onto EXEC.CLASSIFY: back-compat
(no escalation block without --failure-class), cap boundaries at
min(max_escalations, list length) at limit-1/limit/limit+1, opt-in gating,
malformed/hostile provider_escalation config, the --failure-class CLI negative
matrix, config-key registration, and a fast-check budget-limit property.

Red until the resolver, CLI flag, and manifest key land.

Refs #2296

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(#2296): config-gated provider escalation on quota-exceeded

The dynamic_routing tier ladder escalates within one provider, which does not
help when that provider is what ran out of quota. Add an opt-in provider ladder
layered on the existing EXEC.CLASSIFY seam.

- model-resolver: resolveProviderEscalation walks dynamic_routing.provider_escalation
  capped at min(max_escalations, list length), reporting from/to/attempted/exhausted.
  Invalid entries are dropped (ADR 227 shape validation). Stays a leaf module —
  the quota-class policy decision is the caller's, per the CONTEXT.md contract.
- agent-command-router: export a frozen AGENT_FAILURE_CLASSES so the new CLI
  validator cannot drift from the classifier that produces the values.
- resolve-execution: --failure-class flag; emits an escalation block ONLY when
  passed, so the existing JSON contract is byte-identical for every caller.
- config-schema.manifest: register dynamic_routing.provider_escalation.
- execute-phase step 7.1: auto-escalate, honor Retry-After, fail loudly naming
  every model tried once the ladder is spent.

Refs #2296

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(#2296): extract quota recovery to a reference fragment; regen goldens

The step 7.1a addition pushed gsd-core/workflows/execute-phase.md from 93390 to
95111 LF bytes, past the frozen ADR-857 Phase 6 ceiling (hard <93600, margin
<=93400) asserted by tests/fix-2285-claude-orchestration-wiring.test.cjs. The
base sat 10 bytes under the margin, so no inline wording would have fit.

That gate's own rationale is that optional-feature detail belongs in a fragment,
not the host loop. Moved BOTH the new provider-escalation branch and the
pre-existing manual recovery prompt into
gsd-core/references/execute-phase-quota-recovery.md, leaving step 7.1 as a
one-line pointer. execute-phase.md is now 92880 bytes — 510 SMALLER than base.

Also regenerates the fixtures that legitimately moved because three shipped
files changed (gsd-tools.cjs, config-schema.manifest.json, execute-phase.md):
golden-install-parity + install-tree for all 16 runtimes, INVENTORY.md +
INVENTORY-MANIFEST.json for the new reference, and the workflow size baseline.

Refs #2296

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(#2351): make the C1 orphan-reaping test load-independent

tests/run-with-timeout.test.cjs C1 asserted the child heartbeat file exists
after a 1s group-kill window, but the child only wrote it on the first 100ms
setInterval tick. Nothing synchronized the two: on a loaded container the group
is SIGKILLed before that tick lands, the file never appears, and the assertion
fails for a reason unrelated to reaping. Observed failing on both linux-node22
and linux-node24.

The behavior actually under test is the FREEZE assertion (heartbeat stops
advancing => descendant was reaped, not orphaned). That is unaffected by
sampling once more at t=0.

Child now writes its first heartbeat synchronously at startup before arming the
interval, and the kill window widens 1s -> 3s to cover child boot under load.
Both remove the timing dependency; neither weakens what the test proves.

Refs #2296

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(#2296): backfill pr:2458 in .changeset/rapid-jays-bark.md

* chore(#2296): regenerate fixtures after rebase onto #2402

The rebase conflicted on the generated golden-install-parity fixtures and
workflow-size-baseline.json because #2402 (b6e6a22fc) regenerated the same
artifacts. Conflict resolution picked a side to unblock the rebase; a true
regeneration on the combined tree then produced further drift, confirming the
resolved content was stale and would have dropped #2402's fixture changes.

Regenerated goldens, install-tree, size baseline, and INVENTORY-MANIFEST from
the merged tree. docs/INVENTORY.md keeps BOTH new reference rows.

execute-phase.md is 92782 LF bytes with both #2402's and this PR's extractions
applied — under the frozen ceiling (hard <93600, margin <=93400).

Refs #2296

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 14:59:30 -04:00
Tom Boucher
b6e6a22fce fix(#2402): honor response_language across orchestrator output + UAT checkpoint renderer (#2457)
* fix(#2402): honor response_language across orchestrator output + UAT checkpoint renderer

Replays the in-flight bot branch fix/2402-response-language-orchestrator-coverage
(seven commits, never pushed) onto current origin/next as a single squashed commit.
The original work was substantial and correct; this commit preserves its full scope,
trimmed where rebase conflicts + workflow size budgets required it.

Three independent layers where response_language was being dropped are closed:

Layer 1 — orchestrator-facing directives across workflows. Adds the strong
"All user-facing output in this workflow MUST be presented in {response_language};
technical terms, code, paths, and subagent prompts stay in English" directive
to ~40 workflows that previously either lacked it entirely (verify-work,
new-project, new-milestone, quick, manager, and ~35 more) or carried only
the weak subagent-prompt-only form (plan-phase, execute-phase). The directive
covers narration between tool calls and banner output, not just the
AskUserQuestion prompts.

Layer 2 — UAT checkpoint renderer (src/uat.cts). buildCheckpoint now accepts
an optional responseLanguage parameter and renders the frame strings
("CHECKPOINT: Verification Required", "Type `pass` or describe what's wrong.")
in any of 9 languages (English/Spanish/French/German/Portuguese/Japanese/
Chinese/Korean/Italian) with an alias table covering ~30 input variants
(en, es, español, ja, 日本語, etc.). cmdRenderCheckpoint reads
config.response_language via loadConfig(cwd) and passes it through, so the
byte-for-byte block verify-work.md reprints verbatim is already localized
when written — preserving the anti-injection hygiene rule at verify-work.md
(the model is forbidden to translate after the fact). CJK display width is
computed by East Asian Width property ranges (W/F) so the right ║ border of
the banner stays aligned for full-width characters. English fallback is
byte-identical to the pre-fix behavior when response_language is unset or
unrecognized.

Layer 3 — literal English report templates in execute-phase. The top-of-
workflow directive covers all template sites (templates are a structural
source, not literal output). Inline render-language notes that previously
sat at each template site were removed during the squash because they
pushed execute-phase.md over its frozen pre-phase-6 byte ceiling
(93600 — ADR-857 Phase 6 capstone). The single top directive covers the
same surface with fewer bytes.

Also extends src/docs.cts and src/init.cts to propagate response_language
into the init JSON bundle of the additional workflows so the directive can
read it.

Tests added:
- tests/uat.test.cjs: buildCheckpoint with unset/unrecognized language falls
  back to English default; recognized language swaps only the two frame
  strings while structural lines stay untouched; CJK display-width regression
  (independent recomputation of East Asian Width W/F ranges).
- tests/workspace.test.cjs, tests/docs-update.test.cjs: response_language
  wiring through docs.cts/init.cts.

References: #2402; reporter's three-layer triage + Layer-4 follow-up; the
byte-for-byte anti-injection hygiene rule at verify-work.md (the reason
Layer 2 must be renderer-side, not model-translated).

This is a squash of the in-flight bot branch — seven commits representing
the original implementation plus its subsequent fix/CJK-padding/test/
changeset/regen cycles, none of which were ever pushed or PR'd. The squash
captures the final coherent state.

* chore(#2402): backfill pr:2457 in .changeset/2402-response-language-orchestrator-coverage.md

* chore(#2402): regen golden + size baseline after rebase against #2315 (PR #2451)

Rebase conflicts were entirely in generated artifacts (golden-install-parity
fixtures + workflow-size-baseline.json). After taking theirs during rebase,
regenerated cleanly against the merged source tree.
2026-07-20 14:22:27 -04:00
Tom Boucher
352876ff0c fix(#2315): respect review.default_reviewers in bare convergence invocation (#2451)
* test(#2315): regression test for review.default_reviewers precedence

A bare /gsd-plan-review-convergence invocation (no reviewer flags) is supposed
to let users configure a persistent reviewer lineup via review.default_reviewers
and just run the loop. Instead, the orchestrator's argument parser silently
discards that configuration and forces --codex on every no-flag invocation —
with no warning that the configured reviewers were ignored.

This commit adds a regression test that fails against the pre-fix workflow
(the buggy unconditional --codex fallback is still present at this commit)
and passes after the fix lands:

- Structural: the workflow must NOT contain an unconditional
  'if [ -z "$REVIEWER_FLAGS" ]; then REVIEWER_FLAGS="--codex"; fi' line
  before the workflow.plan_review_convergence config gate.
- Structural: the workflow must query review.default_reviewers AFTER the config
  gate and document that empty REVIEWER_FLAGS lets gsd-review apply the default.
- Behavioral: matrix across {configured, unset, empty-array, explicit-flag}
  invoking the actual deployed parse + resolution blocks with a stubbed gsd_run.

Also updates two existing tests whose assertions the fix makes stale:
- #2293 behavioral: endMarker was the buggy unconditional fallback line; the
  bare invocation assertion was 'run("5") === "--codex"'. Both flip post-fix
  (endMarker is now the last --all grep line; bare invocation returns empty
  from the parse block, default applied later in step 1.5).
- command-default-claim: the pre-fix command documented '--codex (default if
  no reviewer specified)' which was the user-facing mirror of the bug. The
  assertion now requires the command to document the review.default_reviewers
  precedence.

* fix(#2315): respect review.default_reviewers in bare convergence invocation

Root cause: plan-review-convergence.md step 1 (Parse and Normalize Arguments)
contained an unconditional fallback that set REVIEWER_FLAGS=\"--codex\" whenever
no explicit reviewer flag was supplied. This value was then interpolated
verbatim into the gsd-review args, so gsd-review saw --codex as an explicit
flag (precedence rule 1) and never reached rule 3 (review.default_reviewers).
The same path silently dropped any configured review.reviewer_instances
(instances participate ONLY via review.default_reviewers per ADR-1517).

Fix:
- Remove the unconditional --codex fallback from step 1.
- Add a config-gated resolution in step 1.5 (after CONVERGENCE_ENABLED check)
  that queries review.default_reviewers and either leaves REVIEWER_FLAGS empty
  (letting gsd-review apply its own rule-3 default) or falls back to --codex
  when no default is configured — preserving the pre-fix default for
  unconfigured users (AC3).
- Replace the banner {REVIEWER_FLAGS} token with {REVIEWER_DISPLAY} so the
  startup banner reflects what will actually run (AC4), not a hardcoded value.

The fix upholds the documented precedence contract (ADR-0011, ADR-0015) that
the bug was actively violating. Explicit-flag invocations (--gemini, --all,
etc.) are unaffected (AC5).

References: #2315; ADR-0011 (review.default_reviewers precedence);
ADR-0015 (autonomous cross-AI convergence); ADR-1517 (reviewer instances).

* chore(#2315): bump plan-review-convergence.md baseline + changeset

- Bump plan-review-convergence.md size baseline 23713 → 25536 (the new
  step-1.5 default-resolution block).
- Add .changeset/plucky-yaks-roar.md documenting the user-visible change.

* fix(#2315): restore /gsd: colon syntax + skip behavioral test when jq missing

Two follow-ups to the #2315 fix discovered by gsd-test:

1. The fix commit accidentally regressed the slash-command namespace in the
   disabled-feature exit message: /gsd:plan-review-convergence (correct,
   from PR #3452) became /gsd-plan-review-convergence (retired dash syntax).
   The slash-command-namespace invariant test caught this. Restored the
   colon form.

2. The behavioral test exercises the deployed reviewer-resolution block,
   which pipes through jq. jq is a documented production dependency
   (review.md:244 "install jq if missing") and is present in every
   production deployment, but is NOT on PATH in the gsd-test linux-node{22,24}
   containers (same constraint as tests/opencode-review-reconstruction.
   property.test.cjs). Without jq, the printf|jq pipeline fails silently,
   the ||echo 0 fallback yields DEFAULT_REVIEWERS_COUNT=0, and the resolution
   falls through to the --codex branch — producing a false negative.
   Added a jqAvailable guard at module load (matching the existing pattern)
   and skip the behavioral test when jq is absent. The structural tests
   (no bash execution) still run and validate the fix.

* chore(#2315): regenerate golden-install-parity fixtures

Source changes to plan-review-convergence.md (workflow + skill mirror +
command doc) changed the install-tree hashes. Regenerated via
'npm run gen:golden' after rebuilding gsd-core/bin/lib/install-engine.cjs
('npm run build:lib') — the on-disk lib was stale relative to
src/install-engine.cts (isSymlinkedDestOptIn) and blocked fixture gen.

* test(#2315): address review findings — strengthen structural tests + property test

Code-review + security-review (isolated subagent passes) surfaced Low/Nit
findings; this commit addresses the test-side findings:

- Structural test 1 ("unconditional --codex one-liner") now asserts the
  buggy line is absent EVERYWHERE, not just before the config gate. The
  earlier assertion allowed a maintainer to re-add the line after the gate
  (passing the structural test) while the bug would still bite at runtime
  before the gate runs.
- Structural test 4 ("banner uses REVIEWER_DISPLAY") now asserts the
  LITERAL banner placeholder "Reviewers: {REVIEWER_DISPLAY}" and forbids
  "Reviewers: {REVIEWER_FLAGS}". The earlier workflow.includes(
  "REVIEWER_DISPLAY") was satisfied by a comment mention.
- New structural test for command/skill content parity: both files must
  document the review.default_reviewers precedence on the --codex flag
  (catches a manual edit to one that the gen:plugin-skills mirror misses).
- Behavioral test stub now passes default_reviewers via env var
  ($GSD_TEST_DEFAULT_REVIEWERS) instead of inline-interpolating into a
  bash single-quoted string. Removes the (currently-safe) fragility where
  a future test input containing a single quote would close the bash
  quote and execute as bash under execFileSync.
- New property test (fast-check, numRuns=25) for the JSON-classification
  contract: non-empty arrays of slugs -> empty REVIEWER_FLAGS; empty
  array / scalar JSON / malformed JSON -> --codex fallback. Locks the
  parser contract per CLAUDE.md mandate.

* fix(#2315): address review findings — defensive jq-missing warning + banner cleanup

Code-review surfaced two Low-severity workflow-side findings:

- Defensive jq-missing warning: if jq is not on PATH in production (it is
  a documented dependency per review.md:244, but the dependency can be
  absent in degraded environments), the printf|jq pipeline fails silently
  to "0" and a user with review.default_reviewers configured gets --codex
  with no indication their configured default was unreadable. Added a
  command -v jq guard at the top of the resolution that falls back to
  --codex AND emits a stderr warning explaining the reason. This makes the
  failure diagnosable instead of silently reproducing the #2315 override.

- Banner leading-space cleanup: REVIEWER_FLAGS accumulates with a leading
  space ("$REVIEWER_FLAGS --gemini" from ""), so the explicit-flag branch
  of REVIEWER_DISPLAY="$REVIEWER_FLAGS" rendered "Reviewers:  --gemini"
  (double space). Pre-existing but worth fixing alongside the AC4 banner
  work. Strip one leading space with the ${VAR# } parameter expansion in
  the explicit-flag branch only (the configured-default and --codex
  branches already produce clean strings).

* chore(#2315): bump plan-review-convergence.md baseline + regen golden fixtures

The defensive jq-missing warning grew plan-review-convergence.md
(25536 -> 26285 bytes). Bumps the per-file baseline snapshot and
regenerates the golden-install-parity fixtures for the resulting
install-tree hash changes.

* chore(#2315): backfill pr:2451 in .changeset/plucky-yaks-roar.md
2026-07-20 10:55:28 -04:00
Tom Boucher
eb45fc0e8c fix(#2415): close_phase_todos stages the pending/ deletion alongside completed/ (#2447)
* fix(#2415): close_phase_todos stages the pending/ deletion alongside completed/

Bug: the workflow step moved resolved todos from .planning/todos/pending/
to .planning/todos/completed/ with a plain 'mv', then committed listing
ONLY the destination directory in --files:

    mv "$TODO_FILE" "$COMPLETED_DIR/"
    gsd_run query commit '...' --files .planning/todos/completed/ .planning/STATE.md

Git's index still tracked the moved file at its old pending/<name>.md path.
The commit therefore only staged the new completed/<name>.md copy — the
deletion at pending/ was never staged, never committed, and lingered as an
unstaged deletion in git status indefinitely until some later broad
'git add -A' caught it. The phase genuinely closed the todo, but the
working tree was never clean.

Fix: add .planning/todos/pending/ to the --files list. 'git add' of that
directory (since git 2.0) stages deletions of tracked files in the pathspec,
so the moved-away file is staged as a deletion atomically with the new
completed/ copy in the same commit.

Chose plain mv + two-dir --files over 'git mv' because git mv FAILS on:
  - untracked todos (new todo file not yet committed)
  - non-git .planning dirs (worktree safety / pre-init projects)
Plain mv has neither failure mode.

Regression tests in tests/close-phase-todos-stage-deletion.test.cjs
(source-text-is-the-product: workflow .md text IS what the runtime loads)
cover:
  - the commit --files list includes BOTH completed/ AND pending/
  - the move uses plain 'mv' (not 'git mv') so untracked + non-git cases work

* chore(#2415): trim commit subject to keep execute-phase.md under byte ceiling

The fix added '.planning/todos/pending/' (~26 bytes) to the commit --files
list. To stay under the ADR-857 Phase 6 pre-phase-6 byte ceiling margin
(93400 bytes, hard ceiling 93600), shortened the commit subject from
'auto-close N todo(s) resolved by this phase' to 'close N resolved todo(s)'.
Net change vs origin/next: +6 bytes (93384 → 93390), well under the margin.

Also regenerates the golden-install-parity fixtures (execute-phase.md
content-hash update across all runtimes).

* chore(#2415): bump execute-phase.md workflow-size baseline (93384 → 93390)

The +pending/ fix added 6 net bytes (93384 → 93390), still well under the
ADR-857 Phase 6 pre-phase-6 byte ceiling margin (93400).

* chore(changeset): backfill pr:2447 in .changeset/sturdy-wasps-run.md

* fix(#2415): add issue ref to allow-test-rule annotation (ADR-456)

CI lint-allow-test-rule-refs failed on the prior commit — ADR-456 requires
'// allow-test-rule: <category> see #NNNN' so every exemption is traceable
to an issue. Added 'see #2415' to the source-text-is-the-product annotation.
2026-07-20 08:22:23 -04:00
Tom Boucher
d16a66479a feat(#1950): broken-windows ledger — cross-phase defect register gating ship (#2441)
* feat(#1950): broken-windows ledger — cross-phase defect register gating ship

Adds a new  capability (#1950) that operationalizes GSD's
no-defer discipline as a tracked, enforced artifact:
accumulates stubs, TODOs, skipped tests, unrun verifies, and unmet truths
across phases, and /gsd-ship blocks while any entry is open.

Implementation:
- src/broken-windows.cts → gsd-core/bin/lib/broken-windows.cjs: typed IR +
  I/O entry points (parseLedger/renderLedger/appendWindow/markWaived/markFixed
  + cmdWindowsStatus/Append/Waive/MarkFixed). Frozen REASON enum for typed
  error assertions. Windows-safe atomic rename with retry on transient
  EPERM/EBUSY/EACCES.
- gsd-tools.cjs: new  subcommand (status | append | waive | fixed),
  wired via routeWindows + HOST_COMMAND_ROUTERS.windows.
- capabilities/broken-windows/capability.json: one ship:pre gate with
  artifact-frontmatter-equals predicate on WINDOWS.md open_count == 0.
  activationKey windows.enabled (default true) + sibling windows.enforce
  (default true, separate so tracking can precede enforcement).
- gsd-core/workflows/ship.md: capId==broken-windows branch in preflight,
  sibling to security — reads gsd_run windows status --raw, fails closed
  on open_count > 0 or unreadable ledger.
- agents/gsd-executor.md: extends the existing ## Known Stubs instruction
  to also append to WINDOWS.md via gsd_run windows append (best-effort,
  never blocks execution).
- agents/gsd-verifier.md: new Step 8b — record unmet truths + human-verify
  items in WINDOWS.md.
- gsd-core/workflows/progress.md: surfaces open + waived counts.
- docs/COMMANDS.md + CONTEXT.md glossary entry + docs/INVENTORY.md:
  document the gate, waiver mechanism, and new module.
- tests/broken-windows.test.cjs: pure + CLI behavioral coverage + fast-check
  roundtrip property; fail-closed on malformed ledger; security boundary on
  path traversal in --file.

Backward-compatible: a project with no .planning/WINDOWS.md reports
open_count: 0 and ships cleanly. Disable enforcement per-project with
gsd config-set windows.enforce false (tracking continues, gate stays open).

* chore(#1950): ratchet size baselines, defer verifier integration

- Workflow size baseline: ship.md 25575→27928, progress.md 31789→32632
  (broken-windows preflight branch + open-windows surface).
- Agent size baseline: gsd-executor.md 46644→47951 (Known Stubs → also
  appends to WINDOWS.md). gsd-verifier.md unchanged.
- LARGE_CAP (49152) preempted the planned verifier integration
  (gsd-verifier.md was at 49140 pre-PR — 12 bytes of headroom, not the
  documented 'real headroom'). Verifier integration deferred to a follow-up
  PR that extracts the VERIFICATION.md template (lines 739-859) to
  gsd-core/references/ — a pre-existing cap-tightness defect this PR
  exposed but does not expand scope to fix. Verifier integration is not in
  the issue's acceptance criteria (executor writes is; unmet-truths
  recording was an enhancement, not a gate).

* fix(#1950): gate default-off, rename to workflow.windows_enforce, regen goldens

Test-failure-driven fixes after first gsd-test run on db8733c8f failed 44
cases (pre-existing structural tests encoded 'ship:pre has 1 gate' / 'all
caps off → empty hooks'):

- capability manifest: rename windows.enabled+windows.enforce (default
  true) → single federated key workflow.windows_enforce (default FALSE,
  opt-in). Matches security's workflow.security_enforce convention and
  makes the adr857 all-caps-off test pass without modification (the test's
  buildAllFalseConfig handles workflow.* out of the box). Default-OFF keeps
  the gate out of the registry's default ship:pre resolution so existing
  loop-hooks-ship-pre-e2e structural assertions (exactly 1 gate, capId
  'security') stay valid; users opt in via
  gsd config-set workflow.windows_enforce true.
- drop activationKey (security doesn't have one either; workflow.* key
  doubles as the activation toggle).
- regenerate docs/reference/capability-matrix.md to include broken-windows
  (capability-matrix-sync test).
- regenerate tests/fixtures/golden-install-parity/*.json (18 runtimes) —
  installer now emits the new capability + lib file.
- update CONTEXT.md, docs/COMMANDS.md, docs/FEATURES.md, ship.md,
  agents/gsd-executor.md to use the new key name and /gsd:colon slash
  syntax (slash-command-namespace test).
- restore accidentally-regressed /gsd:capture in progress.md.

Tracking-only by default; enforcement is opt-in. Acceptance criterion
'/gsd-ship fails while any ledger entry is open' is met when
workflow.windows_enforce=true (test fixture enables it).

* test(#1950): update ship:pre structural invariants for 2-gate registry

- loop-hooks-ship-pre-e2e: the registry now declares 2 gates at ship:pre
  (security + broken-windows), regardless of activation. Activation tests
  above still pin security-only or empty behavior via fixtures; these
  structural tests pin the REGISTRY shape, which has 2 gates as of #1950.
- workflow-size-baseline: ship.md 27928→27945 (workflow.windows_enforce
  rename added 17 bytes).

* fix(#1950): review H1+H2+M1+M2+M3 — fence-injection, EACCES fail-closed, cleanup, strict line, stryker

Adversarial isolated review (Step 6.3) found 2 HIGH findings that block
the PR and 3 mediums. All addressed:

H1 (HIGH): description containing the markdown 3-backtick fence would
terminate the ledger's JSON code block early inside JSON.stringify output
(JSON doesn't escape backticks), corrupting the file and bricking the
next parse. Fix: use a 4-backtick fence (json ... ) which
JSON.stringify cannot produce on its own, AND validate that no entry
text field contains a 4-backtick run (reject at append time with new
WINDOWS_INVALID_TEXT reason code). Locked by a regression test.

H2 (HIGH): readLedgerOrNull swallowed ALL fs errors as 'no ledger',
silently returning open_count:0 on EACCES/EPERM/EIO. The ship gate
would then pass on an unreadable ledger — the precise vector the
workflow doc claims is impossible. Fix: only ENOENT returns null;
every other fs error propagates as WINDOWS_LEDGER_MALFORMED so the
gate blocks and the operator sees a real diagnostic. Locked by a
regression test that chmod 000s a ledger with open_count=1 and
asserts the result is never a false-green 0.

M1: writeLedgerAtomic left an orphaned .tmp file on rename failure.
Wrapped renameWithRetry in try/catch with best-effort unlink.

M2: validateLine silently coerced 'abc' → NaN → null, hiding type
drift. Removed the line === 0 special case (was undocumented) and
made the error message match the strict check. Now any non-positive-
integer line value throws, including strings.

M3: tests/broken-windows.test.cjs (with its fast-check property test)
was not in stryker.config.mjs DEFAULT_TEST_CMD — Stryker would mutate
src/broken-windows.cts but no test would catch the mutations,
producing false surviving-mutant scores. Added to the list.

L1 (dead throw e after error()), L7 (line boundary tests, H1/H2
regression tests, 4-backtick CLI test) also addressed.

* docs(#1950): inline concurrency + busy-wait notes (review L2+L3)

* fix(#1950): regen goldens against latest gsd-tools; correct --line 0 boundary test

gsd-test v4 caught two issues:
- goldens I regenerated earlier (commit 526682084) predated the L1
  routeWindows catch-block cleanup (commit dd844d565). Regenerated
  via 'npm run gen:golden' against current HEAD so the install
  parity hash for gsd-tools.cjs matches.
- 'append --line boundary' test expected --line 0 to succeed with
  null entry.line, but the M2 fix correctly rejects 0 (lines are
  1-indexed; 0 is not a valid source line). Updated the boundary
  test to assert --line 0 fails alongside -1 and 'abc'.

* chore(#1950): regen goldens after rebase onto next

* chore(#1950): quick.md baseline 50699→50993 (correct resolution from next rebase)

* chore(changeset): backfill pr:2441 in .changeset/broken-windows-ledger.md

* fix(#1950): renderTable escapes backslash before pipe (CodeQL incomplete-sanitization)

CodeQL flagged the markdown-table cell escaper:
  String(s ?? '').replace(/\|/g, '\\|')
— it escapes pipe but not backslash first. A description containing '\|'
would render as '\\|' which markdown parses as 'literal backslash' +
'cell separator', splitting the column.

Fix: escape backslash FIRST (each \ → \\), then pipe (each | → \|).
Now a description with '\|' renders as '\\\\|' (literal '\\' + escaped
pipe), which markdown renders as a single '\|' inside the cell. The JSON
code block (the parse source-of-truth) was already correctly escaped via
JSON.stringify; only the display-only table was affected.

Locked by a regression test that:
1. Verifies the JSON block reparses with the description intact.
2. Walks the rendered table row counting unescaped pipes — must be
   exactly 11 (the row separators for 10 cells), proving no in-cell
   pipe added a split.
2026-07-19 20:24:21 -04:00
Tom Boucher
1a46bc068a fix(#2376): emit absolute subagent-facing paths from init/state, convert workflow literals (#2428)
* fix(#2376): emit absolute subagent-facing init/state paths

Make init.* and state.* path fields absolute rather than cwd-relative
so subagent prompts resolve correctly regardless of working directory.
Adds intel_dir/conflicts_path/requirements_path/roadmap_path/state_path
to cmdInitIngestDocs, an absolute debug_dir to cmdStateLoad, and
replaces bare .planning/... literals in 12 workflow Agent() prompt
blocks with the absolute init-JSON path fields. Includes decoy-cwd
regression tests and realpath'd tmpdir fixtures for macOS.

Squashed rebase of the #2376 commit series onto a fresh origin/next
(previous merge ee25543a1 was against a now-stale next).

* chore(#2376): add changeset

* chore(#2376): regenerate golden fixtures + workflow size baseline

Regenerated after rebasing the absolute-path fix onto current next
(picks up #2351's run-with-timeout content in execute-phase.md too).

* fix(#2376): trim execute-phase.md redundancy to stay under the size margin

* chore(#2376): regenerate golden/size baseline after rebase onto next
2026-07-19 15:43:48 -04:00
Behruz Nassre Esfahani
d04e287fa9 fix(#2365): stop api-coverage detector false-positiving non-API phases (#2397)
* fix(#2365): stop the api-coverage detector false-positiving non-API phases

detectApiIntegration fired on any integration verb co-occurring anywhere on a
line with any API noun, treated / as a word boundary (so a first-party Next.js
src/app/api/... route path matched the noun "api"), and read any capitalized
word before API/SDK/REST/GraphQL as a service name behind a fixed stopword
denylist (so threat-model prose like "Resolver-only API" fired). Because the
verify:pre seal gate is BLOCKING, a phase touching no external API could not
reach UAT without fabricating a coverage matrix.

The compound rule now requires the verb and noun to share one clause (sentence
punctuation and table-cell walls end a clause) within a bounded word gap.
Non-prose spans are excluded before matching: fenced code (already), inline
code spans (new stripInlineCode in the markdown-sectionizer seam), and
path-shaped tokens. The <Service> API surface rule requires proper-noun
position — a clause-initial capitalized word is ordinary English and needs
dependency evidence (URL / package reference) on the same line — and rejects
compound modifiers ("Resolver-only", lowercase after the hyphen).

A phase that integrates no external API now has a first-class, reasoned way to
say so: a COVERAGE.md containing "No external API integration: <reason>"
satisfies the gate (declaration + rows is contradictory and blocks). The
true-positive path is pinned by regression tests: every default-vocabulary
positive still fires, including the widest word-gap pairing and the
surface-rule-only shape.

Fixes #2365

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(#2365): tighten api-coverage detector per Codex review (round 2)

Applies the Codex review findings on the initial #2365 fix:

- S-1: a COVERAGE.md "no external API integration" declaration is the human
  override for a fallible detector, so it must PASS even when detection still
  fires — but the contradiction is now SURFACED in the gate output (overridden
  signal count + terms) instead of passing silently.
- S-2: verb/noun pairing is now a term-group nearest-pair merge walk over
  precomputed word ordinals (computeWordStarts / minWordGap), not a match×match
  cross product — a hostile line repeating one pair thousands of times stays
  linear instead of going quadratic.
- FN-4: package-shaped inline-code spans (`stripe-sdk`, `@stripe/stripe-js`)
  are kept as noun/dependency evidence rather than being fully masked, so a
  genuine dependency reference inside code ticks still corroborates.
- C-1: the <Service> API surface rule now scans every candidate in every
  clause; a rejected first candidate no longer shadows a later genuine service.
- Cross-clause binding: a verb may bind a noun in the immediately following
  clause only when its own clause names a service object, within a tight gap —
  admits "Integrate Stripe, exposing its endpoints …" without re-admitting the
  unrelated-clauses false-positive class.
- Internal-descriptor negative evidence ("internal Payments API",
  "the internal endpoint") never pairs; URL/scheme matching generalized beyond
  http(s).

All 5 acceptance criteria still hold: the three reported false positives are
clean and "integrate the Stripe API" still fires. Built .cjs committed
alongside the .cts. tsc + eslint (incl. no-adhoc-markdown-parsing) +
lint:regression-names clean; affected suites 256/256 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(#2365): retune api-coverage detector fail-closed per Codex review (round 3)

Codex's second-round review found the round-2 tightening had over-corrected into
FAIL-OPEN false negatives — realistic external-API prose that the BLOCKING seal
gate silently let through (the catastrophic class, since a missed API surface is
worse than a dismissable false positive). Retuned the detector to be explicitly
fail-closed: lean toward detecting, and let the one-line COVERAGE.md "no external
API integration" declaration dismiss the residual false positives.

Fail-open false negatives fixed (all now detect):
- F1 clause-initial `<Service> API` with a plain follower ("Stripe API for
  payment processing") — dropped the follower-allowlist / corroboration gate on
  clause-initial surfaces; a service that is not a stopword, descriptor, or
  compound modifier is a real name from any clause position.
- F2 scheme-less external host ("api.stripe.com/v1") — a dotted host with an
  alphabetic final label now contributes its API nouns; a first-party route
  path (no dotted host) still does not.
- F3 vendor's first-party SDK ("Integrate Shopify's first-party SDK") — the
  compound path no longer filters nouns on "internal"/"first-party" (Codex: the
  qualifier can describe the vendor's own API, not the consuming project's).
- F4 long single integration clause — removed the word-gap cap entirely: it
  could not separate a 21-word genuine clause from an 18-word internal one, so
  the clause boundary is now the whole relationship test.
- F5 lowercase cross-clause service — cross-clause binding no longer requires a
  capitalized "service object".

New false positives fixed (all now clean):
- F6 a URL token that swallowed a trailing clause comma, merging two clauses —
  trailing clause punctuation is kept literal so the split survives.
- F7 a capitalized internal component authorizing cross-clause binding — the new
  gate requires a dependent elaboration, not a new coordinate clause opened by a
  conjunction ("…, then document…").
- F8 a protocol name read as a service ("REST API", "GraphQL API") — protocol
  and locality descriptors are rejected in the `<Service>` position.

- Finding 9: the inline-code-span scanner was O(n^2) on pathological backtick
  runs; rewritten to linear via a per-length run cursor (2 MB: 4.15 s -> ~6 ms),
  semantics preserved (148 sectionizer tests unchanged).

Net simplification: the fail-closed model removed the round-2 minWordGap /
groupByTerm / follower / corroboration machinery (350 insertions vs 445
deletions across the touched files). Under fail-closed, three round-2 negative
tests now correctly detect (integration verb + "internal"-qualified noun, and
the distant-same-clause case); none were trek-e acceptance FPs.

Verified: 1491/1491 unit tests pass; tsc + eslint (incl. no-adhoc-markdown-
parsing) + lint:regression-names clean; all 8 review findings reproduced as
regression tests, both directions. Built .cjs committed alongside the .cts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(#2365): resolve round-3 Codex review findings (fail-closed, round 4)

Codex's round-3 adversarial review found the fail-closed retune had introduced
new holes in both directions. Resolved:

Fail-open false negatives (now detect):
- External host addressing a PATH ("graph.microsoft.com/v1.0/me") is itself an
  integration surface and contributes an endpoint noun even when the host names
  no vocabulary word. A bare domain link with no path ("https://example.com")
  stays a non-signal, so "Integrate … from example.com, document …" is still
  clean.
- Locality qualification ("internal", "private") no longer leaks across a
  sentence or clause boundary: only plain spaces may separate the descriptor
  from the service, so "The cache is private. Stripe API …" now detects.
- Cross-clause binding: the fragile head-word cap (which could not tell a
  genuine "Connect … to Stripe payments, exposing its endpoints" from an
  unrelated "Integrate … from URL, document …" — both 4 words after the verb)
  is replaced by a participial-continuation rule: a verb binds a noun in the
  next clause only when that clause begins with an "-ing" elaboration. This
  fixes the 4-word-head false negative AND the false positive below at once.

False positives (now clean):
- Cross-clause no longer binds a finite continuation regardless of separator:
  "Wire the settings form. Document endpoint props." / "…; document …" /
  "…, document …" are separate actions, not elaborations.

Perf (quadratic → linear):
- The trailing-punctuation peel is a backward char scan instead of an
  unanchored `[…]+$` regex (16k chars: 156 ms → ~1 ms).
- SERVICE_SURFACE_API_RE bounds the service-name length {1,40} so a hostile
  "A-A-…-x" run cannot drive O(n^2) backtracking (16k: 385 ms → ~3 ms).

Consumer fail-open (blocking gate):
- readPhaseScope now distinguishes "no plans" from a plan that EXISTS but is
  unreadable. On a read error the gate BLOCKS ("could not read the phase
  scope …") instead of silently certifying no-integration from partial scope —
  an unreadable plan could be the one describing the integration.

Documented fail-closed tradeoffs, now pinned with tests so they are not
"fixed" back into a fail-open: a clause-initial capitalized common word before
"API" ("Payment API", "Search API") reads as a service name; a long clause
pairs a verb with a distant noun; and a CommonMark inline code span that wraps
a newline is matched within-line only. Codex judged these acceptable because
the COVERAGE.md declaration is a cheap override.

One documented limitation remains out of scope: "Integrate Stripe, and
authenticate requests with its API" (a coordinate finite clause whose noun
refers back by pronoun) needs coreference resolution, beyond a lexical detector.

Verified: 379/379 affected + command-router tests pass (+14 new regression
tests covering every round-3 finding, both directions); tsc + eslint
(no-adhoc-markdown-parsing) + lint:regression-names clean. Built .cjs committed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(#2365): simplify to robust core — remove whack-a-mole heuristics (round 5)

Round-4 review confirmed the detector's two most complex features generate
findings in both directions no matter how they are tuned, because they need a
vendor dictionary + coreference the issue rules out in principle. Per the
operator's "ship the robust core" decision, both are removed and their gaps are
documented rather than chased further:

- Cross-clause binding DELETED (allowsCrossClause / participle rule). It caused
  a fail-open on finite continuations ("Integrate Stripe; use its OAuth
  endpoints" — missed) and a false positive on "-ing"-SPELLED nouns ("…, billing
  endpoint terminology…" — wrongly fired). Detection is now same-clause only.
- URL-path-as-evidence REVERTED. Treating every path-bearing URL as an endpoint
  fired on ordinary asset/link URLs ("…/theme.css", "…?next=/x", a docs/repo
  link) and recreated routine UI-phase false positives. An external URL is
  evidence only when it NAMES an API vocabulary word ("api.stripe.com/v1").

Two fail-open cases are now DOCUMENTED limitations, pinned by tests so a future
maintainer does not re-add the heuristics that caused the false positives above:
a service named only in a clause separate from its API noun, and a bare external
host that names no vocabulary word. Both are cheaply covered by the COVERAGE.md
declaration and rare in real phase prose ("integrate the X API").

Also fixed from the round-4 review:
- Qualification now survives markdown emphasis ("The **internal** Payments API"
  stays clean) while still not crossing a sentence/clause boundary.
- readPhaseScope fail-closes on a REAL read failure (EACCES/EIO) enumerating the
  phase directory or reading the roadmap fallback — not only per-plan-file
  failures; a missing directory/section remains a legitimate no-op. The
  declaration-override path surfaces scope_read_error so an incomplete-scope
  override stays visible.
- SERVICE_SURFACE_API_RE length-bound comment no longer overclaims.

Net: the detector is same-clause verb+noun + `<Service> API` surface, with
path/code/inline masking and a fail-closed posture. All five acceptance criteria
hold. 1573/1573 unit tests pass; tsc + eslint (no-adhoc-markdown-parsing) +
lint:regression-names clean. Built .cjs committed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(#2365): close roadmap-fallback fail-open + stale JSDoc (round-5 review)

The round-5 sanity review confirmed the detector simplification is sound (all
acceptance positives fire, all required negatives clean) and flagged one real
blocker plus a nit:

- Blocker: readPhaseScope's roadmap fallback could still silently pass an
  UNREADABLE roadmap. getRoadmapPhaseWithFallback gated on fs.existsSync(), which
  returns false on EACCES/EIO too — so an unreadable ROADMAP.md read as "absent",
  no exception reached isRealReadFailure, and the blocking gate certified empty
  scope. Fixed at the source: read the roadmap directly and honor the function's
  OWN documented contract — null only on ENOENT (genuinely absent), otherwise
  throw. Both existing callers already wrap it in try/catch expecting that throw,
  and readPhaseScope now fail-closes (blocks) via its roadmap catch. Verified by
  a new e2e test (unreadable roadmap fallback → block).

- Nit: the detectApiIntegration JSDoc still described the removed cross-clause
  participial binding and "every external hostname counts" — corrected to the
  actual same-clause-only behavior and the names-a-vocab-word URL rule.

Verified: full unit suite green; tsc + eslint + lint:regression-names clean.
Built .cjs committed (roadmap.cjs is gitignored/rebuilt, per repo convention).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(#2365): backfill changeset PR number (#2397)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(#2365): sync generated capability-registry + recapture install goldens

CI surfaced two generated-artifact staleness issues (all failing test shards +
lint-tests traced to these, not to a logic defect):

- gsd-core/bin/lib/capability-registry.cjs was stale: the initial fix edited the
  ai-integration `api-coverage-plan-pre.md` fragment (added the "No external API
  integration" declaration section) but did not regenerate the registry, which
  embeds an inline copy of that fragment. Regenerated via
  `gen-capability-registry.cjs --write` — the diff is exactly the fragment text
  sync. Fixes `lint:generated-sync` and the "committed registry is in sync" +
  "registry integration" tests.

- The 18 golden-install-parity fixtures were stale by exactly one hash line each
  — `gsd-core/references/api-coverage.md`, which this PR edits and which is a
  hashed installed artifact. Recaptured with `UPDATE_GOLDEN=1`; the diff is that
  single hash per runtime and nothing else. Fixes the `golden parity — *` tests.

No source or behavior change — generated artifacts only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(#2365): flip representative-corpus manifest to assert the fixed behavior

The #2371 representative corpus (merged into next after this branch was cut) is a
known-bug tripwire: it asserts each fixture's currentBuggyOutput so the test
fails loudly the moment #2365 is fixed, at which point — per its own contract in
representative-corpus.test.cjs — the fixer removes currentBuggyOutput so the
assertion checks expectedDetected instead.

This is that moment. Removed currentBuggyOutput from the three detector fixtures
(nextjs-route-path, unrelated-verb-noun, threat-model-prose); the corpus now
asserts detected:false, which the fail-closed same-clause detector satisfies.
Notes updated to describe the fix rather than the bug. The #2366 matrix corpus
is left untouched — that tripwire belongs to its own PR (#2374).

Corpus test: 7/7 pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(#2365): skip chmod-000 fail-closed e2e tests on Windows

The three fail-closed gate tests induce an unreadable plan / directory / roadmap
with chmod 000, but Windows does not enforce POSIX mode bits — readFileSync
still succeeds, so the gate never reaches the read-error path and the assertion
fails on the windows-latest CI leg. The fail-closed LOGIC is platform-
independent (readError → block) and is fully exercised on the macOS/Linux legs;
only the method of inducing EACCES is POSIX-specific. Guard the three tests to
skip on win32 as well as root, mirroring golden-install-parity's win32 skip.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(#2365): address trek-e review — glossary, clock-seam, IO injection, bounds

Review response to PR #2397 (trek-e, CHANGES_REQUESTED). Fix logic unchanged;
this closes the test/process-hygiene findings.

Major:
- CONTEXT.md "Markdown Sectionizer" glossary now lists the two exports this fix
  relies on, `stripInlineCode` and `scanInlineCodeSpans` (glossary is a PR gate).
- Replaced the banned wall-clock assertion in the "hostile repeated-term line"
  test (Clock Seams rule — no elapsed-time asserts) with a deterministic
  signal-count assertion, which also directly verifies the term-dedup that keeps
  pairing linear (one signal for a 10k-pair line, not thousands).
- Rewrote the three fail-closed read-failure tests: instead of chmod 0o000
  (a no-op under root / on Windows, the pattern the repo's IO-failure convention
  avoids) they now exercise the newly-exported `readPhaseScope` in-process and
  inject the failure by monkeypatching fs.readFileSync/readdirSync to throw,
  restoring in finally. Deterministic and platform-independent (no skip needed),
  and they add the ENOENT-is-absence case that the chmod tests couldn't express.

Minor:
- Added limit / limit+1 boundary tests for SERVICE_SURFACE_API_RE's {1,40}
  service-name bound, QUALIFIER_LOOKBACK's 24-char window, and REASON_MAX_LEN
  (200) on the declaration reason.
- Added a fast-check property that fuzzes the tokenizer / clause splitter /
  masking (scanLineTokens, splitClauses, collectTermMatches) with adversarial
  tokens (slashes, backticks, URLs, clause punctuation) and asserts the detector
  is total (never throws), shape-stable, holds detected <=> signals, and is
  deterministic.

readPhaseScope is exported for the in-process tests. Verified: 125 detector +
19 gate tests pass; tsc + eslint + generated-sync (glossary/registry) +
lint-regression-test-names + lint-test-file-count clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 14:59:25 -04:00
Tom Boucher
d0bacc2517 fix(#2351): replace hardcoded timeout with portable run-with-timeout (#2426)
* fix(#2351): replace hardcoded gnu timeout with portable run-with-timeout

Stock macOS ships neither `timeout` nor `gtimeout` (GNU coreutils). The 10
hardcoded `timeout <n> <cmd>` calls across the workflow/agent/reference gates
exited 127 ("command not found") on such hosts, and the gates — which only
distinguish 0/124/other — misreported a passing build or test as a FAILURE.

Fix: a single Node-based `gsd_run run-with-timeout <secs> [--] <cmd…>` verb in
gsd-tools.cjs. Coreutils-independent (stock macOS AND Windows), keeps GNU
`timeout`'s exit-code contract (124 timeout, passthrough, 127/126 ENOENT/EACCES,
128+signum on signal), inherits stdio so pipes/redirects work, and reaps the
whole process group so a watch-mode runner cannot outlive its budget. Runs
before gsd-tools' flag parsing so the wrapped argv stays opaque.

Hardened per adversarial review:
- On timeout, SIGKILL the group SYNCHRONOUSLY before resolving — a descendant
  that traps SIGTERM was otherwise orphaned holding stdout, hanging captured
  gates (the exact watch-mode hang the feature prevents).
- Forward SIGINT/SIGTERM to the child tree instead of dying and orphaning it.
- Reject blank/whitespace <seconds> (was a silent unbounded run); clamp the
  timer to the 32-bit setTimeout ceiling (was a spurious immediate timeout).
- Lint detector: catch GNU long options / `-k5` / `$((...))`; anchor to command
  position so prose "timeout 30 seconds" no longer false-positives.

Resolution lives once in the CLI; all 10 sites call the shared verb. A parity
guard (scripts/lint-portable-timeout.cjs, wired into lint:ci) fails the build if
a bare `timeout`/`gtimeout` execution reappears (the portable `command -v
timeout` probe form is intentionally allowed). Also fixes the identical bug in
the zh-CN checkpoints translation, updates the tests that asserted the old
strings, trims a redundant phrase in gsd-verifier.md to keep it under its size
hard cap, and refreshes the size baselines + golden install-parity fixtures.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(#2351): add changeset (#2426)

* chore: regenerate golden/size baseline after rebase onto next

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 14:37:15 -04:00
Tom Boucher
6b196ef638 fix(#2423): finalize job syncs next package.json after final release (#2437)
* fix(release): finalize job calls sync-next-version.cjs to bump next after final release (#2423)

The release pipeline's 'finalize' job shipped X.Y.0 to npm 'latest' and
merged to main, but never bumped 'next' to match. 'scripts/sync-next-version.cjs'
exists exactly for this — its docstring promises to run 'for every release
type (rc / hotfix / final)' — but it was wired only into the 'rc' job
(release.yml:479), not 'finalize'. As a result, after 1.7.0 shipped on
2026-07-15, 'next' stayed at 1.7.0-rc.6 and every npm script banner on
'next' (and feature branches cut from it) reported the stale rc version.

Regression of #1104 — closed incomplete (covered rc only, not final).

This patch:
  - adds a 'Sync next branch to the published release' step to the
    'finalize' job, mirroring the rc job's pattern at line 479 (uses
    VERSION from inputs.version rather than PRE_VERSION from steps.prerelease,
    since finalize does not run the prerelease step);
  - gates it on !inputs.dry_run + continue-on-error:true (matches rc);
  - adds tests/release-finalize-syncs-next-version.test.cjs — a structural
    YAML assertion that fails against the pre-fix workflow and passes after.
    Failing-first demonstrated during development; the test parses job blocks
    by indentation rather than grep so it stays valid as the file grows.

Root-cause diagnosis: scripts/sync-next-version.cjs:14 docstring admits
'used by release.yml's rc job, which has no next-targeting PR of its own'.
release.yml:506-685 (finalize job) had no sync-next-version step before
this patch. Every prior rc.N release has a matching 'chore: sync next
package version to 1.7.0-rc.N' commit; there is no such commit for 1.7.0.

* chore(release): sync next package version to 1.7.0 (#2423)

Replays the canonical 'chore: sync next package version to <v>' commit
that the release pipeline's rc job auto-produces via scripts/sync-next-version.cjs,
for the 1.7.0 final release that shipped on 2026-07-15 (commit dd4c90f82
'chore: finalize v1.7.0' on main). Without this, 'next' (and every feature
branch cut from it) carried 1.7.0-rc.6 indefinitely and reported it in
every npm script banner (e.g. 'lint:ci').

Bumps 43 synchronized manifests via the npm 'version' lifecycle hook
(scripts/sync-manifest-versions.cjs --stage + scripts/gen-capability-registry.cjs
--write), matching the file set of commit 27f69cc48 ('chore: sync next
package version to 1.7.0-rc.6') and commit dd4c90f82 ('chore: finalize
v1.7.0').

This is the immediate Layer-1 repair for #2423. Layer-2 (prevent recurrence)
is the workflow patch in the previous commit; Layer-3 (regression test)
ships with it. Future X.Y.0 final releases will produce this commit
automatically once the workflow fix lands.

* test(release): tighten #2423 dry-run gate assertion to the sync step

Code review of fix/2423 found that test #3 ('gates sync-next-version on
!inputs.dry_run') asserted too loosely: it scanned the entire finalize
block for any '!inputs.dry_run' line, so it would still pass if the
gate were stripped from the sync-next-version step specifically — the
exact regression the test name promises to catch. The finalize job has
multiple steps with their own !inputs.dry_run gates (e.g. Verify
publish), so the loose version masked the very bug it claimed to detect.

Tighten by extracting the specific YAML step block containing
'scripts/sync-next-version.cjs' and asserting the gate appears within
THAT step's lines, not anywhere in the job. Verified the tightened test:

  - PASSES against the post-fix workflow (sync step has its own gate)
  - FAILS when the sync step's gate is stripped (even when other steps
    in finalize retain their own !inputs.dry_run gates) — the exact
    regression that previously slipped through

Adds extractStepBlockContaining(jobBlock, marker) helper alongside the
existing extractJobBlock(text, jobName). Reuses the same indentation-
based parsing, so it stays valid as the file grows.

* chore(changeset): backfill pr:2437 in .changeset/sturdy-ibex-jump.md

CLAUDE.md changeset convention: 'Use placeholder pr:0 during initial commit.
Backfill immediately after gh api POST /pulls returns the real number.'

PR #2437 created from branch fix/2423-release-finalize-sync-next-version.

* fix(#2423): add see #2423 to allow-test-rule exemption per ADR-456

CI lint-allow-test-rule-refs failed on PR #2437: ADR-456 requires new
allow-test-rule exemptions added after the ADR's acceptance to include a
tracking issue number in the comment, in the form
  // allow-test-rule: <reason> (see #NNN)
The exemption added in commit 976c8b0a2 lacked this ref. Fixed.

Verified locally:
  node scripts/lint-allow-test-rule-refs.cjs
    → ok lint-allow-test-rule-refs: 173 grandfathered exemption(s) tracked, no novel untracked offenders
2026-07-19 14:33:19 -04:00
Tom Boucher
40ce95f882 fix(#2358): scope review.md and ship.md temp files to a per-run mktemp directory (#2433)
* fix(#2358): scope review workflow temp files to a per-run mktemp dir

/gsd-review wrote every prompt/section/output temp file to a hardcoded
/tmp path keyed only on the phase number, so two GSD projects sharing
a small phase number collide on the exact same path and a crashed
run's leftover file becomes bait a later, unrelated run can silently
read. ship.md's external peer-review stderr capture was strictly
worse — one shared, unqualified path across every project/phase/run.

Thread a single mktemp -d "${TMPDIR:-/tmp}/gsd-review.XXXXXX" run
directory through every review.md temp path (67 sites) via a new
{run_dir}/$RUN_DIR placeholder, mirroring the existing {phase}
substitution mechanism, and clean it up at the end of the run. Route
ship.md's stderr capture through a per-run mktemp file the same way.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(#2358): regenerate fixtures + lint gate-prep

* fix(#2358): repair failing tests after gate verification

* fix(#2358): thread RUN_DIR scoping into reviewer-instances.md (#1517)

review.md's own invoke_reviewers step lazily loads
gsd-core/references/reviewer-instances.md for the review.reviewer_instances
codepath, but that doc was missed when review.md and ship.md were moved to
the run-scoped {run_dir} temp directory. It still read the combined prompt
from the old /tmp/gsd-review-prompt-{phase}.md (which build_prompt no longer
writes, breaking reviewer-instances functionality outright) and wrote each
instance's output to the old unscoped /tmp/gsd-review-${INSTANCE_NAME}-{phase}.md,
leaving the exact cross-project temp-file collision bug open for that code
path. Both paths now thread through {run_dir}, matching every other reviewer
block in review.md.

Extends the existing #2358 regression test with assertions pinning
reviewer-instances.md's prompt read and output write to {run_dir}, and adds
the Fixed changeset fragment. Regenerated the golden-install-parity content
hashes for reviewer-instances.md via `npm run gen:golden` (paths unchanged;
only the modified file's hash moved).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(#2358): backfill changeset pr (#2433)

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 13:12:03 -04:00
Tom Boucher
a7d83dc234 fix(#2390): warn on goal-shaped phase.add titles, correct auto-detect docs (#2425)
* fix(#2390): phase.add title warning + auto-detect doc fix

phase.add now returns a `warning` field when a description reads as
goal-shaped (>80 chars and/or multi-sentence) rather than title-shaped,
instead of silently writing the whole paragraph verbatim as the
`### Phase N:` header. The CLI still creates the phase as-is (the
strict two-layer slash-vs-CLI interface is unchanged); the warning
just surfaces the gap.

Also clarifies six doc sites (command argument hints, workflow
detection steps, and how-to/reference docs) that described the
phase-number argument as "auto-detecting" the next unplanned phase --
that detection is an orchestrating-workflow/LLM step reading
ROADMAP.md (concretely: `query roadmap.analyze`'s `next_phase`
field), not a `gsd-tools.cjs` CLI feature.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(#2390): regenerate fixtures + lint gate-prep

* fix(#2390): repair failing tests after gate verification

* chore(#2390): add changeset (#2425)

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 07:53:04 -04:00
Tom Boucher
1720aacf0c feat(#1949): <precondition> task element — Design by Contract (#2422)
* test(#1949): add failing-first tests for <precondition> element

Red phase for issue #1949 (Design by Contract: <precondition> element
asserted before task execution). Tests assert:

- docs/reference/plan-md.md documents the new <precondition> element
- agents/gsd-planner.md @-references planner-preconditions.md and stays
  under the 49152-char cap (progressive-disclosure requirement)
- gsd-core/references/planner-preconditions.md exists and documents the
  three emission cases mandated by the issue (user_setup / prior-phase
  artifact / env-var) and the contract triad mapping
- agents/gsd-executor.md asserts <precondition> before task execution
  and routes unmet preconditions through existing checkpoint machinery
- cmdVerifyPlanStructure (behavioral via runGsdTools) accepts plans both
  with and without <precondition> — the additive-validation guarantee
- Parity assertion: plan-md.md and planner-preconditions.md agree on the
  canonical tag spelling (DEFECT.GENERATIVE-FIX-DIVERGENCE guard)

Most prose-contract assertions are Red until the implementation lands.
The behavioral validator assertions pass immediately (regression guards
proving the validator already accepts unknown optional tags).

* feat(#1949): <precondition> task element — Design by Contract

Add an optional <precondition> element to <task> in PLAN.md (issue #1949,
The Pragmatic Programmer Topic 23). The front-of-task side of the plan
contract — preconditions (before) ↔ postconditions (<verify>/<done>/
<acceptance_criteria>, after) ↔ invariants (must_haves.truths, across the
whole plan). Together with the tracer-bullet proposal (#1945), this closes
both ends of the 'outrunning your headlights' failure mode for an
autonomous AI executor.

Acceptance criteria met:
- <precondition> is an optional element on <task>; plans that omit it
  validate unchanged (cmdVerifyPlanStructure checks for presence of
  required tags, does not reject unknown optional tags).
- gsd-executor evaluates the precondition before any other task work.
  Unmet halts execution with a checkpoint:human-verify and no partial
  commit; met or absent produces no visible change to execution flow.
  Unmet is never auto-approved under AUTO_CFG=true — a missing
  prerequisite is a fact the executor cannot establish on its own.
- gsd-planner emits <precondition> in exactly the three cases the issue
  mandates: user_setup consumption, prior-phase artifact dependency, and
  env-var/runtime-config dependency.
- Tests cover met, unmet, and absent preconditions plus the additive-
  validator guarantee.

Files:
- gsd-core/references/planner-preconditions.md (NEW): full emission
  rules, the three cases with worked examples, format guidance,
  anti-patterns, the contract triad mapping, and the executor assertion
  contract. Progressive disclosure.
- agents/gsd-planner.md: slim <precondition> note in Task Anatomy with
  @-reference to the new file. To stay under the 49152-char agent-file
  cap (27-char headroom before this change), the inline
  <comment_text_discipline> and <region_scoped_negative_gate> summaries
  are compressed to one-line pointers — their full rules already live in
  planner-antipatterns.md, so no content is lost.
- agents/gsd-executor.md: new step 0 'Precondition check' in the
  execute_tasks loop, before the type dispatch, routing unmet through
  checkpoint_return_format.
- docs/reference/plan-md.md: new Preconditions section in the schema
  reference, with the canonical example and the three emission cases.
- CONTEXT.md: Precondition glossary entry as a sibling of Tracer Bullet.
- docs/INVENTORY.md + INVENTORY-MANIFEST.json: row for the new
  references/planner-preconditions.md (regen via gen-inventory-manifest).
- tests/precondition-element.test.cjs: failing-first tests covering
  schema docs, planner emission contract, executor assertion contract,
  reference-file presence + the three cases, behavioral additive-
  validator guarantee, and a parity assertion (DEFECT.GENERATIVE-FIX-
  DIVERGENCE guard).
- .changeset/quick-hawks-bark.md: Added fragment.

Companion to #1945 (tracer bullets).

* chore(#1949): regen agent-size baseline + install-tree goldens

Documented baseline regenerations required by the feat(#1949) prose changes
(RULESET.AGENT_SIZE_BUDGET + golden-install-parity):

- npm run size:baseline — locks in the new gsd-executor.md size (+1050
  bytes: the precondition-check step 0 block). gsd-planner.md is net
  smaller (-142 bytes: compressed two inline summary blocks whose full
  rules already lived in planner-antipatterns.md to make room for the
  slim <precondition> pointer). No hard-cap breach.
- npm run gen:golden — pick up the new references/planner-preconditions.md
  + the two changed agent files across all 18 runtime install trees.

Both regens are CI-mandated after intentional agent/reference changes;
see CLAUDE.md 'RULESET.AGENT_SIZE_BUDGET' and the comments in
tests/golden-install-parity.test.cjs.

* fix(#1949): bound <precondition> checks to read-only (security review)

Apply the security-review finding (LOW, isolated /security-review subagent):
the executor's 'run the cheapest check' phrasing for a plan-author-controlled
prose line was broader than ideal — a hostile plan author could craft a
<precondition> whose 'cheapest check' is side-effecting (curl to an attacker
host under the guise of verification, rm -rf before checking, secret emission).

The risk is inherited from GSD's existing plan-trust model (<verify>, <action>,
<done> already direct the executor to run arbitrary shell), so <precondition>
does not materially expand it. But the new prose actively directs execution
('run the check') rather than passively consuming the element, so the bound
is worth making explicit.

Tightened across all four surfaces that describe the check shape:
- agents/gsd-executor.md step 0: 'Verify with read-only checks only — file
  existence, env var presence (no value output), idempotent GET /health-style
  pings. Do NOT run commands with side effects (writes, network POSTs, secret
  emission) as the check; if a side-effecting check seems required, halt and
  surface via checkpoint instead.'
- gsd-core/references/planner-preconditions.md Format section: same bound,
  plus the halt-and-surface escape hatch.
- docs/reference/plan-md.md Preconditions section: mirrored.
- CONTEXT.md Precondition glossary entry: mirrored.

Regenerated agent-size baseline (executor grew 46186 -> 46440; still under
the 49152 cap) and install-tree goldens.

* chore(#1949): backfill changeset pr number 2422

Per CONTRIBUTING.md changeset workflow + feature-builder directive Step 8.7:
backfill the placeholder pr:0 with the real PR number immediately after
gh pr create returns. Avoids the fail_invalid_fragment gate.

* fix(#1949): cite [#1949] on allow-test-rule exemption (ADR-456)

CI's lint:ci runs lint-allow-test-rule-refs which per ADR-456 requires
every // allow-test-rule: exemption on a NEW test file to carry an issue
reference (#NNN or URL). My earlier push omitted it.

Local 'npm run lint' (eslint) does NOT run this check — only 'npm run
lint:ci' does. CLAUDE.md explicitly warns: 'lint:ci ≠ lint — CI runs
lint:ci; a local pass is not the gate.' I should have run lint:ci before
pushing; correcting now.

Pattern matches the companion feature's test file:
tests/tracer-bullet.test.cjs:1  // allow-test-rule: source-text-is-the-product [#1945]
2026-07-19 07:52:36 -04:00
Tom Boucher
8d2f8bcb23 fix(#2388): gate shared requirement completion on sibling plans, revert on gaps (#2424)
* fix(#2388): gate shared-ID requirement marking and revert on gaps_found

Adds requirements.ready-ids (execute-plan.md's update_requirements step)
so a requirement ID declared by multiple plans in a phase only marks
Complete once every declaring plan has produced a SUMMARY.md, and
requirements.revert-phase (execute-phase.md's gaps_found branch) so a
gaps_found verdict reverts the phase's own prematurely-Complete IDs
before the gap report renders. Single-plan IDs still mark immediately.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(#2388): regenerate fixtures + lint gate-prep

* fix(#2388): repair failing tests after gate verification

* chore(#2388): add changeset (#2424)

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 07:52:17 -04:00
Tom Boucher
873bdf51e5 fix(#2352): expand tilde paths in review scope before the deleted-file filter (#2419)
* fix(#2352): tilde-expand SUMMARY.md key-files paths before deleted-file filter

compute_file_scope's "Filter deleted files" step tested the literal `~/...`
value from SUMMARY.md key-files entries with `[ -f "$file" ]`, which bash
never tilde-expands (only a literal `~` in source text expands, not one
arriving as an already-expanded variable value). Real files recorded with a
`~/...` path were silently misclassified as deleted and dropped from
REVIEW_FILES, and a phase whose every recorded file used a tilde path hit the
empty-scope skip as a false negative.

Adds a tilde-normalization loop as step 1 of post-processing (all tiers),
before the deleted-file filter, rewriting a leading `~/` to `${HOME}/...` so
downstream existence checks, the empty-scope short-circuit, and the
FILES_TO_READ/CONFIG_FILES construction all see a real, openable path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(#2352): regenerate fixtures + lint gate-prep

* chore(#2352): add Fixed changeset fragment (pr 2419)

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 21:29:58 -04:00
Tom Boucher
dd5a2211c9 enhance(#1964): semantic knowledge-base recall via MemPalace (keyword fallback) (#2416)
* test(#1964): add failing-first semantic-recall contract tests

Epic #1957 Phase 3C (final). Source-text-is-the-product contract tests:
semantic recall via MemPalace (top-k meaning-similar prior resolutions, catches
same-root-cause/different-wording cases), indexing resolved sessions at archive,
graceful degradation to keyword matching when MemPalace is absent,
knowledge-base.md stays the durable plain-text source of truth, agent Phase 0 /
Matching Logic is semantic-first (the stale 'keyword overlap, not semantic
similarity' claim must go), and no new embedding/vector infra (reuse MemPalace).

Failing-first: reference, the Matching Logic reframe, the Phase 0 consolidation,
and the archive indexing step do not yet exist.

* feat(#1964): semantic knowledge-base recall via MemPalace (keyword fallback)

Epic #1957 Phase 3C (FINAL). Replaces keyword-overlap matching with semantic
recall: at Phase 0 the debugger queries MemPalace with the current symptoms
and surfaces the top-k meaning-similar prior resolutions, catching the
same-root-cause/different-wording cases keyword overlap missed (the self-noted
'keyword overlap, not semantic similarity' limitation). Resolved sessions are
indexed into MemPalace at archive (symptoms + root_cause(s) + fix + recurrence
guard). knowledge-base.md remains the durable plain-text source of truth; when
MemPalace is absent the debugger falls back to keyword-overlap matching
(logged, never a silent skip). No new embedding/vector infrastructure —
MemPalace is reused.

Size-neutral agent edits: the Matching Logic section reframed (keyword-only ->
semantic-first + keyword-fallback + @-include); Phase 0's three keyword bullets
consolidated into one semantic-first bullet; one MemPalace-indexing step added
at archive. Agent at 57222 B (122 B headroom — final phase). Full rules in
gsd-core/references/debugger-semantic-recall.md. INVENTORY + manifest +
agent-size baseline + install-parity goldens + AGENTS.md updated.

* fix(#1964): address orthogonal review (invocation mechanism, index Resolution-not-symptoms + redaction, fallback detail)

- HIGH: the 'query MemPalace' instruction was WHAT-level only; the agent has
  no MCP tools. Added an Invocation section naming the Bash CLI
  (mempalace search --wing <wing>) + MCP-when-registered + wing resolution
  (config.mempalace.wing -> project_code -> project dir), matching every other
  MemPalace integration. Without this the feature silently degraded to keyword
  matching even when MemPalace was present.
- MEDIUM (security x2): index the agent-authored Resolution summary
  (root_cause + fix + recurrence_guard), NOT raw user-supplied Symptoms —
  excludes attacker-controlled prose from the cross-session index AND reduces
  secret/PII leakage. Redact secret-shaped values before indexing. Stated the
  write order (KB append + commit MUST succeed before indexing).
- LOW: restored 'identifiers' + 'case-insensitive' to the keyword fallback;
  added a test asserting the fallback mechanics survived the Phase 0
  consolidation (Error patterns field, 2+ token overlap, identifiers,
  case-insensitive).

* chore(#1964): ratchet agent-size baseline downward (leaner archive bullet shrank gsd-debugger.md 57222->57197)

* chore(#1964): backfill changeset pr number (PR #2416)
2026-07-18 19:01:04 -04:00
Tom Boucher
c67f301867 feat(#1963): emit blameless-postmortem Prevention block at resolution (#2410)
* test(#1963): add failing-first prevention/postmortem contract tests

Epic #1957 Phase 3B. Source-text-is-the-product contract tests: blameless
5-Whys that BRANCHES per Phase 2A RCA (not a single-cause chain; treats agent
error as 'why was that possible?'), the 'why wasn't this caught?' question,
the recurrence-guard taxonomy (regression test / assertion / lint rule / KB
pattern), the KB-entry why_not_caught + recurrence_guard fields with backward
compat, the session-manager prevention summary line, and the Zawinski
scope-boundary (a block, not a subsystem).

Failing-first: reference, archive_session edit, KB schema extension, and
session-manager summary do not yet exist.

* feat(#1963): emit blameless-postmortem Prevention block at resolution

Epic #1957 Phase 3B. At archive_session the debugger now produces a
Prevention block with three blame-free components: a branching 5-Whys causal
chain (branches per Phase 2A RCA, not a single chain; 'agent error' prompts
'why was that possible?', never blame), a 'why wasn't this caught?' answer
naming the missed gate (test/typecheck/lint/review/verify), and a concrete
recurrence guard (regression test / assertion / lint rule / KB pattern).

The knowledge-base entry gains two structured fields (why_not_caught +
recurrence_guard) so future Phase-0 recall surfaces the prior prevention, not
just the prior fix. Additive: old entries without the fields still load. The
session-manager compact summary surfaces a one-line prevention summary.

Full rules extracted to gsd-core/references/debugger-prevention.md (slim
archive_session step + 2 KB fields kept in the agent). INVENTORY + manifest +
agent-size baseline + install-parity goldens + AGENTS.md updated.

* fix(#1963): address orthogonal review (CRITICAL append-template drift + Phase-0 consumption + parity test)

- CRITICAL: the archive_session KB append template omitted Why not caught +
  Recurrence guard (only the Entry Format had them) — the feature's core
  deliverable silently did not happen. Added both fields to the append template
  the agent actually follows (nearest-instruction wins).
- HIGH: Phase 0 (KB read) only surfaced root_cause + fix; the new fields were
  dead data. Extended the Phase 0 Evidence line to consume why_not_caught +
  recurrence_guard when present (absent on old entries — backward compat holds).
- MEDIUM: added a cross-section parity test (every Entry-Format field must also
  appear in the append template — the guard that would have caught the
  Critical) + a Phase-0-consumption assertion.
- MEDIUM: the 'branches per Phase 2A' claim is now wired — reuses
  reasoning_checkpoint.candidate_causes across the four categories.
- MEDIUM: recurrence-guard taxonomy gains type refinement + config-default
  change; LOW: added 'build' gate to both surfaces for parity.
- NIT: compact-summary fallback shape ('no gate existed'); verify the guard
  artifact exists before recording it.

* test(#1963): anchor Phase-0 consumption test on the specific heading

The regex /Phase 0[\s\S]{0,1200}/ matched the first 'Phase 0' in the file
(in knowledge_base_protocol prose), not the Phase 0 block in investigation_loop.
Anchor on '**Phase 0: Check knowledge base**' and widen to 1500 chars.

* chore(#1963): backfill changeset pr number (PR #2410)
2026-07-18 17:38:54 -04:00