Commit Graph

586 Commits

Author SHA1 Message Date
Behruz Nassre Esfahani
8189d2f098 enhance(#1872): document Claude Code advisor inheritance in model profiles (#1922)
* enhance(#1872): document Claude Code advisor inheritance in model profiles

Add an "Advisor Tool (Claude Code)" section to
gsd-core/references/model-profiles.md: session-level advisor is inherited
by all GSD subagents and composes with the per-agent profile/tier system,
candidate executor/advisor pairings per profile (cost/quality/caching
claims attributed to Anthropic's advisor-tool docs, not asserted as GSD
behavior), when it is worth enabling vs not, and the session-level /
no-per-agent-control constraint linking anthropics/claude-code#73072.

Docs-only. Golden-install-parity fixtures recaptured for the edited
reference file (hash-only, one line per runtime).

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

* chore(#1872): add changeset

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

* fix(#1872): use Documentation changeset type for docs-only change

Changeset type was `Changed`, which triggers the docs-required lint
(TRIGGERING_TYPES in scripts/lint-docs-required.cjs). This PR only
touches gsd-core/references/model-profiles.md, so there is no docs/
file to pair with and docs-lint failed. `Documentation` is the correct
type for a docs-only enhancement and is exempt from the trigger.

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

* fix(#1872): put docs-exempt marker on its own line, revert to type Changed

The prior fix (type: Documentation) was invalid — parse.cjs ALLOWED_TYPES
is {Added, Changed, Deprecated, Removed, Fixed, Security}, so both
changeset-lint and docs-lint failed with invalid_type.

Real root cause of the original docs-lint failure: DOCS_EXEMPT_RE is
anchored to match the `<!-- docs-exempt: ... -->` marker only on its own
line, but the marker was tacked onto the end of the prose line, so it was
never captured (docsExempt: null) and the triggering `Changed` fragment
had no docs/ pairing -> fail_docs_missing.

Fix: keep the valid `type: Changed` and move the marker to its own line.
Verified locally: changeset-lint -> ok_fragment_present,
docs-lint -> ok (own-line marker parses to ok_fragments_exempt).

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>
2026-07-02 23:41:39 -04:00
Rezolv
9d7c046eae refactor(#1852): lazy-split plan-phase.md into steps/ (#1934)
* refactor(#1852): lazy-split plan-phase.md into steps/

Extract 3 self-contained, rarely-hit sections into gsd-core/workflows/plan-phase/steps/
via lazy 'Read and execute' pointers (mirrors execute-phase/steps/, ADR-1610 progressive
disclosure — no eager @-import): closed-phase-gate (1.5), prd-express-path (3.5),
windows-troubleshooting. Byte-invariant: plan-phase.md 94459 -> 89775 (-4684), each step
< 32 KiB anchor, resolved instructions unchanged.

Scope note: only 3 sections were extractable. plan-phase.md is guarded by a dense net of
content-presence tests (plan-bounce/enh-3209/phase6-planning-capabilities assert specific
sections/flags inline) that block extracting the larger blocks without also refactoring
those tests — durable low-80s headroom is deferred pending maintainer re-scope (issue #1852).

Cascade: size:baseline regen, 16 golden-install-parity fixtures regen, INVENTORY in sync.
Full plan-phase test surface green (4129/4130, 0 fail); lint:ci exit 0.

* docs(changeset): Changed fragment for #1934 (plan-phase lazy-split)

* docs(changeset): mark #1934 fragment docs-exempt (internal workflow refactor)

* fix(#1852): add gsd_run launcher preamble to prd-express-path step

The extracted prd-express-path.md calls gsd_run but the canonical launcher
preamble lived in the parent plan-phase.md — runtime-launcher-parity (#373)
walks workflows/ recursively and requires every .md using gsd_run to carry
exactly one preamble + the $HOME/.claude fallback arm (same as the existing
execute-phase/steps/ files). Injected via scripts/sync-runtime-launcher.cjs;
golden fixtures + size baseline regenerated. plan-phase.md unchanged (89775).

---------

Co-authored-by: Tom Boucher <trekkie@nomorestars.com>
2026-07-02 23:30:19 -04:00
Tom Boucher
5b5dbe059b feat(#1942): VS Code extension — repo-local IDE host with reachability proof (#1966)
* feat(#1942): VS Code extension — repo-local IDE host with reachability proof

The VS Code extension (vscode/extension.js + vscode/package.json) is a
repo-local, buildable extension (not Marketplace-published). The gsd.invoke
handler dispatches in-process through the GSD command-routing hub. The handler
is exported separately from activate() so it is testable without a VS Code host.

tests/vscode-extension-reachability.test.cjs: proves the handler dispatches
through the hub + returns a result (keystone wired); the manifest declares the
command + engine; resolveEngineRoot finds the gsd-core/ dir.

* docs(changeset): VS Code extension (#1966)

* fix(#1942): register vscode/package.json in VERSIONED_MANIFESTS (#844)

* fix(#1942): align vscode/package.json version with package.json (1.7.0-rc.1)
2026-07-02 23:12:03 -04:00
Tom Boucher
b188c0d085 fix(#1967): build hooks/dist once upfront in run-tests to close scoped-CI empty-dir race (#1968)
* fix(#1967): build hooks/dist once upfront in run-tests to close scoped-CI empty-dir race

hooks/dist/ is gitignored and not built by prepare (build:lib only), so the
scoped CI lane starts with it absent. The first install test's before() hook
triggers build-hooks.js, which creates DIST_DIR empty then fills it file-by-
file; a concurrently-spawned install.js reader can observe the empty window and
fail with 'Failed to install hooks: directory is empty' (intermittently failing
e.g. bug-3683-workflow-colon-namespace-leak on scoped legs).

Add ensureBuiltHooks() to scripts/run-tests.cjs — the same upfront chokepoint as
ensureBuiltArtifacts — to build hooks/dist once, single-process, before any
concurrent test spawns install.js. Completeness is checked against
build-hooks.js HOOKS_TO_COPY (absent/empty/partial/zero-byte -> rebuild; complete
-> no-op). Folds regression coverage into bug-969-test-infra-flake-hardening
(Part C), proven fail-first (ensureBuiltHooks undefined on next).

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

* chore(#1967): set changeset pr to 1968

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 23:07:14 -04:00
Tom Boucher
65071afc3a feat(#1944): pi extension — installable ExtensionAPI host-plugin with reachability proof (#1965)
* feat(#1944): pi extension — installable ExtensionAPI host-plugin with reachability proof

The pi extension (pi/gsd.cjs) is a real, jiti-loadable ExtensionAPI module that
registers /gsd (dispatches through the GSD command-routing hub) + gsd_invoke tool
+ tool_call event. Engine entry: in-process CJS require (Bun-compatible). The
extension resolves the engine tree via walk-up (like the OpenCode plugin).

tests/pi-extension-reachability.test.cjs proves the /gsd handler DISPATCHES
through the hub (returns a JSON result) — the keystone-wired proof that the
command is user-reachable, not just registered on a mock (#1944 AC).

* docs(changeset): pi extension (#1965)

* fix(#1944): add docs-exempt marker to changeset (no standalone docs file)
2026-07-02 22:15:25 -04:00
Tom Boucher
b51cbf96cf feat(#1943): extensionEvents vocabulary — separate from hookEvents (extension-system surface) (#1946)
* feat(#1943): extensionEvents vocabulary — separate from hookEvents (extension-system surface)

* fix(#1943): re-export VALID_EXTENSION_EVENTS from gen-capability-registry (test import path)

* fix(#1943): regenerate capability-registry.cjs + add changeset fragment

* fix(#1943): import VALID_EXTENSION_EVENTS from validator, not gen-capability-registry (golden parity)
2026-07-02 21:45:35 -04:00
Tom Boucher
d3d689a5e9 fix(#1920): resolve host version from gsd-core/VERSION + ship capability generators (#1938)
The flattened install layout broke the third-party capability ecosystem in two
ways. Both are fixed at the host-version / installer boundary.

Gap 1 — host version read as 0.0.0. The running GSD version was resolved via
require('../../../package.json') (loader/source) and require('../../package.json')
(the gsd-tools CLI), which in the installed layout is the versionless CommonJS
marker → the fail-closed fallback reported 0.0.0, so `capability install` rejected
any manifest with a real engines.gsd range as "incompatible with GSD 0.0.0". For
runtimes that get no marker, and for local installs, that walked-up package.json
could even be the USER's own project, reporting a wrong version. Fix:
readHostVersion() (capability-loader.cts, capability-source.cts) and capHostVersion()
(gsd-tools.cjs) now prefer the authoritative gsd-core/VERSION the installer already
writes for EVERY runtime (mirrors resolveVersionFrom(), #1383), falling back to the
runtime-root package.json for the dev/source tree, then fail-closing. This fixes
every runtime and the actual `capability install` CLI path without touching the
marker package.json, so uninstall is unchanged (no data-loss surface).

Gap 2 — scripts/gen-capability-registry.cjs (+ its sibling
gen-loop-host-contract.cjs) were never copied by the installer, so the loader's
never-crash invariant discarded every overlay and fell back to the frozen
first-party registry (installed capabilities silently inert). Now copied,
uninstalled, and manifest-tracked exactly like fix-slash-commands.cjs (#1223).

Regenerates the 16 golden-install-parity fixtures to capture the two newly shipped
generator scripts and the gsd-tools.cjs change.

Regression tests (RED→GREEN): readHostVersion VERSION-first / fallback / fail-closed
resolution; an end-to-end `capability install` against a REAL installed layout
proving the engines gate sees the real host version, not 0.0.0; and a real-install
check that both generators are shipped and manifest-tracked.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 20:34:30 -04:00
Tom Boucher
325e9fad4b feat(#1682): OpenCode session.idle + opencode-subset dialect + Claude parity — Slice 1b/c (#1930)
* feat(#1682): OpenCode session.idle + opencode-subset dialect + Claude parity — Slice 1b/c

- Plugin (.opencode/plugins/gsd-core.js): handle session.idle (↔ Claude Stop
  lifecycle point; no-op sentinel — state already persisted to .planning/).
  Completes the compaction/idle pair (#1914 shipped compaction).
- Declare hookEvents: 'opencode-subset' in the OpenCode descriptor — the
  reserved dialect now has a real consumer (no longer zero-consumer).
- host-integration.cts: add HOOK_EVENT_SURFACES + hookEventSurfaceFor() — the
  pure consumer that resolves a dialect to its host-fireable event surface.
  opencode-subset = session/tool/file subset with NO workflow-phase events
  (engine owns phase sequencing; ADR-1239 §OpenCode binding).
- Tests: hookEventSurfaceFor unit tests (claude/gemini/opencode-subset/null);
  plugin session.idle no-throw; compaction breadcrumb; opencode-subset surface
  parity vs the plugin's handlers (Claude parity).

* fix(#1682): don't declare hookEvents on opencode (hooksSurface:none invariant)

The repo invariant couples runtime.hookEvents to the managed settings.json hook
surface: hooksSurface:'none' runtimes (opencode — plugin owns hooks) must NOT
declare hookEvents. Declaring 'opencode-subset' there violated 3 capability-
registry invariants + 2 install-plan golden masters + opencode golden parity.

The opencode-subset dialect is still IMPLEMENTED — just not via the legacy
descriptor field: hookEventSurfaceFor() (host-integration.cts) is its consumer,
and the OpenCode plugin consumes the subset events at runtime (session.idle
added here; compaction shipped in #1914). Declaring it on the descriptor would
require weakening the hooksSurface:none ⇔ no-hookEvents invariant (flagged for
decision).

* fix(#1682): refresh opencode golden parity for plugins/gsd-core.js (session.idle)

* docs(changeset): OpenCode session.idle + opencode-subset dialect (#1682)

* docs(changeset): backfill PR #1930
2026-07-02 16:16:00 -04:00
Tom Boucher
541de6894f feat(#1682): OpenCode companion-MCP binding (mcp.gsd) — Phase 5 Slice 1a (#1929)
* docs: align PR-FLOW push gate from gsd-test-summary to gsd-test

gsd-test is the application (open-gsd/gsd-test-runner); gsd-test-summary is
the legacy local wrapper. RULESET.PR-FLOW.docker-before-push now names gsd-test
as the pre-push gate (exit 0 / verdict outcome 'passed').

* docs: drop WORKTREE.SEAM local node --test rule; align PROC dispatch to gsd-test

- Remove WORKTREE.SEAM.execution-rule (prefer local node --test) — contradicts
  CLAUDE.md 'NEVER run node --test locally'; CLAUDE.md wins.
- PROC.PARALLEL-FIX-DISPATCH: gsd-test-summary --both -> gsd-test (app; --both
  was a legacy wrapper flag).

* feat(#1682): OpenCode companion-MCP binding (mcp.gsd) — Phase 5 Slice 1a

configureOpencodePermissions registers the Phase-4 companion MCP server
(gsd-mcp-server) as opencode mcp.gsd, so OpenCode connects to GSD's command
(point 1) + state-IO (point 5) with no bespoke plugin (ADR-1239 Phase D).
Idempotent + non-clobbering (add-if-absent; respects a user-defined mcp.gsd).
Local-stdio schema per OpenCode config (packages/core/src/config/mcp.ts);
`-p @opengsd/gsd-core` resolves the bin (name != package) under npx.

Tests: registers-on-object-config; does-not-clobber-user-entry.

* docs(changeset): OpenCode companion-MCP binding (#1682)

* fix(#1682): use PACKAGE_NAME single-source (#516) + refresh opencode golden parity

- mcp.gsd command: replace hardcoded '@opengsd/gsd-core' literal with
  PACKAGE_NAME from gsd-core/bin/lib/package-identity.cjs (#516 single-source).
- opencode golden-install-parity fixture: refresh opencode.json hash for the
  added mcp.gsd block (configureOpencodePermissions output change).

* docs(changeset): add docs-exempt marker (Phase 5 slice)

* docs(changeset): backfill PR #1929
2026-07-02 14:57:44 -04:00
Tom Boucher
ff6b5cb024 feat(#1914): OpenCode native plugin integration (Option 1 file-copy) (#1923)
* feat(#1914): OpenCode native plugin integration (Option 1 file-copy)

Ship a native OpenCode plugin (.opencode/plugins/gsd-core.js) plus the
installer step that delivers it, so GSD's lifecycle hooks run on OpenCode.
OpenCode declares hooksSurface:'none', so GSD's hook scripts already ship to
<configDir>/hooks/ but nothing invokes them; the plugin bridges OpenCode's
event bus onto those scripts as subprocesses (prompt/read/worktree/workflow
guards, injection scanner, context monitor).

Distribution is Option 1 (file copy) per the #1914 triage decision: no
scripts.build rename, no prepare/prepack removal. package.json gains
main + .opencode in files[] for discovery.

Corrected against OpenCode's docs + loader source (not the reference branch):
- Auto-discovery globs {plugin,plugins}/*.{ts,js} — .cjs is never matched, so
  the installed adapter must be .js (config dir carries {"type":"commonjs"}).
- No opencode.json plugin-array patch — that array is npm-only; local files
  are auto-discovered.
- REPO_ROOT is resolved by walking up to the dir holding hooks/ + gsd-core/,
  correct for package tree, global install, and local install.
- Config-hook registration is gated (IS_PACKAGE_TREE) so it never
  double-registers commands/agents/skills already delivered by native copy.

Also fixes an incidental .gitignore drift: 8 ADR-1239 .cts-generated .cjs
artifacts were untracked-and-not-ignored (leak risk) — now ignored.

Tests: tests/opencode-plugin-adapter.test.cjs (14, pure helpers + real
subprocess bridge against stub hooks); golden-install-parity regenerated.
Green on Mac + Linux (gsd-test): 0 failures.

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

* fix(#1914): harden OpenCode plugin export shape + advisory accumulation (adversarial findings)

Address Codex adversarial-review findings:
- HIGH: export `{ id, server }` could trip OpenCode's loader
  (`for (entry of Object.values(mod)) getServerPlugin(entry)` throws on a
  non-extractable value). Make `id` NON-ENUMERABLE and assign module.exports
  from a variable (not a literal) so no string `id` is ever iterated — verified
  loader-safe under real import(pathToFileURL) (default + module.exports alias,
  both objects with .server; no bare id string).
- MEDIUM: sequential advisory hooks clobbered output.metadata._gsdAdvisory;
  now accumulate into an array.
- LOW: resolveRepoRoot fallback returned ".." while the comment said "../.." —
  aligned to "../.." (package-tree depth).

Tests: added a faithful loader-loop emulation (raw CJS + ESM namespace views),
advisory-accumulation, and a real bin/install.js copy→manifest→uninstall
integration test. golden regenerated.

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

* docs(#1914): add changeset fragment for OpenCode plugin integration (PR #1923)

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

* fix(#1914): Windows — assert rewritten path with string include, not path-regex

The Read content-rewrite test built a RegExp from `path.join(root,'gsd-core')`.
On Windows the backslashes in the path are interpreted as regex escapes, so the
assertion never matched and `test (windows-latest, 24)` failed — even though the
adapter rewrote the path correctly. Replace the RegExp with a separator-agnostic
`String.includes` check (the repo's no-path-literal-in-assert concern).

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 13:01:51 -04:00
Behruz Nassre Esfahani
7bef6a6496 fix(#1863): use named flags for state.* calls in executor + workflows (#1873)
* fix(#1863): use named flags for state.* calls in executor + workflows

The named-only state-command router (parseNamedArgs) silently drops
positional args, so state.cjs threw its required-arg error and
metrics/decisions/blockers/session continuity were never recorded.

Convert record-metric / add-decision / add-blocker / record-session in
agents/gsd-executor.md to the named-flag form (mirroring execute-plan.md),
and fix the two remaining positional record-session calls in
gsd-core/workflows/milestone-summary.md and forensics.md. Recapture the
golden-install-parity fixtures and size baselines for the edited files.

Also fix a pre-existing detached-rebuild handle leak in
tests/graphify-auto-update.slow.test.cjs: three dispatch tests returned
after observing only the synchronous "running" status without awaiting the
detached rebuild's terminal state. That leak was latent until the new
#1863 regression block's added runtime shifted --test-force-exit timing
and surfaced it as a non-zero chunk exit. The three tests now await
terminal status via the file's existing waitForBuildStatus helper.

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

* chore(#1863): add changeset

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>
2026-07-02 13:01:10 -04:00
Tom Boucher
8751379cfc fix(#1926): bug-1367 builds hooks/dist precondition in before() (#1927)
The bug-1367 install test ran install.js without building hooks/dist (a
gitignored build artifact). The unit lane's ensureBuiltArtifacts() builds only
bin/lib, not hooks — so on a lane without pre-built hooks the installer's
verifyInstalled(hooks) found the dir empty and hard-failed 'directory is empty',
throwing in before() → hookFailed → L0-L5 cancelledByParent cascade on the
Windows CI lane.

Build hooks in before() via scripts/build-hooks.js (mirrors golden-install-parity's
BUILD_SCRIPT pattern). Self-contained — no lane-ordering dependency.

Closes #1926
2026-07-02 12:42:10 -04:00
jecanore
5657994702 fix(#1716): route resume_from_file to complete_session when no pending tests remain (#1722)
* fix(#1716): route resume_from_file to complete_session when no pending tests remain

When a UAT session has status:partial with blocked_count>0 and pending_count==0 (all remaining tests are blocked, none are pending), resume_from_file found no [pending] test and terminated silently — never routing to complete_session. This blocked the issues==0 auto-transition path even when there were zero code defects.

Guard clause added immediately after the find-pending step: if no [pending] test is found, route to complete_session. complete_session then correctly sets status:partial (because blocked_count>0) without presenting further tests.

Closes #1716

* chore(#1716): add changeset fragment and regenerate golden-install-parity fixtures

Changeset fragment for PR #1722 (type: Fixed).

Golden-install-parity fixtures regenerated for all 16 runtimes — the workflow fix shifts verify-work.md's byte-stable hash in the golden manifest. Regenerated via UPDATE_GOLDEN=1 node --test tests/golden-install-parity.test.cjs.
2026-07-02 11:58:06 -04:00
Rezolv
312c9d3ec3 fix(#1907): validate per-item shape in isValidReport so adapter garbage fails closed (#1910)
* fix(#1907): validate per-item shape in isValidReport so adapter garbage fails closed

isValidReport checked only the container (items is-array, coverage scalars), so a report
like {items:[{}]} sailed through runProbeCli and stringified as green output — despite the
docstring promising it 'fails closed on adapter garbage'. Add a per-item Item-contract guard
(requirement_id/category/status + typed nullable fields) so a future adapter that bypasses
the analyzeCoverage merge and returns per-item garbage inside a well-shaped envelope fails
closed (exit 2) instead of emitting it as valid coverage.

analyzeCoverage always emits fully-populated, validated Items, so the 2 shipped adapters are
unaffected (verified across the edge/prohibition suites).

Refs #1907, epic #1904.

* chore(changeset): Fixed fragment for #1910 (isValidReport per-item)
2026-07-02 11:55:53 -04:00
Rezolv
a4bc04a5ff fix(#1905): normalize hand-authored backstop marker so it can't degrade to green (#1909)
* fix(#1905): normalize hand-authored backstop marker so it can't degrade to green

A hand-authored non-inferable `backstop` truth with a stray trailing space (or
surrounding quotes) silently graded {status:green} instead of abstaining — the exact
#1154 false-pass. `truthVerification` returned null for any value != 'backstop'/'explicit',
and the frontmatter continuation-KV parser preserved the stray whitespace captured inside
the quotes. Normalize the marker before comparison (Postel) AND trim the continuation-KV
value at the parser (durable root cause; also cleans the sibling check_target path).

Regression: a trailing-space/quoted backstop truth now abstains (insufficient_spec),
end-to-end from a hand-authored must_haves.truths block (#1820 rail).

Refs #1905, epic #1904.

* chore(changeset): Fixed fragment for #1909 (backstop marker normalize)
2026-07-02 11:55:49 -04:00
Behruz Nassre Esfahani
12b35eeeaf fix(#1729): resolve phase headers with a pre-colon parenthetical tag (#1765)
* fix(#1729): resolve phase headers with a pre-colon parenthetical tag

A phase header may carry a parenthetical tag between the number and the
colon, e.g. `### Phase 26 (Cluster B): Title`. Every phase-header regex
built `Phase\s+<num>` immediately against the colon delimiter, so the
tagged phase was invisible: the resolver returned found:false and, just
as bad, the capture-all enumeration/parse paths (roadmap analyze,
milestone listing + milestone-scope filter, verify, init/import, state
total_phases, validate, the command router, preamble stripping, and the
phase-remove renumbering rewrite) silently dropped, miscounted, or
failed to renumber it — wrong phase_count, progress_percent, next_phase,
or corrupt numbering after a removal.

The fix tolerates the tag at the header seam. Parameterized resolver
sites compose the exported OPTIONAL_PHASE_TAG_SOURCE fragment; literal
enumeration sites inline its character-for-character mirror
`(?:\s*\([^)\n]*\))?`, placed immediately before the colon so it cannot
alter an existing match (optional, single-line, one paren pair, no
capture-group shift). In the renumber-on-removal rewrite the tag is
folded into the re-emitted suffix capture so it survives verbatim. Both
forms are documented to change together and a drift-guard test asserts
their behavioral equivalence over a header corpus.

Deliberately excluded: roadmap-upgrade.cts (legacy one-time migration),
where tolerating the tag would silently drop it on header rewrite — that
needs its own data-preserving treatment. Known boundaries left for
follow-up: checklist/bullet-style phase entries (`- [ ] Phase N (tag):`)
and a malformed space-before-colon variant, both pre-existing.

Validated empirically against the issue's reproduction: `roadmap
get-phase 26` resolves and `roadmap analyze` lists Phase 26 with the tag
excluded from the name (phase_count 2, next 26); an all-tagged versioned
roadmap now scopes correctly instead of falling back to a pass-all
filter. Regression coverage in tests/phase.test.cjs asserts resolver
parity (pre- vs post-colon), padding tolerance (#3537), decimal
sub-phases, no cross-phase false match, the shared seam, enumeration
coherence, renumber-preserves-tag, and seam/mirror drift. Full unit
suite green (7291 pass, 0 fail); eslint + regression-name +
resolution-provenance + changeset lints pass. Reviewed by Codex
(no critical/high; the two enumeration misses it surfaced are folded in).

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

* chore(#1729): add changeset for pre-colon phase-tag fix

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 11:55:44 -04:00
Behruz Nassre Esfahani
d96c7ef865 fix(#1779): emit valid YAML for unsafe scalars in reconstructFrontmatter (#1807)
* fix(#1779): emit valid YAML for unsafe scalars in reconstructFrontmatter

reconstructFrontmatter wraps a scalar or block-array item in double quotes
when it contains a YAML indicator (`:`/`#`, plus `[`/`{` for top-level
scalars), but interpolated the raw value with no escaping. A value carrying
both an indicator and a literal `"` (or `\`) serialized to invalid YAML, e.g.
`upstream: "https://x (Tom; "Git. Ship. Done")"`, which fails under any strict
parser (js-yaml, PyYAML) and corrupts the whole block on the next
syncStateFrontmatter whole-block regen.

- escapeDoubleQuoted() escapes `\`, `"`, and control chars (newline/tab/CR/C0
  controls + DEL → YAML `\n`/`\t`/`\r`/`\xHH`) so any wrapped value is valid.
- scalarNeedsDoubleQuoting() routes values that mis-parse or round-trip lossily
  when bare through that escaped form: the empty string (bare `k:` reloads as
  null), an embedded `"`/`\` or control char, a leading YAML indicator
  (quote / `&`*`!` anchor-alias-tag / `|`>` block scalar / flow `[]{},` / `#` /
  reserved `%`@`backtick / `-`?`:` before a space), or leading/trailing space.
  Applied at all four wrap sites.

Scope stays on serialization correctness; it does NOT broaden the lossy
object-list handling deferred to #1572/#1660. Known limitation: lone UTF-16
surrogates are still lossy through UTF-8 encoding (out of scope, extremely
unlikely in frontmatter values).

Regression test asserts strict js-yaml round-trip across all four wrap sites
plus backslash, control-char (incl. NUL), empty-string, leading-indicator, and
leading/trailing-whitespace classes; test-the-test confirms each fails on the
unescaped output. Frontmatter suite 549/549 green, golden-install-parity 16/16
unchanged (no serialization churn).

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

* chore(#1779): add changeset for frontmatter quote-escaping fix

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 11:43:47 -04:00
Tom Boucher
92091d71f2 fix(#1871): wire phase archival end-to-end (phases archive cmd + default + atomic) (#1924)
Follow-up to #1919 (archive-then-remove core). Closes the remaining #1871
acceptance criteria so phase history is preserved across the full milestone
lifecycle, not just at phases.clear:

- #2 src/milestone.cts + src/phases-command-router.cts: extract shared
  archivePhaseDirectories() helper; add cmdPhasesArchive (the previously
  half-wired phases.archive alias now routes instead of erroring Unknown).
- #4 gsd-tools.cjs + src/milestone.cts: milestone complete archives phase
  dirs by default (--no-archive-phases opts out; --archive-phases is now a
  harmless no-op). complete-milestone.md updated to drop the redundant manual
  Yes/Skip archive prompt.
- #3 gsd-core/workflows/new-milestone.md: §6 stages the archive move + source
  removal (git add .planning/milestones/ .planning/phases/) in the same commit
  as the milestone start, so the archive lands atomically — no orphaned
  uncommitted deletions, no un-archived dirs inherited.
- docs/CLI-TOOLS.md (+ ja/zh/ko/pt) + help/modes/full.md: flag accuracy.
- tests: phases archive command (#2) + milestone complete default archive /
  --no-archive-phases opt-out (#4). Goldens + workflow size baseline refreshed.

Closes #1871
2026-07-02 11:14:01 -04:00
Tom Boucher
5195a36cc5 fix(#1871): phases clear archives dirs instead of destroying them (#1919)
cmdPhasesClear hard-deleted committed phase directories (rmSync) with no
archive, so browsable phase history was silently lost at a milestone switch.
The #1447 dirty-tree guard was a no-op for the common committed case (a clean
tree passes the guard, then rmSync destroyed the dirs, leaving orphaned
uncommitted deletions and no archive).

Archive-then-remove: move each non-999 phase dir to
milestones/<version>-phases/ (version from getMilestoneInfo; timestamp
fallback; collision-safe) using retryRenameSync — mirroring the existing
archivePhases path in cmdMilestoneComplete. The #1447 uncommitted-changes
guard is retained as a secondary backstop.

Tests in tests/new-milestone-clear-phases.test.cjs updated: phase content is
asserted to SURVIVE in milestones/*-phases/ (archived), not be destroyed —
including the committed-dirs case that previously codified the no-op.

Closes #1871
2026-07-02 10:25:31 -04:00
Tom Boucher
88da609b3e fix(#1911): milestone complete --ws archives to the workstream (#1917)
cmdMilestoneComplete hardcoded three archive paths to root .planning/
while its siblings (roadmap/requirements/state/phases) used workstream-aware
planningPaths. So `milestone complete <v> --ws <name>` scattered its archive
into root and never created workstream-local milestones/.

Derive the archive base from the workstream-aware planning root:
- milestonesPath / archiveDir / auditFile now use planningPaths(cwd).planning
- flat mode (no --ws) is a no-op (planningPaths(cwd).planning == root .planning)

Regression in tests/milestone.test.cjs: --ws archives into the workstream
milestones dir, not root.

Closes #1911
2026-07-02 10:25:11 -04:00
Tom Boucher
8084f626ba fix(#1912): init.progress fails safe in workstream mode with no active ws (#1918)
cmdInitProgress used planningDir(cwd), which resolves to root .planning
when no active workstream and no --ws/GSD_WORKSTREAM is set — regardless
of mode:workstream. So /gsd-progress confidently reported a stale root
milestone with no signal it was stale.

Fail safe: when .planning/workstreams/ has workstreams AND no active
workstream is resolved, error with an actionable hint naming the available
workstreams and the --ws / `workstream set` fix. Flat mode (no workstreams
dir) and --ws <name> are unchanged.

Regression in tests/init.test.cjs: errors when workstreams exist but none
active (no stale root report); succeeds with --ws; flat mode unchanged.

Closes #1912
2026-07-02 10:24:53 -04:00
Tom Boucher
05cd55d43b fix(#1913): derive workstream progress status from shipped signals (#1916)
workstream progress trusted the mutable STATE.md `Status` field, so a
shipped/archived milestone whose field was left at `executing` was reported
as executing — a stale hand-maintained field became the source of truth
instead of the authoritative archive/tag/ROADMAP signals.

Derive status in the inventory builder from a milestoneShipped signal
(archived milestone snapshot under milestones/, or a SHIPPED marker in the
workstream ROADMAP), collected by inspectWorkstream. The inventory now
reports `status_source` (field|derived) and `status_conflict` (true when
the derived value disagrees with the stale field), and a shipped workstream
is never reported executing.

- src/workstream-inventory-builder.cts: milestoneShipped input + status_source/status_conflict outputs + derivation
- src/workstream-inventory.cts: workstreamMilestoneShipped() signal detector wired into inspectWorkstream
- tests/workstream-inventory.test.cjs: regression (builder unit + inspectWorkstream integration + negative)

Closes #1913
2026-07-02 10:24:41 -04:00
Tom Boucher
3c13903dcd feat(#1866): agent-side self-load of configured agent_skills
Each of the 22 consumer agents now self-loads its configured agent_skills
in its mandatory init step, so .planning/config.json agent_skills.<type>
reaches the agent on every runtime — including Cursor and /gsd-autonomous,
where Skill()-delegated workflow bash init did not reliably execute.

- gsd-core/references/agent-skills-bootstrap.md: shared contract
  (query + Read + dedup guard that skips when <agent_skills> is already
  in the prompt, so Claude's orchestrator-side injection never doubles)
- 22 agents/gsd-*.md: one self-load line naming the agent's own type
- gsd-core/workflows/autonomous.md: note that delegated agents self-load
- tests/agent-skills-bootstrap.test.cjs: regression + parity (CONSUMER_AGENTS
  bijection + fast-check property) — Generative-Fix-Divergence guard
- docs: ADR-1866, CONFIGURATION dual-injection How It Works, INVENTORY
  row, Changed changeset

Closes #1866
2026-07-01 20:09:01 -04:00
Tom Boucher
c32698dc9e docs(#1855): add changeset fragment for marketplace manifest 2026-07-01 11:51:11 -04:00
Tom Boucher
251cfa1f3c fix: docs-exempt + de-dup PR ref on sonnet-5 changeset (adversarial findings)
- Added-type fragment needs docs or a docs-exempt marker (lint-docs-required);
  Sonnet 5 operator docs already landed on next via #1851 → docs-exempt.
- Serializer auto-appends (#pr); drop the manual (#1848) from the body so it
  doesn't render (#1847) (#1848) (#1848). Keep the #1847 issue ref inline.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 23:04:13 -04:00
Tom Boucher
1bd04e1565 docs(#1847): add Claude Sonnet 5 changeset for next-line changelog + refresh stale model examples
The 1.6.1 forward-port (#1851) used the no-changelog opt-out instead of carrying
a changeset, so Sonnet 5 — unlike every other 1.6.1 fix (#1580/#1591/#1693 whose
fragments live on next) — had no fragment and would be MISSING from the 1.7.0
changelog. Add the fragment so the release render reflects current shipping code.
Also note the bold-checklist form in the #1591 fragment, and refresh two stale
claude-sonnet-4-6 illustrative examples to current IDs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 23:04:13 -04:00
Tom Boucher
93e5d2dd84 fix(#1525): skip deferred phases on autonomous reruns (#1846)
* fix(#1525): skip deferred phases on autonomous reruns

* chore(#1525): add changeset fragment

* chore(#1525): fix changeset body

* test(#1525): refresh install parity fixtures

* test(#1525): shrink autonomous workflow

* test(#1525): refresh autonomous baselines

* test(#1525): tolerate Windows temp cleanup flake
2026-06-30 21:29:38 -04:00
Tom Boucher
be54c0dbf5 fix(#1838): exclude backlog 999.x milestone phases (#1843)
* fix(#1838): exclude backlog 999.x milestone phases

* chore(#1838): add changeset fragment
2026-06-30 21:29:35 -04:00
Tom Boucher
88a7500e91 fix(#1836): count prefixed milestone phase dirs (#1844)
* fix(#1836): count prefixed milestone phase dirs

* chore(#1836): add changeset fragment
2026-06-30 20:43:28 -04:00
Tom Boucher
9e32462dd8 fix(#1588): ignore fenced and backlog roadmap phases (#1845)
* fix(#1588): ignore fenced and backlog roadmap phases

* chore(#1588): add changeset fragment

* chore(#1588): fix changeset body

* test(#1588): fold init regression into init suite
2026-06-30 20:43:25 -04:00
Behruz Nassre Esfahani
50ff7a8707 fix(#1776): scope prune phase resolution to ## Current Position (#1832)
* fix(#1776): scope prune phase resolution to ## Current Position

cmdStatePrune resolved the current phase by extracting the `Phase` field over
the WHOLE STATE.md body. stateExtractField's fallback chain ends in a pipe-table
match (`| Phase | N |`), so a STATE.md lacking a `Current Phase` field and a
prose `Phase:` line — but carrying an unrelated `Phase`-labelled table row (e.g.
a historical verification table) — resolved that stale table cell as the current
phase and computed a wrong cutoff (bailing "Only N phases" or pruning at a stale
boundary).

Resolve the phase via the same canonical chain buildStateFrontmatter uses —
frontmatter `current_phase` → `Current Phase` field → prose `Phase: X of Y` —
but scope ONLY the prose term to the `## Current Position` section via the
fence-aware locateCurrentPosition seam (new exported sliceCurrentPositionSection).
Frontmatter and the explicit `Current Phase` field stay document-wide (they are
unambiguous); the shared stateExtractField is not narrowed for any other caller.

Tests (folded into tests/state-prune.test.cjs): a stray `| Phase | 2 |` table
with the real phase in frontmatter no longer drives the cutoff (fail-first on
base); template-conformant STATE.md is unchanged; and a fast-check
boundary-containment property that a `| Phase | N |` row outside Current Position
never leaks into the scoped resolution.

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

* chore(#1776): add changeset for prune Current Position scoping

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>
2026-06-30 13:16:40 -04:00
Behruz Nassre Esfahani
dae7f81482 fix(#1528): drop next-phase guidance from security-blocked verify-work presentation (#1687)
* fix(#1528): drop next-phase guidance from security-blocked verify-work presentation

When security enforcement blocks phase advancement (no SECURITY.md produced),
the verify-work presentation told the user advancement was blocked but still
offered `/gsd:plan-phase {next}` and `/gsd:execute-phase {next}`, competing
with the current-phase fix. Remove those two next-phase lines so the blocked
state routes only to the current-phase resolution (secure-phase, ui-review).
The post-transition presentation — reached only after the completion contract
passes — still offers next-phase planning, which is the correct place for it.

Regression coverage added to tests/ui-review-next-guidance.test.cjs: the
security-blocked block must not offer next-phase actions, and the
post-completion block must still offer them. Regenerated workflow size baseline.

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

* chore(#1528): add changeset for security-blocked next-phase fix

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

* test(#1528): recapture golden-install-parity fixtures for verify-work.md change

Rebased onto next; verify-work.md's installed hash changed across all 16
runtime fixtures. Diff confined to the single gsd-core/workflows/verify-work.md
key per runtime. Assert mode 16/16 green.

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>
2026-06-30 10:52:30 -04:00
Tom Boucher
1f649838b8 Merge branch 'next' into fix/1698-codex-output-last-message 2026-06-30 10:04:59 -04:00
Tom Boucher
bad2c76c5e feat(#1826): cmdStateRebuild CLI + dry-run + verbose + integration tests + docs (#1830)
Phase 2 of approved feature #1817. Wires the pure `rebuildCore` transition
(Phase 1, #1827) to the `gsd state rebuild` CLI subcommand per ADR-1817
§5 (heavy/manual counterpart to lightweight, auto-triggered `state sync`).

Source changes:
- src/state.cts: implement cmdStateRebuild. Locks via
  readModifyWriteStateMd (real path) or read-only (dry-run). Wires
  phaseInventoryProvider to a real .planning/phases/ disk scan (same
  canonical source buildStateFrontmatter uses). --dry-run emits a
  structured preview without writing. --verbose tees the audit-log
  entries to stderr (treated as data-only per ADR-1577).
- src/state-command-router.cts: register cmdStateRebuild in the
  StateModule interface + add the rebuild handler with --dry-run and
  --verbose flag parsing.
- src/command-aliases.cts: register the state.rebuild canonical +
  'state rebuild' alias + mutation=true (so the manifest covers the
  new subcommand for SDK parity / dispatch hub tests).

Tests (tests/state-rebuild-cli.test.cjs, 5 cases):
- state rebuild with no flags reconciles drifted body + drops orphan
  table rows + appends audit log (end-to-end #1, #2, audit log).
- state rebuild --dry-run computes the diff, writes nothing (criterion #5).
- state rebuild --verbose tees the log; audit-log section still written.
- Running rebuild twice on the just-rebuilt file is byte-identical
  (criterion #6 end-to-end).
- Missing STATE.md produces a clean 'STATE.md not found' message, no
  stack trace (CONTRIBUTING QA matrix).

Verified locally:
- node --test tests/state-rebuild-cli.test.cjs → 5/5 pass
- node --test tests/state-rebuild.test.cjs → 18/18 pass (Phase 1 regression)
- node --test tests/state-transition.test.cjs → 85/85 pass (ADR-1769 regression)

Docs (docs/COMMANDS.md): document `state rebuild [--dry-run] [--verbose]`
with the canonical-command block format used by `state sync` /
`state prune`.

Changeset (.changeset/1817-state-rebuild.md): type=Added, user-facing
description of the new subcommand (closes #1817 epic on merge).
2026-06-29 16:15:55 -04:00
Rezolv
18995380ce feat(#1154): honest verifier — abstain (insufficient_spec) on non-inferable backstop truths (#1738)
* feat(verify-phase): honest verifier — abstain (insufficient_spec) on non-inferable backstop truths (#1154)

Carry the edge-probe's existing `backstop` (non-inferable) tier through the
plan-phase projection as a structured flat-scalar marker instead of a prose
parenthetical, and make verify-phase abstain -> human_needed (never silent-pass)
on a backstop truth it cannot confirm with explicit evidence. Truth-axis mirror
of #644's prohibition judgment-tier (ADR-550 D4).

Engine (deterministic, CI-tested per ADR-550 D5 — never the LLM verdict):
- src/probe-core.cts: truthStatement/truthVerification normalizers, projectTruths
  (conservative serializer), dispositionForUnverifiableTruth (backstop+no-evidence
  -> unverified/flagged/insufficient_spec; backstop+evidence -> green; inferable
  -> green, the over-abstention guard).
- src/roadmap.cts: coerceTruthToString now reads `statement` first so an object-form
  backstop truth is surfaced, not dropped (Hyrum backward-compat for truth-readers).

Workflow/agent/docs: plan-phase emits the structured marker (flat scalar, ADR-550
#1278); verify-phase + gsd-verifier add the abstain arm; new references/honest-verifier.md;
FEATURES/COMMANDS document insufficient_spec; ADR-550 amended (truth-axis D4 mirror).

Decisions adopted (trek-e review): insufficient_spec feeds existing human_needed with a
distinguishable reason (no new VERIFIER_STATUS); changeset Changed; round-trip parity
test; abstain-on-unconfirmed-backstop regression test red-first.

Implementation notes (deviations from the issue's proposed file list, verified live):
- frontmatter.cts needs no change — its flat parser already round-trips object-form truths.
- verify.cts needs no change — it grades artifacts/key_links structurally; truths are
  LLM-graded at the workflow layer, so consumption lives there + the deterministic helper.
- No CJS<->SDK hand-sync — the SDK seam was retired (ADR-0174); src/*.cts is sole source.

Regenerated artifacts: golden-install-parity fixtures, INVENTORY-MANIFEST, size baselines.

* chore(#1154): add changeset (Changed) for honest verifier

User-facing changelog fragment for #1738. Typed `Changed` (not `Added`) per
trek-e review condition 3 — the verify behavior shifts for backstop-bearing specs
(a confident silent `passed` becomes `human_needed`), which is user-visible even
though the schema marker is additive.

* docs(#1154): score-formula also excludes abstained insufficient_spec truths (review nit-1)

trek-e review nit: the verify-phase score sentence said PRESENT_BEHAVIOR_UNVERIFIED
truths were "the only ones excluded" from verified_truths. Post-#1154 an abstained
`insufficient_spec` backstop truth is also excluded (it is not ✓ VERIFIED and routes
to human_needed). Behavior was already correct; this tightens the wording.
Regenerated golden-install-parity fixtures + workflow-size baseline for the touched
verify-phase.md. (Nit-2 — a dedicated insufficient_spec_items frontmatter list — is
intentionally not taken: the current design is ADR-550-D4-conformant, the abstain
cause rides as a distinguishable report reason, and adding it would exceed the
approved scope.)

---------

Co-authored-by: Tom Boucher <trekkie@nomorestars.com>
2026-06-29 00:14:32 -04:00
Tom Boucher
ac001be49e fix(#1778): use 1.6 named-flag frontmatter.set form in thread workflow (#1816)
* fix(#1778): use 1.6 named-flag frontmatter.set form in thread workflow

The thread workflow's CLOSE and RESUME branches called frontmatter.set with
the pre-1.6 fully-positional shape (frontmatter.set <file> <field> <value>).
Since 1.6 the dispatcher (gsd-tools.cjs) parses the file positionally and
reads field/value from the named flags --field/--value via parseNamedArgs;
the positional form leaves field/value undefined, cmdFrontmatterSet errors
'file, field, and value required', and the status/updated writes are
silently skipped. Closing a thread never marked it status: resolved and
resuming never marked it status: in_progress.

Switch all four sites (CLOSE status+updated, RESUME status+updated) to the
1.6 hybrid form that verify-work.md already uses:
  frontmatter.set <file> --field <field> --value <value>

Add a regression test with three guards: (1) behavioral — the named-flag
form writes the field while the positional form errors with the documented
message and does not mutate the file; (2) workflow parity — no workflow
under gsd-core/workflows/ emits the positional form, so a future edit that
reintroduces it anywhere fails CI; (3) thread-specific — CLOSE writes
status: resolved and RESUME writes status: in_progress via the named flags.

* docs(#1778): add changeset fragment for thread workflow frontmatter fix

* docs(#1778): fix unclosed inline-code backtick in changeset fragment

* fix(#1778): move regression into owning test + regen baselines

lint-regression-test-names rejects new bug-NNNN-*.test.cjs files; move the
#1778 regression (behavioral named-vs-positional + workflow-parity scan +
thread CLOSE/RESUME assertions) into tests/frontmatter-cli.test.cjs, the
canonical home for frontmatter CLI regressions, and delete the standalone
file. frontmatter-cli.test.cjs already carries the allow-test-rule exemption
for workflow .md content tests.

gsd-core/workflows/thread.md ships to every runtime and is size-tracked, so
recapture the 16 golden-install-parity fixtures (thread.md hash) and the
per-file workflow size baseline (thread.md 12400 -> 12464) via UPDATE_GOLDEN=1
and npm run size:baseline.
2026-06-28 22:42:44 -04:00
Tom Boucher
fd576528a7 fix(#1747): register four search-provider keys in the config schema (#1814)
* fix(#1747): register four search-provider keys in the config schema

buildNewProjectConfig emits seven search-provider availability flags and
research-provider.cts providerAvailability() consumes all seven, but only
three were registered in VALID_CONFIG_KEYS (config-schema.manifest.json).
config-loader.cts then printed an 'unknown config key(s)' warning for the
four unregistered keys (tavily_search, ref_search, perplexity, jina) on
every freshly generated .planning/config.json.

Register the four missing keys in the schema manifest and document them
alongside brave/exa/firecrawl in CONFIGURATION.md. Add a regression test
plus a structural drift guard that requires every config-driven
research-provider flag to be in VALID_CONFIG_KEYS, so a future provider
addition cannot silently reintroduce the drift.

* fix(#1747): move regression into owning test file + add changeset

lint-regression-test-names rejects new bug-NNNN-*.test.cjs files; move the
#1747 regression (four provider keys in VALID_CONFIG_KEYS + provider-flag
drift guard) into tests/bug-2530-valid-config-keys.test.cjs, the canonical
home for VALID_CONFIG_KEYS regressions, and delete the standalone file.

Add the missing .changeset fragment — config-schema.manifest.json lives
under gsd-core/ (user-facing), so changeset-lint requires a fragment.

* test(#1747): regenerate golden-install-parity fixtures for schema change

Adding four provider keys to config-schema.manifest.json shifts its shipped
content hash (65dea848 -> 7d398e94); recapture all 16 runtime fixtures via
UPDATE_GOLDEN=1. Each fixture changes exactly one line — the manifest hash.
2026-06-28 22:42:36 -04:00
Tom Boucher
2d314c3a28 fix(#1772): read full multi-line command in graphify-update hook Gate 2 (#1815)
* fix(#1772): read full multi-line command in graphify-update hook Gate 2

The PostToolUse hook joined tool_name + newline + tool_input.command and
extracted the command with sed -n '2p' — line 2 only. Agent runtimes
(Claude Code's Bash tool among them) routinely emit HEAD-advancing commits
as multi-line scripts ('cd /path', then 'git add', then 'git commit …'), so
line 2 is the 'cd', Gate 2's *"git commit"* match failed, and the rebuild
silently no-op'd on real commits despite graphify.auto_update: true.

Capture line 2 through EOF (sed -n '2,$p') so the case glob sees the full
multi-line command string. Single-line behavior is unchanged (the match
only widens); non-HEAD-advancing multi-line commands still no-op cleanly.

Regression tests cover multi-line commit/merge/pull dispatch plus a
multi-line no-op no-regression guard.

* docs(#1772): add changeset fragment for graphify-update multi-line fix

* test(#1772): regenerate golden-install-parity fixtures for hook change

gsd-graphify-update.sh ships to 9 graphify-aware runtimes; widening the
sed range (2p -> 2,$p) shifts its shipped hash. Recapture the 9 affected
fixtures via UPDATE_GOLDEN=1 — each changes exactly one line (the hook hash).
2026-06-28 22:42:23 -04:00
Tom Boucher
4f6fda852e fix(#1591): phase.complete recognizes checkbox-list phases in isLastPhase fallback (#1819)
* fix(#1591): phase.complete recognizes checkbox-list phases in the isLastPhase fallback

When the active milestone's phase checklist is written as `- [ ] Phase N:`
checkbox items inside a <details> block (the @Azd325 structure) and the next
phase has no directory yet, the disk-based next-phase resolver finds nothing
and phase.complete falls back to the roadmap-enumeration guard at the
isLastPhase site. That guard's phasePattern was heading-only
(/#{2,4}\s*Phase…/), so it never matched checklist items → is_last_phase=true
and next_phase=null on a mid-milestone phase, and STATE.md was wrongly marked
'Milestone complete' with total_phases decremented.

Broaden the marker alternation to match BOTH heading-style (### Phase N:) and
checkbox-list items (- [ ] Phase N: / - [x] Phase N:); the number/name
captures are unchanged. extractCurrentMilestone already surfaces the
<details>-wrapped checklist correctly, so no parser change is needed. The
heading-only sibling patterns elsewhere in phase.cts are left untouched
(scope discipline — only the reproduced isLastPhase fallback is changed).

Regression: a phase complete 36 on a <details>-wrapped v2.0 checklist
(Phases 36-38, only 36 has a dir) returns is_last_phase=false, next_phase=37,
and does NOT flip STATE.md to 'Milestone complete'.

* docs(#1591): add changeset fragment for phase.complete checkbox-list fix

* test(#1752): add total_phases-preservation regression for the #1591 follow-up

#1752 is the scoped follow-up to #1591 — same <details>-wrapped-checkbox
defect, with the additional emphasis on the total_phases decrement cascade.
The #1591 fix (is_last_phase=false) already resolves it: with all 8 phase
dirs on disk, phase.complete 36 on a v2.0 <details> checklist leaves
total_phases at 8 (not decremented to 7) and does not flip STATE.md to
'Milestone complete'. Verified manually before adding the test.

Add the #1752 regression case (8 phase dirs, curated total_phases: 8) to the
phase complete command block in tests/phase.test.cjs, and update the changeset
to reference both issues (#1591, #1752) since this is one user-facing change
resolving both.
2026-06-28 22:41:57 -04:00
Tom Boucher
38c2c1805e fix(#1761): skip conflated progress in state json read-path when milestone unbounded (#1818)
* fix(#1761): skip conflated progress in state json read-path when milestone unbounded

ADR-1769 Phase 7 (#1794) closed the state sync WRITE path — when a milestone
version is asserted in frontmatter but the ROADMAP has no versioned heading
for it, sync leaves Progress untouched. But the state json READ path rebuilds
progress via buildStateFrontmatter, whose roadmapPhaseCount loop counts phase
headings across the WHOLE document when extractCurrentMilestone can't bound
the milestone. state json therefore reported a conflated total_phases (sum of
sibling milestones) + a derived percent — exactly the value the sync guard
was added to prevent. (Repro from the issue: total_phases 8 = 4+4, percent 13.)

Mirror the cmdStateSync guard inside buildStateFrontmatter: when the asserted
milestone cannot be bounded to a versioned ROADMAP heading (the same
versionedHeading test the sync path uses), fall back to the on-disk
phase-dir count for total_phases and skip percent. Bounded milestones
(versioned ROADMAP, or no milestone asserted) are unchanged. The signal rides
on the existing _diskScanCache (new milestoneBounded field) so neither
extractCurrentMilestone's return contract nor its other callers change.

Regression: extend tests/bug-1761-state-sync-wrong-progress.test.cjs with the
read-path case (unbounded → no percent, no conflated total_phases) and a
bounded control (versioned ROADMAP → unchanged percent + total_phases).

* docs(#1761): add changeset fragment for state json read-path fix
2026-06-28 22:41:38 -04:00
Tom Boucher
a47979bb92 refactor(#1679): ADR-1239 Phase B — collapse program + command chains [AC2 slice 4] (#1813)
* refactor(#1679): ADR-1239 Phase B — collapse program + command chains [AC2 slice 4]

Phase 2 AC2 slice 4. Collapses two more duplicated runtime->string chains in
bin/install.js's post-install next-step message:

- program (14 branches): an EXACT duplicate of runtimeLabel -> getRuntimeLabel.
- command (14 branches): the per-runtime /gsd-new-project invocation syntax
  (gemini '/gsd:', codex '$', cursor skill-mention, kimi '/skill:', default
  '/gsd-new-project') -> new getRuntimeNewProjectCommand(runtime) helper.

- src/runtime-name-policy.cts: RUNTIME_NEW_PROJECT_COMMANDS table +
  getRuntimeNewProjectCommand(runtime) (sibling to runtimeFlags/getRuntimeLabel).
- bin/install.js: import getRuntimeNewProjectCommand; replace the program +
  command chains with single lookups.
- tests/runtime-label-policy.test.cjs: 2 new tests for
  getRuntimeNewProjectCommand (4 overrides + default for the other 12).

runtime === count: 53 -> 25 (-28). Cumulative Phase 2 this session: 129 -> 25
(-104). golden-install-parity 16/16 (program/command are stdout-only so not
parity-covered, but program matches RUNTIME_LABELS exactly and command values
are preserved verbatim in the table). AC2 data-collapse now essentially
exhausted; remaining 25 branches are the ADR-1235 agent-loop tail + per-runtime
semantic behavior.

* chore(changeset): add Changed fragment for program+command collapse (#1679)
2026-06-28 15:22:13 -04:00
Tom Boucher
f954bb4cac refactor(#1679): ADR-1239 Phase B — collapse is<Runtime> flag blocks into runtimeFlags [AC2 slice 3] (#1811)
* refactor(#1679): ADR-1239 Phase B — collapse is<Runtime> flag blocks into runtimeFlags [AC2 slice 3]

Phase 2 AC2 slice 3. Collapses the four duplicated 'const isX = runtime === x'
declaration blocks in bin/install.js (uninstall / writeManifest / install / a
fourth helper — 48 of the 101 remaining runtime=== branches) into a single
runtimeFlags(runtime) helper in src/runtime-name-policy.cts, sibling to
getDirName / getRuntimeLabel / getGlobalConfigHomeFragment.

The purest add-a-host tax: a new runtime meant remembering to add ~12 flag lines
to each of four functions. Now it is one entry in RUNTIME_FLAG_IDS.

- src/runtime-name-policy.cts: RUNTIME_FLAG_IDS + runtimeFlags(runtime) -> frozen
  map of is<Runtime> booleans (single runtime=== source, via loop).
- bin/install.js: import runtimeFlags; replace the 4 declaration blocks with one
  destructure each. ZERO usage-site churn (flag names preserved; install.js's
  eslint block has no no-unused-vars rule so destructure-all is clean).
- tests/runtime-flags.test.cjs: 4 tests (each runtime sets exactly its flag,
  claude/unknown/empty -> all false, all 15 flags present + frozen, drift guard).

runtime === count: 101 -> 53 (-48). golden-install-parity 16/16 byte-identical
(behavior-identical collapse). AC2 data-collapse now substantially complete;
ADR-1235 agent-loop tail + per-runtime semantic residue remain (separate).

* chore(changeset): add Changed fragment for runtimeFlags collapse (#1679)
2026-06-28 14:59:29 -04:00
Tom Boucher
41193a44bd feat(#1681): ADR-1239 Phase C-2 — gsd-mcp-server bin entry + lifecycle test [slice 3b] (#1810)
* feat(#1681): ADR-1239 Phase C-2 — gsd-mcp-server bin entry + lifecycle test [slice 3b]

Phase 4 slice 3b (closes #1681). The companion MCP server bin entry so any
MCP-consuming host connects via 'npx gsd-mcp-server' (or its bin on PATH) and
gets GSD command (point 1) + state IO (point 5) with no bespoke plugin.

- gsd-core/bin/gsd-mcp-server.cjs: #!/usr/bin/env node shim requiring
  ./lib/mcp-server.cjs + runServer({stdin, stdout}); non-zero exit on fatal
  error (justified n/no-process-exit disable). Mirrors gsd-tools.cjs.
- package.json: add 'gsd-mcp-server' bin entry.
- tests/gsd-mcp-server-bin.test.cjs: 3 process-lifecycle tests — initialize +
  tools/list round-trip + clean exit, malformed-line -> parse error + server
  keeps running, empty stdin -> clean exit. Synchronous spawnSync (bounded;
  server exits on stdin EOF, no orphan).

Phase 4 trust-gate (#1806) + loader wiring (#1808) + server module (#1809) +
this bin/lifecycle slice = all of #1681's deliverables. Concrete host binding ->
Phase 5 (#1682). npm-integrity + eslint + security + inventory all clean.

* docs(#1681)+chore(changeset): how-to for the companion MCP server + Added fragment

docs/how-to/connect-gsd-mcp-server.md — Diataxis how-to guide for connecting
any MCP-capable host to gsd-mcp-server: goal-oriented flow (add config → restart
→ verify), real-world per-host conditionals, troubleshooting, and a trimmed
reference table. Explanation/reference linked out (ADR-1239, capability-trust-
model) per Diataxis boundary rules rather than mixed in.

.changeset/humble-seals-rest.md — type: Added (first user-reachable surface of
the epic: a new bin command). The how-to doc satisfies the docs-required gate.

* fix(#1681): move gsd-mcp-server shim to top-level bin/ (out of the runtime-copied tree)

The shim at gsd-core/bin/gsd-mcp-server.cjs was inside the tree the installer
copies into every runtime config dir, so it leaked into all 16 runtimes and
broke golden-install-parity. The MCP server is a PACKAGE bin the host spawns
(npx gsd-mcp-server), not a per-runtime artifact — so it belongs at top-level
bin/ alongside install.js (which is also never copied into a runtime config).

- gsd-core/bin/gsd-mcp-server.cjs -> bin/gsd-mcp-server.js (require path now
  ../gsd-core/bin/lib/mcp-server.cjs).
- package.json: bin entry -> bin/gsd-mcp-server.js.
- tests/gsd-mcp-server-bin.test.cjs: SHIM path updated.
- eslint.config.mjs: add bin/gsd-mcp-server.js to the bin/install.js block
  (drops the n/no-process-exit disable — the n plugin isn't loaded for that
  block, so the disable referenced an undefined rule).

golden-install-parity 16/16 restored; lifecycle + unit tests green; eslint 0;
lint:ci all ok.
2026-06-28 14:27:43 -04:00
Tom Boucher
2b38356275 feat(#1681): ADR-1239 Phase C-2 — companion MCP server module (points 1 + 5) [slice 3a] (#1809)
* feat(#1681): ADR-1239 Phase C-2 — companion MCP server module (points 1 + 5) [slice 3a]

Phase 4 slice 3a. A minimal, dependency-free stdio JSON-RPC 2.0 server exposing
two of the six interface points so any MCP-consuming host (Claude/Codex/OpenCode/
VS Code/Gemini/Cursor/Cline/Hermes) can drive GSD with no bespoke plugin:

- point 1 (command): tool gsd_invoke_command -> createHub/dispatch.
- point 5 (state IO): tools gsd_read_state / gsd_write_state -> the Phase 3
  stateIO seam (filesystem default).

- src/mcp-server.cts: handleMessage(request, ctx) pure JSON-RPC handler
  (initialize / tools/list / tools/call) + runServer({input, output}) thin
  line-delimited-JSON loop over injectable streams. 3 tools wired to the
  existing engine surfaces. NO new dependency (hand-rolled JSON-RPC; the repo
  ships only claude-agent-sdk + ws — an MCP SDK is a separate packaging call).
- tests/gsd-mcp-server.test.cjs: 9 tests (initialize, tools/list, state
  read/write round-trip, command dispatch, unknown tool / missing name /
  unknown method / notification / parse error, injectable-stream round-trip).

Bin entry / packaging / manifest-version-sync / process-lifecycle docs ->
slice 3b. This slice ships the importable, tested server surface a host (or the
bin shim) drives. Proactive CI gates: ADR-457 ignores + INVENTORY-MANIFEST +
injection-scan audit. All clean locally (9 tests + security 15/15 + inventory +
eslint 0 problems).

* chore(changeset): add Changed fragment for companion MCP server module (#1681)
2026-06-28 12:45:25 -04:00
Tom Boucher
d2518e142d feat(#1681): ADR-1239 Phase C-2 — wire trust gate into loadRegistry (configHome confinement) [slice 2] (#1808)
* feat(#1681): ADR-1239 Phase C-2 — wire trust gate into loadRegistry (configHome confinement) [slice 2]

Phase 4 slice 2. loadRegistry({includeInstalled:true, configHome}) now rejects
(skip + warn, fail-closed) any installed third-party descriptor whose declared
destSubpath resolves outside the supplied configHome, BEFORE it is composed.

- src/capability-loader.cts: LoadRegistryOptions.configHome?:string (optional,
  backward-compatible). Require external-descriptor-trust.cjs (typed). Before
  overlayCaps.push(cap), if configHome set, assertDescriptorConfined(cap,
  configHome) — on throw, skip('configHome confinement rejected: ...') +
  continue. Fail-closed via the loader's existing per-candidate skip semantics.
- tests/external-descriptor-loader-wiring.test.cjs: integration test — escaping
  overlay skipped with confinement reason when configHome set; confined overlay
  composes; omitted configHome = no load-time check (backward-compatible).

Defense-in-depth with Phase 2: load-time rejects malformed descriptors early
(this slice); install-time assertDestWithinConfigHome bounds actual writes
(#1679 AC3). Existing capability-loader.test.cjs 54/54 (no regression —
additive optional option). Companion MCP server -> slice 3.

* chore(changeset): add Changed fragment for loadRegistry configHome confinement wiring (#1681)
2026-06-28 12:25:02 -04:00
Tom Boucher
21b81ea068 feat(#1681): ADR-1239 Phase C-2 — external-descriptor trust gate (configHome confinement) [slice 1] (#1806)
* feat(#1681): ADR-1239 Phase C-2 — external-descriptor trust gate (configHome confinement) [slice 1]

Phase 4 slice 1. Load-time, fail-closed configHome write-confinement for
installed third-party host-plugin descriptors — defense-in-depth on top of the
existing opt-in/schema/consent/first-party-wins loader gates + Phase 2's
install-time assertDestWithinConfigHome (#1679 AC3).

- src/external-descriptor-trust.cts: isPathConfined(target, root) pure
  cross-platform containment primitive + assertDescriptorConfined(descriptor,
  configHome) — walks runtime.artifactLayout global/local destSubpaths, throws
  fail-closed (naming descriptor + path) on the first escape. Rejects ../escape
  + absolute-outside-root. Missing layout / invalid entries skipped.
- tests/external-descriptor-confinement.test.cjs: 7 tests (containment
  primitive, benign passes, global/local/absolute escapes rejected, missing
  layout, invalid entries).

Load-time twin of Phase 2's install-time gate — rejects malformed/escaping
descriptors BEFORE consent even matters. NOT wired into loadRegistry yet
(slice 2, 4 callers, medium blast radius); this ships the reusable gate + tests.
Not the ADR-1577 prompt-injection breaker (separate concern, shared word trust).

Proactive CI gates: ADR-457 ignores + INVENTORY-MANIFEST + injection-scan audit.
All clean locally (7 tests + security + inventory + eslint 0 problems).

* chore(changeset): add Changed fragment for external-descriptor trust gate (#1681)
2026-06-28 12:00:43 -04:00
Tom Boucher
abd21b2968 feat(#1680): ADR-1239 Phase C-1 — hook-bus + stateIO seams [AC4] (#1805)
* feat(#1680): ADR-1239 Phase C-1 — hook-bus + stateIO seams [AC4]

Phase 3 slice 4 (AC4, final #1680 slice). The last two adapter seams behind
the negotiated hookBus/stateIO axes:

- src/hook-bus.cts: createHookBus({bus}, {hostEmit?}) -> host/engine/none.
  engine = in-process pub/sub (handler errors isolated); host = host-owned,
  fail-closed emit until a host emitter is bound; none = silent no-op (degrade
  to rule-text). PORTABLE_EVENT_FLOOR = SessionStart/PreToolUse/PostToolUse/
  Stop/SessionEnd (the claude dialect all hook hosts share).
- src/state-io.cts: createStateIO({io}, {backend?}) -> filesystem (today's
  behavior — straight fs) / sandboxed-storage / session-log-append (fail-closed
  seams until a host backend is bound).

Proactive CI gates: ADR-457 ignores + INVENTORY-MANIFEST entries for both new
.cjs; injection-scan 'act as' substring audit; unused-import check. All clean
locally (11 tests + security 15/15 + inventory + eslint 0 problems).

Phase 3 (#1680) seam layer now complete. Concrete host binding -> Phase 5
(#1682, D15/D18).

* chore(changeset): add Changed fragment for hook-bus + stateIO seams (#1680)
2026-06-28 11:44:36 -04:00
Tom Boucher
b152f7e64c feat(#1680): ADR-1239 Phase C-1 — model adapter seam (passive + active) [AC3] (#1804)
* feat(#1680): ADR-1239 Phase C-1 — model adapter seam (passive + active) [AC3]

Phase 3 slice 3 (AC3). Two model-layer adapters selected by the negotiated
modelMode axis (host-integration.cts):

- passive: formalizes today's tier routing from src/model-resolver.cts —
  resolveModel delegates straight to resolveModelForTier (byte-for-behavior).
  This is the CLI runtimes (claude/gemini/codex/opencode/cursor/...): GSD injects
  prompts / a per-agent model field.
- active: a host-supplied sendRequest seam (VS Code vscode.lm / pi providers) —
  GSD calls the model through the host. Ships as a fail-closed seam (throws until
  a provider is bound); Phase 5 wires a concrete provider.

createModelAdapter({modelMode}, {sendRequest?}) — factory gating throws on
invalid mode. Proactive CI gates applied: ADR-457 eslint ignores entry +
INVENTORY-MANIFEST cli_modules entry + comment-wording audited for the
injection-scan substring trap.

* chore(changeset): add Changed fragment for model adapter seam (#1680)
2026-06-28 11:27:44 -04:00
Behruz Nassre Esfahani
f9143a400c chore(#1698): add changeset
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 08:15:37 -07:00
Tom Boucher
da368311ea feat(#1680): ADR-1239 Phase C-1 — imperative embedding adapter (composes loadRegistry) [AC2] (#1803)
* feat(#1680): ADR-1239 Phase C-1 — imperative embedding adapter (composes loadRegistry) [AC2]

Phase 3 slice 2 (AC2). The engine-as-library path: createImperativeAdapter
composes loadRegistry({includeInstalled:true}) — first-party-wins + consent +
fail-closed gates, identical trust semantics to the CLI — and binds the engine
surface behind the SAME HostIntegrationInterface the declarative adapter (AC1)
satisfies, plus a registry accessor for the composed capability set.

- src/adapter-imperative.cts: createImperativeAdapter({runtime}, {loadOptions})
  → ImperativeAdapter (kind:'imperative' + .registry + install/uninstall
  delegating to install-engine). Thin: delegates the loop, does not reimplement.
- tests/adapter-imperative.test.cjs: kind (16 runtimes), registry composition
  (loadRegistry called with includeInstalled:true), loadOptions pass-through,
  install/uninstall delegation, fail-closed construction.
- eslint.config.mjs + docs/INVENTORY-MANIFEST.json: ADR-457 ignores entry +
  cli_modules entry for the new emitted .cjs (the two drift gates that bit AC1,
  applied proactively here).

Concrete host binding (OpenCode/VS Code/pi) deferred to Phase 5 (#1682).

* test: remove dead readStateMd helper from bug-1760 test

readStateMd was defined but never called (writeStateMd is the only state-md
helper this test uses). Clears the lone no-unused-vars warning so the repo
lints fully clean (0 problems). No behavior change — test still passes 2/2.

* chore(changeset): add Changed fragment for imperative embedding adapter (#1680)

* fix(adapter-imperative): reword comment to avoid injection-scan substring match

The prompt-injection scan regex 'act\s+as\s+(?:a|an|the)' was matching the
'act as the' substring inside 'contract as the declarative adapter' (contrACT
AS THE). Reword 'contract as' -> 'shape as' — no 'act' substring, identical
meaning. Clears the 'lib source files are clean' + 'codebase prompt injection
scan' security-gate failures.
2026-06-28 11:10:59 -04:00