Files
msd-core/gsd-core/workflows/plan-phase.md
Tom Boucher 9e6c404151 chore: merge release v1.4.0 to main (#884)
* fix(#663): resolve open CodeQL/Dependabot security alerts (ReDoS, prototype pollution, workflow perms, qs DoS) (#665)

* fix(#663): resolve open CodeQL/Dependabot security alerts

- ReDoS: collapse ambiguous nested quantifiers in phase-heading regexes
  (verify/validate/commands) and the plan-filename lookahead (phase) to
  provably-equivalent non-backtracking forms
- prototype pollution: guard __proto__/constructor/prototype in setConfigValue
- remove dead no-op .replace(/-/g,'-') in phase.cts
- escape all regex metachars in bug-2839 test
- add contents:read permissions to security-scan + install-smoke workflows
- pin qs >= 6.15.2 via overrides (DoS GHSA)
- broaden prompt-injection allowlist to translated security-model docs

Closes #663

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

* test(#663): regression tests for prototype-pollution guard and roadmap-phase ReDoS

Behavioral test that config-set rejects __proto__/constructor/prototype keys
without polluting Object.prototype, plus a ReDoS guard (timing-bound) and
behavior-preservation assertions for the collapsed phase-heading regexes.

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

* test(#663): make ReDoS regression assert structured result, not elapsed time

Replace elapsed-time assertions (which tripped local/no-elapsed-assertion
ESLint rule and were unsound for synchronous ReDoS) with structured-result
assertions on adversarial inputs: assert that malformed phase headings/
unchecked-item lines without a terminating colon/space yield an empty Set,
which is both the correct behavior and an exercise of the fixed linear regex
on the catastrophic-backtracking input shape.

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

* chore(#663): add Security changeset fragment for #665

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

* test(#663): fold prototype-pollution regression into config.test.cjs

The standalone bug-663-config-prototype-pollution.test.cjs was a 9th
config-module test file, tripping lint-test-file-count (the allowlist is
ratcheted and must not grow). Consolidated into config.test.cjs instead.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* chore(#660): bump next to 1.3.1-dev.0 (-dev stream per ADR 660) (#672)

After the 1.3.0 release, next moves onto the -dev prerelease stream so the
trunk self-identifies as unreleased (floor = next patch). First manual
exercise of the ADR-660 release model.

Refs #660

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* ci(#660): use scoped GSD_BOT_PR_TOKEN for backmerge & release merge-back PR creation (#673)

The open-gsd org blocks the Actions GITHUB_TOKEN from creating PRs, so
auto-backmerge and the release finalize merge-back PR steps can't open
their PRs (must be done manually). Point those two steps at a scoped
secret (pull-requests:write + contents:write), falling back to
GITHUB_TOKEN so behavior is unchanged until the secret is added.

Refs #660

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* fix: accept published installer migration checksums

* fix(#670): self-healing recovery for installer-migration checksum drift (#675)

Editing the body of an already-released installer migration drifts its computed
checksum (it hashes plan.toString()). The integrity guard then hard-aborted
every prior install on upgrade with "applied migration checksum changed" — a
100% reproducible blocker (v1.3.0, all platforms).

Already-applied migrations are filtered out of `pending` and never re-run, so
a drifted checksum is functionally inert. ADR-0008 anticipates checksum-mismatch
state as something the install-state layer must handle gracefully (plan -> apply
-> recover/report), not abort on.

This supersedes the published-checksum allowlist merged in #674 (per-release
maintenance debt — every historical checksum hand-pinned, still throws for any
unregistered value) with a general, self-healing recovery:

- Replace the throwing guard with non-fatal `collectAppliedChecksumDrift`,
  surfaced on `plan.checksumDrift`.
- Reconcile drifted stored checksums durably on the next state write
  (`reconcileDriftedChecksums`), idempotently (no perpetual writes).
- Relocate the "shipped migration bodies are immutable" rule to a CI baseline
  test that locks every shipped migration's checksum and fails on body drift —
  where #615 should have been caught, instead of blocking users.

Removes #674's legacyChecksums field, per-migration checksum pins,
published-checksums.json fixture, and compat test.

Fixes #670

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* fix(#676): consolidate hotfix into release.yml (delete standalone hotfix workflow) (#678)

* fix(#676): consolidate hotfix into release.yml; delete standalone hotfix workflow

npm allows only one trusted publisher per package and it is release.yml, so the
standalone hotfix.yml (token-auth) could never publish via OIDC (ENEEDAUTH on the
v1.3.1 finalize). Fold the patch/hotfix path into release.yml — the sole OIDC
trusted publisher — and delete hotfix.yml.

- validate-version accepts X.Y.Z (Z>0) → hotfix/X.Y.Z + base_tag; rejects rc for
  hotfixes; X.Y.0 still → release/X.Y.0.
- create branches hotfix/X.Y.Z from the base tag with optional auto-cherry-pick
  (default on) of fix:/chore: from next; release path unchanged.
- finalize is branch-agnostic already and publishes @latest via the existing
  OIDC trusted publisher (no NODE_AUTH_TOKEN).
- CONTRIBUTING branching table updated; hotfix.yml removed.

Fixes #676
Supersedes #677.

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

* fix(#676): add hotfix/patch path to release.yml (OIDC trusted publisher)

The companion to the hotfix.yml deletion: release.yml now handles patch
versions (X.Y.Z) via hotfix/X.Y.Z branches and publishes @latest through the
existing OIDC trusted publisher. CONTRIBUTING branching table updated.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* fix(#676): update tests/docs referencing deleted hotfix.yml (#680)

hotfix.yml was deleted (folded into release.yml). Remove the now-broken
release-coverage-scope and policy-release-no-npm-self-upgrade assertions that
readFileSync'd hotfix.yml (release.yml equivalents retained), drop the dead
install-smoke.yml path trigger, and update VERSIONING.md / docs/branching.md
prose to describe hotfixes via the Release workflow with a patch version.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* fix: bump hono to 4.12.23 on next to clear moderate advisory

Same moderate hono advisory (GHSA-3hrh-pfw6-9m5x et al.) that blocked the 1.3.1
hotfix is present on next (was 4.12.19); bump to keep the npm-audit gate green.

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

* fix(#690): promote CHANGELOG 1.3.x + gate release-notes promotion (#694)

* fix(#690): promote CHANGELOG 1.3.x + gate release-notes promotion

/gsd:update showed an empty "What's New" preview after updating to 1.3.1
because CHANGELOG.md's 1.3.x content was never promoted out of [Unreleased]
into dated sections, so `scripts/changeset/cli.cjs extract` returned exit 2
("no releases in range").

- CHANGELOG.md: split [Unreleased] into dated [1.3.0] and [1.3.1] sections
  (1.3.1 = hono advisory bump + installer-migration checksum self-heal, #670;
  1.3.0 = the feature release), restoring an empty [Unreleased].
- scripts/changeset/cli.cjs: new `verify` subcommand that exits non-zero when
  CHANGELOG has no dated `## [x.y.z]` heading for a version; hoist shared
  stripV/resolveChangelogPath helpers used by extract + verify.
- .github/workflows/release.yml: gate the finalize job on `verify` (after the
  build, before tag/publish) so an unpromoted CHANGELOG can never ship again.
- gsd-core/workflows/update.md: move `rm -f $CHANGELOG_TMP` after the
  human-readable extract re-run so the preview no longer degrades to
  "(changelog unavailable)".
- tests: regression guard for the 1.3.x headings + extract range + verify
  command coverage (present/absent/undated/v-prefixed/--json/prerelease).

Closes #690

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

* chore(#690): add changeset fragment for #694

Fixed-type fragment for the user-facing /gsd:update preview fix and the
release-notes promotion gate.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* fix(#698): make auto-backmerge main→next actually land (admin-merge via PAT) (#699)

The Auto Back-Merge workflow could open a back-merge PR but never landed it,
and silently reported success on failure. Fixes:

- Merge via admin bypass using GSD_BOT_PR_TOKEN (the PAT) instead of auto-merge,
  since back-merge PRs structurally can't satisfy next's required checks
  (Issue-link / PR-template / changeset-lint).
- Stop swallowing create/merge failures with "|| echo ::warning" — real
  failures now fail the job. (That greenwashing hid the whole bug.)
- Resolve the PR number with `--jq '.[0].number // empty'` (a no-match returns
  the string "null", not empty) and capture a freshly-created PR's number from
  the create URL to avoid GitHub API eventual-consistency races.
- Merge the exact PR number (env-bound) rather than by branch name.
- Force-push the disposable SHA-named bot branch, guarded by a
  chore/backmerge-main-to-next-* name check so a mislabeled PR can't redirect
  the force-push.
- Apply labels non-fatally so a missing label can't abort PR creation.
- On a genuine merge conflict, fail loudly (::error + exit 1) instead of
  pushing an empty branch and opening a PR with no diff.

Closes #698

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* fix(#637): route 3 more workflows through gsd_run launcher (hardcoded $HOME sweep) (#642)

* fix(#637): route 3 more workflows through gsd_run launcher (hardcoded $HOME sweep)

The hardcoded `node "$HOME/.claude/gsd-core/bin/gsd-tools.cjs"` invocation form
fixed in plan-phase.md (#621) survived in three more workflows. Same bug class:
on a global/shim-only install with no project-local runtime, the hardcoded path
can miss a working install, so the step reports the tool "not found" instead of
resolving it via the launcher. #3668 introduced gsd_run resolution; these sites
were missed.

- plan-review-convergence.md: convert the 3 hardcoded invocations (init,
  roadmap get-phase, state planned-phase) to gsd_run. File already carried the
  canonical preamble (first gsd_run is the earlier convergence-enabled check).
- ingest-docs.md, spec-phase.md: convert their hardcoded invocations to gsd_run
  and inject the canonical launcher preamble via
  `node scripts/sync-runtime-launcher.cjs` (these files previously had no
  gsd_run and no preamble). The injected preamble is byte-equal to
  _runtime-launcher.snippet.sh and precedes the first gsd_run call, per
  runtime-launcher-parity invariant (B).
- Add tests/bug-637-workflow-no-hardcoded-home-tool.test.cjs: repo-wide
  regression guard asserting NO workflow .md invokes gsd-tools via a hardcoded
  $HOME path. Generalizes the plan-phase-only guard from #621 — the parity test
  guards retired $GSD_SDK / bare /gsd-tools tokens but not this form, which is
  how it survived across four files. Fails on the pre-fix files, passes after.

runtime-launcher-parity 7/7; full unit suite green (3477 pass / 0 fail).

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

* chore(#637): add changeset fragment for PR #642

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

* test(#637): update stale bug-2801 assertion to expect gsd_run

bug-2801 pinned ingest-docs.md to the hardcoded node "$HOME/.../gsd-tools.cjs" init form, which #637 replaces with the gsd_run launcher. Flip the assertion to expect gsd_run init ingest-docs; the bare-gsd-tools rejection and CLI-handler tests are unchanged, and bug-637's repo-wide guard now owns the no-hardcoded-$HOME invariant.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Tom Boucher <trekkie@nomorestars.com>

* fix(#705): route hardcoded $HOME gsd-tools invocations in agents/commands through gsd_run (#707)

* fix(#705): route hardcoded $HOME gsd-tools invocations in agents/commands through gsd_run

The hardcoded `node "$HOME/.claude/gsd-core/bin/gsd-tools.cjs" <cmd>` form
(fixed for workflows in #621/#637) survived in agent/command surfaces and
misresolves on global/shim-only installs. Route every agent-executed
invocation through the resolved `gsd_run` launcher in gsd-phase-researcher,
gsd-planner (load_graph_context extracted to a shared reference to stay under
the planner size budget), import, and graphify. Add a regression guard over
agents/ + commands/ + gsd-core/references/ bash blocks. User-facing display
messages and docs are intentionally left untouched.

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

* fix(#705): use repo changeset fragment format (type: Fixed, pr: 707)

The hand-written fragment used the standard changesets package format
(package: bump) which lacks the type:/pr: frontmatter the repo's
docs-required lint consumes (fail_malformed_fragment / missing_type).
Regenerated via scripts/changeset/new.cjs.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* fix(#685): set windowsHide on all Windows child-process spawns (#688)

* fix(#685): set windowsHide on all Windows child-process spawns

A visible "gsd-core" console window flashed on Windows whenever a gsd-core
child process spawned without `windowsHide: true`. The most visible offenders
fire on every SessionStart / `/clear` (execNpm's `shell:true` npm view via the
update-check worker) and on every Edit/Write/MultiEdit in a worktree (the
worktree-path guard's git probe).

Add `windowsHide: true` to every external-binary spawn in the runtime source:
- hooks/gsd-context-monitor.js (record-session spawn)
- hooks/gsd-worktree-path-guard.js (SPAWNOPT)
- hooks/gsd-workflow-guard.js (git branch --show-current)
- src/shell-command-projection.cts (execGit / execNpm / execTool)
- src/check-command-router.cts (git log execFileSync)
- src/roadmap-upgrade.cts (git status/rev-parse/reset/clean execSync)

gsd-check-update.js already had it (the precedent). probeTty's tty call is
POSIX-only and intentionally untouched. Adds a regression test that asserts
each site plus a repo-wide completeness guard so a future external-binary
spawn that omits windowsHide fails CI. No behavior change off-Windows.

Closes #685

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

* chore(#685): set changeset pr number to 688

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* fix(#687): bound agy print mode with its native --print-timeout (#689)

`/gsd-review --agy` hung indefinitely on large prompts. agy's print mode runs the
full tool-enabled agent, and on a big, file-path-rich prompt its agentic Cascade
loops on the code_search/grep tool and never converges; the transcript fallback
only runs after agy exits, so it can't recover a run that never exits.

The agy CLI exposes no per-tool deny (that lives in the Antigravity SDK), but it
does expose --print-timeout — agy's native print-mode cap. Pass it explicitly so a
stalled run self-terminates through the tool's own mechanism; a non-zero exit
discards any partial output so the existing transcript fallback / "review failed"
stub take over. Adds a regression test.

Closes #687

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* fix(#669): /gsd-review --cursor actually invokes cursor-agent (#686)

* fix(#669): /gsd-review --cursor actually invokes cursor-agent

The Cursor reviewer branch in review.md never ran the agent:
- detection probed `cursor` (the IDE launcher) instead of the headless
  `cursor-agent` binary
- the invocation used the two-token `cursor agent` (the IDE treats `agent`
  as a file-path argument, so the agent never starts)
- the prompt was piped via stdin, but `cursor-agent -p` reads the prompt
  from a command-line argument, and `2>/dev/null` hid the empty result

Probe `cursor-agent`; invoke `cursor-agent -p --mode ask --trust
--output-format text` with the prompt passed as a file-path-reference
argument (avoids the OS arg-length limit on large prompts); capture stderr
so failures are diagnosable. Invert tests/cursor-reviewer.test.cjs to assert
the corrected contract, with negative guards against the two-token form and
the stdin pipe. The sibling `agy` reviewer already used the argument form.

Closes #669

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

* chore(#669): set changeset pr number to 686

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* chore: archive 463 shipped changeset fragments before wiring render (#714)

CHANGELOG promotion was a manual operator step that was never run, so 463
fragments for work already shipped in <=1.3.1 accumulated in .changeset/.
Their notes were already hand-curated into the dated [1.2.0]/[1.3.0]/[1.3.1]
CHANGELOG sections (#690 backfill, PR #694). Rendering them now would
duplicate and mis-attribute shipped work.

Move them to .changeset/archived/ (read non-recursively by all changeset
tooling, so never rendered), keeping only the 3 genuinely-unreleased
fragments at the top level. Prep for wiring `render` into the release
finalize job (#690 follow-up).

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* chore(release): wire CHANGELOG render into release finalize job (#690 follow-up) (#715)

* feat(#690): wire CHANGELOG render into release finalize job

CHANGELOG promotion has always been a manual operator step, which is why
1.3.0/1.3.1 shipped unpromoted (#690). PR #694 added a `verify` latch that
fails a release lacking a dated heading, but nothing performed the promotion.

Wire `changeset render` into the finalize job, after build/test and before
the verify gate, committing the promoted CHANGELOG so it ships with the
release. Add a `--allow-empty` flag to cmdRender so a zero-fragment release
still emits a dated heading (with a '_No notable changes._' placeholder)
instead of writing nothing and tripping the verify gate.

Note: requires the changeset-archive cleanup (separate PR) to land first, so
the first render consumes only genuinely-unreleased fragments.

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

* chore(#713): set changeset pr number to 715

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* docs: document --only and --text flags for /gsd-autonomous (#695) (#716)

Adds --only N and --text to the COMMANDS.md reference table and the
run-phases-autonomously how-to guide, revised to fit the Diataxis
framework (reference: factual/parallel rows; how-to: goal-framed sections).

* feat(#656): Research module — content-addressed cache + provider seam + registry-API legitimacy (#664)

* feat(#656): add Research Store module (content-addressed cache, TTL staleness)

Content-addressed research cache behind a clock seam: researchKey (sha256, deterministic), putResearch/getResearch ({hit,stale}, never throws), ttlForSource (curated HIGH 30d / MED 7d / web LOW 1d), two-tier resolveStorePath (curated -> ~/.gsd/research-cache, web/synthesis -> project .planning/research/.cache). 28 behavioral + property tests; boundary coverage at ttl-1/ttl/ttl+1.

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

* feat(#656): add Research Provider module (waterfall + confidence + plan)

Single source of truth for the Balanced provider waterfall (docs Context7->Ref->Jina, web Exa+Tavily, fallback Perplexity/Brave, Firecrawl scrape-only). classifyConfidence stamps HIGH|MEDIUM|LOW by provider (never throws). providerAvailability maps config flags to usable providers. planResearch checks the Research Store (injected seam) and returns cache-hits + a per-question fetch plan, falling through the waterfall to the always-available websearch terminal. 22 behavioral + property tests.

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

* feat(#656): add Package Legitimacy module (registry-API verdicts, slopcheck optional)

Replaces the pip-install-or-degrade slopcheck prose gate with code: classifyPackage (pure, never throws) computes OK|SUS|SLOP from tunable thresholds (minAgeDays 30, minWeeklyDownloads 1000, requireRepo). checkPackages queries injectable npm/PyPI/crates registry adapters (real https with 5s timeout, degraded-not-thrown on failure); slopcheck is one optional adapter that can only escalate severity, never degrade to [ASSUMED]. 34 behavioral + property tests; boundary coverage on age and downloads (limit-1/limit/limit+1).

Known follow-up: real npm adapter must add api.npmjs.org last-week downloads fetch (currently null -> unknown-downloads). Issue #656. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(#656): detect Tavily/Ref/Perplexity/Jina provider keys; complete npm downloads adapter

config: add tavily_search/ref_search/perplexity/jina availability flags (env var or ~/.gsd/<x>_api_key), mirroring brave_search/exa_search/firecrawl, so the Research Provider waterfall can gate them. package-legitimacy: real npm adapter now fetches api.npmjs.org last-week downloads (bounded, degraded-not-thrown) so weeklyDownloads is populated. +12 config tests; 34 legitimacy tests unchanged.

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

* feat(#656): expose Research seam via gsd-tools query (research-plan, research-store, package-legitimacy)

Routes the L2-hybrid surface so agents reach it as CLI: 'query research-store get/put' (cache, HOME-sandboxable), 'query research-plan --input' (cache-hits + fetch plan from planResearch), 'query package-legitimacy check --ecosystem' (async registry verdicts). Commands skip .planning root resolution and appear in top-level usage. 5 behavioral runGsdTools tests; command-contract unchanged (335).

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

* docs(#656): document Research module (CONTEXT predicates, ADR-0656, architecture, changeset)

Adds GSD-RESEARCH.* + DEFECT.RESEARCH-PROVIDER-PROSE-DRIFT predicates to CONTEXT.md, ADR-0656 recording the L2-hybrid seam decision, a docs/ARCHITECTURE.md Research Module subsection, and an Added changeset fragment (pr:0, backfill on PR). Notes the #657 deferrals (agent collapse + install.js MCP mapping).

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

* chore(#656): sync inventory for research modules

Regenerate INVENTORY-MANIFEST.json and bump docs/INVENTORY.md CLI Modules count 82->85 with rows for research-store/research-provider/package-legitimacy (DEFECT.INVENTORY-DRIFT).

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

* chore(#656): eslint-ignore generated research .cjs artifacts (ADR-457)

research-store/research-provider/package-legitimacy .cjs are tsc-generated from src/*.cts, so they belong in the ESLint ignore block (lint the .cts source, not the emitted .cjs). Fixes tests/551-eslint-bin-lib-coverage.

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

* chore(#656): backfill changeset pr number to #664

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

* chore(#656): satisfy eslint lint-tests gate

Fix 20 eslint errors in the new research files: use helpers.cleanup() instead of raw fs.rmSync() in tests (local/no-raw-rmsync-in-tests, Windows-EBUSY retry budget); drop redundant '| string' union members and unnecessary type assertions; deterministic object normalization in researchKey (no-base-to-string). Logic unchanged; 6180 tests still green.

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

* fix(#656): harden package legitimacy per review (W1/W2/I3/I4)

W1: httpsGet now reads statusCode; npm/PyPI/crates map 404 -> exists:false -> SLOP (registry-existence is the #1 slopsquatting defense; previously only npm caught it). Transport made injectable (_setHttpGet) for hermetic 404 tests. W2: suspicious-postinstall is now terminal SLOP independent of the optional slopcheck adapter, and the regex drops the bare https?:// arm (over-fired on esbuild/sharp/node-gyp) for shell-exec/download-exec signatures only. I3: checkPackages now threads version to registry.lookup and adapters verify that specific version exists. I4: moreServerVerdict -> moreSevereVerdict. +11 regression tests (all RED-first); 45 total green.

Addresses review by @davesienkowski on #664. Issue #656. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(#656): research-store tier coherence + freshness + version TTL (W4/I1/I2/I4)

I1: tier now derives from source (curated -> user ~/.gsd, else -> project .planning), not kind, so put-tier and get-tier can't diverge; kind is a key component only. W4: getResearch searches both tiers and returns the freshest (non-stale preferred), never letting a stale curated entry shadow a fresh web one; blank version caps TTL at 1 day (no 30d on version-blind keys). I2: atomic platformWriteSync instead of raw fs.writeFileSync on the shared global path. I4: dropped the dead ttlForSource arm. CLI get now searches both tiers. +5 RED-first regression tests; 38 green.

Addresses review by @davesienkowski on #664. Issue #656. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(#656): expose classifyConfidence as a CLI route, killing dead code (W3)

Adds 'gsd-tools query classify-confidence --provider X [--verified]' so research agents get the confidence tier FROM CODE (provider waterfall + verification lever) instead of asserting it in prose. classifyConfidence previously had no runtime caller. HIGH means 'trusted provider'; --verified raises web results to MEDIUM (verification semantics documented in ADR-0656). +4 behavioral tests.

Addresses review by @davesienkowski on #664 (W3). Issue #656. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(#656): close Codex adversarial-review findings (path-traversal, version-age, malformed-cache)

HIGH: research key must be 64-hex sha256 (isValidResearchKey) + resolved-path containment check in put/get + CLI validation -> blocks '../../x' arbitrary-file-write. HIGH: package legitimacy now derives publishedAt from the REQUESTED version (npm time[version], PyPI releases[version] upload_time, crates versions[].created_at) so a new malicious version of an old package can't inherit old age and evade 'too-new'. MEDIUM: getResearch validates entry shape (finite fetched_at + positive ttl + required fields) -> malformed cache entry is a miss, not fresh-forever. +regression tests (RED-first); 111 green.

Codex adversarial review (required pre-PR gate). Issue #656. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(#656): close code-review correctness findings

(1) package-legitimacy CLI now rejects unknown --flags instead of silently consuming the following package as a flag value; only --ecosystem takes a value. (2) crates recent_downloads (90-day) normalized to a weekly figure before the minWeeklyDownloads threshold (was ~13x too lenient). (3) research-plan --input validates parsed JSON is an object with an Array questions before destructuring -> clean usage error instead of an uncaught TypeError on null/bad input. (4) research-store put rejects a flag value that is itself a --flag (no more storing '--source' as content). (5) planResearch skips questions whose text is not a non-empty string instead of emitting question:undefined. +13 RED-first regression tests; 143 green.

Code-review gate. Issue #656. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(#657): extract researcher documentation_lookup to shared @-reference

6 researcher agents carried a near-duplicate <documentation_lookup> block; consolidate into gsd-core/references/research-documentation-lookup.md (@-included). Unifies the ctx7 CLI fallback to the safer 'command -v ctx7' guard (drops silent 'npx --yes ctx7@latest' execution in 5 agents). Behavior-preserving dedup; inventory 63->64 references. Phase A of the agent collapse.

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

* refactor(#657): extract researcher philosophy + verification-protocol to shared @-references

philosophy and the pitfalls+pre-submission-checklist common-core were near-duplicated in project/phase researchers; consolidate into gsd-core/references/research-{philosophy,verification-protocol}.md (@-included). phase-researcher keeps its 3 extra checklist items inline. Pre-submission domains checklist made agent-agnostic so project-researcher doesn't lose features/architecture coverage. Write-contract intentionally left inline (bug-214 tests assert it verbatim). Inventory 64->66 refs. Behavior-preserving. Phase A.

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

* feat(#657): wire gsd-phase-researcher to the Research seam (Phase B / S1)

The phase researcher now CALLS the code seam instead of carrying inline mechanics: provider waterfall -> 'gsd-tools query research-plan' (+ research-store put to cache digests); confidence-tier prose -> 'gsd-tools query classify-confidence'; slopcheck pip-install protocol -> 'gsd-tools query package-legitimacy check'. This makes the Research module a real runtime consumer (validates the seam end-to-end, addresses reviewer S1) and removes the duplicated waterfall/confidence/slopcheck prose. RESEARCH.md output contract, commit step, structured returns, and Phase-A @-includes unchanged. package-legitimacy-gate.test.cjs rewritten prose-grep -> behavioral (asserts the seam invocation).

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

* feat(#657): wire gsd-project-researcher to the seam + add tavily/ref/jina MCP tools (Phase C.1)

project-researcher now calls gsd-tools query research-plan / classify-confidence (+ research-store put) instead of the inline provider waterfall + confidence-tier prose (mirrors the phase-researcher rewire; no package-legitimacy — phase-only). Output contract (STACK/FEATURES/ARCHITECTURE/PITFALLS/SUMMARY.md + sections, no-commit, structured returns, Phase-A @-includes) unchanged. Adds mcp__tavily/ref/jina__* to the project/phase/ui researcher tools frontmatter (Balanced provider set) so install.js MCP mapping (C.2) has a consumer.

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

* test(#657): cover tavily/ref/jina MCP install handling + frontmatter parity guard (Phase C.2)

Investigation: exa/firecrawl have no explicit per-runtime tool-mapping — every mcp__<server>__* except context7 rides the generic passthrough (Copilot lowercases; OpenCode/Cursor/Windsurf/Augment keep as-is; Gemini auto-discovers). tavily/ref/jina are handled identically, no install path broken. Added 12 copilot-install passthrough tests + a mcp-tool-inheritance parity guard (tavily co-declared with exa, jina with firecrawl, ref present across the 3 web researchers) so the MCP set can't drift. No io.github registry ids invented (none sourceable in-repo); documented as a follow-up. 488 tests green.

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

* feat(#657): profiles as source of truth for researcher agents + drift-guard (Phase C.3)

scripts/research-profiles.cjs declares each of the 7 researcher agents' identity + contract (name, description, color, tools, required @-includes, required gsd-tools seam calls, output-contract markers). scripts/gen-research-agents.cjs --check validates every committed agent against its profile; --write regenerates ONLY the frontmatter from profiles (body untouched) and is a verified no-op against the current agents (zero diff = fidelity). tests/research-agent-profiles.test.cjs is the DEFECT.GENERATIVE-FIX drift guard. Design note: profiles govern the generatable/contract surface rather than destructively regenerating the disparate operational prose bodies (those were deduped via @-includes in Phase A). scripts/ is not inventoried (no inventory change).

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

* fix(#657): complete agent provider-dispatch + parity guard; align legitimacy field; validate profiles

Adversarial-review findings: (HIGH) the seam-wired agents' Step-C dispatch only mapped 6 providers, so a planResearch result of jina/ref/perplexity/brave (reachable via the waterfall fallbacks) had no handling -> agent stall; completed both agents' dispatch to all 9 PROVIDER_WATERFALL ids + a catch-all, and added a parity test asserting agent dispatch stays in sync with research-provider PROVIDER_WATERFALL (DEFECT.GENERATIVE-FIX). (MEDIUM) phase-researcher package-legitimacy JSON example used 'package' but the module returns 'name' -> aligned. (LOW) gen-research-agents checkAgent now returns a clear failure for a malformed profile instead of throwing. +parity/validation tests (RED-first).

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

* fix(#656): make classifyConfidence verification-evidence-driven (W3)

Confidence conflated provider authority with claim verification — context7/ref
stamped HIGH purely by provider identity, and the only verification lever was a
self-set --verified flag. Split into two axes: provider authority (static) +
verification evidence (code-computed). HIGH now requires ground-truth
corroboration (legitimacyVerdict OK), independent of provider; authority alone
caps at MEDIUM; SLOP caps at LOW; the self-reported --verified is demoted to a
MEDIUM-only web lever. HIGH = corroborated-against-authoritative-source, not a
correctness guarantee. Adds --legitimacy-verdict to the classify-confidence CLI;
updates CONTEXT.md predicate + ADR-0656 (tier set unchanged, ADR-consistent).

Addresses davesienkowski's W3 review on #664.

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

* fix(#656): bind classify-confidence verdict to code, closing CLI self-grading

Adversarial review found the new --legitimacy-verdict flag was caller-supplied,
so an agent could self-assert OK->HIGH without any real legitimacy check —
reintroducing the exact self-grading hole W3 closes. Remove the free flag; the
CLI now computes the verdict via checkPackages only when --package/--ecosystem
is given (code-computed, not agent-asserted). Update the stale CLI test
(context7 alone -> MEDIUM) and extend the property test to vary legitimacyVerdict.

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

---------

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

* feat(#717): re-base workflow size budget on bytes + document quality rationale (#719)

* feat(#717): re-base workflow size budget on bytes + document quality rationale

Re-base tests/workflow-size-budget.test.cjs from line counts to byte
counts (matches Codex's 32,768-byte project_doc_max_bytes cap; deterministic,
no tokenizer). Tier ceilings: XL=90000, LARGE=54000, DEFAULT=38000, GRACE=3000;
discuss-phase target re-expressed as <30 KB. The #597 tighten-only ratchet and
per-file budget semantics are preserved unchanged — only the unit swaps.

byteCount() uses fs.statSync().size to match `wc -c` (includes trailing
newline), deliberately not lineCount()'s newline-stripping.

Document the context-rot / attention-budget QUALITY rationale (independent of
prompt caching) in the test JSDoc and docs/ARCHITECTURE.md, plus the
Goodhart caveat: the byte budget measures one file, so the real goal is
bounded *loaded* context — eager @-imports game the proxy; legitimate
extraction is lazy. Update CONTEXT.md RULESET.WORKFLOW_SIZE_BUDGET to bytes and
remove a stale duplicate ruleset entry that still said "1800 lines".

Defers the #3182 MVP-mode split (tracked separately): MVP is a cross-cutting
concern woven through plan-phase/execute-phase, not a discrete extractable mode.

Closes #717

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

* docs(#717): add changeset fragment for byte-budget re-base

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* feat(#159): auto-use existing RESEARCH.md in /gsd:plan-phase --research-phase (#718)

* feat(#159): auto-use existing RESEARCH.md in /gsd:plan-phase --research-phase

When RESEARCH.md already exists in research-only mode and neither --research
nor --view is passed, emit a one-line notice and exit cleanly instead of
prompting update/view/skip. This matches the promptless auto-use of standard
/gsd:plan-phase <N> (§5.1) and removes the §5.0/§5.1 inconsistency, making
AI-agent and CLI invocations non-interactive in the common case. The two
explicit-flag escape hatches (--research to refresh, --view to print) cover
any deviation.

Closes #159

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

* chore(#159): point changeset fragment at PR #718

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

* docs(#159): tighten research-phase reference register (Diataxis)

Make the 'no modifier' research-phase entries descriptive rather than
imperative and drop the trailing 'pass --research/--view' clauses, which
duplicated the adjacent --research/--view documentation. Reference docs
describe; the recovery flags are documented in their own entries. The
emitted runtime notice in the workflow keeps naming the flags (in-band
recovery), unchanged.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* chore: clean up clear-cut ESLint warnings (#732) (#734)

Pay down pre-existing error→warn lint debt. Removes dead imports/vars, unused functions, redundant regex/string escapes, and stale eslint-disable directives; converts unused `catch (_e)` to optional catch binding (src/*.cts).

No behavior change. Lint 345→125 warnings (0 errors); deferred categories (n/no-process-exit, test-sleeps, control-regex) tracked in #732 for follow-up. Full test suite green (0 failures); code-review verified all removals unused and all escape fixes semantics-preserving.

Closes #732

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* chore(lint): justify intentional no-control-regex (ANSI strip) + ratchet to error (#737)

The 5 no-control-regex warnings are all the same intentional ANSI-color-strip
pattern /\x1b\[[0-9;]*m/g across 5 test files. The \x1b (ESC) control char is
the required leading byte of an ANSI SGR sequence, so matching it is the whole
point of stripping color codes from captured CLI/console output. Add an inline
eslint-disable-next-line with justification at each site (not a refactor — the
control char is essential, not accidental), then flip no-control-regex from
warn to error so the debt can't regrow.

Refs #736

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* chore(lint): refactor magic-sleep tests to async waits + ratchet rules to error (#735)

Replace raw setTimeout/Atomics.wait synchronization sleeps in 4 test files
with a shared async delay()/waitFor() poll-for-condition helper in
tests/helpers.cjs, then flip local/no-magic-sleep-in-tests and
no-restricted-syntax from warn to error so the debt can't regrow.

- tests/helpers.cjs: add delay(ms) + waitFor(predicate, opts), exported
- bug-1974: setTimeout backoff -> await delay()
- config.test: drop Atomics.wait sleep(); async retry via await delay()
- graphify: waitForBuildStatus/cleanupHookRepo async via await delay()
- locking-bugs: 3 Atomics.wait poll loops -> await waitFor()
- eslint.config.mjs: ratchet both rules warn -> error

Refs #733

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* fix(#704): exclude } and ) from Codex path-rewrite lookbehind (no literal $gsd-core in installs) (#710)

* fix(#704): exclude } and ) from Codex path-rewrite lookbehind

Shell variable expressions like \${VAR}/gsd-core/ and command-substitution
paths like \$(cmd)/gsd-local-patches were being rewritten to \$gsd-core and
\$gsd-local-patches respectively because the negative lookbehind in
convertSlashCommandsToCodexSkillMentions did not include } or ).

Add both characters to the lookbehind set:
  (?<![a-zA-Z0-9./})])

Also adds regression test:
  tests/bug-704-codex-launcher-path-corruption.test.cjs

Closes #704

* chore: add changeset for #704

* test: use RUNTIME_ROOT_PATH in assertion to eliminate dead-code lint warning

Replace the partial hard-coded fragment '}/gsd-core/bin/' with the
existing RUNTIME_ROOT_PATH const so the assertion both compiles clean
(no unused variable) and self-documents which canonical launcher path
must survive Codex conversion intact (#704).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore: link changeset to PR #710

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(#706): skip rescue of already-committed SUMMARY to avoid worktree cleanup merge_failed (#709)

* fix(#706): skip rescueSummaryArtifacts when SUMMARY is already committed

rescueSummaryArtifacts now probes `git cat-file -e HEAD:<path>` before
copying a SUMMARY.md into the main checkout.  When the file is already
committed on the worktree branch, copying it as an untracked file causes
`git merge --no-ff` to abort with "untracked working tree files would be
overwritten by merge" — a permanent merge_failed cleanup-wave failure.

Fail-closed on timeout: if cat-file is unreliable we skip rescue (the
merge will surface the collision as it did before, which is recoverable).

Adds 4 new test cases in worktree-safety.test.cjs covering:
- committed SUMMARY skipped, merge succeeds (#706 regression case)
- committed SUMMARY skipped even when timeout (fail-closed)
- uncommitted SUMMARY still rescued (existing contract preserved)
- rescue failure on ENOSPC still propagates (unchanged)

Closes #706

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore: add changeset for #706

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(#706): treat cat-file exit 128 as uncertain — skip rescue (fail-closed)

The previous guard skipped rescue only when `exitCode === 0` (committed) or
`timedOut`. Any other non-zero exit, including `128` (fatal git error: corrupt
object store, unborn HEAD, missing repo), fell through and PROCEEDED with
rescue — potentially re-creating the #706 untracked-file merge collision.

Fix: rescue ONLY when `exitCode === 1` (cat-file definitively reports the
object absent). All other outcomes — 0 (committed), 128 (fatal), null/SIGTERM
(timeout), or any other code — are treated as "uncertain → skip rescue".

Also corrects the JSDoc bullet that still referenced `git ls-files
--error-unmatch` (the old mechanism); updated to `git cat-file -e HEAD:<relPath>`.

Regression test added: asserts rescue is SKIPPED when cat-file returns exit 128,
leaving the merge to surface the issue safely rather than silently copying an
already-committed file.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore: link changeset to PR #709

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(scripts): replace process.exit() with ExitError + runMain handler (#739) (#740)

Part 1 of 2 of the n/no-process-exit cleanup (umbrella #738): convert every
process.exit() call in standalone scripts/** CLIs to the rule-compliant pattern.

- New shared helper scripts/lib/cli-exit.cjs: ExitError(code,message) + runMain()
  which translates a thrown ExitError / returned number into process.exitCode
  (never process.exit()), flushing output and still firing process.on('exit').
- main()-based entrypoints: throw new ExitError(code) for errors, return <code>
  for verdicts; invoked via runMain(main). Child exit codes preserved via return.
- top-level-only scripts: imperative body extracted into main() so mid-flow
  aborts (throw ExitError) actually halt; pure consts/helpers stay at module scope.
- diff-touches-shipped-paths.cjs: stdin event handling restructured to an async
  read so the whole flow runs under runMain; uncaughtException/unhandledRejection
  nets replaced by an in-band catch that preserves EXIT_ERROR=2.

Exit codes verified unchanged for every converted script (success/error/help and
the 0/1/2 semantic codes in diff-touches). Rule stays warn here; flipped to error
in part 2 (#738) once gsd-core/bin/** is also clean.

Refs #739

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(bin): replace process.exit() in CLI entrypoints + ratchet rule to error (#738) (#741)

Part 2 of 2 of the n/no-process-exit cleanup (completes umbrella #738; part 1
was #739/scripts). Converts the 20 flagged process.exit() calls in the three
hand-written gsd-core/bin CLI entrypoints and flips n/no-process-exit to error.

- New src/cli-exit.cts -> gsd-core/bin/lib/cli-exit.cjs (ExitError + runMain),
  the gsd-core-side equivalent of scripts/lib/cli-exit.cjs; registered in
  .gitignore, eslint ignores, and the inventory manifest like its siblings.
- gsd-tools.cjs: 13 apply-prompt-budget exits -> throw ExitError; main()->runMain.
- verify-reapply-patches.cjs: 6 exits -> throw ExitError / return verdict; runMain.
- check-latest-version.cjs: 1 exit -> return verdict; runMain.
- eslint.config.mjs: n/no-process-exit warn -> error.

Scope note: the gsd-core/bin/lib/*.cjs modules (core, state, profile-pipeline,
roadmap-command-router, adr-parser, ui-safety-gate) are tsc-generated and
eslint-ignored (ADR-457), so their process.exit calls were never flagged and are
intentionally left untouched. Only the linted hand-written entrypoints are in scope.

Exit codes verified unchanged for all three entrypoints.

Closes #738

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(#720): lazy-load MVP-only reference bodies on non-MVP runs (#746)

* refactor(#720): lazy-load MVP-only reference bodies (eager @-import → gated Read)

Convert eager @-imports of MVP-only reference bodies into lazy "Read" instructions
gated on MVP_MODE / WALKING_SKELETON / MVP+TDD, so non-MVP planning/execution runs
no longer pull MVP guidance into context. Covers both the workflow files and the
planner/executor agent definitions (the dominant context-cost path):

- workflows/plan-phase.md: planner-mvp-mode.md + skeleton-template.md (L146/936/937/941)
- workflows/execute-phase.md: execute-mvp-tdd.md halt-report ref, now gated on gate-trip (L191)
- agents/gsd-planner.md: planner-mvp-mode.md, user-story-template.md, skeleton-template.md
- agents/gsd-executor.md: execute-mvp-tdd.md

The dedicated always-MVP mvp-phase workflow keeps its eager imports (intentional).
Behaviour is unchanged; non-MVP runs simply carry less loaded context. Adds a
regression guard mirroring the discuss-phase lazy-load test, and documents the
conformance in docs/ARCHITECTURE.md.

Refs #720

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

* docs(#720): add changeset fragment (pr #746)

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(#712): replace Codex slash-command denylist lookbehind with positive-boundary match (#747)

* refactor(#712): replace Codex slash-command denylist lookbehind with positive-boundary match

The hyphen-style /gsd-<cmd> -> $gsd-<cmd> conversion in
convertSlashCommandsToCodexSkillMentions used a negative-lookbehind DENYLIST
enumerating characters that must NOT precede a real mention. #637 -> #704 showed
this is an unbounded treadmill: each new unanticipated preceding char (/, ., word
chars, then }, )) leaked the same path-corruption bug class, and a backtick-wrapped
path (`/gsd-core/workflows/update.md`) still leaked through.

Replace it with a POSITIVE two-boundary definition of a mention:
  1. Left: opens at start-of-string, whitespace, or an inline-prose delimiter
     (backtick/quote/paren/bracket).
  2. Right: the command token is not followed by a path separator `/` (a path
     continues, a command does not). The (?![a-z0-9/-]) lookahead also blocks
     regex backtracking to a shorter command.

This closes the whole class by construction (no preceding-char denylist to
maintain) and fixes the backtick-wrapped-path corruption the #704 test
documented as a pre-existing gap, while preserving conversion of legitimate
backtick-wrapped mentions (e.g. CONTEXT.md's `/gsd-execute-phase` lists).

The colon-style /gsd: replace is intentionally left unguarded (it never appears
as a filesystem path segment) and is annotated as such.

Tests assert the regex directly (function now exported) across a convert/
don't-convert matrix plus one end-to-end pipeline assertion for the headline
backtick-path case.

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

* chore(#712): add changeset fragment for PR #747

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* fix(#730): scope current-milestone Phase Details section in roadmap parser (#748)

`extractCurrentMilestone()` scoped the current-milestone window to its
`## Phases` checklist subsection and terminated at the milestone's own
`## Milestone … (Phase Details)` heading, so the `### Phase N:` detail
headers fell outside scope. Every parser-backed command — `init.phase-op`
(and thus `/gsd:discuss-phase`, `/gsd:plan-phase`), `state`, `roadmap list`,
and `validate health` (W006) — therefore could not resolve phases of any
milestone after the first until a `.planning/phases/` directory already
existed, blocking discuss/plan.

The parser now additionally includes the current milestone's `(Phase Details)`
section in scope, located via the already-computed version matches and anchored
(boundary-aware) to the selected milestone's version token so sibling
sub-milestones sharing a version prefix do not cross-pollinate. The existing
heading selection and primary window are unchanged.

Adds tests/bug-730-milestone-phase-details-scope.test.cjs covering the
two-milestone reproduction, first-milestone non-regression, direct
getRoadmapPhaseInternal resolution, validate-health W006 visibility, a
three-milestone roadmap, and the closed-sibling sub-milestone case.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* fix(#683): auto-degrade phase execution to sequential on worktree base mismatch (#749)

* fix(#683): auto-degrade phase execution to sequential on worktree base mismatch

Claude Code forks worktree-isolated executors off the repository default
branch (origin/HEAD), not the orchestrator's HEAD. Running /gsd-execute-phase
on a branch diverged from the default (unmerged milestone/feature branch) left
every executor without the phase's plan files and tripped the
worktree-branch-check guard with `exit 42` — 100% reproducible, all OSes.

- New module src/worktree-base-ref.cts: HEAD-vs-fork-base drift detection
  (origin/HEAD with symbolic-ref fallback) and no-clobber worktree.baseRef
  management, exposed as `worktree base-check` / `worktree set-baseref`.
- execute-phase.md: pre-dispatch, for Claude Code with worktrees enabled,
  auto-degrades the run to sequential on the main tree when a base mismatch
  is detected, recommending worktree.baseRef:"head". The exit-42 guard stays
  as a backstop.
- Installer: fresh local Claude installs set worktree.baseRef:"head" in
  .claude/settings.local.json (no-clobber, respecting an explicit shared
  settings.json value); upgrades print an opt-in notice pointing at
  `gsd-tools worktree set-baseref`.
- Docs: how-to guide, CLI/config reference, planning-config cross-ref.

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

* fix(#683): auto-apply worktree.baseRef on upgrade; gate fresh+upgrade on use_worktrees

Per maintainer direction: on a local Claude Code UPGRADE, set
worktree.baseRef:"head" automatically (no opt-in notice) when the project's
workflow.use_worktrees is enabled, instead of merely printing a remediation
notice. For consistency the FRESH path is now gated the same way: both paths
compute worktrees-enabled once (bounded walk-up read of .planning/config.json,
default enabled unless workflow.use_worktrees === false) and apply the
no-clobber baseRef only when enabled — never overwriting an explicit value in
settings.local.json or a shared settings.json. gsd-tools worktree set-baseref
remains for manual use. Docs + changeset updated; tests hardened (file-exists
assertions, fresh+disabled case, upgrade idempotency).

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

* fix(#683): measure workflow byte-budget on LF, fixing Windows-only CI failure

The workflow-size-budget test failed only on Windows: git checks out the .md
files as CRLF (no eol=lf in .gitattributes) and byteCount used
fs.statSync().size (raw on-disk bytes), counting an extra \r per line. That
inflated execute-phase.md — the XL high-water-mark file pinned near its ceiling
by the tighten-only ratchet — from 88492 LF bytes to ~90245 on Windows, over
the 90000 XL ceiling, while passing on the LF-checkout Mac/Linux runners.

The ceilings are explicitly "calibrated against raw `wc -c`" on an LF checkout,
so the measurement should be LF-based on every platform. byteCount now reads the
file and counts Buffer.byteLength after stripping CR, making the budget
platform-independent (a no-op on LF checkouts; verified statSync === normalized
for all 88 workflow files). No ceilings changed. Added a regression test
asserting CRLF and LF content of the same file count identically.

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

* fix(#683): make worktree-base-ref test path mocks Windows-safe (path.join)

tests/worktree-base-ref.test.cjs keyed its injected readFile/writeFile mocks
(and a few expected `file` values) with forward-slash template literals like
`${claudeDir}/settings.local.json`. The module composes those paths with
path.join(), which emits backslashes on Windows, so the mock keys never matched
the module's lookup → readFile returned null → resolveEffectiveBaseRef /
cmdWorktreeBaseCheck / cmdWorktreeSetBaseRef (and the JSONC variants) failed on
the Windows full-test runner only (they passed on Mac/Linux, and the install
tests passed because they use the real filesystem). The module is correct;
only the test fixtures hardcoded '/'.

All mock keys and path assertions now use path.join(base, ...) mirroring the
module, so they match on every platform (no-op on POSIX). 19 path references
across 16 lines.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* feat(#703): add --granularity override flag to /gsd:plan-phase (#750)

* feat(#703): add --granularity override flag to /gsd:plan-phase

Add a `--granularity <coarse|standard|fine>` flag to /gsd:plan-phase that
overrides the configured planning granularity for a single invocation.

The override is a new highest-priority tier above the existing precedence
chain (granularities[phaseType] -> granularity -> planning.granularity ->
'standard') in resolveGranularityInternal; when the flag is absent, resolution
is byte-for-byte unchanged. cmdInitPlanPhase now resolves with phaseType
'planning' so granularities.planning participates, and emits the resolved
value in the init JSON, which the plan-phase workflow forwards to the planner
prompt. Invalid values are rejected at the CLI boundary via a shared
assertValidGranularityOverride helper.

Closes #703

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

* chore(#703): set changeset pr to 750

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* fix(#751): recognise config-set prototype-pollution guard in CodeQL + test dynamic-key vectors (#752)

CodeQL alert #26 (js/prototype-pollution-utility) kept firing on setConfigValue
because its dataflow does not trace the #663 Set-based, pre-loop keys.some(...)
forbidden-key check as a sanitising barrier on the write site.

- src/config.cts: replace the Set + pre-loop check with inline literal
  comparisons (key === '__proto__' || 'prototype' || 'constructor') on the exact
  key used to index `current`, immediately before each write (intermediate keys
  in the descent loop, plus the final key). Same forbidden set, same error
  message and ERROR_REASON.CONFIG_PARSE_FAILED — behaviour unchanged from #663,
  but the barrier is now CodeQL-recognised.
- tests/config.test.cjs: add regression tests for schema-valid dynamic-prefix
  keys (agent_skills.__proto__, agent_skills.constructor, agent_skills.prototype,
  features.__proto__, review.models.constructor) that pass the isValidConfigKey
  schema gate and reach the guard. Each asserts the guard's own message fires
  (not the schema gate's "Unknown config key") and Object.prototype is not
  polluted. The prior #663 tests never reached the guard — their keys are
  rejected by the schema gate first — so the guard's real attack surface was
  untested.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* feat(#25): scope gsd-verifier Step 7b to enumerate-or-single-test; forbid full-suite re-runs (#753)

* feat(#25): scope gsd-verifier Step 7b to enumerate-or-single-test; forbid full-suite re-runs

Step 7b's lone test example (`npm test -- --grep "$PHASE_TEST_PATTERN"`) is
mocha/vitest/jest-specific, where `--grep` filters which tests *execute*. Models
generalized it to `cargo test --workspace 2>&1 | grep X` (runs the whole suite,
filters only *output*) and repeated it once per must-have, adding minutes per
verification with no new evidence after the first run.

Replace the example with language-agnostic guidance: prove a test EXISTS via
enumeration (`cargo test -- --list` / `pytest --collect-only` / `npx vitest
list` / `go test -list`), and prove it PASSES via a single named test
(`cargo test <name> -- --exact` / `pytest -k` / `npx vitest run -t`). Add a
Spot-check constraint forbidding more than one full-suite run per verification
or piping a full run through grep per must-have, while still permitting one
saved run + grep when a full run is genuinely required. docs/AGENTS.md gains a
one-line Key-behaviors note, and a new test asserts the Step 7b content.

Scoped per the maintainer decision on the issue: folded into Step 7b (no new
top-level Step 7a) with no VERIFICATION.md label changes.

Closes #25

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

* chore(#25): add Changed changeset fragment for PR #753

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* feat(#52): add agent_skills_security.trusted_global_roots allowlist for global skills (#754)

* feat(#52): add agent_skills_security.trusted_global_roots allowlist

Opt-in allowlist so a global: agent skill whose SKILL.md realpath resolves
outside the default global skills base (e.g. ~/.claude/skills) is accepted
when its real target lies under a user-declared trusted root. Default [] is
byte-identical to prior behavior; the symlink-escape guard is preserved and
simply re-applied against each declared root.

- src/security.cts: loadTrustedGlobalRoots — tilde-expand (~ and ~/), reject
  project-relative and dangerously broad roots (filesystem/UNC root, homedir),
  realpath-canonicalize each root every run and drop non-existent ones.
- src/init.cts: on base-check failure the guard consults the trusted roots
  (hoisted out of the loop); emits a stderr NOTE when a skill is accepted via
  a trusted root so the widened boundary is visible.
- src/core.cts: thread agent_skills_security through loadConfig.
- config-schema.manifest.json: allow the new key path.
- docs/CONFIGURATION.md: document the option and its security model.
- tests/agent-skills.test.cjs: unit + end-to-end CLI coverage (regression,
  feature, negative, broad-root hardening, stderr NOTE).

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

* docs(#52): add changeset fragment for trusted_global_roots (#754)

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(#651): consolidate verification-status routing into one queryable seam (#755)

* refactor(#651): consolidate verification-status routing into one queryable seam

The passed/gaps_found/human_needed verification status was re-encoded as
bare strings across three prose surfaces (gsd-verifier emits, execute-phase
routes, ship gates), each independently deciding the per-status next action
with no parity coupling — the DEFECT.GENERATIVE-FIX class.

Give the enum one home: src/verification.cts (-> bin/lib/verification.cjs)
exposing `gsd_run query verification.status <phaseDir>` returning a typed
{status, next_action, next_command}. ship.md and execute-phase.md now consume
the query instead of re-deriving the routing in prose; gsd-verifier.md points
at the shared vocabulary as the single emitter (values unchanged).

Also fixes the latent broad-grep status misread (DEFECT.FRONTMATTER-SCALAR-
BROAD-GREP): execute-phase.md read `grep "^status:"` over the whole report, so
a body `status:` line could misroute a valid phase. Extraction is now
frontmatter-scoped in one place. A parity test fails if a verifier status
gains no route. Lands the two CONTEXT.md DEFECT entries captured on the issue.

Closes #651

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

* chore(#651): set changeset pr to 755

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* fix(#758): trigger draft-PR auto-close on pull_request_target (#760)

Bare `pull_request` hands fork PRs a read-only GITHUB_TOKEN, so the
close/comment API calls 403 and a first-time/external contributor's draft
PR survives — bypassing the auto-close for exactly the population the job
targets. Switch to `pull_request_target`, which runs in the base-repo
context with a write-capable token even for fork PRs. Safe because the job
never checks out or executes PR-supplied code; it only reads event metadata
and calls the GitHub API. The minimal `permissions: pull-requests: write`
block still constrains the token.

Add a regression guard in tests/workflow-maintainer-skip.test.cjs asserting
the workflow triggers on pull_request_target and not bare pull_request.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* docs(#58): add ADR for Runtime Install Policy Module boundary (#762)

Record the Runtime Install Policy Module decision and ownership
boundary: install policy projects a pure, typed install plan by
composing artifact placements (ADR-3660) and command text (ADR-0009)
plus per-runtime config intentions, with no filesystem IO; runtime
adapters consume the plan and execute concrete file mutations and
format-specific config rendering. Explicitly records what stays
outside the policy module (TOML/JSON/Markdown serialization, merge
semantics, filesystem effects).

Adds the ADR index row in docs/adr/README.md and a glossary entry in
CONTEXT.md. Leads the installer-refactor chain (#58 -> #60 -> #56).

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* feat(#759): non-destructive CHANGELOG preview in the rc release job (#763)

The rc action publishes a release candidate to @next for testing but
never surfaces the curated CHANGELOG section for the version under test —
render only runs destructively at finalize (#715), so there was no safe
way to preview the upcoming notes during the RC window.

Add a --preview mode to scripts/changeset/cli.cjs cmdRender: it renders
the dated release section to stdout via the existing renderChangelog/
serializeChangelog path (with priorChangelog: null, so only the new
section is emitted), reuses the shared injectEmptyPlaceholder helper for
zero-fragment releases, and returns WITHOUT writing CHANGELOG.md or
deleting any .changeset fragment. Wire a "Preview CHANGELOG" step into
the rc job that renders to a file (standalone command, so a malformed
fragment fails the step) and cats it to the job summary and log.

Closes #759

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* feat(#761): add scheduled base-context sweep to close SHA-branch-evading draft PRs (#765)

close-draft-prs.yml (on pull_request_target after #760) cannot close fork draft
PRs whose head branch name looks like a Git SHA — GitHub never dispatches
pull_request_target for such branches, and a pull_request run from a fork gets a
read-only token. So a draft PR on a SHA-named fork branch evades the auto-close.

Add close-draft-prs-sweep.yml: a schedule (every 6h) + workflow_dispatch sweep
running in base-repo context with pull-requests: write that paginates open PRs,
filters to non-OWNER/MEMBER/COLLABORATOR drafts, and closes + comments them with
the identical policy/message as the event-driven workflow. Re-fetches each
candidate before mutating (TOCTOU guard), closes before commenting so
enforcement is never gated on the explanatory comment, and core.setFailed on
partial failures. The per-PR workflow remains the fast path; this is the
safety net for the documented residual bypass.

Extends tests/workflow-maintainer-skip.test.cjs with structural guards locking
the triggers, write permission, maintainer carve-out, pagination, and message.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* fix discord release changelog announcements

* test(#339): regression guard for gsd-sdk refs in runtime surfaces (#691)

* test(#339): add regression guard for gsd-sdk refs in runtime surfaces

Lock in the already-clean runtime surface so a retired `gsd-sdk`/`GSD_SDK`
reference cannot creep back into a shipped prompt or hook. Scans
gsd-core/workflows, gsd-core/references, commands/gsd, agents, and hooks
(excluding the gitignored dist/ build artifact).

Complements gsd-tools-path-refs.test.cjs, which only catches the
`gsd-sdk query` binary form; this catches any runtime reference. A second
test guards against an empty sweep so a future dir rename can't silently
turn the guard into a no-op.

Intentionally does NOT touch bin/install.js (live stale-package-detection
mechanics per #339 triage) or CI/lint scripts (legitimate stale detection).

Refs #339

* test(#339): split file content on /\r?\n/ for Windows CRLF parity

The windows-test-parity-guard lint requires test files that readFileSync +
split to use /\r?\n/, not '\n', so CRLF files don't leave a trailing \r.
New test files are not in the PR #3649 allowlist.

* test(#339): cover .sh/.json runtime files in workflows surface

Address #691 review: the gsd-core/workflows surface scanned only .md,
silently skipping two deployed runtime files — _runtime-launcher.snippet.sh
(synced into every hook) and discuss-phase/templates/checkpoint.json. Add
.sh/.json so the guard covers all 290 deployed runtime files (was 288/290),
not just the .md subset.

* test(#339): cover templates/contexts surfaces + per-ext empty-sweep guard

Address PR #691 review (trek-e):
- Add gsd-core/templates and gsd-core/contexts to RUNTIME_SURFACES —
  both are deep-copied by the installer and runtime-loaded via
  @~/.claude/gsd-core/templates/*.md anchors, so a reintroduced gsd-sdk
  ref there would have slipped past the guard. (major)
- Reword the bin/install.js exclusion rationale: it has zero gsd-sdk refs
  today (subsystem removed in #515, shim retired in #522); the real reason
  it is excluded is that it is installer code, not a deployed prompt/hook
  surface. (minor)
- Make the empty-sweep guard assert coverage per configured extension, not
  per surface — .md files alone kept gsd-core/workflows green even if
  .sh/.json were dropped, silently un-covering _runtime-launcher.snippet.sh
  and discuss-phase/templates/*.json. (low)

---------

Co-authored-by: Tom Boucher <trekkie@nomorestars.com>

* refactor(#60): make runtime config adapter registry explicit (#795)

* refactor(#60): make runtime config adapter registry explicit

Replace scattered inline `runtime === '...'` config-mutation branching in
bin/install.js with an explicit, typed adapter registry. The new
src/runtime-config-adapter-registry.cts maps each of the 15 supported
runtimes to a config intent { installSurface, writesSharedSettings,
finishPermissionWriter }; install()/finishInstall() dispatch by resolved
intent instead of runtime-name checks (cursor/windsurf/trae collapse to one
profile-marker-only branch).

Behavior-preserving: the same config files are written for the same runtimes
(opencode still writes both settings.json and its permissions; kilo writes
only its permissions; codex minimal-mode and opencode GSD_TEST_MODE guards
unchanged). Unknown runtimes fail loudly via TypeError, with an Object.hasOwn
barrier so prototype-chain keys (__proto__/constructor) also throw rather than
returning a bogus intent. Leads the installer-refactor chain (#58 -> #60 ->
#56), building on ADR-58.

Closes #60

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

* chore(#60): add changeset for runtime config adapter registry

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

* docs(#60): register Runtime Config Adapter Registry in CONTEXT.md glossary

Per docs/contributor-standards.md, every new Module/seam must get a
`### <Name>` entry under the domain glossary. Adds the entry for the
runtime-config-adapter-registry seam introduced in this PR (interface,
policy boundary, source file, ADR-58 / #60 cross-references).

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* feat(#764): skip cross-platform test matrix for docs-only and inert-CI PRs (#798)

test.yml had no paths filter and the ci-test-scope classifier treated docs/
and every .github/workflows/* as code_changed, so documentation edits and
product-irrelevant automation tweaks still spun up the full Linux/Windows/macOS
matrix. Narrow the heavy matrix to changes that can actually affect the product
or the test pipeline.

- ci-test-scope.cjs: drop docs/ from code_changed (docs-only -> full skip; the
  required-tests fan-in still reports green). Add src/ to code_changed (it was
  missing -> a source-only PR previously skipped all tests). Add INERT_WORKFLOWS
  allowlist + isInertCi() + an "inert CI" rule, and a product_changed output that
  gates the heavy test/coverage jobs. Fail-safe: any workflow not on the inert
  allowlist defaults to the full matrix. A module-load assertion throws if a
  PROTECTED_WORKFLOWS entry (test/install-smoke/mutation/security-scan/release)
  is ever added to the inert set, so a weakening edit fails CI loudly.
- test.yml: keep the static 3-lane matrix (so the H1 shell-policy linter can
  still statically verify the Windows lane), gate test/coverage on
  product_changed, add a lightweight ubuntu-only test-inert job, and branch the
  required-tests fan-in on product_changed.
- docs-required.yml: run docs-parity-live-registry (gated on docs/ changes) so
  pure-docs PRs still catch live-registry drift without the matrix.
- tests: cover docs-only, inert-only, src/, pipeline, unknown-workflow fail-safe,
  mixed escalation, the code_changed=false -> no-lanes invariant, and protected-
  workflow tamper-evidence.

Closes #764

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* feat(#766): distribute gsd-core as a native Claude Code plugin (#797)

* feat(#766): distribute gsd-core as a native Claude Code plugin

Add an additive .claude-plugin/plugin.json manifest plus hooks/hooks.json so gsd-core can be installed as a first-class Claude Code plugin (marketplace or zero-friction @skills-dir), with /gsd-core: namespaced commands and lifecycle management — alongside the unchanged npm/file-copy installer.

- .claude-plugin/plugin.json: validated with 'claude plugin validate --strict'
- hooks/hooks.json: mirrors the installer's always-on Claude hook wiring via ${CLAUDE_PLUGIN_ROOT}
- package.json: ship .claude-plugin in the npm tarball
- tests/issue-766-plugin-manifest.test.cjs: manifest + always-on-hook-contract drift guards
- docs: install-on-your-runtime.md + FEATURES.md

Closes #766

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

* docs(#766): add ADR-766 + glossary entry for Claude Code Plugin Manifest Module

Record the plugin manifest as the Seam projecting gsd-core's artifact surfaces onto the Claude Code plugin contract (sibling of the Runtime Artifact Layout Module, ADR-3660), with the defined kind->field mapping and the always-on hook projection rule.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(#56): retire legacy runtime directory helpers into runtime-homes projection (#802)

* refactor(#56): retire legacy runtime directory helpers into runtime-homes projection

Consolidate per-runtime global config-dir resolution onto the single
canonical projection runtime-homes:getGlobalConfigDir. Extend it with the
explicitDir override (CLI --config-dir) and the opencode/kilo
OPENCODE_CONFIG/KILO_CONFIG file-path precedence the installer helpers had,
making it byte-for-behavior equivalent to the old getGlobalDir across all
15 install runtimes.

Delete bin/install.js's getGlobalDir/getOpencodeGlobalDir/getKiloGlobalDir
(and the orphaned local expandTilde), repoint all 9 call-sites, and remove
getGlobalDir from module.exports (net -242 lines in the installer). Migrate
the 5 test importers to the canonical projection; harden default/XDG
assertions against ambient *_CONFIG env vars. getAgentsDir now respects
OPENCODE_CONFIG/KILO_CONFIG consistently with the installer (intentional
convergence). Update CONTEXT.md Installer Module entry.

Completes the installer-refactor chain #58 -> #60 -> #56.

Closes #56

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

* chore(#56): add changeset for runtime directory helper retirement

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* feat(#786): elevate GitHub Copilot installer — lifecycle hook + AGENTS.md (#804)

* feat(#786): elevate Copilot installer with lifecycle hook + AGENTS.md

Emit a self-contained sessionStart hook config (.github/hooks/gsd-session.json
local, ~/.copilot/hooks/gsd-session.json global) and write AGENTS.md at the repo
root (Copilot CLI reads it as primary instructions) alongside
copilot-instructions.md. The hook is an inline `command` hook (no separate hook
script), so it cannot dangle. Uninstall removes both and preserves user content.

Verified against GitHub Copilot CLI primary docs: hooks-configuration (camelCase
events, version+hooks shape, inline bash/powershell command hooks) and
add-custom-instructions (AGENTS.md read at repo root as primary instructions).

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

* chore(#786): set changeset pr number to 804

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* fix(#783): resolve Kilo global skills base to ~/.kilo/skills (#806)

* fix(#783): resolve Kilo global skills base to ~/.kilo/skills

getGlobalSkillsBase('kilo') returned ~/.config/kilo/skills (the XDG config
dir), but Kilo Code discovers global skills from ~/.kilo/skills/ (the .kilo
dir in HOME), independent of the kilo.jsonc config dir. Add a HOME-relative
special case so the resolver matches Kilo's actual discovery path.

The config dir (~/.config/kilo) and the installer's command/ path are
correct and unchanged. This corrects the path used by doctor/status and
agent-skills-block resolution; the installer writes commands (not skills)
for Kilo, so no files were being written to the wrong location.

Closes #783

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

* chore(#783): set changeset pr to 806

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* feat(#785): write .cursor/commands/ Cursor 1.6 slash-command surface (#805)

* feat(#785): write .cursor/commands/ as Cursor 1.6 slash-command surface

Cursor 1.6 (released 2025-09-12) introduced plain-markdown slash commands
in `.cursor/commands/<name>.md` — no frontmatter, invocable via `/` in the
Agent input. GSD previously emitted only `~/.cursor/skills/` for Cursor.

This PR wires a second artifact kind for `cursor` in
`runtime-artifact-layout.cts`: `convertedCommandsKind('commands', 'gsd-',
'convertClaudeCommandToCursorCommand', configDir)`. The new kind applies the
same `convertClaudeToCursorMarkdown` transforms (tool renames, brand
substitution, slash-command normalisation) and then strips YAML frontmatter
so the output is plain prose. Skills output is unchanged.

`stageCommandsForRuntimeFlat` in `install-profiles.cts` stages each source
`.md` as a flat `<stem>.md` in a temp dir; the existing `_copyStaged` commands
path then prefixes and copies to `<configDir>/commands/`.

`.cursor/mcp.json` is explicitly OUT OF SCOPE: GSD ships no MCP server; the
`mcpServers` schema cannot be usefully populated by the installer.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(#785): address review nit

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(#787): elevate Cline — .clinerules/ dir form, PreToolUse hook, AGENTS.md (#803)

* feat(#787): elevate Cline — .clinerules/ dir form, PreToolUse hook, AGENTS.md

Migrate the installer's Cline output from a single-file .clinerules to the
.clinerules/ directory form (.clinerules/gsd.md), which is the prerequisite for
Cline's v3.36 hooks (a path cannot be both a file and a directory). Add a
.clinerules/hooks/PreToolUse lifecycle hook implementing Cline's JSON stdin ->
{cancel,errorMessage,contextModification} protocol; it guards .planning/
artifacts and fails open. On global installs, merge GSD instructions into the
cross-tool ~/.agents/AGENTS.md target (marker-delimited, merge-safe). A legacy
single-file .clinerules is migrated in place; --uninstall removes the new
artifacts and strips the AGENTS.md GSD block.

Also fixes the uninstall targetDir for Cline local installs (it pointed at
./.cline instead of the project root) and re-runs writeManifest after the
Cline artifacts are written so they are hash-tracked.

Self-contained: implemented independently of the #782 Cline skills work.

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

* fix(#787): address review findings

- Scope PreToolUse hook path-walk to PATH_KEY fields only (eliminates false
  positive when doc body content mentions .planning/)
- Use lstatSync + isSymbolicLink() for migration guard so GSD never writes
  through a user's symlinked .clinerules into an external directory
- Add regression tests for both cases

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore(#787): set changeset pr: 803

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* fix(#812): honor COPILOT_HOME in Copilot global config-dir resolution (#814)

* fix(#812): honor COPILOT_HOME in Copilot global config-dir resolution

getGlobalConfigDir('copilot') resolved the global config directory using
only --config-dir > COPILOT_CONFIG_DIR > ~/.copilot, ignoring the
COPILOT_HOME env var. Per GitHub's Copilot CLI docs, COPILOT_HOME
overrides the default ~/.copilot location (and user-level hooks are read
from $COPILOT_HOME/hooks/), so a global --copilot install wrote all
artifacts (skills, agents, copilot-instructions.md, the gsd-session.json
hook) to ~/.copilot even when the user relocated their Copilot home,
making them undiscoverable by Copilot CLI.

Mirror the codex/CODEX_HOME branch: precedence is now
--config-dir > COPILOT_CONFIG_DIR > COPILOT_HOME > ~/.copilot. Uninstall
uses the same resolver, so it stays symmetric.

Also: document COPILOT_HOME in the installer --help notes, the
USER-GUIDE env-var table, and the installer-migrations Copilot row; and
clear COPILOT_HOME in the two default-path test suites so they stay
hermetic now that the resolver honors it.

Closes #812

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

* chore(#812): add changeset for PR #814

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* feat(#788): expand Qwen Code hook-event coverage (#807)

* feat(#788): expand Qwen Code hook-event coverage to 4 new events

Register SubagentStop, Stop, PreCompact (gsd-context-monitor.js) and
UserPromptSubmit (gsd-prompt-guard.js) in the Qwen Code installer.
Guard is isQwen-only — Claude Code and all other runtimes are unchanged.
Uninstall loop extended to include the 4 new event names.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(#788): reconcile to 3 Qwen-only events — defer UserPromptSubmit

gsd-prompt-guard exits unless tool_name is Write|Edit (PreToolUse
payload shape); UserPromptSubmit carries raw user-prompt text with no
tool_name field, so wiring it would be a silent no-op.  Deferred to a
follow-on issue.

Artifacts made consistent:
- bin/install.js: drop UserPromptSubmit registration block; uninstall
  loop drops UPS from event list
- .changeset/788-qwen-hook-events.md: corrected to 3 events + rationale
- docs/how-to/install-on-your-runtime.md: remove UPS row from hook table
- tests/enh-788-qwen-hook-events.test.cjs: assert UPS NOT registered;
  fix idempotency suite to persist settings between installs; drop
  UPS-specific assertions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore(#788): update changeset PR number to #807

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(#788): prune stale install-bucket allowlist entry for enh-788 test

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* enhancement(#782): emit gsd skills to ~/.cline/skills for Cline >= v3.48 (#809)

Cline added a global skills system (~/.cline/skills/<name>/SKILL.md) in
v3.48.0, but gsd treated Cline as rules-only and emitted zero skills
(getGlobalSkillsBase('cline')=null, empty artifact kinds). This makes gsd
emit skills for Cline at global scope, alongside the existing .clinerules.

- runtime-homes: getGlobalSkillsBase('cline') -> ~/.cline/skills (was null)
- runtime-artifact-layout: cline emits a skills kind for GLOBAL scope only
  (local stays .clinerules-only), mirroring claude's scope dispatch
- install.js: convertClaudeCommandToClineSkill emits name+description-only
  SKILL.md frontmatter (Cline/agentskills.io spec; no Claude-specific
  allowed-tools/argument-hint/agent), hyphen-normalized + .cline/-rewritten
  body; global cline routed through the skills path while .clinerules is
  still written; _applyRuntimeRewrites cline case handles custom
  CLINE_CONFIG_DIR; convertClaudeToCliineMarkdown also rewrites bare
  ~/.claude and CLAUDE_CONFIG_DIR
- docs: install-on-your-runtime.md documents Cline global skills vs local rules
- tests: converter (name+description-only), global emission, skills+.clinerules
  coexistence, scope-aware layout, custom-dir paths, idempotency

Closes #782

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* feat(#790): emit Augment slash commands (~/.augment/commands/) (#808)

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* enh(#784): emit native skills for OpenCode + Kilo runtimes (#810)

* feat(#784): emit native skills for OpenCode + Kilo runtimes

OpenCode and Kilo share a config schema and both discover on-demand
skills from skills/<name>/SKILL.md. The installer previously emitted
only flat commands (command/) and file-based agents (agents/) for these
runtimes. Add a shared OpenCode-family skill writer that stages each GSD
command as a spec-compliant SKILL.md (name matching the directory,
description 1-1024 chars), wired through the runtime artifact layout so
uninstall cleans skills/ automatically. Skills respect the active
install profile.

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

* fix(#784): correct skill body paths + preserve user dev-preferences

Address adversarial-review findings:
- Add opencode/kilo cases to _applyRuntimeRewrites so staged SKILL.md
  bodies are re-pointed from the converter's hardcoded default config dir
  to the actual install target (fixes --local / --config-dir installs;
  commands/agents already did this by applying pathPrefix pre-conversion).
- Preserve user-owned skills/gsd-dev-preferences across reinstall in
  installOpencodeFamilySkills (snapshot+restore around the gsd-* prune),
  matching installRuntimeArtifacts.
- Export installOpencodeFamilySkills and add regression tests.

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

* fix(#784): guarantee command/skill body parity, fix kilo-alt double-rewrite

Follow-up adversarial-review found the post-conversion path rewrite could
double-rewrite custom Kilo dirs (kilo -> kilo-alt -> kilo-alt-alt) because
the kilo pathPrefix is a $HOME (non-absolute) superset of the hardcoded
default base. Restructure so OpenCode/Kilo skills mirror copyFlattenedCommands
exactly: stage raw commands, apply pathPrefix BEFORE conversion via a new
shared applyOpencodeFamilyPathPrefix() helper (now used by both the command
and skill writers), then convert. This guarantees byte-for-byte command/
skill body parity for global, --local, and --config-dir installs and removes
the prefix-overlap hazard. Drop the fragile _applyRuntimeRewrites opencode/
kilo case. Strengthen the path regression test.

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

* refactor(#784): derive opencode/kilo skills from the same staged command set

Pass the installer's _stageSkills() output directly to
installOpencodeFamilySkills instead of re-staging via the layout, so the
command/ and skills/ surfaces always cover the identical profile-resolved
set — including the --minimal/--core-only alias path, which stages
differently from a plain --profile=core. Verified: minimal install now
emits 8 commands and 8 skills.

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

* chore(#784): set changeset PR number to 810

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

* test(#784): fully escape backslashes in test helper (CodeQL js/incomplete-string-escaping)

Replace the dot-only escape `replace(/[.]/g, '\\.')` with a complete
regex-escape pattern `replace(/[\\.*+?^${}()|[\]]/g, '\\$&')` so all
regex metacharacters (including backslash itself) in `defaultBase` are
safely escaped before interpolation into `new RegExp(...)`.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* fix(#813): apply per-runtime skill path rewrites in applySurface (#817)

* fix(#813): apply per-runtime skill path rewrites in applySurface

applySurface() re-staged skill artifacts but, unlike installRuntimeArtifacts(),
never applied the per-runtime path rewrites. So /gsd:surface
(profile/enable/disable/reset) overwrote installed SKILL.md bodies with the
converter's default ~/.claude paths instead of the install target (pathPrefix),
silently regressing skill path references for every skillsKind runtime until
the next reinstall.

applySurface now mirrors installRuntimeArtifacts: for kind.kind === 'skills' it
derives pathPrefix the same way and applies applyRuntimeContentRewritesInPlace
on the staged dir before syncing.

- bin/install.js: export applyRuntimeContentRewritesInPlace
- runtime-artifact-layout.cts: carry resolved scope on Layout; export
  getInstallExports; type computePathPrefix/applyRuntimeContentRewritesInPlace
  on InstallExports
- surface.cts: lazily derive pathPrefix (only when a skills kind exists) and
  apply the rewrite via the shared getInstallExports accessor — single source of
  truth with install, only skills kinds rewritten (matches install)
- tests: regression test parameterized over cursor + codex asserting
  post-applySurface bodies carry the install pathPrefix, not ~/.claude
- CONTEXT.md: glossary updated for the applySurface rewrite parity + scope seam

Closes #813

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

* chore(#813): add changeset fragment for PR #817

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

* test(#813): normalize configDir prefix to forward slashes for Windows CI

The #813 regression assertion compared skill bodies against a raw
${configDir}/ prefix, but production derives pathPrefix via
path.resolve(configDir).replace(/\\/g, '/'). On Windows, mkdtempSync
returns backslash paths while the rewritten body uses forward slashes,
so the assertion would fail Windows-only (not covered by local gsd-test).
Normalize the expected prefix the same way production does.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* fix(#816): mirror install command-prefix handling in _syncGsdDir (#822)

* fix(#816): mirror install command-prefix handling in _syncGsdDir

applySurface() via _syncGsdDir handled command artifacts differently from a
fresh install. For flat command dirs (cursor/augment/opencode/kilo) install's
_copyStaged adds kind.prefix (gsd-<stem>.md) and _removeGsdEntries prunes
prefix-scoped, but _syncGsdDir copied staged files verbatim (unprefixed) and
pruned by exact name. So every /gsd:surface toggle wrote wrong filenames,
orphaned the installed gsd-*.md, and deleted user-authored command files.

_syncGsdDir's commands/agents branch now mirrors install:
- flat command dirs get the gsd- prefix on copy; namespaced dirs (commands/gsd)
  and agents keep staged names, using install's namespacedByDir rule
- prune is prefix-scoped so user files in shared flat dirs are preserved;
  namespaced commands/gsd stays membership-pruned so superseded commands are
  still removed on profile shrink

The naming rule is intentionally re-implemented (not via require('bin/install.js')
to avoid its module-load banner side-effect); a strict parity test asserts
applySurface and installRuntimeArtifacts produce identical command filenames for
opencode/kilo/cursor/augment/gemini, guarding against drift.

Closes #816

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

* chore(#816): add changeset fragment for PR #822

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* chore(#771): convert agent color: hex/magenta values to documented named colors (#823)

* chore(#771): convert agent color: hex/magenta values to documented named colors

Claude Code's sub-agent `color:` field documents only 8 named colors
(red, blue, green, yellow, purple, orange, pink, cyan). Twelve agent
files used hex values and two used the undocumented `magenta`; convert
each to the nearest documented named color so the intended per-agent
TUI color differentiation is spec-compliant.

- agents/*.md: 14 color values hex/magenta -> nearest named color
- scripts/research-profiles.cjs: update the 3 generated research-agent
  profiles (source of truth) so gen-research-agents stays in sync
- docs/AGENTS.md: update documented colors; add missing Color rows for
  gsd-nyquist-auditor, gsd-project-researcher, gsd-phase-researcher
- tests/agent-frontmatter.test.cjs: add regression guard asserting every
  agent color: is in the documented named-color set

Closes #771

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

* chore(#771): add changeset

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* feat(#773): add --ephemeral and --dangerously-bypass-hook-trust to automated codex exec wrappers (#824)

* feat(#773): add --ephemeral and --dangerously-bypass-hook-trust to automated codex exec invocations

Automated codex exec calls in the review workflow now carry --ephemeral
(no session-state accumulation across CI runs) and
--dangerously-bypass-hook-trust (skip hook-trust prompts for hooks
whose provenance gsd-core already controls). Both flags were verified
present in the installed codex CLI (codex exec --help).

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

* fix(#773): correct changeset pr: reference to #824

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* feat(#775): ship a gemini-extension.json extension package (#818)

Add a Gemini CLI extension package so users can install, update, and
remove GSD through Gemini's own extension lifecycle and have it appear in
`gemini extensions list`:

  gemini extensions install https://github.com/open-gsd/gsd-core
  gemini extensions update gsd-core
  gemini extensions uninstall gsd-core
  gemini extensions link /path/to/gsd-core   # dev

This mirrors the additive Claude Code plugin manifest (#766): a thin,
version-stamped manifest enforced by an in-repo drift test. The extension
ships the context-file payload (GEMINI.md), loaded into every Gemini
session; slash-command/agent/hook TOML projection into the extension is a
documented follow-up. The manual `npx gsd-core --gemini` installer (which
provides the /gsd:* commands) is unchanged — purely additive, no breaking
change.

- gemini-extension.json: name=binName, version tracks package.json,
  description, contextFileName=GEMINI.md (minimal; no mcpServers — gsd
  ships no MCP server)
- GEMINI.md: Gemini-session context payload
- package.json: add both artifacts to files[] so they publish
- CONTEXT.md: add "Gemini Extension Package" glossary entry
- docs: USER-GUIDE + install-on-your-runtime how-to
- tests/issue-775-gemini-extension.test.cjs: manifest validity, version
  parity with package.json, contextFileName existence, files[] publication

Closes #775

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* feat(#768): pre-populate settings.json permissions.allow/deny for Claude Code (#819)

* feat(#768): pre-populate settings.json permissions.allow/deny for Claude Code

Adds mergeClaudePermissions() to bin/install.js which non-destructively
appends GSD's known-safe tool-call patterns to permissions.allow and
defense-in-depth credential-file patterns to permissions.deny during
Claude Code installs. Merge is idempotent (no duplicates on reinstall)
and additive (existing user entries preserved). Uninstall removes only
the exact GSD-owned entries.

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

* chore: update changeset pr number to 819

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* feat(#774): emit service_tier/model_verbosity in Codex agent TOML + agents/openai.yaml skill chip (#828)

* feat(#774): emit service_tier/model_verbosity in Codex agent TOML + agents/openai.yaml skill chip

- Add service_tier = "flex" and model_verbosity = "low" to the Codex
  ConfigProfile TOML for light-tier agents (gsd-research-synthesizer,
  gsd-codebase-mapper, gsd-plan-checker, and 8 others identified via
  AGENT_DEFAULT_TIERS). Field names/values verified against Codex schema
  (profile_toml.rs / config_types.rs Verbosity enum). Non-light agents
  are unaffected.

- Add generateCodexSkillMetadataYaml() and writeCodexSkillMetadataFiles():
  after installRuntimeArtifacts, iterate every gsd-* skill directory,
  read the short-description already emitted in the SKILL.md frontmatter
  by convertClaudeCommandToCodexSkill, and write agents/openai.yaml with
  interface.display_name and interface.short_description for the Codex
  TUI skill picker chip.
  - yamlQuote (JSON.stringify) handles all YAML-unsafe chars.
  - User-owned gsd-dev-preferences dir is never overwritten.
  - Errors per-skill are swallowed so a bad SKILL.md can't abort install.
  - agents/openai.yaml is covered by the snapshot/rollback system and
    manifest hash (writeManifest hashes skill dirs recursively).
  - Uninstall symmetry: _removeGsdEntries removes whole gsd-* dirs.

- 21 new tests in codex-config.test.cjs covering service_tier/verbosity
  TOML emission, YAML generation (round-trip via js-yaml), and
  writeCodexSkillMetadataFiles including an e2e integration test.

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

* fix(#774): correct docs-lint coverage — proper changeset format + USER-GUIDE entry

Rewrite the changeset fragment from old @opengsd/gsd-core:patch format to the
required type:/pr: schema so the docs-lint parser can consume it.  Add a new
"Codex skill picker and agent scheduling (#774)" section to docs/USER-GUIDE.md
describing the flex-tier scheduling and /skills TUI chip enrichments — both are
user-visible and belong in docs rather than behind a docs-exempt marker.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* feat(#769): adopt context:fork + effort on heavy workflow skills (#820)

* feat(#769): emit context:fork + effort: frontmatter on heavy workflow skills

Add `context: fork` and `effort: xhigh` to the three heaviest workflow
commands (plan-phase, execute-phase, autonomous) and `effort: low` to the
two quick-status commands (progress, stats).

On Claude Code, `context: fork` runs the skill in an isolated subagent
context window so the main session's context budget is protected.
`effort: xhigh` / `effort: low` signal the appropriate token-budget tier to
the runtime. Both fields are silently ignored by runtimes that do not
recognise them (Gemini, Codex, Cursor, etc.) — no behaviour change outside
Claude Code.

Update convertClaudeCommandToClaudeSkill in bin/install.js to preserve
`context:` and `effort:` when rewriting source command files to SKILL.md for
a Claude global install. Add install-suite tests to assert the fields are
present in both source commands and the installed SKILL.md output.

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

* test(#769): tighten regex assertions + add execute/plan-phase effort coverage

Fix low-severity adversarial finding: tighten test regex patterns from
`\s*` to `[ \t]*` so they cannot match across newlines (CRLF parity).
Add missing effort: xhigh assertions for gsd-execute-phase and gsd-plan-phase
SKILL.md install output to complete the black-box coverage gap.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* feat(#772): adopt stable Codex hook events + commandWindows for Windows parity (#827)

* feat(#772): adopt stable Codex hook events + commandWindows for Windows parity

Register three new stable Codex hook events (SubagentStart, Stop,
PostToolUse) wired to gsd-context-monitor.js so Codex installs get
the same context-headroom tracking at subagent and session boundaries
that Claude/Qwen already have.

Add commandWindows field to the SessionStart hook entry on Windows so
Codex uses the .cmd shim directly (Git Bash/MSYS cannot POSIX-exec
node.exe). commandWindows is only emitted on win32; POSIX is unchanged.

Refactor reconcileCodexHooksJsonSessionStart into a generic
reconcileCodexHooksJsonEvent so any event name can be reconciled with
the same dedup/preserve-user-entries logic.

Add gsd-context-monitor.js and .cmd to MANAGED_HOOK_COMMAND_BASENAMES
_BY_SURFACE so idempotent re-runs de-duplicate entries correctly.

30 new tests covering: export surface, event registration for each of
the three events, commandWindows parity (POSIX vs win32), idempotency,
uninstall, and user-entry preservation.

Closes #772

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

* fix(#772): windows path normalization + docs-lint

- Normalize scriptPath backslashes to forward slashes in
  ensureCodexHooksJsonEvent and ensureCodexHooksJsonSessionStart so that
  isManagedHookCommand can match stored commands against configDir on
  Windows CI runners. path.resolve returns backslash paths on Windows,
  but when platform is not 'win32' (e.g. platform:'linux' in tests),
  projectManagedHookCommand skips normalization — producing a mismatch
  that breaks idempotency deduplication (the same hook entry appended
  twice on re-register). Forward-slash paths are always valid in both
  Node.js and Codex, so the normalization is safe for all platforms.
- Fix changeset pr: 0 → 827 to resolve fail_malformed_fragment.
- Add Codex hook coverage table to docs/how-to/install-on-your-runtime.md
  documenting the SubagentStart/Stop/PostToolUse events + commandWindows
  Windows-parity field added by this enhancement.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* feat(#778): cross-runtime command enrichment (Gemini {{args}}/!{}, Qwen priority) (#825)

* feat(#778): cross-runtime command enrichment (Gemini {{args}}/!{}, Qwen priority)

Enrich the installer's per-runtime command/skill generators with native,
verified, additive fields:

- Gemini CLI: map Claude's $ARGUMENTS -> Gemini's {{args}} in generated TOML
  commands so typed arguments interpolate; inject live .planning/STATE.md into
  /gsd:progress via a fixed, injection-safe !{cat .planning/STATE.md 2>/dev/null}
  shell block (no interpolated input).
- Qwen Code: emit the optional numeric `priority` field on main-loop skills so
  the most-used workflows sort first in the /skills list (higher = earlier per
  the Qwen skills spec; the issue's inverse numbering was corrected).

OpenCode per-command model/agent/subtask/variant enrichment was evaluated and
intentionally not implemented: `model` reintroduces the #1156
ProviderModelNotFoundError regression for non-Anthropic providers (the converter
deliberately strips model:), `subtask`/`agent` change execution semantics for
GSD's interactive commands, and `variant` is not in the OpenCode command schema.

Schemas verified against primary docs (Gemini custom-commands, Qwen skills,
OpenCode commands/skills). Adds tests/enh-778-* and how-to + USER-GUIDE docs.

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

* chore(#778): set changeset PR number to 825

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* feat(#789): elevate CodeBuddy — slash commands (#830)

* feat(#789): elevate CodeBuddy — emit slash commands (+ document subagent/MCP scope)

Emit a CodeBuddy slash-command surface so GSD workflows appear in the
'/' menu, reaching parity with other elevated runtimes.

- Add convertClaudeCommandToCodebuddyCommand and register a commands/
  artifact kind for the codebuddy runtime (commands/gsd-<name>.md),
  consistent with the Cursor (#785) and Augment (#790) commands surfaces.
- Mark emitted skills user-invocable:false so the commands surface is the
  sole '/' entry point (no duplicate /gsd-* entries); skills stay
  model-invocable. CodeBuddy's SKILL.md supports this field.
- Normalize $HOME/.codebuddy (bare + slash) path forms in runtime
  rewrites so --config-dir/local installs don't leak the default home.
- Report installed commands/ count on install; uninstall prunes gsd-*
  commands while preserving user-owned commands.

Scope: subagents (~/.codebuddy/agents/) are already emitted by the
generic agents block (unchanged); no mcp.json is written (gsd ships no
MCP server, and CodeBuddy's mcp.json registers only external servers).

Closes #789

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

* chore(#789): set changeset pr number to 830

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* feat(#776): Gemini hook events (BeforeAgent/AfterAgent/BeforeModel) + hooksConfig.enabled check (#829)

* feat(#776): Gemini hook events (BeforeAgent/AfterAgent/BeforeModel) + hooksConfig.enabled check

Register three new Gemini-CLI hook events on install:
  - BeforeAgent: fires before agent planning; wired to gsd-context-monitor
  - AfterAgent: fires after final response generation; wired to gsd-context-monitor
  - BeforeModel: fires before each LLM call (per-turn); wired to gsd-context-monitor

All three reuse gsd-context-monitor.js (no new hook files). Uninstall cleanup
loop extended to remove the new events. Non-array guard added for robustness
against malformed settings.

Also detect hooksConfig.enabled:false in Gemini settings and emit a clear
warning — without this check, all registered hooks silently do nothing.

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

* chore: update changeset pr: 829

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

* docs(#776): document Gemini hook events

Add hook coverage table to the Gemini CLI section of install-on-your-runtime.md,
covering the three new events (BeforeAgent/AfterAgent/BeforeModel wired to
gsd-context-monitor) plus a callout for the hooksConfig.enabled:false silent
failure mode detected by the installer.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* feat(#777): register Cursor-native hooks (.cursor/hooks.json) for session-start/post-tool parity (#831)

* feat(#777): register Cursor-native hooks (.cursor/hooks.json) for session-start/post-tool parity

- Add gsd-cursor-session-start.js: injects STATE.md presence reminder (or
  new-project nudge) into Cursor sessions via the sessionStart hook event
- Add gsd-cursor-post-tool.js: emits an additional_context nudge when
  write-class tool calls touch .planning/ files (postToolUse hook event)
- Add 'cursor-hooks-json' installSurface to runtime-config-adapter-registry;
  writeCursorHooksJson/reconcileCursorHooksJson write the canonical
  { version: 1, hooks: { sessionStart, postToolUse } } JSON shape with
  idempotent reconciliation that preserves user-owned hook entries
- Hook scripts are copied with /gsd:→gsd- rewrite so installed files
  contain no colon-form slash-command refs (bug-376 invariant)
- 20 new tests in tests/cursor-hooks.test.cjs cover all reconciler paths,
  entry helpers, removal, runtime adapter surface, and hook script behavior
- Update CONTEXT.md, ARCHITECTURE.md, installer-migrations.md, and
  000-first-time-baseline.cts to include Cursor hooks.json surface

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

* fix(#777): build hooks/dist on demand in bug-376 test for scoped/windows CI

hooks/dist is gitignored and only produced by `npm run build:hooks`.
The CI scoped (ubuntu-latest/node-22) and windows (windows-latest/node-24)
test jobs do NOT run build:hooks before executing tests, so bug-376's
prerequisite suite was failing with "hooks/dist not found" on both legs.

Add ensureHooksDist() helper (mirrors bug-3357 pattern) that builds
hooks/dist on demand in the before() hooks of prerequisite and Suite 3.
Also add ensureHooksDist() call to Suite 3's before() so the snapshot
step is also hermetic.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* docs(#832): add how-to guide for minimal install / skill profiles (#835)

Add docs/how-to/install-minimal-and-add-skills.md covering the --minimal
/ --core-only / --profile=core install, the core/standard/full profiles,
and growing the surface live via /gsd:surface or on reinstall. Register
it in the docs/README.md How-to guides index.

Closes #832

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* feat(#815): add /gsd-update --next to install the @next RC channel (#839)

Adds an opt-in --next (alias --rc) flag to /gsd-update targeting the @next RC dist-tag (ADR #660), with a {latest,next} allowlist enforced at three layers, channel-aware version check + banner, and byte-for-byte unchanged default @latest behavior.

Closes #815

* fix(#837): three-dot diff in ci-test-scope so docs-only PRs skip the heavy matrix (#841)

CI test-scope detection diffed changed files with a two-dot
`git diff --name-only base head`, where base is the moving tip of `next`.
A PR branch cut from a slightly older `next` surfaced every product file
`next` had gained since the merge-base, flipping product_changed/full_matrix
and running the full Windows/macOS matrix + coverage on docs-only PRs.

Switch to a three-dot `git diff --name-only base...head` (vs the merge-base),
matching GitHub's PR "Files changed" semantics. Add a regression test that
builds a stale-base topology, plus a guard test pinning `fetch-depth: 0` on
the `changes` job (required for the merge-base to be locally available).

Closes #837

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* feat(#836): no-LLM duplicate-issue detection + challenge + 1-day auto-close (#843)

* feat(#836): no-LLM duplicate-issue detection + challenge + 1-day auto-close

Adds a deterministic (no-LLM) duplicate-issue governance lifecycle:

- scripts/issue-dedupe.cjs: pure, unit-tested module (tokenize, Sørensen–Dice
  title similarity, scoreCandidates, renderChallengeComment, shouldClose) with
  fail-safe destructive-action guards.
- duplicate-check.yml (issues:opened): scores new-issue title against open
  issues, posts a challenge comment + applies the pending `possible-duplicate`
  label on a clear match.
- duplicate-sweep.yml (daily cron): closes possible-duplicate issues whose
  challenge comment is >24h old with no human reply and no 👎 veto; honors
  exempt labels; re-checks the label immediately before close (TOCTOU guard);
  strips the label on close to avoid reopen loops.
- remove-duplicate-label.yml (issue_comment:created): clears the label and
  applies needs-maintainer-review when any human responds.
- bug_report.yml / docs_issue.yml: add the required "I searched existing
  issues" preflight checkbox so all five forms force a pre-search attestation.
- docs/agents/triage-labels.md: document the label + lifecycle.

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

* chore(#836): add changeset fragment for duplicate-issue detection

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* feat(#770): register Claude Code lifecycle hooks (SubagentStop/Stop/PreCompact/FileChanged) (#821)

* feat(#770): register Claude Code lifecycle hooks (SubagentStop/Stop/PreCompact/FileChanged)

Wire three new context-tracking events (SubagentStop, Stop, PreCompact) to
gsd-context-monitor so context-headroom warnings surface at model-stop and
subagent-finalisation moments — not just on PostToolUse.  Add a new
FileChanged hook (gsd-config-reload.js) that hot-reloads .planning/config.json
context mid-session when the user edits it, injecting a config summary as
hookSpecificOutput.additionalContext.  Updates plugin manifest hooks.json,
managed-hooks-registry, installer-migration-report allowlist, and
shell-command-projection cleanup tables.  Tests: 21 new assertions in
enh-770-claude-hook-events.test.cjs; enh-788 and issue-766 test suites updated.

Closes #770

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

* docs(#770): document newly-registered Claude Code lifecycle hooks

Add a Hook coverage table to the Claude Code npm installer section of
docs/how-to/install-on-your-runtime.md describing SubagentStop, Stop,
PreCompact, and the new FileChanged (gsd-config-reload.js) hook that
hot-reloads .planning/config.json mid-session. Also fixes the changeset
frontmatter (adds type: Added + pr: 821) so docs-lint can consume the
fragment.

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

* fix(#770): add gsd-config-reload.js to INVENTORY.md and regenerate manifest

The feat commit added hooks/gsd-config-reload.js but did not bump the
Hooks count in docs/INVENTORY.md (14→15) or add the new row, and did not
regenerate docs/INVENTORY-MANIFEST.json. Both inventory-counts and
inventory-manifest-sync tests failed across the full CI matrix.

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

* fix(#770): make lifecycle-hook tests deterministic on scoped runner

Replace the shared hooks/dist/ ensemble setup (ensureHooksDist /
teardownHooksDist) in the Claude hook tests with per-test isolation:
pre-populate each test's own tmpDir/.claude/hooks/ with stub files and
pass installerMigrations:[] to install() so the first-time-baseline
migration does not remove the stubs before the copy step can run.

Root cause: hooks/dist/ is gitignored and absent on a fresh npm ci.
ensureHooksDist() created it and teardownHooksDist() deleted it, but
with --test-concurrency=4 both test files ran concurrently as separate
Node.js worker processes sharing the same filesystem.  One file's
afterEach teardown deleted hooks/dist/ while the other file's install()
was copying from it, producing an ENOENT (reproduced 2/10 runs locally).

The additional issue: even with pre-placed stubs surviving the copy race,
the 000-first-time-baseline migration classified hooks/gsd-*.js as
bundled-gsd-hook artifacts, auto-removed them, and the copy step never
re-ran (hooks/dist/ absent) — leaving contextMonitorFile missing and all
hook registrations silently skipped (the 'got: []' symptom).

Fix: pre-populate targetDir/hooks/ per-test (isolated temp dir) AND pass
installerMigrations:[] so the baseline scan is skipped.  The Qwen suites
already used this pattern correctly; the Claude suites are aligned to it.

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

* fix(#770): ship gsd-config-reload.js by adding it to build-hooks HOOKS_TO_COPY

The #770 feature added hooks/gsd-config-reload.js and registered it in
MANAGED_HOOKS, the installer, INVENTORY, and the test EXPECTED_ALL_HOOKS
list — but never added it to scripts/build-hooks.js HOOKS_TO_COPY. As a
result the hook was never copied into hooks/dist/ during the build, so:

  - the hook would never ship to users (real production bug — the
    FileChanged config-reload feature was dead-on-arrival), and
  - install-minimal-hooks.test.cjs #1755 ("all expected hooks are copied
    from hooks/dist/ to target", ".js hooks are executable after copy",
    "manifest contains .js hook entries") failed on any environment with
    a clean checkout (no pre-existing hooks/dist/): coverage, full test
    macos-22/macos-24, test ubuntu-24.

The failures were masked locally only by a stale hooks/dist/ left from a
prior build (build-hooks copies into dist without clearing it). On CI's
fresh `npm ci` there is no dist, so the omission surfaced.

Fix: add 'gsd-config-reload.js' to HOOKS_TO_COPY so build-hooks stages it
into hooks/dist/ alongside the other JS hooks. Verified by removing
hooks/dist/ and rerunning the full suite green (0 fail).

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

* fix(#770): make config prototype-pollution beforeEach deterministic on scoped runner

Root cause: the #663 and alert-#26 prototype-pollution describe blocks
seeded .planning/config.json in beforeEach via a bare
runGsdTools('config-ensure-section') whose result was discarded. That
command runs in a spawned gsd-tools child; on the scoped CI lane
(--test-concurrency=4, config.test.cjs scheduled alongside the heavy
install/tarball suites that #770 pulled into the targeted set) the child
can be transiently killed under resource pressure (non-zero exit, empty
stderr — an OS-level kill, not an app error). The swallowed failure left
config.json absent, so the first subtest's readConfig() threw ENOENT
opening <tmp>/.planning/config.json. Only 1 of 4 subtests failed,
confirming a per-invocation transient, not a deterministic miss; the full
suite schedules files differently so config.test.cjs did not collide with
those heavy neighbors → passed there.

Fix: add ensureConfigReady(tmpDir) which retries config-ensure-section on
ANY failure or missing file and throws a clear diagnostic if it still
cannot create config.json, then use it in both prototype-pollution
beforeEach blocks. Setup is now deterministic under load; the #663/alert-#26
security assertions are unchanged.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* fix(#844): sync runtime manifest versions on npm version bump (#845)

* fix(#844): sync runtime manifest versions on npm version bump

The release workflow bumps package.json via `npm version` but never
stamped the runtime-integration manifests that must track it
(.claude-plugin/plugin.json #766, gemini-extension.json #775), so the
first RC/finalize whose version diverged from the -dev stream failed the
test suite before tagging/publishing.

Add scripts/sync-manifest-versions.cjs (single VERSIONED_MANIFESTS
registry) wired to a `version` npm lifecycle hook that stamps + stages
the manifests on every `npm version` — covering all four release bump
sites and local bumps with no workflow edits. A regression guard test
fails if any repo JSON whose version matches package.json is not
registered, forcing future version-bearing manifests into the sync.

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

* docs(#844): add changeset for manifest version sync fix

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* chore: bump to 1.4.0-rc.2

* chore: finalize v1.4.0

* chore: promote CHANGELOG for v1.4.0

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Colin <colin@solvely.net>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Joe <44273333+jslitzkerttcu@users.noreply.github.com>
Co-authored-by: Solvely-Colin <211764741+Solvely-Colin@users.noreply.github.com>
2026-06-08 14:37:33 -04:00

84 KiB
Raw Blame History

Create executable phase prompts (PLAN.md files) for a roadmap phase with integrated research and verification. Default flow: Research (if needed) -> Plan -> Verify -> Done. Orchestrates gsd-phase-researcher, gsd-planner, and gsd-plan-checker agents with a revision loop (max 3 iterations).

<required_reading> Read all files referenced by the invoking prompt's execution_context before starting.

@/.claude/gsd-core/references/ui-brand.md @/.claude/gsd-core/references/revision-loop.md @/.claude/gsd-core/references/gate-prompts.md @/.claude/gsd-core/references/agent-contracts.md @~/.claude/gsd-core/references/gates.md </required_reading>

<available_agent_types> Valid GSD subagent types (use exact names — do not fall back to 'general-purpose'):

  • gsd-phase-researcher — Researches technical approaches for a phase
  • gsd-pattern-mapper — Analyzes codebase for existing patterns, produces PATTERNS.md
  • gsd-planner — Creates detailed plans from phase scope
  • gsd-plan-checker — Reviews plan quality before execution </available_agent_types>

0. Git Branch Invariant

Do not create, rename, or switch git branches during plan-phase. Branch identity is established at discuss-phase and is owned by the user's git workflow. A phase rename in ROADMAP.md is a plan-level change only — it does not mutate git branch names. If phase_slug in the init JSON differs from the current branch name, that is expected and correct; leave the branch unchanged.

1. Initialize

Load all context in one call (paths only to minimize orchestrator context):

_GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; GSD_TOOLS="${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}"; if [ -f "$GSD_TOOLS" ]; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif command -v gsd-tools >/dev/null 2>&1; then GSD_TOOLS="$(command -v gsd-tools)"; gsd_run() { "$GSD_TOOLS" "$@"; }; elif [ -f "$HOME/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="$HOME/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found at $GSD_TOOLS and gsd-tools is not on PATH. Run: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi
GRAN_PARAM=""; if [[ "$ARGUMENTS" =~ (^|[[:space:]])--granularity[[:space:]]+([^[:space:]-][^[:space:]]*) ]]; then GRAN_PARAM="--granularity ${BASH_REMATCH[2]}"; fi
INIT=$(gsd_run query init.plan-phase "$PHASE" $GRAN_PARAM)
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
AGENT_SKILLS_RESEARCHER=$(gsd_run query agent-skills gsd-phase-researcher)
AGENT_SKILLS_PLANNER=$(gsd_run query agent-skills gsd-planner)
AGENT_SKILLS_CHECKER=$(gsd_run query agent-skills gsd-plan-checker)
CONTEXT_WINDOW=$(gsd_run query config-get context_window 2>/dev/null || echo "200000")
TDD_MODE=$(gsd_run query config-get workflow.tdd_mode 2>/dev/null || echo "false")
MVP_MODE_CFG=$(gsd_run query config-get workflow.mvp_mode 2>/dev/null || echo "false")

When TDD_MODE is true, the planner agent is instructed to apply type: tdd to eligible tasks using heuristics from references/tdd.md. The planner's <required_reading> is extended to include @~/.claude/gsd-core/references/tdd.md so gate enforcement rules are available during planning.

When CONTEXT_WINDOW >= 500000, the planner prompt includes the 3 most recent prior phase CONTEXT.md and SUMMARY.md files PLUS any phases explicitly listed in the current phase's Depends on: field in ROADMAP.md. Explicit dependencies always load regardless of recency (e.g., Phase 7 declaring Depends on: Phase 2 always sees Phase 2's context). Bounded recency keeps the planner's context budget focused on recent work.

Parse JSON for: researcher_model, planner_model, checker_model, research_enabled, plan_checker_enabled, nyquist_validation_enabled, commit_docs, text_mode, phase_found, phase_dir, phase_number, phase_name, phase_slug, padded_phase, has_research, has_context, has_reviews, has_plans, plan_count, phase_status (#3569), planning_exists, roadmap_exists, phase_req_ids, response_language, granularity.

If response_language is set: Include response_language: {value} in all spawned subagent prompts so any user-facing output stays in the configured language.

File paths (for <files_to_read> blocks): state_path, roadmap_path, requirements_path, context_path, research_path, verification_path, uat_path, reviews_path. These are null if files don't exist.

If planning_exists is false: Error — run /gsd:new-project first.

1.5. Closed-Phase Gate (#3569)

The init JSON includes phase_status — one of Pending | Planned | In Progress | Executed | Complete | Needs Review. Complete means the phase has all summaries AND a VERIFICATION.md with status: passed. Replanning a closed phase silently rewrites plan docs that no longer match the shipped code, so the workflow must hard-stop here unless the operator explicitly overrides.

Parse phase_status from the init JSON, then:

FORCE_REPLAN=false
if [[ "$ARGUMENTS" =~ (^|[[:space:]])--force([[:space:]]|$) ]]; then
  FORCE_REPLAN=true
fi

if [ "${phase_status}" = "Complete" ]; then
  if [[ "$ARGUMENTS" =~ (^|[[:space:]])--reviews([[:space:]]|$) ]]; then
    # --reviews on a closed phase is never legitimate — concerns belong in a
    # new phase or issue against the closed phase's commits.
    cat <<EOF >&2
Phase ${phase_number} (${phase_name}) is already CLOSED (VERIFICATION status: passed).
/gsd:plan-phase --reviews cannot replan a closed phase. If the review surfaced
real concerns, open a follow-up phase or file an issue against the closed
phase's commits. There is no --force override for --reviews on a closed phase.
EOF
    exit 1
  fi
  if [ "$FORCE_REPLAN" != "true" ]; then
    cat <<EOF >&2
Phase ${phase_number} (${phase_name}) is already CLOSED (VERIFICATION status: passed).
Replanning a closed phase will overwrite plan docs that no longer match the
shipped code. If you intentionally want to replan over closed work, re-run
with: /gsd:plan-phase ${phase_number} --force

Otherwise, to view what shipped, see: ${verification_path}
EOF
    exit 1
  fi
  # FORCE_REPLAN=true: continue, but emit a banner so the operator sees the
  # decision in the transcript and in any committed plan docs.
  echo "WARNING: Replanning CLOSED phase ${phase_number} under --force. Verify the closeout was wrong before committing new plan docs." >&2
fi

The gate fires only on Complete. Executed and Needs Review are not gated — those states mean planning was finished but verification did not pass, and replanning is a legitimate next step.

2. Parse and Normalize Arguments

Extract from $ARGUMENTS: phase number (integer or decimal like 2.1), flags (--research, --skip-research, --research-phase <N>, --gaps, --skip-verify, --skip-ui, --prd <filepath>, --ingest <path-or-glob>, --ingest-format <auto|nygard|madr|narrative>, --reviews, --text, --bounce, --skip-bounce, --chunked, --mvp, --tdd, --granularity <coarse|standard|fine>, --force (override closed-phase gate, see §1.5)).

--research-phase <N> — research-only mode (#3042 + #3044). When this flag is present, parse <N> as the phase number (overrides any positional phase argument), set RESEARCH_ONLY=true, and treat the rest of this workflow as a research-dispatch only — the planner spawn (step 8), plan-checker, verification, gaps, bounce, and post-planning-gaps blocks all skip on RESEARCH_ONLY. Use this for cross-phase research, doc review before committing to a planning approach, and correction-without-replanning loops. Replaces the deleted /gsd-research-phase command.

In research-only mode, two modifiers control behavior when RESEARCH.md already exists:

  • --research — force-refresh re-research without prompting. Re-spawns the researcher unconditionally and overwrites the existing RESEARCH.md. (This is the existing --research flag's standard "force re-research" semantics, reused here.)
  • --view — view-only: print existing RESEARCH.md to stdout, do not spawn the researcher. Sets VIEW_ONLY=true. Cheapest mode for the correction-without-replanning loop. If RESEARCH.md does not exist, error with a hint to drop --view.
RESEARCH_ONLY=false
VIEW_ONLY=false
if [[ "$ARGUMENTS" =~ --research-phase[[:space:]]+([0-9]+(\.[0-9]+)?) ]]; then
  RESEARCH_ONLY=true
  PHASE="${BASH_REMATCH[1]}"
fi
if $RESEARCH_ONLY && [[ "$ARGUMENTS" =~ (^|[[:space:]])--view([[:space:]]|$) ]]; then
  VIEW_ONLY=true
fi

--granularity <coarse|standard|fine> — CLI override (#703). When present, this value is the resolved granularity passed to the planner — it wins over any per-phase granularities.<type> config, top-level granularity config, or project defaults. The init JSON always includes a granularity field reflecting the resolved value; read it from there. Invalid values (anything other than coarse, standard, fine) cause an error at the CLI boundary.

Set TEXT_MODE=true if --text is present in $ARGUMENTS OR text_mode from init JSON is true. When TEXT_MODE is active, replace every AskUserQuestion call with a plain-text numbered list and ask the user to type their choice number. This is required for Claude Code remote sessions (/rc mode) where TUI menus don't work through the Claude App.

MVP_MODE resolution. Resolve MVP_MODE once via the centralized phase.mvp-mode query verb. Precedence (first hit wins): CLI flag → ROADMAP.md **Mode:** mvp → workflow.mvp_mode config → false. The verb is the single source of truth — do not re-implement the chain.

MVP_FLAG_ARG=""
if [[ "$ARGUMENTS" =~ (^|[[:space:]])--mvp([[:space:]]|$) ]]; then MVP_FLAG_ARG="--cli-flag"; fi
if [[ "$ARGUMENTS" =~ (^|[[:space:]])--tdd([[:space:]]|$) ]]; then TDD_MODE=true; fi

Defer the phase.mvp-mode query until PHASE is finalized (after explicit argument parsing/fallback phase detection + validation). The verb returns true|false; full result also exposes source (cli_flag | roadmap | config | none) for diagnostics. Mode is all-or-nothing per phase (PRD decision Q1).

Walking Skeleton gate. When MVP_MODE=true AND phase_number == "01" AND there are zero prior phase summaries (new project), the planner runs in Walking Skeleton mode (per PRD decision Q2 — new projects only). Detect with:

WALKING_SKELETON=false
if [ "$MVP_MODE" = "true" ] && [ "$padded_phase" = "01" ]; then
  PRIOR_SUMMARIES=$(gsd_run query phases.list --pick summaries_total 2>/dev/null || echo "0")
  if [ "$PRIOR_SUMMARIES" = "0" ]; then WALKING_SKELETON=true; fi
fi

When WALKING_SKELETON=true:

  • Planner is instructed to produce SKELETON.md in the phase directory alongside PLAN.md. The template lives at ~/.claude/gsd-core/references/skeleton-template.md — the planner reads it when producing SKELETON.md (lazy; not loaded on non-skeleton runs).
  • The plan must scaffold project + routing + one real DB read/write + one real UI interaction + dev deployment — the thinnest possible end-to-end working slice.

Interaction with --prd <filepath>. --mvp and --prd compose. The PRD express path (Step 3.5) creates CONTEXT.md from the PRD file and continues to research; the Walking Skeleton gate fires independently from the conditions above. When both are active on Phase 1 of a new project, the planner receives WALKING_SKELETON=true and PRD-derived context simultaneously — the PRD informs what the skeleton should prove. No precedence is needed; the two signals are orthogonal. See references/mvp-concepts.md for the broader interaction map.

Extract express-path args from $ARGUMENTS: PRD_FILE (--prd <filepath>), INGEST_PATH (--ingest <path-or-glob>), and optional INGEST_FORMAT (--ingest-format <auto|nygard|madr|narrative>, default auto).

--prd and --ingest are mutually exclusive. If both are present, error and exit: Invalid arguments: cannot combine \--prd` with `--ingest`.`

If no phase number: Detect next unplanned phase from roadmap.

If phase_found is false: Validate phase exists in ROADMAP.md. If valid, create the directory using expected_phase_dir from init (includes project_code prefix when set):

mkdir -p "${expected_phase_dir}"

Set phase_dir="${expected_phase_dir}" after creation.

Existing artifacts from init: has_research, has_plans, plan_count.

Set CHUNKED_MODE from flag or config:

CHUNKED_CFG=$(gsd_run query config-get workflow.plan_chunked 2>/dev/null || echo "false")
CHUNKED_MODE=false
if [[ "$ARGUMENTS" =~ --chunked ]] || [[ "$CHUNKED_CFG" == "true" ]]; then
  CHUNKED_MODE=true
fi

2.5. Validate --reviews Prerequisite

Skip if: No --reviews flag.

If --reviews AND --gaps: Error — cannot combine --reviews with --gaps. These are conflicting modes.

If --reviews AND has_reviews is false (no REVIEWS.md in phase dir):

Error:

No REVIEWS.md found for Phase {N}. Run reviews first:

/gsd:review --phase {N}

Then re-run /gsd:plan-phase {N} --reviews

Exit workflow.

3. Validate Phase

PHASE_INFO=$(gsd_run query roadmap.get-phase "${PHASE}")

If found is false: Error with available phases. If found is true: Extract phase_number, phase_name, goal from JSON.

Now that PHASE is finalized, resolve MVP mode:

MVP_MODE=$(gsd_run query phase.mvp-mode "${PHASE}" $MVP_FLAG_ARG --pick active)

3.5. Handle PRD Express Path

Skip if: No --prd flag in arguments.

If --prd <filepath> provided:

  1. Read the PRD file:
PRD_CONTENT=$(cat "$PRD_FILE" 2>/dev/null)
if [ -z "$PRD_CONTENT" ]; then
  echo "Error: PRD file not found: $PRD_FILE"
  exit 1
fi
  1. Display banner:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 GSD ► PRD EXPRESS PATH
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Using PRD: {PRD_FILE}
Generating CONTEXT.md from requirements...
  1. Parse the PRD content and generate CONTEXT.md. The orchestrator should:

    • Extract all requirements, user stories, acceptance criteria, and constraints from the PRD
    • Map each to a locked decision (everything in the PRD is treated as a locked decision)
    • Identify any areas the PRD doesn't cover and mark as "Claude's Discretion"
    • Extract canonical refs from ROADMAP.md for this phase, plus any specs/ADRs referenced in the PRD — expand to full file paths (MANDATORY)
    • Create CONTEXT.md in the phase directory
  2. Write CONTEXT.md:

# Phase [X]: [Name] - Context

**Gathered:** [date]
**Status:** Ready for planning
**Source:** PRD Express Path ({PRD_FILE})

<domain>
## Phase Boundary

[Extracted from PRD — what this phase delivers]

</domain>

<decisions>
## Implementation Decisions

{For each requirement/story/criterion in the PRD:}
### [Category derived from content]
- [Requirement as locked decision]

### Claude's Discretion
[Areas not covered by PRD — implementation details, technical choices]

</decisions>

<canonical_refs>
## Canonical References

**Downstream agents MUST read these before planning or implementing.**

[MANDATORY. Extract from ROADMAP.md and any docs referenced in the PRD.
Use full relative paths. Group by topic area.]

### [Topic area]
- `path/to/spec-or-adr.md` — [What it decides/defines]

[If no external specs: "No external specs — requirements fully captured in decisions above"]

</canonical_refs>

<specifics>
## Specific Ideas

[Any specific references, examples, or concrete requirements from PRD]

</specifics>

<deferred>
## Deferred Ideas

[Items in PRD explicitly marked as future/v2/out-of-scope]
[If none: "None — PRD covers phase scope"]

</deferred>

---

*Phase: XX-name*
*Context gathered: [date] via PRD Express Path*
  1. Commit:
gsd_run query commit "docs(${padded_phase}): generate context from PRD" --files "${phase_dir}/${padded_phase}-CONTEXT.md"
  1. Set context_content to the generated CONTEXT.md content and continue to step 5 (Handle Research).

Effect: This completely bypasses step 4 (Load CONTEXT.md) since we just created it. The rest of the workflow (research, planning, verification) proceeds normally with the PRD-derived context.

3.6. Handle ADR Ingest Express Path

Skip if: No --ingest flag in arguments.

If --ingest <path-or-glob> provided:

  1. Display banner: GSD ► ADR Ingest Express Path with {INGEST_PATH} and {INGEST_FORMAT}.
  2. Parse each resolved ADR through gsd-core/bin/lib/adr-parser.cjs (--input, --format) and collect normalized records.
  3. Status gate: reject superseded/rejected/deprecated; warn on proposed; missing status defaults to accepted.
  4. Empty-decisions fallback: if all parsed ADRs have zero decisions[], emit ADR ingest produced no locked decisions; fall back to discuss-phase for this phase. and exit with /gsd:discuss-phase {N} guidance.
  5. Generate CONTEXT.md using <domain>, <decisions>, <canonical_refs>, <specifics>, <deferred>, <scope_fence>, map consequences_positive[] to Success Criteria and consequences_negative[] to Risk Summary, and include **Source:** ADR Ingest Express Path ({INGEST_PATH}).
  6. Commit with gsd-tools.cjs query commit "docs(${padded_phase}): generate context from ADR ingest" --files "${phase_dir}/${padded_phase}-CONTEXT.md" and set context_content; continue to step 5.

Effect: This bypasses step 4 (Load CONTEXT.md) since CONTEXT.md was synthesized from ADR input.

4. Load CONTEXT.md

Skip if: PRD express path or ADR ingest express path was used (CONTEXT.md already created in step 3.5/3.6).

Check context_path from init JSON.

If context_path is not null, display: Using phase context from: ${context_path}

If context_path is null (no CONTEXT.md exists):

Read discuss mode for context gate label:

DISCUSS_MODE=$(gsd_run query config-get workflow.discuss_mode 2>/dev/null || echo "discuss")

If TEXT_MODE is true, present as a plain-text numbered list:

No CONTEXT.md found for Phase {X}. Plans will use research and requirements only — your design preferences won't be included.

1. Continue without context — Plan using research + requirements only
[If DISCUSS_MODE is "assumptions":]
2. Gather context (assumptions mode) — Analyze codebase and surface assumptions before planning
[If DISCUSS_MODE is "discuss" or unset:]
2. Run discuss-phase first — Capture design decisions before planning

Enter number:

Otherwise use AskUserQuestion:

  • header: "No context"
  • question: "No CONTEXT.md found for Phase {X}. Plans will use research and requirements only — your design preferences won't be included. Continue or capture context first?"
  • options:
    • "Continue without context" — Plan using research + requirements only If DISCUSS_MODE is "assumptions":
    • "Gather context (assumptions mode)" — Analyze codebase and surface assumptions before planning If DISCUSS_MODE is "discuss" (or unset):
    • "Run discuss-phase first" — Capture design decisions before planning

If "Continue without context": Proceed to step 5. If "Run discuss-phase first": IMPORTANT: Do NOT invoke discuss-phase as a nested Skill/Task call — AskUserQuestion does not work correctly in nested subcontexts (#1009). Instead, display the command and exit so the user runs it as a top-level command:

Run this command first, then re-run /gsd:plan-phase {X} ${GSD_WS}:

/gsd:discuss-phase {X} ${GSD_WS}

Exit the plan-phase workflow. Do not continue.

4.5. Check AI-SPEC

Skip if: ai_integration_phase_enabled from config is false, or --skip-ai-spec flag provided.

AI_SPEC_FILE=$(ls "${PHASE_DIR}"/*-AI-SPEC.md 2>/dev/null | head -1)
AI_PHASE_CFG=$(gsd_run query config-get workflow.ai_integration_phase 2>/dev/null || echo "true")

Skip if AI_PHASE_CFG is false.

If AI_SPEC_FILE is empty: Check phase goal for AI keywords:

echo "${phase_goal}" | grep -qi "agent\|llm\|rag\|chatbot\|embedding\|langchain\|llamaindex\|crewai\|langgraph\|openai\|anthropic\|vector\|eval\|ai system"

If AI keywords detected AND no AI-SPEC.md:

◆ Note: This phase appears to involve AI system development.
  Consider running /gsd:ai-integration-phase {N} before planning to:
  - Select the right framework for your use case
  - Research its docs and best practices
  - Design an evaluation strategy

  Continue planning without AI-SPEC? (non-blocking — /gsd:ai-integration-phase can be run after)

Use AskUserQuestion with options:

  • "Continue — plan without AI-SPEC"
  • "Stop — I'll run /gsd:ai-integration-phase {N} first"

If "Stop": Exit with /gsd:ai-integration-phase {N} reminder. If "Continue": Proceed. (Non-blocking — planner will note AI-SPEC is absent.)

If AI_SPEC_FILE is non-empty: Extract framework for planner context:

FRAMEWORK_LINE=$(grep "Selected Framework:" "${AI_SPEC_FILE}" | head -1)

Pass ai_spec_path and framework_line to planner in step 7 so it can reference the AI design contract.

5. Handle Research

Skip if: --gaps flag or --skip-research flag or --reviews flag.

5.0. Research-Only Modifiers (--view, --research)

Skip if: RESEARCH_ONLY is false.

Three branches in research-only mode (--research-phase <N>):

  1. --view: print RESEARCH.md to stdout, no spawn, exit. If RESEARCH.md is missing, error with: --view requires an existing RESEARCH.md; drop --view to spawn the researcher.
  2. --research (force-refresh): re-spawn researcher unconditionally — fall through to "Spawn gsd-phase-researcher" below.
  3. Neither flag AND has_research=true: auto-use the existing research and exit cleanly — do not prompt, do not re-spawn. Emit RESEARCH.md already exists for Phase ${PHASE}, using it. To force-refresh, re-invoke with --research; to print, re-invoke with --view. Path: ${research_path} then exit. The explicit-flag escape hatches cover any deviation; this matches §5.1's promptless auto-use of existing research, removing the §5.0/§5.1 inconsistency (#159).
if [[ "$VIEW_ONLY" == "true" ]]; then
  [[ -f "$research_path" ]] || { echo "Error: --view requires an existing RESEARCH.md (Phase ${PHASE}). Drop --view to spawn the researcher."; exit 1; }
  cat "$research_path"; exit 0
fi

5.1. Standard Research Decision

Skip if RESEARCH_ONLY=true (the research-only mode in 5.0 already determined the path: spawn or exit). Without this guard, an LLM following the workflow could fall through into "use existing, skip to step 6" → planner spawn, violating the research-only contract. CR #3045 finding: this gate makes the early-exit unreachable from any non-research-only branch.

If has_research is true (from init) AND no --research flag: Use existing, skip to step 6.

If RESEARCH.md missing OR --research flag:

If no explicit flag (--research or --skip-research) and not --auto: Ask the user whether to research, with a contextual recommendation based on the phase:

If TEXT_MODE is true, present as a plain-text numbered list:

Research before planning Phase {X}: {phase_name}?

1. Research first (Recommended) — Investigate domain, patterns, and dependencies before planning. Best for new features, unfamiliar integrations, or architectural changes.
2. Skip research — Plan directly from context and requirements. Best for bug fixes, simple refactors, or well-understood tasks.

Enter number:

Otherwise use AskUserQuestion:

AskUserQuestion([
  {
    question: "Research before planning Phase {X}: {phase_name}?",
    header: "Research",
    multiSelect: false,
    options: [
      { label: "Research first (Recommended)", description: "Investigate domain, patterns, and dependencies before planning. Best for new features, unfamiliar integrations, or architectural changes." },
      { label: "Skip research", description: "Plan directly from context and requirements. Best for bug fixes, simple refactors, or well-understood tasks." }
    ]
  }
])

If user selects "Skip research": skip to step 6.

If --auto and research_enabled is false: Skip research silently (preserves automated behavior).

Display banner:

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 GSD ► RESEARCHING PHASE {X}
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

◆ Spawning researcher... (runs in a subagent — no output until it returns, ~1–5 min; expected, not a freeze)

Spawn gsd-phase-researcher

PHASE_DESC=$(gsd_run query roadmap.get-phase "${PHASE}" --pick section)

Research prompt:

<objective>
Research how to implement Phase {phase_number}: {phase_name}
Answer: "What do I need to know to PLAN this phase well?"
</objective>

<files_to_read>
- {context_path} (USER DECISIONS from /gsd:discuss-phase)
- {requirements_path} (Project requirements)
- {state_path} (Project decisions and history)
</files_to_read>

${AGENT_SKILLS_RESEARCHER}

<additional_context>
**Phase description:** {phase_description}
**Phase requirement IDs (MUST address):** {phase_req_ids}

**Project instructions:** Read ./CLAUDE.md if exists — follow project-specific guidelines
**Project skills:** Check .claude/skills/ or .agents/skills/ directory (if either exists) — read SKILL.md files, research should account for project skill patterns
</additional_context>

<output>
Write to: {phase_dir}/{phase_num}-RESEARCH.md
</output>
Agent(
  prompt=research_prompt,
  subagent_type="gsd-phase-researcher",
  model="{researcher_model}",
  description="Research Phase {phase}"
)

ORCHESTRATOR RULE — CODEX RUNTIME: After calling Agent() above, stop working on this task immediately. Do not read more files, edit code, or run tests related to this task while the subagent is active. Wait for the subagent to return its result. This prevents duplicate work, conflicting edits, and wasted context. Only resume when the subagent result is available.

Handle Researcher Return

  • ## RESEARCH COMPLETE: Display confirmation, continue to step 6
  • ## RESEARCH BLOCKED: Display blocker, offer: 1) Provide context, 2) Skip research, 3) Abort

Research-Only Early Exit (--research-phase)

Skip if: RESEARCH_ONLY is false (the default).

If RESEARCH_ONLY=true: the user invoked /gsd:plan-phase --research-phase <N> for research-only mode. Do not continue to Section 5.5+ (validation strategy, planner, plan-checker, verification, gaps, bounce, post-planning-gaps). Print the research-complete summary and exit cleanly:

✓ Research-only mode complete (#3042)

  Phase:       ${PHASE}
  RESEARCH.md: ${research_path}

Re-run /gsd:plan-phase ${PHASE} to plan the phase using this research,
or /gsd:plan-phase ${PHASE} --research to refresh research and plan.

This exits the workflow. The planner / plan-checker / verifier blocks below are skipped.

5.5. Create Validation Strategy

Skip if nyquist_validation_enabled is false OR research_enabled is false.

If research_enabled is false and nyquist_validation_enabled is true: warn "Nyquist validation enabled but research disabled — VALIDATION.md cannot be created without RESEARCH.md. Plans will lack validation requirements (Dimension 8)." Continue to step 6.

But Nyquist is not applicable for this run when all of the following are true:

  • research_enabled is false
  • has_research is false
  • no --research flag was provided

In that case: skip validation-strategy creation entirely. Do not expect RESEARCH.md or VALIDATION.md for this run, and continue to Step 6.

grep -l "## Validation Architecture" "${PHASE_DIR}"/*-RESEARCH.md 2>/dev/null || true

If found:

  1. Read template: ~/.claude/gsd-core/templates/VALIDATION.md
  2. Write to ${PHASE_DIR}/${PADDED_PHASE}-VALIDATION.md (use Write tool)
  3. Fill frontmatter: {N} → phase number, {phase-slug} → slug, {date} → current date
  4. Verify:
test -f "${PHASE_DIR}/${PADDED_PHASE}-VALIDATION.md" && echo "VALIDATION_CREATED=true" || echo "VALIDATION_CREATED=false"
  1. If VALIDATION_CREATED=false: STOP — do not proceed to Step 6
  2. If commit_docs: commit "docs(phase-${PHASE}): add validation strategy"

If not found: Warn and continue — plans may fail Dimension 8.

5.55. Security Threat Model Gate

Skip if workflow.security_enforcement is explicitly false. Absent = enabled.

SECURITY_CFG=$(gsd_run query config-get workflow.security_enforcement --raw 2>/dev/null || echo "true")
SECURITY_ASVS=$(gsd_run query config-get workflow.security_asvs_level --raw 2>/dev/null || echo "1")
SECURITY_BLOCK=$(gsd_run query config-get workflow.security_block_on --raw 2>/dev/null || echo "high")

If SECURITY_CFG is false: Skip to step 5.6.

If SECURITY_CFG is true: Display banner:

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 GSD ► SECURITY THREAT MODEL REQUIRED (ASVS L{SECURITY_ASVS})
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Each PLAN.md must include a <threat_model> block.
Block on: {SECURITY_BLOCK} severity threats.
Opt out: set security_enforcement: false in .planning/config.json

Continue to step 5.6. Security config is passed to the planner in step 8.

5.6. UI Design Contract Gate

Skip if workflow.ui_phase is explicitly false AND workflow.ui_safety_gate is explicitly false in .planning/config.json. If keys are absent, treat as enabled.

UI_PHASE_CFG=$(gsd_run query config-get workflow.ui_phase 2>/dev/null || echo "true")
UI_GATE_CFG=$(gsd_run query config-get workflow.ui_safety_gate 2>/dev/null || echo "true")

If both are false: Skip to step 6.

Check if phase has frontend indicators:

PHASE_SECTION=$(gsd_run query roadmap.get-phase "${PHASE}" 2>/dev/null)
# Shell-free word-boundary gate (#3718): Node.js helper — no locale env-var dependency.
# Reads via stdin to avoid OS ARG_MAX limits on large phase text.
# Resolve the helper against the GSD install dir via RUNTIME_DIR (#448) — NOT the consuming
# project's git root — falling back to git toplevel / $HOME/.claude. Exit codes mirror grep (0=UI,1=none).
_GSD_RT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
UI_GATE_JS=$(for _c in "$_GSD_RT/gsd-core/bin/lib/ui-safety-gate.cjs" "$_GSD_RT/bin/lib/ui-safety-gate.cjs" "$_GSD_RT/.claude/bin/lib/ui-safety-gate.cjs" "$HOME/.claude/gsd-core/bin/lib/ui-safety-gate.cjs" "$HOME/.claude/bin/lib/ui-safety-gate.cjs"; do [ -f "$_c" ] && { echo "$_c"; break; }; done)
if [ -n "$UI_GATE_JS" ]; then printf '%s' "$PHASE_SECTION" | node "$UI_GATE_JS" >/dev/null 2>&1; HAS_UI=$?; else echo "WARN: ui-safety-gate.cjs not found via RUNTIME_DIR/\$HOME (#448) — assuming UI present" >&2; HAS_UI=0; fi

If HAS_UI is 0 (frontend indicators found):

Check for existing UI-SPEC:

UI_SPEC_FILE=$(ls "${PHASE_DIR}"/*-UI-SPEC.md 2>/dev/null | head -1)

If UI-SPEC.md found: Set UI_SPEC_PATH=$UI_SPEC_FILE. Display: Using UI design contract: ${UI_SPEC_PATH}

If UI-SPEC.md missing AND --skip-ui flag is present in $ARGUMENTS: Skip silently to step 6.

If UI-SPEC.md missing AND UI_GATE_CFG is true:

Read ephemeral chain flag (same field as check.auto-mode → auto_chain_active):

AUTO_CHAIN=$(gsd_run query check auto-mode --pick auto_chain_active 2>/dev/null || echo "false")

If AUTO_CHAIN is true (running inside a --chain or --auto pipeline):

Auto-generate UI-SPEC without prompting:

Skill(skill="gsd-ui-phase", args="${PHASE} --auto ${GSD_WS}")

After gsd-ui-phase returns, re-read:

UI_SPEC_FILE=$(ls "${PHASE_DIR}"/*-UI-SPEC.md 2>/dev/null | head -1)
UI_SPEC_PATH="${UI_SPEC_FILE}"

Continue to step 6.

If AUTO_CHAIN is false (manual invocation):

Output this markdown directly (not as a code block):

## ⚠ UI-SPEC.md missing for Phase {N}
▶ Recommended next step:
`/gsd:ui-phase {N} ${GSD_WS}` — generate UI design contract before planning
───────────────────────────────────────────────
Also available:
- `/gsd:plan-phase {N} --skip-ui ${GSD_WS}` — plan without UI-SPEC (not recommended for frontend phases)

Exit the plan-phase workflow. Do not continue.

If HAS_UI is 1 (no frontend indicators): Skip silently to step 5.7.

5.7. Schema Push Detection Gate

Detects schema-relevant files in the phase scope and injects a mandatory [BLOCKING] schema push task into the plan. Prevents false-positive verification where build/types pass because TypeScript types come from config, not the live database.

Check if any files in the phase scope match schema patterns:

PHASE_SECTION=$(gsd_run query roadmap.get-phase "${PHASE}" --pick section 2>/dev/null)

Scan PHASE_SECTION, CONTEXT.md (if loaded), and RESEARCH.md (if exists) for file paths matching these ORM patterns:

ORM File Patterns
Payload CMS src/collections/**/*.ts, src/globals/**/*.ts
Prisma prisma/schema.prisma, prisma/schema/*.prisma
Drizzle drizzle/schema.ts, src/db/schema.ts, drizzle/*.ts
Supabase supabase/migrations/*.sql
TypeORM src/entities/**/*.ts, src/migrations/**/*.ts

Also check if any existing PLAN.md files for this phase already reference these file patterns in files_modified.

If schema-relevant files detected:

Set SCHEMA_PUSH_REQUIRED=true and SCHEMA_ORM={detected_orm}.

Determine the push command for the detected ORM:

ORM Push Command Non-TTY Workaround
Payload CMS npx payload migrate CI=true PAYLOAD_MIGRATING=true npx payload migrate
Prisma npx prisma db push npx prisma db push --accept-data-loss (if destructive)
Drizzle npx drizzle-kit push npx drizzle-kit push
Supabase supabase db push Set SUPABASE_ACCESS_TOKEN env var
TypeORM npx typeorm migration:run npx typeorm migration:run -d src/data-source.ts

Inject the following into the planner prompt (step 8) as an additional constraint:

<schema_push_requirement>
**[BLOCKING] Schema Push Required**

This phase modifies schema-relevant files ({detected_files}). The planner MUST include
a `[BLOCKING]` task that runs the database schema push command AFTER all schema file
modifications are complete but BEFORE verification.

- ORM detected: {SCHEMA_ORM}
- Push command: {push_command}
- Non-TTY workaround: {env_hint}
- If push requires interactive prompts that cannot be suppressed, flag the task for
  manual intervention with `autonomous: false`

This task is mandatory — the phase CANNOT pass verification without it. Build and
type checks will pass without the push (types come from config, not the live database),
creating a false-positive verification state.
</schema_push_requirement>

Display: Schema files detected ({SCHEMA_ORM}) — [BLOCKING] push task will be injected into plans

If no schema-relevant files detected: Skip silently to step 6.

6. Check Existing Plans

ls "${PHASE_DIR}"/*-PLAN.md 2>/dev/null || true

If exists AND --reviews flag: Skip prompt — go straight to replanning (the purpose of --reviews is to replan with review feedback).

If exists AND no --reviews flag: Offer: 1) Add more plans, 2) View existing, 3) Replan from scratch.

7. Use Context Paths from INIT

Extract from INIT JSON:

_gsd_field() { node -e "const o=JSON.parse(process.argv[1]); const v=o[process.argv[2]]; process.stdout.write(v==null?'':String(v))" "$1" "$2"; }
STATE_PATH=$(_gsd_field "$INIT" state_path)
ROADMAP_PATH=$(_gsd_field "$INIT" roadmap_path)
REQUIREMENTS_PATH=$(_gsd_field "$INIT" requirements_path)
RESEARCH_PATH=$(_gsd_field "$INIT" research_path)
VERIFICATION_PATH=$(_gsd_field "$INIT" verification_path)
UAT_PATH=$(_gsd_field "$INIT" uat_path)
CONTEXT_PATH=$(_gsd_field "$INIT" context_path)
REVIEWS_PATH=$(_gsd_field "$INIT" reviews_path)
PATTERNS_PATH=$(_gsd_field "$INIT" patterns_path)

# Detect spike/sketch findings skills (project-local)
SPIKE_FINDINGS_PATH=$(ls ./.claude/skills/spike-findings-*/SKILL.md 2>/dev/null | head -1 || true)
SKETCH_FINDINGS_PATH=$(ls ./.claude/skills/sketch-findings-*/SKILL.md 2>/dev/null | head -1 || true)

7.5. Verify Nyquist Artifacts

Skip if nyquist_validation_enabled is false OR research_enabled is false.

Also skip if all of the following are true:

  • research_enabled is false
  • has_research is false
  • no --research flag was provided

In that no-research path, Nyquist artifacts are not required for this run.

VALIDATION_EXISTS=$(ls "${PHASE_DIR}"/*-VALIDATION.md 2>/dev/null | head -1)

If missing and Nyquist is still enabled/applicable — ask user:

  1. Re-run: /gsd:plan-phase {PHASE} --research ${GSD_WS}
  2. Disable Nyquist with the exact command: gsd-tools.cjs query config-set workflow.nyquist_validation false
  3. Continue anyway (plans fail Dimension 8)

Proceed to Step 7.8 (or Step 8 if pattern mapper is disabled) only if user selects 2 or 3.

7.8. Spawn gsd-pattern-mapper Agent (Optional)

Skip if workflow.pattern_mapper is explicitly set to false in config.json (absent key = enabled). Also skip if no CONTEXT.md and no RESEARCH.md exist for this phase (nothing to extract file lists from).

Check config:

PATTERN_MAPPER_CFG=$(gsd_run query config-get workflow.pattern_mapper 2>/dev/null || echo "true")

If PATTERN_MAPPER_CFG is false: Skip to step 8.

If PATTERNS.md already exists (PATTERNS_PATH is non-empty from step 7): Skip to step 8 (use existing).

Display banner:

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 GSD ► PATTERN MAPPING PHASE {X}
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

◆ Spawning pattern mapper... (runs in a subagent — no output until it returns, ~1–5 min; expected, not a freeze)

Pattern mapper prompt:

<pattern_mapping_context>
**Phase:** {phase_number} - {phase_name}
**Phase directory:** {phase_dir}
**Padded phase:** {padded_phase}

<files_to_read>
- {context_path} (USER DECISIONS from /gsd:discuss-phase)
- {research_path} (Technical Research)
</files_to_read>

**Output file:** {phase_dir}/{padded_phase}-PATTERNS.md

Extract the list of files to be created/modified from CONTEXT.md and RESEARCH.md. For each file, classify by role and data flow, find the closest existing analog in the codebase, extract concrete code excerpts, and produce PATTERNS.md.
</pattern_mapping_context>

Spawn with:

Agent(
  prompt="{above}",
  subagent_type="gsd-pattern-mapper",
  model="{researcher_model}",
)

ORCHESTRATOR RULE — CODEX RUNTIME: After calling Agent() above, stop working on this task immediately. Do not read more files, edit code, or run tests related to this task while the subagent is active. Wait for the subagent to return its result. This prevents duplicate work, conflicting edits, and wasted context. Only resume when the subagent result is available.

Handle return:

  • ## PATTERN MAPPING COMPLETE: Update PATTERNS_PATH to the created file path, continue to step 8.
  • Any error or empty return: Log warning, continue to step 8 without patterns (non-blocking).

After pattern mapper completes, update the path variable:

PATTERNS_PATH="${PHASE_DIR}/${PADDED_PHASE}-PATTERNS.md"

7.9. Regenerate API-SURFACE.md (intel gate)

INTEL_CFG=$(gsd_run query config-get intel.enabled 2>/dev/null || echo "false")
# false (absent = false) → API_SURFACE_PATH stays empty; step-8 planner entry omitted
if [ "$INTEL_CFG" = "true" ]; then
  gsd_run intel api-surface
  API_SURFACE_PATH=".planning/intel/API-SURFACE.md"
  echo "✓ API surface regenerated: ${API_SURFACE_PATH}"  # injected into step 8 as HINT
fi

8. Spawn gsd-planner Agent

Display banner:

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 GSD ► PLANNING PHASE {X}
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

◆ Spawning planner... (runs in a subagent — no output until it returns, ~1–5 min; expected, not a freeze)

Planner prompt:

<planning_context>
**Phase:** {phase_number}
**Mode:** {standard | gap_closure | reviews}

<files_to_read>
- {state_path} (Project State)
- {roadmap_path} (Roadmap)
- {requirements_path} (Requirements)
- {context_path} (USER DECISIONS from /gsd:discuss-phase)
- {research_path} (Technical Research)
- {PATTERNS_PATH} (Pattern Map — analog files and code excerpts, if exists)
- {verification_path} (Verification Gaps - if --gaps)
- {uat_path} (UAT Gaps - if --gaps)
- {reviews_path} (Cross-AI Review Feedback - if --reviews)
- {UI_SPEC_PATH} (UI Design Contract — visual/interaction specs, if exists)
- {SPIKE_FINDINGS_PATH} (Spike Findings — validated patterns, constraints, landmines from experiments, if exists)
- {SKETCH_FINDINGS_PATH} (Sketch Findings — validated design decisions, CSS patterns, visual direction, if exists)
- {API_SURFACE_PATH} (API Surface — HINT ONLY, if intel.enabled; see <intel_surface_hint> below)
${CONTEXT_WINDOW >= 500000 ? `
**Cross-phase context (1M model enrichment):**
- CONTEXT.md files from the 3 most recent completed phases (locked decisions — maintain consistency)
- SUMMARY.md files from the 3 most recent completed phases (what was built — reuse patterns, avoid duplication)
- LEARNINGS.md files from the 3 most recent completed phases (structured decisions, patterns, lessons, surprises — skip silently if a phase has no LEARNINGS.md; prefix each block with \`[from Phase N LEARNINGS]\` for source attribution; if total size exceeds 15% of context budget, drop oldest first)
- CONTEXT.md, SUMMARY.md, and LEARNINGS.md from any phases listed in the current phase's "Depends on:" field in ROADMAP.md (regardless of recency — explicit dependencies always load, deduplicated against the 3 most recent)
- Skip all other prior phases to stay within context budget
` : ''}
</files_to_read>
${API_SURFACE_PATH ? `
<intel_surface_hint>
**API Surface (HINT — may be incomplete):** When \`intel.enabled\` is true, \`.planning/intel/API-SURFACE.md\` lists symbols extracted from the codebase by regex/JS analysis. Prefer symbols listed there when referencing existing code. This surface is regex/JS-derived and MAY BE INCOMPLETE — a symbol's absence means *unknown*, not *nonexistent*. Never treat the surface as exhaustive. If you reference a symbol that is not in the surface and this phase creates it, list it under "Artifacts this phase produces".
</intel_surface_hint>
` : ''}
${AGENT_SKILLS_PLANNER}

**Phase requirement IDs (every ID MUST appear in a plan's `requirements` field):** {phase_req_ids}

**Project instructions:** Read ./CLAUDE.md if exists — follow project-specific guidelines
**Project skills:** Check .claude/skills/ or .agents/skills/ directory (if either exists) — read SKILL.md files, plans should account for project skill rules

${TDD_MODE === 'true' ? `
<tdd_mode_active>
**TDD Mode is ENABLED.** Apply TDD heuristics from @~/.claude/gsd-core/references/tdd.md to all eligible tasks:
- Business logic with defined I/O → type: tdd
- API endpoints with request/response contracts → type: tdd
- Data transformations, validation, algorithms → type: tdd
- UI, config, glue code, CRUD → standard plan (type: execute)
Each TDD plan gets one feature with RED/GREEN/REFACTOR gate sequence.
</tdd_mode_active>
` : ''}

**MVP_MODE:** ${MVP_MODE} (when true, follow vertical-slice rules from `~/.claude/gsd-core/references/planner-mvp-mode.md`; when false, ignore MVP guidance entirely.)
**WALKING_SKELETON:** ${WALKING_SKELETON} (when true, the first deliverable must be a Walking Skeleton — Read the template at `~/.claude/gsd-core/references/skeleton-template.md` and produce SKELETON.md alongside PLAN.md.)
**Granularity:** {granularity}

${MVP_MODE === 'true' ? `
<mvp_mode_active>
**MVP Mode is ENABLED.** Read `~/.claude/gsd-core/references/planner-mvp-mode.md` now and follow its vertical-slice planning rules. Each plan must deliver a complete vertical slice — thin end-to-end functionality rather than horizontal layers.
</mvp_mode_active>
` : ''}
</planning_context>

<downstream_consumer>
Output consumed by /gsd:execute-phase. Plans need:
- Frontmatter (wave, depends_on, files_modified, autonomous)
- Tasks in XML format with read_first and acceptance_criteria fields (MANDATORY on every task)
- Verification criteria
- must_haves for goal-backward verification
- **"Artifacts this phase produces" section (MANDATORY)** — list every symbol this phase creates: decorators, classes, functions, CLI flags, struct/dataclass fields, new file paths. The plan-review-convergence source-grounding pass reads this section to exclude newly-created symbols from drift verification; omitting it causes new symbols to be flagged for acknowledgement.
</downstream_consumer>

<deep_work_rules>
## Anti-Shallow Execution Rules (MANDATORY)

Every task MUST include these fields — they are NOT optional:

1. **`<read_first>`** — Files the executor MUST read before touching anything. Always include:
   - The file being modified (so executor sees current state, not assumptions)
   - Any "source of truth" file referenced in CONTEXT.md (reference implementations, existing patterns, config files, schemas)
   - Any file whose patterns, signatures, types, or conventions must be replicated or respected

2. **`<acceptance_criteria>`** — Verifiable conditions that prove the task was done correctly. Rules:
   - Every criterion must be checkable as a source assertion, behavior assertion, test command, or CLI output
   - NEVER use subjective language ("looks correct", "properly configured", "consistent with")
   - Include exact strings, patterns, values, command outputs, or observable behavior where that is the right proof
   - Examples:
     - Code: `auth.py contains def verify_token(` / `test_auth.py exits 0`
     - Behavior: `POST /api/auth/login returns 200 + httpOnly JWT cookie for valid credentials`
     - Config: `.env.example contains DATABASE_URL=` / `Dockerfile contains HEALTHCHECK`
     - Docs: `README.md contains '## Installation'` / `API.md lists all endpoints`
     - Infra: `deploy.yml has rollback step` / `docker-compose.yml has healthcheck for db`

3. **`<action>`** — Must include CONCRETE values, not references. Rules:
   - NEVER say "align X with Y", "match X to Y", "update to be consistent" without specifying the exact target state
   - Include concrete identifiers and reference values: config keys, function signatures, SQL table names, class names, import paths, env vars, endpoint paths, etc.
   - If CONTEXT.md has a comparison table or expected values, copy only the target identifiers/values needed to remove ambiguity
   - Do not include full file contents, fenced code blocks, or complete implementations in `<action>`
   - The executor should understand the intended target state from `<action>` and use `<read_first>` files for current implementation details, patterns, and source-of-truth context

**Why this matters:** Executor agents work from the plan text. Vague instructions like "update the config to match production" produce shallow one-line changes. Concrete instructions like "add DATABASE_URL, set POOL_SIZE=20, add REDIS_URL, and read config/runtime.ts before editing" produce complete work without turning the planner into the executor.
</deep_work_rules>

<quality_gate>
- [ ] PLAN.md files created in phase directory
- [ ] Each plan has valid frontmatter
- [ ] Tasks are specific and actionable
- [ ] Every task has `<read_first>` with at least the file being modified
- [ ] Every task has `<acceptance_criteria>` with behavior, test-command, CLI, or source assertions
- [ ] Every `<action>` contains concrete identifiers without fenced code blocks or full implementations
- [ ] Dependencies correctly identified
- [ ] Waves assigned for parallel execution
- [ ] must_haves derived from phase goal
- [ ] Every PLAN.md includes an "Artifacts this phase produces" section listing symbols created by this phase (decorators, classes, functions, CLI flags, struct/dataclass fields, new file paths)
</quality_gate>

If CHUNKED_MODE is false (default): Spawn the planner as a single long-lived Agent:

Agent(
  prompt=filled_prompt,
  subagent_type="gsd-planner",
  model="{planner_model}",
  description="Plan Phase {phase}"
)

ORCHESTRATOR RULE — CODEX RUNTIME: After calling Agent() above, stop working on this task immediately. Do not read more files, edit code, or run tests related to this task while the subagent is active. Wait for the subagent to return its result. This prevents duplicate work, conflicting edits, and wasted context. Only resume when the subagent result is available.

If CHUNKED_MODE is true: Skip the Agent() call above — proceed to step 8.5 instead.

8.5. Chunked Planning Mode

Skip if CHUNKED_MODE is false.

Chunked mode splits the single long-lived planner Agent run into a short outline Agent run followed by N short per-plan Agent runs. Each run is bounded to ~3–5 min; each plan is committed individually for crash resilience. If any run hangs and the terminal is force-killed, rerunning /gsd:plan-phase {N} --chunked resumes from the last successfully committed plan.

Intended for new or in-progress chunked runs. To recover plans already written by a prior non-chunked run, use step 6's "Add more plans" or proceed directly to /gsd:execute-phase — don't start a fresh chunked run over existing non-chunked plans.

8.5.1 Outline Phase (outline-only mode, ~2 min)

Resume detection: If ${PHASE_DIR}/${PADDED_PHASE}-PLAN-OUTLINE.md already exists and is valid (contains the ## OUTLINE COMPLETE marker), skip this sub-step — the outline already exists from a previous run. Proceed directly to 8.5.2.

OUTLINE_FILE="${PHASE_DIR}/${PADDED_PHASE}-PLAN-OUTLINE.md"
if [[ -f "$OUTLINE_FILE" ]] && grep -q "^## OUTLINE COMPLETE" "$OUTLINE_FILE"; then
  # reuse existing outline — skip to 8.5.2
fi

Display:

◆ Chunked mode: spawning outline planner... (runs in a subagent — no output until it returns, ~1–5 min; expected, not a freeze)

Spawn the planner in outline-only mode — it must write only the outline manifest, not any PLAN.md files:

Agent(
  prompt="{same planning_context as step 8, plus:}

  **Chunked mode: outline-only.**
  Do NOT write any PLAN.md files in this Task.
  Write only: {PHASE_DIR}/{PADDED_PHASE}-PLAN-OUTLINE.md

  The outline must be a markdown table with columns:
  Plan ID | Objective | Wave | Depends On | Requirements

  Return: ## OUTLINE COMPLETE with plan count.",
  subagent_type="gsd-planner",
  model="{planner_model}",
  description="Outline Phase {phase} (chunked)"
)

ORCHESTRATOR RULE — CODEX RUNTIME: After calling Agent() above, stop working on this task immediately. Do not read more files, edit code, or run tests related to this task while the subagent is active. Wait for the subagent to return its result. This prevents duplicate work, conflicting edits, and wasted context. Only resume when the subagent result is available.

Handle return:

  • ## OUTLINE COMPLETE: Read PLAN-OUTLINE.md, extract plan list. Continue to 8.5.2.
  • Any other return or empty: Display error. Offer: 1) Retry outline, 2) Stop.

8.5.2 Per-Plan Tasks (single-plan mode, ~3-5 min each)

For each plan entry extracted from PLAN-OUTLINE.md:

  1. Resume check: If ${PHASE_DIR}/{plan_id}-PLAN.md already exists on disk and has valid YAML frontmatter (opening --- delimiter present), skip this plan (do not overwrite completed work — resume safety).

    PLAN_FILE="${PHASE_DIR}/${plan_id}-PLAN.md"
    if [[ -f "$PLAN_FILE" ]] && head -1 "$PLAN_FILE" | grep -q '^---'; then
      continue  # plan already written, skip
    fi
    
  2. Display:

    ◆ Chunked mode: planning {plan_id} ({k}/{N})... (runs in a subagent — no output until it returns, ~1–5 min; expected, not a freeze)
    
  3. Spawn the planner in single-plan mode — it must write exactly one PLAN.md file:

    Agent(
      prompt="{same planning_context as step 8, plus:}
    
      **Chunked mode: single-plan.**
      Write exactly ONE plan file: {PHASE_DIR}/{plan_id}-PLAN.md
      Plan to write: {plan_id} — {objective}
      Wave: {wave} | Depends on: {depends_on}
      Phase requirement IDs to cover in this plan: {plan_requirements}
    
      Return: ## PLAN COMPLETE with the plan ID.",
      subagent_type="gsd-planner",
      model="{planner_model}",
      description="Plan {plan_id} (chunked {k}/{N})"
    )
    

    ORCHESTRATOR RULE — CODEX RUNTIME: After calling Agent() above, stop working on this task immediately. Do not read more files, edit code, or run tests related to this task while the subagent is active. Wait for the subagent to return its result. This prevents duplicate work, conflicting edits, and wasted context. Only resume when the subagent result is available.

  4. Verify disk: Check ${PHASE_DIR}/{plan_id}-PLAN.md exists. If missing: offer 1) Retry, 2) Stop.

  5. Commit per-plan:

    gsd_run query commit "docs(${PADDED_PHASE}): plan ${plan_id} (chunked)" --files "${PHASE_DIR}/${plan_id}-PLAN.md"
    

After all N plans are written and committed, treat this as ## PLANNING COMPLETE and continue to step 9.

9. Handle Planner Return

  • ## PLANNING COMPLETE: Display plan count. If --skip-verify or plan_checker_enabled is false (from init): skip to step 13. Otherwise: step 10.
  • ## PHASE SPLIT RECOMMENDED: The planner determined the phase exceeds the context budget for full-fidelity implementation of all source items. Handle in step 9b.
  • ## ⚠ Source Audit: Unplanned Items Found: The planner's multi-source coverage audit found items from REQUIREMENTS.md, RESEARCH.md, ROADMAP goal, or CONTEXT.md decisions that are not covered by any plan. Handle in step 9c.
  • ## CHECKPOINT REACHED: Present to user, get response, spawn continuation (step 12)
  • ## PLANNING INCONCLUSIVE: Show attempts, offer: Add context / Retry / Manual
  • Empty / truncated / no recognized marker: → Filesystem fallback (step 9a).

9a. Filesystem Fallback (Planner)

Triggered when: Agent() returns but the return contains no recognized marker (## PLANNING COMPLETE, ## PHASE SPLIT RECOMMENDED, ## ⚠ Source Audit, ## CHECKPOINT REACHED, ## PLANNING INCONCLUSIVE).

DISK_PLANS=$(ls "${PHASE_DIR}"/*-PLAN.md 2>/dev/null | wc -l | tr -d ' ')

If DISK_PLANS > 0: The planner wrote plans to disk but the Agent() return was empty or truncated (the Windows stdio hang pattern — the subagent finished but the return never arrived). Display:

◆ Planner wrote {DISK_PLANS} plan(s) to disk but did not emit a PLANNING COMPLETE marker.
  This is a known Windows stdio hang pattern — work is likely recoverable.

  Plans found on disk:
  {ls output of *-PLAN.md}

Offer 3 options:

  1. Accept plans — treat as ## PLANNING COMPLETE and continue through step 9 ## PLANNING COMPLETE handling (so --skip-verify / plan_checker_enabled=false are honored — may skip to step 13 rather than step 10)
  2. Retry planner — re-spawn the planner with the same prompt (return to step 8)
  3. Stop — exit; user can re-run /gsd:plan-phase {N} to resume

If DISK_PLANS is 0 and no marker: The planner produced no output. Treat as ## PLANNING INCONCLUSIVE and handle accordingly.

9b. Handle Phase Split Recommendation

When the planner returns ## PHASE SPLIT RECOMMENDED, it means the phase's source items exceed the context budget for full-fidelity implementation. The planner proposes groupings.

Extract from planner return:

  • Proposed sub-phases (e.g., "17a: processing core (D-01 to D-19)", "17b: billing + config UX (D-20 to D-27)")
  • Which source items (REQ-IDs, D-XX decisions, RESEARCH items) go in each sub-phase
  • Why the split is necessary (context cost estimate, file count)

Present to user:

## Phase {X} exceeds context budget for full-fidelity implementation

The planner found {N} source items that exceed the context budget when
planned at full fidelity. Instead of reducing scope, we recommend splitting:

**Option 1: Split into sub-phases**
- Phase {X}a: {name} — {items} ({N} source items, ~{P}% context)
- Phase {X}b: {name} — {items} ({M} source items, ~{Q}% context)

**Option 2: Proceed anyway** (planner will attempt all, quality may degrade past 50% context)

**Option 3: Prioritize** — you choose which items to implement now,
rest become a follow-up phase

Use AskUserQuestion with these 3 options.

If "Split": Use /gsd:phase --insert to create the sub-phases, then replan each. If "Proceed": Return to planner with instruction to attempt all items at full fidelity, accepting more plans/tasks. If "Prioritize": Use AskUserQuestion (multiSelect) to let user pick which items are "now" vs "later". Create CONTEXT.md for each sub-phase with the selected items.

9c. Handle Source Audit Gaps

When the planner returns ## ⚠ Source Audit: Unplanned Items Found, it means items from REQUIREMENTS.md, RESEARCH.md, ROADMAP goal, or CONTEXT.md decisions have no corresponding plan.

Extract from planner return:

  • Each unplanned item with its source artifact and section
  • The planner's suggested options (A: add plan, B: split phase, C: defer with confirmation)

Present each gap to user. For each unplanned item:

## ⚠ Unplanned: {item description}

Source: {RESEARCH.md / REQUIREMENTS.md / ROADMAP goal / CONTEXT.md}
Details: {why the planner flagged this}

Options:
1. Add a plan to cover this item (recommended)
2. Split phase — move to a sub-phase with related items
3. Defer — add to backlog (developer confirms this is intentional)

Use AskUserQuestion for each gap (or batch if multiple gaps).

If "Add plan": Return to planner (step 8) with instruction to add plans covering the missing items, preserving existing plans. If "Split": Use /gsd:phase --insert for overflow items, then replan. If "Defer": Record in CONTEXT.md ## Deferred Ideas with developer's confirmation. Proceed to step 10.

10. Spawn gsd-plan-checker Agent

Display banner:

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 GSD ► VERIFYING PLANS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

◆ Spawning plan checker... (runs in a subagent — no output until it returns, ~1–5 min; expected, not a freeze)

Checker prompt:

<verification_context>
**Phase:** {phase_number}
**Phase Goal:** {goal from ROADMAP}

<files_to_read>
- {PHASE_DIR}/*-PLAN.md (Plans to verify)
- {roadmap_path} (Roadmap)
- {requirements_path} (Requirements)
- {context_path} (USER DECISIONS from /gsd:discuss-phase)
- {research_path} (Technical Research — includes Validation Architecture)
</files_to_read>

${AGENT_SKILLS_CHECKER}

**Phase requirement IDs (MUST ALL be covered):** {phase_req_ids}

**Project instructions:** Read ./CLAUDE.md if exists — verify plans honor project guidelines
**Project skills:** Check .claude/skills/ or .agents/skills/ directory (if either exists) — verify plans account for project skill rules
</verification_context>

<expected_output>
- ## VERIFICATION PASSED — all checks pass
- ## ISSUES FOUND — structured issue list
</expected_output>
Agent(
  prompt=checker_prompt,
  subagent_type="gsd-plan-checker",
  model="{checker_model}",
  description="Verify Phase {phase} plans"
)

ORCHESTRATOR RULE — CODEX RUNTIME: After calling Agent() above, stop working on this task immediately. Do not read more files, edit code, or run tests related to this task while the subagent is active. Wait for the subagent to return its result. This prevents duplicate work, conflicting edits, and wasted context. Only resume when the subagent result is available.

11. Handle Checker Return

  • ## VERIFICATION PASSED: Display confirmation, proceed to step 13.
  • ## ISSUES FOUND: Display issues, check iteration count, proceed to step 12.
  • Empty / truncated / no recognized marker: → Filesystem fallback (step 11a).

Thinking partner for architectural tradeoffs (conditional): If features.thinking_partner is enabled, scan the checker's issues for architectural tradeoff keywords ("architecture", "approach", "strategy", "pattern", "vs", "alternative"). If found:

The plan-checker flagged an architectural decision point:
{issue description}

Brief analysis:
- Option A: {approach_from_plan} — {pros/cons}
- Option B: {alternative_approach} — {pros/cons}
- Recommendation: {choice} aligned with {phase_goal}

Apply this to the revision? [Yes] / [No, I'll decide]

If yes: include the recommendation in the revision prompt. If no: proceed to revision loop as normal. If thinking_partner disabled: skip this block entirely.

11a. Filesystem Fallback (Checker)

Triggered when: Checker Agent() returns but the return contains neither ## VERIFICATION PASSED nor ## ISSUES FOUND.

DISK_PLANS=$(ls "${PHASE_DIR}"/*-PLAN.md 2>/dev/null | wc -l | tr -d ' ')

If DISK_PLANS > 0: Plans exist on disk; the checker return was empty or truncated (the Windows stdio hang pattern — the subagent finished but the return never arrived). Display:

◆ Checker return was empty or truncated. {DISK_PLANS} plan(s) exist on disk.
  This is a known Windows stdio hang pattern — checker may have completed without returning.

Offer 3 options:

  1. Accept verification — treat as ## VERIFICATION PASSED and continue to step 13
  2. Retry checker — re-spawn the checker with the same prompt (return to step 10)
  3. Stop — exit; user can re-run /gsd:plan-phase {N} to resume

If DISK_PLANS is 0: No plans on disk — something is seriously wrong. Display error and stop.

12. Revision Loop (Max 3 Iterations)

Track iteration_count (starts at 1 after initial plan + check). Track prev_issue_count (initialized to Infinity before the loop begins). Track stall_reentry_count (starts at 0; incremented each time "Adjust approach" re-enters step 8).

If iteration_count < 3:

Parse issue count from checker return: count BLOCKER + WARNING entries in the YAML issues block (structured output from gsd-plan-checker). If the checker's return contains no YAML issues block (i.e., the plan was approved with no issues), treat issue_count as 0 and skip the stall check — the plan passed. Proceed to step 13.

Display: Revision iteration {N}/3 -- {blocker_count} blockers, {warning_count} warnings

Stall detection: If issue_count >= prev_issue_count: Display: Revision loop stalled — issue count not decreasing ({issue_count} issues remain after {N} iterations)

If stall_reentry_count < 2: Ask user: Question: "Issues remain after {N} revision attempts with no progress. Proceed with current output?" Options: "Proceed anyway" | "Adjust approach" If "Proceed anyway": accept current plans and continue to step 13. If "Adjust approach": increment stall_reentry_count, open freeform discussion, then re-enter step 8 (full replanning). Note: re-entry resets iteration_count and prev_issue_count but stall_reentry_count persists across re-entries and is capped at 2.

If stall_reentry_count >= 2: Display: Stall persists after 2 re-planning attempts. The following issues could not be resolved automatically: List the remaining issues from the checker. Suggest: "Consider resolving these issues manually or running /gsd:debug to investigate root causes." Options: "Proceed anyway" | "Abandon" If "Proceed anyway": accept current plans and continue to step 13. If "Abandon": stop workflow.

Set prev_issue_count = issue_count.

Revision prompt:

<revision_context>
**Phase:** {phase_number}
**Mode:** revision

<files_to_read>
- {PHASE_DIR}/*-PLAN.md (Existing plans)
- {context_path} (USER DECISIONS from /gsd:discuss-phase)
</files_to_read>

${AGENT_SKILLS_PLANNER}

**Checker issues:** {structured_issues_from_checker}
</revision_context>

<instructions>
Make targeted updates to address checker issues.
Do NOT replan from scratch unless issues are fundamental.
Return what changed.
</instructions>
Agent(
  prompt=revision_prompt,
  subagent_type="gsd-planner",
  model="{planner_model}",
  description="Revise Phase {phase} plans"
)

ORCHESTRATOR RULE — CODEX RUNTIME: After calling Agent() above, stop working on this task immediately. Do not read more files, edit code, or run tests related to this task while the subagent is active. Wait for the subagent to return its result. This prevents duplicate work, conflicting edits, and wasted context. Only resume when the subagent result is available.

After planner returns -> spawn checker again (step 10), increment iteration_count.

If iteration_count >= 3:

Display: Max iterations reached. {N} issues remain: + issue list

Offer: 1) Force proceed, 2) Provide guidance and retry, 3) Abandon

12.5. Plan Bounce (Optional External Refinement)

Skip if: --skip-bounce flag, --gaps flag, or bounce is not activated.

Activation: Bounce runs when --bounce flag is present OR workflow.plan_bounce config is true. The --skip-bounce flag always wins (disables bounce even if config enables it). The --gaps flag also disables bounce (gap-closure mode should not modify plans externally).

Prerequisites: workflow.plan_bounce_script must be set to a valid script path. If bounce is activated but no script is configured, display warning and skip:

⚠ Plan bounce activated but no script configured.
Set workflow.plan_bounce_script to the path of your refinement script.
Skipping bounce step.

Read pass count:

BOUNCE_PASSES=$(gsd_run query config-get workflow.plan_bounce_passes 2>/dev/null || echo "2")
BOUNCE_SCRIPT=$(gsd_run query config-get workflow.plan_bounce_script 2>/dev/null | jq -r '.' 2>/dev/null || true)

Display banner:

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 GSD ► BOUNCING PLANS (External Refinement)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Script: ${BOUNCE_SCRIPT}
Max passes: ${BOUNCE_PASSES}

For each PLAN.md file in the phase directory:

  1. Backup: Copy *-PLAN.md to *-PLAN.pre-bounce.md
cp "${PLAN_FILE}" "${PLAN_FILE%.md}.pre-bounce.md"
  1. Invoke bounce script:
"${BOUNCE_SCRIPT}" "${PLAN_FILE}" "${BOUNCE_PASSES}"
  1. Validate bounced plan — YAML frontmatter integrity: After the script returns, check that the bounced file still has valid YAML frontmatter (opening and closing --- delimiters with parseable content between them). If the bounced plan breaks YAML frontmatter validation, restore the original from the pre-bounce.md backup and continue to the next plan:
⚠ Bounced plan ${PLAN_FILE} has broken YAML frontmatter — restoring original from pre-bounce backup.
  1. Handle script failure: If the bounce script exits non-zero, restore the original plan from the pre-bounce.md backup and continue to the next plan:
⚠ Bounce script failed for ${PLAN_FILE} (exit code ${EXIT_CODE}) — restoring original from pre-bounce backup.

After all plans are bounced:

  1. Re-run plan checker on bounced plans: Spawn gsd-plan-checker (same as step 10) on all modified plans. If a bounced plan fails the checker, restore original from its pre-bounce.md backup:
⚠ Bounced plan ${PLAN_FILE} failed checker validation — restoring original from pre-bounce backup.
  1. Commit surviving bounced plans: If at least one plan survived both the frontmatter validation and the checker re-run, commit the changes:
gsd_run query commit "refactor(${padded_phase}): bounce plans through external refinement" --files "${PHASE_DIR}/*-PLAN.md"

Display summary:

Plan bounce complete: {survived}/{total} plans refined

Clean up: Remove all *-PLAN.pre-bounce.md backup files after the bounce step completes (whether plans survived or were restored).

13. Requirements Coverage Gate

After plans pass the checker (or checker is skipped), verify that all phase requirements are covered by at least one plan.

Skip if: phase_req_ids is null or TBD (no requirements mapped to this phase).

Step 1: Extract requirement IDs claimed by plans

# Collect all requirement IDs from plan frontmatter
PLAN_REQS=$(grep -h "requirements_addressed\|requirements:" ${PHASE_DIR}/*-PLAN.md 2>/dev/null | tr -d '[]' | tr ',' '\n' | sed 's/^[[:space:]]*//' | sort -u)

Step 2: Compare against phase requirements from ROADMAP

For each REQ-ID in phase_req_ids:

  • If REQ-ID appears in PLAN_REQS → covered ✓
  • If REQ-ID does NOT appear in any plan → uncovered ✗

Step 3: Check CONTEXT.md features against plan objectives

Read CONTEXT.md <decisions> section. Extract feature/capability names. Check each against plan <objective> blocks. Features not mentioned in any plan objective → potentially dropped.

Step 4: Report

If all requirements covered and no dropped features:

✓ Requirements coverage: {N}/{N} REQ-IDs covered by plans

→ Proceed to step 14.

If gaps found:

## ⚠ Requirements Coverage Gap

{M} of {N} phase requirements are not assigned to any plan:

| REQ-ID | Description | Plans |
|--------|-------------|-------|
| {id} | {from REQUIREMENTS.md} | None |

{K} CONTEXT.md features not found in plan objectives:
- {feature_name} — described in CONTEXT.md but no plan covers it

Options:
1. Re-plan to include missing requirements (recommended)
2. Move uncovered requirements to next phase
3. Proceed anyway — accept coverage gaps

If TEXT_MODE is true, present as a plain-text numbered list (options already shown in the block above). Otherwise use AskUserQuestion to present the options.

13a. Decision Coverage Gate

After the requirements coverage gate passes, verify that every trackable decision captured by discuss-phase in CONTEXT.md <decisions> is referenced by at least one plan. This is the translation gate from issue #2492 — its job is to refuse to mark a phase planned when a discuss-phase decision silently dropped on the way into the plans.

Skip if workflow.context_coverage_gate is explicitly set to false (absent key = enabled). Also skip if no CONTEXT.md exists for this phase (nothing to translate) or if its <decisions> block is empty.

GATE_CFG=$(gsd_run query config-get workflow.context_coverage_gate 2>/dev/null || echo "true")
if [ "$GATE_CFG" != "false" ]; then
  GATE_RESULT=$(gsd_run query check.decision-coverage-plan "${PHASE_DIR}" "${CONTEXT_PATH}")
  # BLOCKING: refuse to mark phase planned when a trackable decision is uncovered.
  # `passed: true` covers both real-pass and skipped cases (gate disabled / no CONTEXT.md /
  # no trackable decisions). Verify-phase counterpart deliberately omits this exit-1 — that
  # gate is non-blocking by design (review finding F15).
  echo "$GATE_RESULT" | jq -e '(.passed // .data.passed) == true' >/dev/null || {
    echo "$GATE_RESULT" | jq -r '(.message // .data.message // "Decision coverage gate failed.")'
    exit 1
  }
fi

The handler returns JSON:

{
  "passed": true,
  "skipped": false,
  "total":  2,
  "covered": 2,
  "uncovered": [ { "id": "D-01", "text": "...", "category": "..." } ],
  "message": "..."
}

If passed is true (or skipped is true): Display ✓ Decision coverage: {M}/{N} CONTEXT.md decisions covered by plans (or (skipped — gate disabled) / (skipped — no decisions)) and proceed to step 13b.

If passed is false: Display the handler's message block. It already names each uncovered decision (D-NN | category | text) and tells the user what to do — cite the id in a relevant plan's must_haves / truths, or move the decision under ### Claude's Discretion / tag it [informational] if it should not be tracked. Then offer:

Options:
1. Re-plan to cover missing decisions (recommended)
2. Edit CONTEXT.md to mark dropped decisions as [informational] / Discretion
3. Proceed anyway — accept the coverage gap

If TEXT_MODE is true, present as a plain-text numbered list. Otherwise use AskUserQuestion. Selecting "Proceed anyway" continues to step 13b but records the override in STATE.md so verify-phase can re-surface it.

Why this gate blocks: failing here is cheap. The plans are the contract between discuss-phase and execute-phase; if a decision isn't visible in any plan, no executor will implement it. Catching that now beats discovering it after thousands of dollars of execution.

13b. Record Planning Completion in STATE.md

After plans pass all gates, record that planning is complete so STATE.md reflects the new phase status:

gsd_run query state.planned-phase --phase "${PHASE_NUMBER}" --name "${PHASE_NAME}" --plans "${PLAN_COUNT}"

This updates STATUS to "Ready to execute", sets the correct plan count, and timestamps Last Activity.

13c. Annotate ROADMAP with Wave Dependencies and Cross-cutting Constraints

After plans are finalized, annotate the ROADMAP.md plan list for this phase with:

  • Wave dependency notes — a bold header before each wave group ("Wave 2 (blocked on Wave 1 completion)")
  • Cross-cutting constraints — a "Cross-cutting constraints:" subsection listing must_haves.truths entries that appear in 2 or more plans

This step is derived entirely from existing PLAN frontmatter — no extra LLM pass is required.

gsd_run query roadmap.annotate-dependencies "${PHASE_NUMBER}"

This operation is idempotent: if wave headers or cross-cutting constraints already exist in the ROADMAP phase section, the command returns without modifying the file. Skip this step if plan_count is 0.

13d. Commit Plans if commit_docs is true

If commit_docs is true (from the init JSON parsed in step 1), commit the generated plan artifacts (including any ROADMAP.md annotations from step 13c):

gsd_run query commit "docs(${PADDED_PHASE}): create phase plan" --files "${PHASE_DIR}"/*-PLAN.md .planning/STATE.md .planning/ROADMAP.md

This commits all PLAN.md files for the phase plus the updated STATE.md and ROADMAP.md to version-control the planning artifacts. Skip this step if commit_docs is false.

13e. Post-Planning Gap Analysis

After all plans are generated, committed, and the Requirements Coverage Gate (§13) has run, emit a single unified gap report covering both REQUIREMENTS.md and the CONTEXT.md <decisions> section. This is a proactive, post-hoc report — it does not block phase advancement and does not re-plan. It exists so that any requirement or decision that slipped through the per-plan checks is surfaced in one place before execution begins.

Skip if: workflow.post_planning_gaps is false. Default is true.

POST_PLANNING_GAPS=$(gsd_run query config-get workflow.post_planning_gaps --default true 2>/dev/null || echo true)
if [ "$POST_PLANNING_GAPS" = "true" ]; then
  # Scope to this phase's mapped REQ-IDs (#447); null/TBD skips the requirements comparison (CONTEXT.md decisions still reported), mirroring §13.
  gsd_run gap-analysis --phase-dir "${PHASE_DIR}" --phase-req-ids "$(gsd_run query init.plan-phase "$PHASE" --pick phase_req_ids 2>/dev/null || echo TBD)"
fi

(gsd-tools.cjs gap-analysis reads .planning/REQUIREMENTS.md, ${PHASE_DIR}/CONTEXT.md, and ${PHASE_DIR}/*-PLAN.md, then prints a markdown table with one row per REQ-ID and D-ID. Word-boundary matching prevents REQ-1 from being mistaken for REQ-10.)

Output format (deterministic; sorted REQUIREMENTS.md → CONTEXT.md, then natural sort within source):

## Post-Planning Gap Analysis

| Source | Item | Status |
|--------|------|--------|
| REQUIREMENTS.md | REQ-01 | ✓ Covered |
| REQUIREMENTS.md | REQ-02 | ✗ Not covered |
| CONTEXT.md | D-01 | ✓ Covered |
| CONTEXT.md | D-02 | ✗ Not covered |

⚠ N items not covered by any plan

Skip-gracefully behavior:

  • REQUIREMENTS.md missing → CONTEXT-only report.
  • CONTEXT.md missing → REQUIREMENTS-only report.
  • Both missing or <decisions> block missing → "No requirements or decisions to check" line, no error.

This step is non-blocking. If items are reported as not covered, the user may re-run /gsd:plan-phase --gaps to add plans, or proceed to execute-phase as-is.

14. Present Final Status

Route to <offer_next> OR auto_advance depending on flags/config.

15. Auto-Advance Check

Check for auto-advance trigger using values already loaded in step 1:

  1. Parse --auto and --chain flags from $ARGUMENTS
  2. Use auto_chain_active and auto_advance from the INIT JSON parsed in step 1 — do not issue additional config-get calls for these values (they are already present in the init output). Issuing redundant config-get calls for values already in INIT can cause infinite read loops on some runtimes.
  3. Sync chain flag with intent — if user invoked manually (no --auto and no --chain), clear the ephemeral chain flag from any previous interrupted --auto chain. This does NOT touch workflow.auto_advance (the user's persistent settings preference):
    if [[ ! "$ARGUMENTS" =~ --auto ]] && [[ ! "$ARGUMENTS" =~ --chain ]]; then
      gsd_run query config-set workflow._auto_chain_active false || true
    fi
    

Set local variables from INIT (parsed once in step 1):

  • AUTO_CHAIN = auto_chain_active from INIT JSON (boolean, default false)
  • AUTO_CFG = auto_advance from INIT JSON (boolean, default false)

If --auto or --chain flag present AND AUTO_CHAIN is not true: Persist chain flag to config (handles direct invocation without prior discuss-phase):

if ([[ "$ARGUMENTS" =~ --auto ]] || [[ "$ARGUMENTS" =~ --chain ]]) && [[ "$AUTO_CHAIN" != "true" ]]; then
  gsd_run query config-set workflow._auto_chain_active true
fi

If --auto or --chain flag present OR AUTO_CHAIN is true OR AUTO_CFG is true:

Display banner:

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 GSD ► AUTO-ADVANCING TO EXECUTE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Plans ready. Launching execute-phase...

Launch execute-phase using the Skill tool to avoid nested Task sessions (which cause runtime freezes due to deep agent nesting):

Skill(skill="gsd-execute-phase", args="${PHASE} --auto --no-transition ${GSD_WS}")

The --no-transition flag tells execute-phase to return status after verification instead of chaining further. This keeps the auto-advance chain flat — each phase runs at the same nesting level rather than spawning deeper Task agents.

Handle execute-phase return:

  • PHASE COMPLETE → Display final summary:
    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
     GSD ► PHASE ${PHASE} COMPLETE ✓
    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    
    Auto-advance pipeline finished.
    
    Next: /gsd:discuss-phase ${NEXT_PHASE} --auto ${GSD_WS}
    
  • GAPS FOUND / VERIFICATION FAILED → Display result, stop chain:
    Auto-advance stopped: Execution needs review.
    
    Review the output above and continue manually:
    /gsd:execute-phase ${PHASE} ${GSD_WS}
    

If neither --auto nor config enabled: Route to <offer_next> (existing behavior).

<offer_next> Output this markdown directly (not as a code block):

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ GSD ► PHASE {X} PLANNED ✓ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Phase {X}: {Name} — {N} plan(s) in {M} wave(s)

Wave Plans What it builds
1 01, 02 [objectives]
2 03 [objective]

Research: {Completed | Used existing | Skipped} Verification: {Passed | Passed with override | Skipped}

───────────────────────────────────────────────────────────────

▶ Next Up — [${PROJECT_CODE}] ${PROJECT_TITLE}

Execute Phase {X} — run all {N} plans

/clear then:

/gsd:execute-phase {X} ${GSD_WS}

───────────────────────────────────────────────────────────────

Also available:

  • cat .planning/phases/{phase-dir}/*-PLAN.md — review plans
  • /gsd:plan-phase {X} --research — re-research first
  • /gsd:review --phase {X} --all — peer review plans with external AIs
  • /gsd:plan-phase {X} --reviews — replan incorporating review feedback

─────────────────────────────────────────────────────────────── </offer_next>

<windows_troubleshooting> Windows users: If plan-phase freezes during agent spawning (common on Windows due to stdio deadlocks with MCP servers — see Claude Code issue anthropics/claude-code#28126):

  1. Force-kill: Close the terminal (Ctrl+C may not work)
  2. Clean up orphaned processes:
    # Kill orphaned node processes from stale MCP servers
    Get-Process node -ErrorAction SilentlyContinue | Where-Object {$_.StartTime -lt (Get-Date).AddHours(-1)} | Stop-Process -Force
    
  3. Clean up stale task directories:
    # Remove stale subagent task dirs (Claude Code never cleans these on crash)
    Remove-Item -Recurse -Force "$env:USERPROFILE\.claude\tasks\*" -ErrorAction SilentlyContinue
    
  4. Reduce MCP server count: Temporarily disable non-essential MCP servers in settings.json
  5. Retry: Restart Claude Code and run /gsd:plan-phase again

If freezes persist, try --skip-research to reduce the agent chain from 3 to 2 agents:

/gsd:plan-phase N --skip-research

</windows_troubleshooting>

<success_criteria>

  • .planning/ directory validated
  • Phase validated against roadmap
  • Phase directory created if needed
  • CONTEXT.md loaded early (step 4) and passed to ALL agents
  • Research completed (unless --skip-research or --gaps or exists)
  • gsd-phase-researcher spawned with CONTEXT.md
  • Existing plans checked
  • gsd-planner spawned with CONTEXT.md + RESEARCH.md
  • Plans created (PLANNING COMPLETE or CHECKPOINT handled)
  • gsd-plan-checker spawned with CONTEXT.md
  • Verification passed OR user override OR max iterations with user decision
  • User sees status between agent spawns
  • User knows next steps </success_criteria>