diff --git a/.changeset/1528-verify-work-security-blocked-next-phase.md b/.changeset/1528-verify-work-security-blocked-next-phase.md new file mode 100644 index 000000000..2525e0ef4 --- /dev/null +++ b/.changeset/1528-verify-work-security-blocked-next-phase.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 1687 +--- +The `verify-work` security-blocked presentation no longer offers next-phase planning. When security enforcement blocks phase advancement (no `SECURITY.md` produced), the workflow now routes only to the current-phase fix instead of competing `/gsd:plan-phase {next}` and `/gsd:execute-phase {next}` options. diff --git a/.changeset/1591-phase-complete-details-wrapped-checklist.md b/.changeset/1591-phase-complete-details-wrapped-checklist.md new file mode 100644 index 000000000..1c2ef3625 --- /dev/null +++ b/.changeset/1591-phase-complete-details-wrapped-checklist.md @@ -0,0 +1,6 @@ +--- +type: Fixed +pr: 1819 +--- +**`phase.complete` no longer reports a false `is_last_phase` on a `
`-wrapped checkbox checklist (#1591, #1752)** — when the active milestone's phase checklist was written as `- [ ] Phase N:` checkbox items inside a `
` block and the next phase had no directory on disk yet (still in planning), `phase.complete`'s `isLastPhase` roadmap-enumeration fallback used a heading-only pattern (`/#{2,4}\s*Phase…/`) that never matched checkbox items. It returned `is_last_phase: true, next_phase: null` on a mid-milestone phase and — via the milestone-complete cascade — wrongly flipped STATE.md to `Milestone complete` and decremented `progress.total_phases` (e.g. 8 → 7). The pattern now matches heading-style (`### Phase N:`), plain checkbox-list phases (`- [ ] Phase N:` / `- [x] Phase N:`), and the canonical **bold** checklist form the roadmap template emits (`- [ ] **Phase N: Name**`); `extractCurrentMilestone` already surfaces the `
`-wrapped checklist correctly, so no parser change was needed. Only the reproduced `phase.complete` fallback is changed; the heading-only sibling patterns elsewhere in `phase.cts` are untouched. + diff --git a/.changeset/1682-opencode-mcp-binding.md b/.changeset/1682-opencode-mcp-binding.md new file mode 100644 index 000000000..4b71a79c5 --- /dev/null +++ b/.changeset/1682-opencode-mcp-binding.md @@ -0,0 +1,6 @@ +--- +type: Added +pr: 1929 +--- + +**OpenCode installs now auto-register the GSD companion MCP server (`mcp.gsd`)** — `--opencode` install writes a `mcp.gsd` entry (local stdio → `gsd-mcp-server`) into `opencode.json`, so OpenCode drives GSD's command + planning-state surface over MCP with no bespoke plugin (ADR-1239 Phase D / #1682). Idempotent and non-clobbering; a user-defined `mcp.gsd` is preserved. (#1682) diff --git a/.changeset/1682-opencode-subset-dialect.md b/.changeset/1682-opencode-subset-dialect.md new file mode 100644 index 000000000..ef9e18138 --- /dev/null +++ b/.changeset/1682-opencode-subset-dialect.md @@ -0,0 +1,6 @@ +--- +type: Added +pr: 1930 +--- + +**OpenCode plugin handles `session.idle` + the `opencode-subset` hook dialect is implemented** — the GSD OpenCode plugin now recognizes `session.idle` (↔ Claude `Stop` lifecycle point), completing the compaction/idle pair (#1914 shipped compaction). The reserved `opencode-subset` dialect gains a consumer — `hookEventSurfaceFor()` in `host-integration.cts` — describing OpenCode's session/tool/file event subset (no workflow-phase events; the engine owns phase sequencing, ADR-1239 §OpenCode binding). Adds a Claude-parity test asserting the plugin covers the full declared subset. (#1682) diff --git a/.changeset/1747-new-project-search-provider-keys.md b/.changeset/1747-new-project-search-provider-keys.md new file mode 100644 index 000000000..08f8fdbe0 --- /dev/null +++ b/.changeset/1747-new-project-search-provider-keys.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 1814 +--- +**`/gsd-settings` no longer warns about four search-provider keys on fresh projects (#1747)** — `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`). Running `/gsd-settings` on a freshly generated `.planning/config.json` printed `unknown config key(s) … tavily_search, ref_search, perplexity, jina — these will be ignored` even though the user never hand-edited the config. The four missing keys are now registered alongside `brave_search`/`firecrawl`/`exa_search` and documented in `docs/CONFIGURATION.md`; a drift guard in `tests/bug-2530-valid-config-keys.test.cjs` now requires every config-driven research-provider flag to be in the schema, so a future provider addition cannot reintroduce the drift. diff --git a/.changeset/1761-state-json-unbounded-milestone-read-path.md b/.changeset/1761-state-json-unbounded-milestone-read-path.md new file mode 100644 index 000000000..bd1c4f043 --- /dev/null +++ b/.changeset/1761-state-json-unbounded-milestone-read-path.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 1818 +--- +**`gsd-tools state json` no longer reports conflated progress for an unversioned milestone (#1761)** — the ADR-1769 Phase 7 fix (#1794) taught `state sync` to leave Progress untouched when a milestone version is asserted but the ROADMAP has no versioned heading for it, but the `state json` **read** path still rebuilt progress via `buildStateFrontmatter`, whose phase-heading count fell back to the whole document and summed sibling milestones. `state json` therefore reported a conflated `total_phases` (e.g. 8 = 4+4 across two milestones) plus a derived `percent`, contradicting the sync guard on the very same project. The read path now mirrors the sync guard: when the asserted milestone cannot be bounded to a versioned ROADMAP heading, `total_phases` falls back to the on-disk phase-dir count and `percent` is omitted. Bounded milestones (versioned ROADMAP, or no milestone asserted) are unchanged; the signal rides on the existing `_diskScanCache` so `extractCurrentMilestone`'s return contract and its other callers are untouched. diff --git a/.changeset/1772-graphify-update-multi-line-command.md b/.changeset/1772-graphify-update-multi-line-command.md new file mode 100644 index 000000000..7e188785c --- /dev/null +++ b/.changeset/1772-graphify-update-multi-line-command.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 1815 +--- +**`gsd-graphify-update.sh` now reads the full multi-line command in Gate 2 (#1772)** — the PostToolUse auto-update hook joined `tool_name` + `\n` + `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 was the `cd`, Gate 2's `*"git commit"*` match failed, and the rebuild silently no-op'd on real commits even with `graphify.auto_update: true`. The failure was invisible in manual probes because a single-line `git commit -m x` passes line 2 verbatim. The hook now captures line 2 through EOF (`sed -n '2,$p'`) so the `case` glob sees the full command string; single-line behavior is unchanged and multi-line commands without a HEAD-advancing op still no-op cleanly. diff --git a/.changeset/1778-thread-workflow-frontmatter-set-args.md b/.changeset/1778-thread-workflow-frontmatter-set-args.md new file mode 100644 index 000000000..5117b9fae --- /dev/null +++ b/.changeset/1778-thread-workflow-frontmatter-set-args.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 1816 +--- +**`/gsd-thread close|resume` now writes the thread status/updated frontmatter (#1778)** — the thread workflow's CLOSE and RESUME branches invoked `frontmatter.set` with the pre-1.6 fully-positional shape (`frontmatter.set `), but since 1.6 the dispatcher parses the file positionally and reads `field`/`value` from the named flags `--field`/`--value` via `parseNamedArgs`. The positional form left `field`/`value` undefined, `cmdFrontmatterSet` errored `file, field, and value required`, and the writes were silently skipped — so closing a thread never marked it `status: resolved` and resuming never marked it `status: in_progress`, with the error scrolling past on every thread command. All four sites (CLOSE `status`+`updated`, RESUME `status`+`updated`) now use the 1.6 hybrid form that `verify-work.md` already uses (`frontmatter.set --field --value `). diff --git a/.changeset/1817-state-rebuild.md b/.changeset/1817-state-rebuild.md new file mode 100644 index 000000000..8a7ddd6a3 --- /dev/null +++ b/.changeset/1817-state-rebuild.md @@ -0,0 +1,5 @@ +--- +type: Added +pr: 1830 +--- +**`gsd-tools state rebuild`** — new subcommand that re-derives STATE.md body structure from canonical sources (frontmatter + `.planning/phases/` disk scan), reconciling drifted `## Current Position` prose, dropping orphaned rows from the `**By Phase:**` table, clearing template-placeholder field values, and de-duplicating `## Session Continuity Archive` blocks. Every mutation is recorded in a `## Rebuild Log` audit section. Idempotent (running twice on a clean file is a no-op). Supports `--dry-run` (preview) and `--verbose` (tee log to stderr). Heavier, manual counterpart to the lightweight auto-triggered `state sync`. diff --git a/.changeset/1847-claude-sonnet-5-standard-tier.md b/.changeset/1847-claude-sonnet-5-standard-tier.md new file mode 100644 index 000000000..34682694f --- /dev/null +++ b/.changeset/1847-claude-sonnet-5-standard-tier.md @@ -0,0 +1,7 @@ +--- +type: Added +pr: 1848 +--- +**Claude Sonnet 5 is now the `standard` (sonnet) tier model.** The model catalog and provider presets resolve the sonnet/standard tier to `claude-sonnet-5` (GA 2026-06-30) across the Anthropic-backed runtimes (`claude`, `copilot`, and the `anthropic`/`anthropic-fable` presets), plus the OpenRouter-style `anthropic/claude-sonnet-5` for `opencode`/`hermes`, replacing the superseded `claude-sonnet-4-6`. Opus and Haiku tier defaults are unchanged (the `haiku` high-effort preset's escalation slot tracks the current sonnet model). Shipped in 1.6.1. (#1847) + + diff --git a/.changeset/1906-node-test-causation-mandatory.md b/.changeset/1906-node-test-causation-mandatory.md new file mode 100644 index 000000000..4c6fc413e --- /dev/null +++ b/.changeset/1906-node-test-causation-mandatory.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 2001 +--- +**Node-test prohibition proofs now require a clean-fixture causation control** — a `node-test` prohibition's fail-first proof no longer accepts a deceptive content-independent negative test (one that reds merely because `GSD_PROHIB_SUBJECT` is *set*, ignoring the subject's content). The `check_clean_fixture` control is now **mandatory** for the `node-test` kind: a descriptor that omits it is un-provable and hard-gates, rather than greening on the violation alone. **Breaking (Hyrum):** a previously-green node-test prohibition with no clean fixture now hard-gates — blast radius is zero in-tree (no `node-test` prohibition ships today). The `lint-rule` kind is unchanged (its subject IS the linted file, no `GSD_PROHIB_SUBJECT` indirection). (#1906) diff --git a/.changeset/1920-installer-capability-generators-host-version.md b/.changeset/1920-installer-capability-generators-host-version.md new file mode 100644 index 000000000..37d491fe0 --- /dev/null +++ b/.changeset/1920-installer-capability-generators-host-version.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 1938 +--- +**Third-party capabilities now work on installed layouts.** `capability install` no longer rejects capabilities with a real `engines.gsd` range as "incompatible with GSD 0.0.0" — the host version is now read from the authoritative `gsd-core/VERSION` file across every runtime and the `capability install` CLI. The installer also now ships the registry generator scripts (`gen-capability-registry.cjs`, `gen-loop-host-contract.cjs`), so installed third-party capabilities actually compose into the loop instead of being silently discarded. diff --git a/.changeset/agile-newts-roar.md b/.changeset/agile-newts-roar.md new file mode 100644 index 000000000..a767ec9c2 --- /dev/null +++ b/.changeset/agile-newts-roar.md @@ -0,0 +1,7 @@ +--- +type: Changed +pr: 1813 +--- +**Internal: the installer's `program` (display-name) + `command` (slash-invocation) chains are now single-source lookups** — the 14-line `program` chain (an exact duplicate of `runtimeLabel`) → `getRuntimeLabel`, and the 14-line `command` chain (the per-runtime `/gsd-new-project` syntax: gemini `/gsd:`, codex `$`, cursor skill-mention, kimi `/skill:`, default `/gsd-new-project`) → new `getRuntimeNewProjectCommand(runtime)` helper (ADR-1239 Phase B / #1679 AC2 slice 4). `runtime ===` count in `bin/install.js`: 53 → 25 (cumulative this session: 129 → 25). Stdout strings preserved byte-for-byte; no install-output change (golden-parity 16/16). No user-facing change. + + diff --git a/.changeset/bold-orcas-wander.md b/.changeset/bold-orcas-wander.md new file mode 100644 index 000000000..dbdce1386 --- /dev/null +++ b/.changeset/bold-orcas-wander.md @@ -0,0 +1,7 @@ +--- +type: Changed +pr: 1811 +--- +**Internal: the installer's per-function `is` flag-declaration blocks are now a single `runtimeFlags` lookup** — the four duplicated `const isX = runtime === 'x'` blocks in `bin/install.js` (uninstall / writeManager / install / a fourth helper — 48 branches) are collapsed into one `runtimeFlags(runtime)` helper in `runtime-name-policy.cts` (ADR-1239 Phase B / #1679 AC2 slice 3). The add-a-host tax for flags is removed (one `RUNTIME_FLAG_IDS` entry, not four declaration blocks). Install output is byte-identical for all 16 runtimes (golden-parity asserted); `runtime ===` count in `bin/install.js`: 101 → 53. No user-facing change. + + diff --git a/.changeset/bold-ravens-wake.md b/.changeset/bold-ravens-wake.md new file mode 100644 index 000000000..0d1b55798 --- /dev/null +++ b/.changeset/bold-ravens-wake.md @@ -0,0 +1,7 @@ +--- +type: Changed +pr: 1808 +--- +**Internal: third-party descriptor loader enforces `configHome` write-confinement at load time** — `loadRegistry({includeInstalled:true, configHome})` now rejects (skip + warn, fail-closed) any installed third-party host-plugin descriptor whose declared `destSubpath` resolves outside the supplied `configHome`, before it is composed into the registry (ADR-1239 Phase C-2 / #1681 slice 2). The `configHome` option is optional and backward-compatible (omitted → no load-time check; install-time gate still bounds writes). No user-facing change for existing flows. + + diff --git a/.changeset/calm-eagles-tumble.md b/.changeset/calm-eagles-tumble.md new file mode 100644 index 000000000..d2131759c --- /dev/null +++ b/.changeset/calm-eagles-tumble.md @@ -0,0 +1,6 @@ +--- +type: Added +pr: 1965 +--- + +**GSD now ships a pi extension** — a real, jiti-loadable ExtensionAPI module (`pi/gsd.cjs`) that registers `/gsd` (dispatches through the GSD command-routing hub) + `gsd_invoke` tool + `tool_call` event, installable at `~/.pi/agent/extensions/`. A reachability test proves the `/gsd` handler dispatches through the engine (keystone wired, not just registered on a mock). (#1965) diff --git a/.changeset/clever-rams-march.md b/.changeset/clever-rams-march.md new file mode 100644 index 000000000..3912be397 --- /dev/null +++ b/.changeset/clever-rams-march.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 1910 +--- +Fixed: probe-core's runProbeCli now fails closed on per-item adapter garbage inside a well-shaped report envelope, matching its documented 'fails closed on adapter garbage' contract. diff --git a/.changeset/curious-koalas-gather.md b/.changeset/curious-koalas-gather.md new file mode 100644 index 000000000..263a88a61 --- /dev/null +++ b/.changeset/curious-koalas-gather.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 1722 +--- +**`/gsd:verify-work` no longer silently terminates when all remaining UAT tests are blocked** — sessions with `blocked_count > 0` and `pending_count == 0` now route to `complete_session` as expected, enabling the zero-issues auto-transition path. diff --git a/.changeset/daring-badgers-forage.md b/.changeset/daring-badgers-forage.md new file mode 100644 index 000000000..f644f7212 --- /dev/null +++ b/.changeset/daring-badgers-forage.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 1709 +--- +Codex reviewer now captures the review via codex's --output-last-message flag instead of redirecting stdout, so Windows process-teardown output no longer pollutes the review file and slips past the empty-output guard. diff --git a/.changeset/daring-deer-leap.md b/.changeset/daring-deer-leap.md new file mode 100644 index 000000000..310fb39bd --- /dev/null +++ b/.changeset/daring-deer-leap.md @@ -0,0 +1,7 @@ +--- +type: Changed +pr: 1802 +--- +**Internal: the declarative embedding adapter is now named + bound behind a minimal `HostIntegrationInterface`** — `createDeclarativeAdapter({runtime})` (new `src/adapter-declarative.cts`) delegates in-process to `install-engine`'s `installRuntimeArtifacts`/`uninstallRuntimeArtifacts`, formalizing today's projection path as one of the two embedding adapters behind a common contract (ADR-1239 Phase C-1 / #1680 AC1). Output is byte-identical to today's install (gated by `golden-install-parity`). The full 6-point interface binding surface is deferred until the imperative adapter (AC2) fixes the shape (ADR-1239 open wire-shape question). No user-facing change — the adapter is not yet wired to any runtime path. + + diff --git a/.changeset/eager-ibex-bark.md b/.changeset/eager-ibex-bark.md new file mode 100644 index 000000000..467e97c0e --- /dev/null +++ b/.changeset/eager-ibex-bark.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 1844 +--- +**`init milestone-op` now counts project_code-prefixed phase directories correctly** — fully shipped milestones using the standard prefixed directory layout no longer report `completed_phases: 0` or stay falsely incomplete. (#1844) diff --git a/.changeset/gallant-cats-hum.md b/.changeset/gallant-cats-hum.md new file mode 100644 index 000000000..e795dba0b --- /dev/null +++ b/.changeset/gallant-cats-hum.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 1846 +--- +**Autonomous reruns now skip phases with deferred verification until you resume them explicitly** — if a prior `/gsd-autonomous` run recorded `verification_deferred_human` or `verification_deferred_gaps`, later reruns no longer drop back into the same prompt loop and instead point you at the saved resume command. (#1846) diff --git a/.changeset/gallant-wasps-wave.md b/.changeset/gallant-wasps-wave.md new file mode 100644 index 000000000..118c6ddc1 --- /dev/null +++ b/.changeset/gallant-wasps-wave.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 1832 +--- +state prune now resolves the current phase from the canonical location — frontmatter current_phase, the Current Phase field, or the prose Phase: line scoped to the ## Current Position section — instead of extracting Phase over the whole document, where stateExtractField's pipe-table fallback could latch onto an unrelated | Phase | N | row (e.g. a historical verification table) and compute a wrong prune cutoff. diff --git a/.changeset/graceful-badgers-click.md b/.changeset/graceful-badgers-click.md new file mode 100644 index 000000000..cec90ae43 --- /dev/null +++ b/.changeset/graceful-badgers-click.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 1765 +--- +Phase headers that place a parenthetical tag before the colon (`### Phase 26 (Cluster B): Title`) now resolve and enumerate the same as untagged headers. Previously the resolver returned not-found and `roadmap analyze`/listing silently dropped the phase (wrong phase_count, progress, and next_phase). Tag tolerance is applied at every phase-header read site; untagged and all existing header formats parse unchanged. diff --git a/.changeset/graceful-geese-click.md b/.changeset/graceful-geese-click.md new file mode 100644 index 000000000..6f65b9056 --- /dev/null +++ b/.changeset/graceful-geese-click.md @@ -0,0 +1,7 @@ +--- +type: Changed +pr: 1809 +--- +**Internal: companion MCP server module (interface points 1 + 5)** — `handleMessage`/`runServer` (new `src/mcp-server.cts`) is a minimal, dependency-free stdio JSON-RPC 2.0 server exposing `gsd_invoke_command` (→ the command-routing hub) + `gsd_read_state`/`gsd_write_state` (→ the Phase 3 stateIO seam), so any MCP-consuming host can drive GSD with no bespoke plugin (ADR-1239 Phase C-2 / #1681 slice 3a). Bin entry / packaging deferred to slice 3b. No user-facing change — the server is not yet wired to a bin entry. + + diff --git a/.changeset/graceful-moles-gather.md b/.changeset/graceful-moles-gather.md new file mode 100644 index 000000000..fbc0e4a1a --- /dev/null +++ b/.changeset/graceful-moles-gather.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 1873 +--- +Executor and milestone-summary/forensics workflows now call state.* commands with named flags so the named-only router records metrics, decisions, blockers, and session continuity instead of silently dropping positional args. diff --git a/.changeset/graceful-wasps-caper.md b/.changeset/graceful-wasps-caper.md new file mode 100644 index 000000000..a0eadf8dd --- /dev/null +++ b/.changeset/graceful-wasps-caper.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 1927 +--- +**bug-1367 install test no longer fails on Windows CI when hooks/dist isn't pre-built** — the test ran install.js without building its hooks/dist precondition (a gitignored build artifact the unit lane doesn't build), so on a lane without pre-built hooks the installer hit "Failed to install hooks: directory is empty" and the before-hook threw. The test now builds hooks in its own before() (mirroring golden-install-parity). (#1926) diff --git a/.changeset/happy-jays-travel.md b/.changeset/happy-jays-travel.md new file mode 100644 index 000000000..c580ce023 --- /dev/null +++ b/.changeset/happy-jays-travel.md @@ -0,0 +1,7 @@ +--- +type: Changed +pr: 1803 +--- +**Internal: the imperative embedding adapter now composes the capability registry behind the same `HostIntegrationInterface`** — `createImperativeAdapter({runtime})` (new `src/adapter-imperative.cts`) calls `loadRegistry({includeInstalled:true})` (first-party-wins + consent + fail-closed — identical trust semantics to the CLI) and binds the engine surface behind the same contract the declarative adapter (AC1) satisfies, plus a `registry` accessor for an in-process host to bind its primitives to (ADR-1239 Phase C-1 / #1680 AC2). Concrete host binding is deferred to Phase 5. No user-facing change — the adapter is not yet wired to any runtime path. + + diff --git a/.changeset/hooks-dist-scoped-ci-race.md b/.changeset/hooks-dist-scoped-ci-race.md new file mode 100644 index 000000000..906946dfd --- /dev/null +++ b/.changeset/hooks-dist-scoped-ci-race.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 1968 +--- +Build the gitignored `hooks/dist/` artifact once upfront in `scripts/run-tests.cjs` (the same chokepoint as `ensureBuiltArtifacts`), before any concurrent install test spawns `install.js`. Closes the scoped-CI first-build empty-dir race that intermittently failed install tests with `Failed to install hooks: directory is empty` (e.g. `bug-3683-workflow-colon-namespace-leak`). (#1967) diff --git a/.changeset/humble-dogs-gather.md b/.changeset/humble-dogs-gather.md new file mode 100644 index 000000000..6a0cc0e60 --- /dev/null +++ b/.changeset/humble-dogs-gather.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 1916 +--- +**workstream progress no longer reports shipped milestones as `executing`** — `gsd-tools workstream progress` now derives each workstream's status from authoritative shipped signals (an archived milestone snapshot under milestones/, or a SHIPPED marker in the workstream ROADMAP) instead of trusting the mutable STATE.md `Status` field, so a stale field can never hide a shipped/archived milestone. The output adds `status_source` (`field` | `derived`) and `status_conflict` (true when the derived value disagrees with the stale field). (#1913) diff --git a/.changeset/humble-geese-roam.md b/.changeset/humble-geese-roam.md new file mode 100644 index 000000000..a3789d05a --- /dev/null +++ b/.changeset/humble-geese-roam.md @@ -0,0 +1,7 @@ +--- +type: Changed +pr: 1804 +--- +**Internal: the model adapter seam exposes `passive` + `active` adapters selected by `modelMode`** — `createModelAdapter({modelMode})` (new `src/model-adapter.cts`): `passive` formalizes today's tier routing (delegates to `model-resolver.resolveModelForTier`), `active` is a host-supplied `sendRequest` seam (VS Code `vscode.lm` / pi providers), fail-closed until Phase 5 binds a concrete provider (ADR-1239 Phase C-1 / #1680 AC3). No user-facing change — the seam is not yet wired to any runtime path. + + diff --git a/.changeset/humble-seals-rest.md b/.changeset/humble-seals-rest.md new file mode 100644 index 000000000..e5392f53a --- /dev/null +++ b/.changeset/humble-seals-rest.md @@ -0,0 +1,5 @@ +--- +type: Added +pr: 1810 +--- +**`gsd-mcp-server` — companion MCP server (interface points 1 + 5)** — a new bin command (`npx @opengsd/gsd-core gsd-mcp-server`) runs a stdio JSON-RPC 2.0 MCP server exposing `gsd_invoke_command` (→ the GSD command-routing hub) + `gsd_read_state` / `gsd_write_state` (→ `.planning/` state), so any MCP-consuming host (Claude Code, Codex, OpenCode, VS Code, Gemini CLI, Cursor, Cline, Hermes) can drive GSD with no bespoke plugin (ADR-1239 Phase C-2 / #1681). Dependency-free (hand-rolled JSON-RPC). How-to: `docs/how-to/connect-gsd-mcp-server.md`. diff --git a/.changeset/humble-tunas-munch.md b/.changeset/humble-tunas-munch.md new file mode 100644 index 000000000..0c5430279 --- /dev/null +++ b/.changeset/humble-tunas-munch.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 1807 +--- +reconstructFrontmatter now emits valid YAML for scalars and block-array items that were previously serialized unescaped. Values carrying a YAML indicator plus a literal quote/backslash, embedded control characters, the empty string, a leading YAML indicator, or leading/trailing whitespace are now routed through a properly escaped double-quoted form, so frontmatter round-trips through strict parsers (js-yaml, PyYAML) instead of corrupting the block on the next state sync. diff --git a/.changeset/humble-zebras-zip.md b/.changeset/humble-zebras-zip.md new file mode 100644 index 000000000..593da8622 --- /dev/null +++ b/.changeset/humble-zebras-zip.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 1919 +--- +**`phases clear` archives phase directories instead of destroying them** — at a milestone switch, committed phase directories were hard-deleted (`rmSync`) with no archive, silently losing browsable phase history (the #1447 dirty-tree guard was a no-op for the common committed case). Phase directories are now moved to `milestones/-phases/` (collision-safe; timestamp fallback when no version resolves), so history survives the switch. The #1447 uncommitted-changes guard is retained as a secondary backstop. (#1871) diff --git a/.changeset/kind-tigers-romp.md b/.changeset/kind-tigers-romp.md new file mode 100644 index 000000000..7ff5f7fc5 --- /dev/null +++ b/.changeset/kind-tigers-romp.md @@ -0,0 +1,5 @@ +--- +type: Added +pr: 1998 +--- +**Long-running compute can now be externalized as async external jobs instead of blocking the agent turn** — a default-off external-job capability lets executors submit SLURM jobs, commit a .planning/async-jobs manifest, defer SUMMARY.md, and return external_job_waiting; the core loop already reconciles these manifests (#1165), so this adds the producer half (SLURM adapter, pure manifest module, planner/executor fragments, operation policy). (#1105) diff --git a/.changeset/lively-lynx-snooze.md b/.changeset/lively-lynx-snooze.md new file mode 100644 index 000000000..084679130 --- /dev/null +++ b/.changeset/lively-lynx-snooze.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 1918 +--- +**`/gsd-progress` no longer reports a stale root milestone in workstream mode** — in a multi-workstream project with no active workstream set, `gsd-tools query init.progress` silently fell back to root `.planning/STATE.md` (often stale) and reported it confidently. It now fails safe with an actionable error naming the available workstreams and the `--ws`/`workstream set` fix, so a stale root value is never reported. Flat mode and `--ws ` are unchanged. (#1912) diff --git a/.changeset/lucky-tigers-leap.md b/.changeset/lucky-tigers-leap.md new file mode 100644 index 000000000..ee7b80a21 --- /dev/null +++ b/.changeset/lucky-tigers-leap.md @@ -0,0 +1,6 @@ +--- +type: Added +pr: 1966 +--- + +**GSD now ships a repo-local VS Code extension** — a buildable extension (`vscode/extension.js` + `vscode/package.json`) that registers `gsd.invoke` (dispatches through the GSD command-routing hub) in the VS Code command palette. A reachability test proves the handler dispatches through the engine (keystone wired). Not Marketplace-published; mirrors the OpenCode plugin's bar. (#1966) diff --git a/.changeset/merry-mice-travel.md b/.changeset/merry-mice-travel.md new file mode 100644 index 000000000..544e94c8e --- /dev/null +++ b/.changeset/merry-mice-travel.md @@ -0,0 +1,5 @@ +--- +type: Changed +pr: 1738 +--- +**Honest verifier — verify-phase now abstains on non-inferable `backstop` truths instead of confidently false-passing them (#1154).** When the spec's edge-probe marks a truth non-inferable (`verification: backstop`) and the verifier cannot confirm it with explicit evidence (a passing wired held-out/property test, or a directly-observed behavior), it now reports `human_needed` with reason `insufficient_spec` ("unverified — held-out test recommended") rather than a silent `passed`. Autonomous runs complete with "N unverified non-inferable checks"; interactive runs route to the end-of-phase human checkpoint. Inferable truths are never abstained (over-abstention guard); abstention is exogenous (driven by the tag, not self-judgment). Truth-axis mirror of the prohibition judgment-tier (ADR-550 D4). diff --git a/.changeset/nimble-ibex-tumble.md b/.changeset/nimble-ibex-tumble.md new file mode 100644 index 000000000..d9665dbae --- /dev/null +++ b/.changeset/nimble-ibex-tumble.md @@ -0,0 +1,7 @@ +--- +type: Changed +pr: 1922 +--- +Document Claude Code's advisor-tool inheritance in the model-profiles reference: the session-level advisor is inherited by all GSD subagents and composes with per-agent tiering, with candidate executor/advisor pairings, when it is worth enabling, and the session-level (no per-agent control) constraint. + + diff --git a/.changeset/nimble-jays-roar.md b/.changeset/nimble-jays-roar.md new file mode 100644 index 000000000..104f03d4d --- /dev/null +++ b/.changeset/nimble-jays-roar.md @@ -0,0 +1,5 @@ +--- +type: Added +pr: 1861 +--- +GSD Core ships a `.claude-plugin/marketplace.json` marketplace manifest so Claude-plugin-compatible runtimes (ZCODE et al.) can discover and install gsd-core from a custom marketplace source. Additive — the existing `.claude-plugin/plugin.json` and the Claude Code install path are unchanged. The catalog version (`plugins[0].version`) tracks `package.json` via the release version-sync. diff --git a/.changeset/patient-mice-greet.md b/.changeset/patient-mice-greet.md new file mode 100644 index 000000000..efd8cba58 --- /dev/null +++ b/.changeset/patient-mice-greet.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 1843 +--- +**`init milestone-op` now ignores backlog `999.x` headings when counting milestone phases** — parked backlog items no longer inflate `phase_count` or pin `all_phases_complete` false for an otherwise finished milestone. (#1843) diff --git a/.changeset/plucky-bears-snooze.md b/.changeset/plucky-bears-snooze.md new file mode 100644 index 000000000..82078b1bf --- /dev/null +++ b/.changeset/plucky-bears-snooze.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 1924 +--- +**Phase archival is now wired end-to-end across the milestone lifecycle** — finishes the #1871 follow-up: `phases archive` is now a real command (the half-wired alias is routed, no longer errors Unknown), `milestone complete` archives phase dirs by default (`--no-archive-phases` opts out), and `new-milestone` §6 stages the archive move + source removal in the same commit so history is preserved atomically rather than left as orphaned uncommitted deletions. (#1871) diff --git a/.changeset/plucky-moles-sing.md b/.changeset/plucky-moles-sing.md new file mode 100644 index 000000000..5a674322f --- /dev/null +++ b/.changeset/plucky-moles-sing.md @@ -0,0 +1,7 @@ +--- +type: Changed +pr: 1805 +--- +**Internal: hook-bus + stateIO adapter seams** — `createHookBus({bus})` (new `src/hook-bus.cts`, `host`/`engine`/`none` — engine is in-process pub/sub, host fail-closed, none silent) + `createStateIO({io})` (new `src/state-io.cts`, `filesystem`/`sandboxed-storage`/`session-log-append` — filesystem delegates to fs, the rest are fail-closed seams) (ADR-1239 Phase C-1 / #1680 AC4). Completes the Phase 3 adapter seam layer; concrete host binding is Phase 5. No user-facing change. + + diff --git a/.changeset/proud-bears-purr.md b/.changeset/proud-bears-purr.md new file mode 100644 index 000000000..d59c07316 --- /dev/null +++ b/.changeset/proud-bears-purr.md @@ -0,0 +1,5 @@ +--- +type: Added +pr: 1798 +--- +**`/gsd:next` smart-entry workflow** — adds a state-aware entry point that classifies the current project situation (no-project, blocked, verify-failed, planning, executing, verify-pending, complete, and more) and recommends the right next GSD command. The `gsd-tools smart-entry [--json]` classifier handles phase ordering including decimal phase IDs; the `/gsd:next` skill surfaces the workflow with tiered fallback behavior. diff --git a/.changeset/proud-bears-roam.md b/.changeset/proud-bears-roam.md new file mode 100644 index 000000000..69728b6ee --- /dev/null +++ b/.changeset/proud-bears-roam.md @@ -0,0 +1,5 @@ +--- +type: Added +pr: 1923 +--- +OpenCode now runs GSD's lifecycle safety hooks (prompt-injection guard, read-before-edit guard, injection scanner, worktree/workflow guards, context monitor) via a native plugin installed to `~/.config/opencode/plugins/gsd-core.js`. OpenCode declares `hooksSurface: 'none'`, so these hooks were previously inert; the plugin bridges OpenCode's event bus onto GSD's existing hook scripts. Installed automatically by `npx @opengsd/gsd-core --opencode` and removed on uninstall. diff --git a/.changeset/proud-rams-greet.md b/.changeset/proud-rams-greet.md new file mode 100644 index 000000000..ac8930ac3 --- /dev/null +++ b/.changeset/proud-rams-greet.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 1845 +--- +**Roadmap phase lookup now ignores fenced examples and the backlog sentinel lane** — `roadmap get-phase` and `init plan-phase` no longer return fenced sample headings as real phases or treat `999.x` backlog items as active milestone work. (#1845) diff --git a/.changeset/rapid-orcas-sing.md b/.changeset/rapid-orcas-sing.md new file mode 100644 index 000000000..1689b63e7 --- /dev/null +++ b/.changeset/rapid-orcas-sing.md @@ -0,0 +1,7 @@ +--- +type: Changed +pr: 1934 +--- +**Lazy-split `plan-phase.md` into a `steps/` directory** — ~4.7 KB lighter eager context per `/gsd-plan-phase` call via byte-invariant progressive disclosure (ADR-1610). (#1852) + + diff --git a/.changeset/rapid-pumas-click.md b/.changeset/rapid-pumas-click.md new file mode 100644 index 000000000..6b057bcb7 --- /dev/null +++ b/.changeset/rapid-pumas-click.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 1917 +--- +**`milestone complete --ws` now archives into the workstream instead of root** — the archive paths (MILESTONES.md, the milestones/ archive dir, and the per-version MILESTONE-AUDIT.md) were hardcoded to root `.planning/`, so a workstream milestone close scattered its artifacts into root and never produced a workstream-local archive. They now derive from the workstream-aware planning base (`planningPaths(cwd).planning`); flat-mode (no --ws) is unchanged. (#1911) diff --git a/.changeset/serene-birds-rest.md b/.changeset/serene-birds-rest.md new file mode 100644 index 000000000..b5ecf5a08 --- /dev/null +++ b/.changeset/serene-birds-rest.md @@ -0,0 +1,5 @@ +--- +type: Added +pr: 1946 +--- +**Host-integration descriptors now carry an `extensionEvents` vocabulary** — the extension-system event surface (OpenCode, pi) is a separate descriptor field from managed `hookEvents`, so OpenCode declares `extensionEvents:opencode` without conflicting with the hooksSurface:none invariant. (#1946) diff --git a/.changeset/serene-lemurs-march.md b/.changeset/serene-lemurs-march.md new file mode 100644 index 000000000..8f2b0a678 --- /dev/null +++ b/.changeset/serene-lemurs-march.md @@ -0,0 +1,5 @@ +--- +type: Changed +pr: 1868 +--- +**GSD subagents now self-load configured agent_skills regardless of orchestrator bash** — projects that map skills via `.planning/config.json` `agent_skills.` no longer silently lose them on `/gsd-autonomous` or Cursor, where `Skill()`-delegated workflow bash init did not reliably run. Each of the 22 consumer agents queries its own type at init and reads the listed skills, with a dedup guard so runtimes that also inject orchestrator-side (Claude Code) never carry two copies. (#1866) diff --git a/.changeset/sturdy-voles-dart.md b/.changeset/sturdy-voles-dart.md new file mode 100644 index 000000000..608c1b962 --- /dev/null +++ b/.changeset/sturdy-voles-dart.md @@ -0,0 +1,7 @@ +--- +type: Changed +pr: 1800 +--- +**Internal: install/uninstall runtime labels are now sourced from a single `getRuntimeLabel` lookup** — the two duplicated `runtimeLabel` assignment chains in `bin/install.js` (uninstall + install) are collapsed into one curated label table in `runtime-name-policy.cts`, sibling to the registry-derived `getDirName` (ADR-1239 Phase B, #1679). Install output is byte-identical for all 16 runtimes (golden-parity asserted). Two console-label inconsistencies are normalized as a side effect: `kimi` shows 'Kimi CLI' in both sites, and `cline` uninstall no longer falls through to 'Claude Code'. + + diff --git a/.changeset/tidy-pumas-munch.md b/.changeset/tidy-pumas-munch.md new file mode 100644 index 000000000..5fa63d5a3 --- /dev/null +++ b/.changeset/tidy-pumas-munch.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 1909 +--- +Fixed: a hand-authored non-inferable backstop truth with a stray trailing space or surrounding quotes no longer silently grades green — it correctly abstains (insufficient_spec), restoring the #1154 honest-verifier guarantee. diff --git a/.changeset/wise-elks-caper.md b/.changeset/wise-elks-caper.md new file mode 100644 index 000000000..6fe2c999f --- /dev/null +++ b/.changeset/wise-elks-caper.md @@ -0,0 +1,7 @@ +--- +type: Changed +pr: 1806 +--- +**Internal: external-descriptor trust gate — load-time `configHome` confinement** — `assertDescriptorConfined(descriptor, configHome)` (new `src/external-descriptor-trust.cts`) fail-closed rejects any installed third-party host-plugin descriptor whose declared `destSubpath` resolves outside the user-approved `configHome`, before its install plan runs (ADR-1239 Phase C-2 / #1681 slice 1). Defense-in-depth load-time twin of Phase 2's install-time `assertDestWithinConfigHome`. Not yet wired into the loader (slice 2). No user-facing change. + + diff --git a/.changeset/witty-seals-snooze.md b/.changeset/witty-seals-snooze.md new file mode 100644 index 000000000..31d48e0d7 --- /dev/null +++ b/.changeset/witty-seals-snooze.md @@ -0,0 +1,7 @@ +--- +type: Changed +pr: 1801 +--- +**Internal: the installer's runtime → global-config-home hook-pathogen fragment is now a single `getGlobalConfigHomeFragment` lookup** — the 14-branch `if (runtime === 'x') return "'...'"` chain in `getConfigDirFromHome` (`bin/install.js`, the hook `path.join()` codegen mapping) is collapsed into one table in `runtime-name-policy.cts`, sibling to `getRuntimeLabel` (ADR-1239 Phase B, #1679 AC2 slice 2). Generated hook output is byte-identical for all 16 runtimes (golden-parity asserted); antigravity's dynamic env-overridable resolution is preserved in the caller. No user-facing change. + + diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json new file mode 100644 index 000000000..f5fabdb5c --- /dev/null +++ b/.claude-plugin/marketplace.json @@ -0,0 +1,20 @@ +{ + "name": "gsd-core", + "description": "Marketplace for GSD Core — meta-prompting, context engineering, and spec-driven development system for AI coding agents.", + "owner": { + "name": "open-gsd", + "url": "https://github.com/open-gsd" + }, + "plugins": [ + { + "name": "gsd-core", + "description": "GSD Core is a meta-prompting, context engineering, and spec-driven development system for AI coding agents.", + "version": "1.7.0-rc.2", + "source": "./", + "author": { + "name": "open-gsd", + "url": "https://github.com/open-gsd" + } + } + ] +} diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 26dad411a..7d2215103 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "gsd-core", "displayName": "GSD Core", - "version": "1.6.0", + "version": "1.7.0-rc.2", "description": "GSD Core is a meta-prompting, context engineering, and spec-driven development system for AI coding agents.", "author": { "name": "open-gsd", diff --git a/.github/workflows/auto-backmerge.yml b/.github/workflows/auto-backmerge.yml index 3548234c5..d9cb9be9e 100644 --- a/.github/workflows/auto-backmerge.yml +++ b/.github/workflows/auto-backmerge.yml @@ -115,7 +115,7 @@ jobs: # filter those out. A substantive change still parks: it leaves # non-"version" lines (deps in package.json; resolved/integrity in the # lockfile when a dependency actually changes). - VERSION_STAMP_MANIFESTS='package.json package-lock.json .claude-plugin/plugin.json gemini-extension.json' + VERSION_STAMP_MANIFESTS='package.json package-lock.json .claude-plugin/plugin.json .claude-plugin/marketplace.json gemini-extension.json' DROPPED=$(printf '%s\n' "$DROPPED" | while IFS= read -r f; do [ -n "$f" ] || continue case " $VERSION_STAMP_MANIFESTS " in diff --git a/.github/workflows/auto-label-issues.yml b/.github/workflows/auto-label-issues.yml index 2b19882cb..109a25c2b 100644 --- a/.github/workflows/auto-label-issues.yml +++ b/.github/workflows/auto-label-issues.yml @@ -5,7 +5,7 @@ on: types: [opened] concurrency: - group: ${{ github.workflow }}-${{ github.ref }} + group: ${{ github.workflow }}-${{ github.event.issue.number }} cancel-in-progress: true jobs: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 927ab0c58..f39cf0242 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -319,7 +319,7 @@ jobs: needs: [validate-version, install-smoke-rc] if: inputs.action == 'rc' runs-on: ubuntu-latest - timeout-minutes: 10 + timeout-minutes: 30 permissions: contents: write id-token: write diff --git a/.gitignore b/.gitignore index 79a512a4a..a222f8625 100644 --- a/.gitignore +++ b/.gitignore @@ -68,7 +68,17 @@ build/ # Published via prepublishOnly; built before test via pretest. Grows as modules migrate. /tsconfig.build.tsbuildinfo /gsd-core/bin/lib/host-integration.cjs +/gsd-core/bin/lib/host-integration-sdk.cjs +/gsd-core/bin/lib/handshake-serialized.cjs /gsd-core/bin/lib/install-engine.cjs +/gsd-core/bin/lib/embedding-adapter.cjs +/gsd-core/bin/lib/adapter-declarative.cjs +/gsd-core/bin/lib/adapter-imperative.cjs +/gsd-core/bin/lib/model-adapter.cjs +/gsd-core/bin/lib/hook-bus.cjs +/gsd-core/bin/lib/state-io.cjs +/gsd-core/bin/lib/mcp-server.cjs +/gsd-core/bin/lib/external-descriptor-trust.cjs /gsd-core/bin/lib/cli-skew-check.cjs /gsd-core/bin/lib/capability-loader.cjs /gsd-core/bin/lib/capability-source.cjs @@ -102,6 +112,7 @@ build/ /gsd-core/bin/lib/installer-migration-report.cjs /gsd-core/bin/lib/prompt-budget.cjs /gsd-core/bin/lib/secrets.cjs +/gsd-core/bin/lib/smart-entry.cjs /gsd-core/bin/lib/phase-lifecycle.cjs /gsd-core/bin/lib/workstream-name-policy.cjs /gsd-core/bin/lib/decisions.cjs diff --git a/.opencode/plugins/gsd-core.js b/.opencode/plugins/gsd-core.js new file mode 100644 index 000000000..89e6c4544 --- /dev/null +++ b/.opencode/plugins/gsd-core.js @@ -0,0 +1,711 @@ +/** + * GSD plugin for OpenCode.ai (CommonJS) + * + * Architecture: SUBPROCESS REUSE. Instead of re-implementing hook logic inside + * the plugin, this file is a thin adapter that spawns the existing Claude Code + * hook scripts under hooks/ as child processes. The hooks speak a stable + * protocol (JSON on stdin, JSON + exit code on stdout); this adapter: + * 1. Translates OpenCode plugin events into Claude Code hook payloads + * 2. Spawns `node /.js` with the payload on stdin + * 3. Translates hook output back into OpenCode semantics + * - block → throw Error (OpenCode returns the error to the model) + * - advisory → output.metadata + console.error (best-effort surfacing) + * + * Namespace conversion (/gsd:xxx → /gsd-xxx) reuses scripts/fix-slash-commands.cjs + * via require(), keeping the single source of truth. + * + * ── Two distribution shapes, one adapter (issue #1914) ───────────────────── + * This single file serves both distribution paths, distinguished at load time + * by REPO_ROOT (path.resolve(__dirname, "../..")): + * + * • Option 1 — file copy (the supported GSD path). `bin/install.js` copies + * this file to /plugins/gsd-core.js, so REPO_ROOT is the + * OpenCode config dir. GSD's own install already stages `hooks/*.js` and + * `gsd-core/` there (ADR-857 skips hook *registration* for OpenCode, not the + * file copy), so the hook bridge and content rewriting resolve natively. + * Commands/agents/skills are ALREADY registered by GSD's native file copy in + * this mode, so the plugin's own config-hook registration is redundant and is + * SKIPPED (see IS_PACKAGE_TREE) to avoid double-registration. + * + * • Option 2 — package / git-spec. When loaded from the package tree (npm + * `main`, or an OpenCode git-spec install), REPO_ROOT is the package root and + * the source layout (commands/gsd/, agents/, skills/) is present. Here the + * plugin IS the sole registrar, so it registers commands/agents/skills too. + * + * IS_PACKAGE_TREE keys off the presence of the SOURCE command layout + * (commands/gsd/), which only exists in the package tree — never in an installed + * config dir (that uses the flattened command/ layout). The hook bridge and + * Read-time content rewriting run in BOTH modes; only the config-hook + * registration of commands/agents/skills is gated. + * + * Runtime-specific hooks are deliberately excluded: + * - gsd-statusline.js / gsd-update-banner.js (Claude Code statusline) + * - gsd-cursor-*.js (Cursor-specific) + * - *.sh scripts (invoked directly by commands/agents, not hook events) + */ + +"use strict"; + +const path = require("path"); +const fs = require("fs"); +const os = require("os"); +const { spawnSync } = require("child_process"); + +// Resolve REPO_ROOT to the directory that actually holds the GSD payload +// (hooks/ + gsd-core/). This must work across three physical layouts because a +// single adapter file serves both distribution shapes (see header): +// • package/git-spec tree: /.opencode/plugins/gsd-core.js → +// • global file-copy: ~/.config/opencode/plugins/gsd-core.js → ~/.config/opencode +// • local file-copy: /.opencode/plugins/gsd-core.js → /.opencode +// A fixed "../.." only works for the first; the copied layouts sit one level +// shallower. Walking up to the first ancestor containing BOTH payload markers +// resolves all three deterministically. Falls back to the package-tree +// assumption ("../..") if no ancestor matches (keeps graceful degradation). +function resolveRepoRoot(startDir) { + let dir = startDir; + for (let i = 0; i < 6; i++) { + if ( + fs.existsSync(path.join(dir, "hooks")) && + fs.existsSync(path.join(dir, "gsd-core")) + ) { + return dir; + } + const parent = path.dirname(dir); + if (parent === dir) break; // filesystem root + dir = parent; + } + // No ancestor carried both markers (broken/partial layout — the plugin can't + // function regardless). Fall back to the package-tree assumption ("../.."), + // matching the historical fixed-depth behavior and the .opencode/plugins/ + // source layout. + return path.resolve(startDir, "../.."); +} + +// CJS: __dirname is a global, no need to derive from import.meta.url +const REPO_ROOT = resolveRepoRoot(__dirname); +const HOOKS_DIR = path.join(REPO_ROOT, "hooks"); +const COMMANDS = path.join(REPO_ROOT, "commands", "gsd"); +const AGENTS = path.join(REPO_ROOT, "agents"); +const SKILLS = path.join(REPO_ROOT, "skills"); +const GSD_CORE = path.join(REPO_ROOT, "gsd-core"); + +// True only when loaded from the package/source tree (Option 2), detected by the +// presence of the SOURCE command layout (commands/gsd/). In an installed OpenCode +// config dir (Option 1) this directory is absent — the flattened command/ layout +// is used instead — so the plugin skips its own command/agent/skill registration +// and lets GSD's native file copy own that surface (avoids double-registration). +const IS_PACKAGE_TREE = fs.existsSync(COMMANDS); + +// --------------------------------------------------------------------------- +// Namespace conversion — reuse the single source of truth +// --------------------------------------------------------------------------- + +let _cmdNames = null; +let _transformFn = null; + +/** + * Lazily load scripts/fix-slash-commands.cjs and cache the transform function + * + command name list. Returns null if the module is unavailable (the plugin + * still works, just without namespace conversion). + */ +function getNamespaceConverter() { + if (_transformFn) return _transformFn; + try { + const mod = require( + path.join(REPO_ROOT, "scripts", "fix-slash-commands.cjs"), + ); + _cmdNames = mod.readCmdNames(); + _transformFn = mod.transformContentToHyphen; + return _transformFn; + } catch { + return null; + } +} + +// --------------------------------------------------------------------------- +// Session state — tracked across plugin hook invocations +// --------------------------------------------------------------------------- + +let currentSessionId = null; +let currentCwd = process.cwd(); + +// --------------------------------------------------------------------------- +// Tool name / argument mapping (OpenCode ↔ Claude Code) +// --------------------------------------------------------------------------- + +const TOOL_NAME_MAP = { + read: "Read", + write: "Write", + edit: "Edit", + apply_patch: "MultiEdit", + multi_edit: "MultiEdit", + bash: "Bash", + webfetch: "WebFetch", + web_search: "WebSearch", + websearch: "WebSearch", + task: "Task", + subagent: "Task", +}; + +function mapToolName(tool) { + if (!tool) return ""; + return TOOL_NAME_MAP[String(tool).toLowerCase()] || tool; +} + +// Build a Claude-style `tool_input` object from OpenCode's `output.args`. +function mapToolInput(args) { + const input = {}; + if (!args || typeof args !== "object") return input; + + // File-path keys (OpenCode uses filePath/path; Claude uses file_path) + const filePath = args.filePath || args.path || args.file_path; + if (filePath) input.file_path = filePath; + + // Content for Write + if (args.content !== undefined) input.content = args.content; + + // Edit patch fields + if (args.new_string !== undefined) input.new_string = args.new_string; + if (args.newString !== undefined) input.new_string = args.newString; + if (args.old_string !== undefined) input.old_string = args.old_string; + if (args.oldString !== undefined) input.old_string = args.oldString; + + // Bash command + if (args.command !== undefined) input.command = args.command; + + // Web + if (args.url !== undefined) input.url = args.url; + if (args.query !== undefined) input.query = args.query; + + return input; +} + +// --------------------------------------------------------------------------- +// Hook subprocess runner +// --------------------------------------------------------------------------- + +/** + * Spawn a Claude Code hook script and pipe a JSON payload to its stdin. + * + * Hooks follow the convention: + * - stdout: JSON object (decision/advisory) or empty + * - exit 0: allow (with optional advisory JSON on stdout) + * - exit 2: block (Claude convention; reason in stdout JSON) + * - any error: exit 0 silently (hooks swallow their own errors) + * + * @param {string} hookFile filename under hooks/, e.g. "gsd-prompt-guard.js" + * @param {object} payload stdin JSON (hook_event_name, tool_name, ...) + * @param {object} [opts] + * @param {number} [opts.timeout=8000] spawn timeout in ms + * @param {string} [opts.cwd] working directory for the child + * @returns {{ stdout: string, exitCode: number, timedOut: boolean }} + */ +function runHook(hookFile, payload, opts = {}) { + const hookPath = path.join(HOOKS_DIR, hookFile); + if (!fs.existsSync(hookPath)) { + return { stdout: "", exitCode: 0, timedOut: false }; + } + const timeout = opts.timeout ?? 8000; + let result; + try { + result = spawnSync(process.execPath, [hookPath], { + input: JSON.stringify(payload), + encoding: "utf8", + timeout, + cwd: opts.cwd || currentCwd, + windowsHide: true, + }); + } catch { + // Spawn failure — never break the tool call + return { stdout: "", exitCode: 0, timedOut: false }; + } + + const stdout = (result.stdout || "").trim(); + const exitCode = result.status == null ? 0 : result.status; + return { stdout, exitCode, timedOut: result.signal === "SIGTERM" }; +} + +// --------------------------------------------------------------------------- +// Hook output translation → OpenCode semantics +// --------------------------------------------------------------------------- + +/** + * Parse a hook's stdout and apply its effect to the OpenCode output object. + * + * - Block → throw Error(parsed.reason) so OpenCode aborts the tool call + * - Advisory→ append to output.metadata._gsdAdvisory[] and log to stderr + * - Silent → no-op + * + * @param {{ stdout: string, exitCode: number }} hookResult + * @param {object} [output] OpenCode mutable output object (optional) + */ +function handleHookResult(hookResult, output) { + const { stdout, exitCode } = hookResult; + if (!stdout && exitCode !== 2) return; // silent allow + + let parsed = null; + if (stdout) { + try { + parsed = JSON.parse(stdout); + } catch { + // Non-JSON stdout (e.g. a stray log) — treat exit 2 as hard block, else allow + } + } + + // Block: explicit decision OR Claude exit-code-2 convention + const isBlock = exitCode === 2 || (parsed && parsed.decision === "block"); + if (isBlock) { + const reason = + (parsed && parsed.reason) || "Blocked by GSD hook (no reason provided)."; + throw new Error(reason); + } + + // Advisory: inject additionalContext into metadata + log + const advisory = + parsed && + parsed.hookSpecificOutput && + parsed.hookSpecificOutput.additionalContext; + if (advisory) { + if (output) { + output.metadata = output.metadata || {}; + // Accumulate: a single tool call can run several advisory hooks in + // sequence (prompt guard, read guard, worktree guard, workflow guard). + // Storing a scalar would let a later advisory clobber an earlier one, so + // collect them all. + if (!Array.isArray(output.metadata._gsdAdvisory)) { + output.metadata._gsdAdvisory = []; + } + output.metadata._gsdAdvisory.push(advisory); + } + // Best-effort visibility when metadata isn't surfaced to the model + console.error(advisory); + } +} + +// --------------------------------------------------------------------------- +// Frontmatter helpers (for config registration) +// --------------------------------------------------------------------------- + +function parseFrontmatter(content) { + const m = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/); + if (!m) return { frontmatter: {}, body: content }; + const fm = {}; + for (const line of m[1].split("\n")) { + const i = line.indexOf(":"); + if (i > 0) { + let v = line.slice(i + 1).trim(); + if (v.startsWith('"') && v.endsWith('"')) v = v.slice(1, -1); + fm[line.slice(0, i).trim()] = v; + } + } + return { frontmatter: fm, body: m[2] }; +} + +// Rewrite @~/.claude/ includes to point at the repo root. +// Also applies /gsd:xxx → /gsd-xxx namespace conversion via the shared +// transform from scripts/fix-slash-commands.cjs (single source of truth). +function rewriteRefs(content) { + let out = content.replace(/@~\/\.claude\//g, `@${REPO_ROOT}/`); + const transform = getNamespaceConverter(); + if (transform && _cmdNames && _cmdNames.length) { + out = transform(out, _cmdNames); + } + return out; +} + +function loadDir(dir, keyFn, valFn) { + const result = {}; + if (!fs.existsSync(dir)) return result; + for (const f of fs.readdirSync(dir).filter((f) => f.endsWith(".md"))) { + const raw = fs.readFileSync(path.join(dir, f), "utf8"); + const { frontmatter, body } = parseFrontmatter(raw); + result[keyFn(f)] = valFn(body, frontmatter, f); + } + return result; +} + +// --------------------------------------------------------------------------- +// Runtime content transform — for Read tool results on GSD-managed files +// --------------------------------------------------------------------------- + +// Directories whose .md files may contain ~/.claude/ paths and gsd: namespace +// refs. When the model reads these via the Read tool, we transparently rewrite +// both so OpenCode sees correct paths and hyphen-form command names. +const GSD_MANAGED_DIRS = [ + path.join(GSD_CORE, "workflows"), + path.join(GSD_CORE, "references"), + path.join(GSD_CORE, "templates"), + path.join(GSD_CORE, "contexts"), + COMMANDS, + AGENTS, + SKILLS, +]; + +function isGsdManagedFile(filePath) { + if (!filePath) return false; + const resolved = path.resolve(filePath); + return GSD_MANAGED_DIRS.some( + (dir) => resolved === dir || resolved.startsWith(dir + path.sep), + ); +} + +// Rewrite content for OpenCode consumption: +// 1. @-include paths: @~/.claude/ → @/ +// 2. plain-text paths: ~/.claude/gsd-core/ → / +// 3. namespace: gsd:xxx → gsd-xxx (via fix-slash-commands.cjs) +function rewriteContent(content) { + let out = content; + out = out.replace(/@~\/\.claude\//g, `@${REPO_ROOT}/`); + out = out.replace(/~\/\.claude\/gsd-core\//g, `${GSD_CORE}/`); + const transform = getNamespaceConverter(); + if (transform && _cmdNames && _cmdNames.length) { + out = transform(out, _cmdNames); + } + return out; +} + +// --------------------------------------------------------------------------- +// Skills cache — copy SKILL.md files with rewritten @-include paths +// --------------------------------------------------------------------------- +// +// OpenCode's skill loader reads SKILL.md files directly from disk and resolves +// @-includes internally — this bypasses our tool.execute hooks. To make +// @~/.claude/gsd-core/... includes resolve, we copy all SKILL.md files to a +// cache directory with paths rewritten to the actual GSD_CORE location. +// +// Only used in package-tree mode (Option 2). In an installed OpenCode config +// dir (Option 1) skills are already staged + registered by GSD's native file +// copy, so we never register skills from the plugin (see IS_PACKAGE_TREE). + +const SKILLS_CACHE = path.join( + os.homedir(), + ".cache", + "opencode", + "gsd-skills", +); + +function prepareSkillsCache() { + if (!fs.existsSync(SKILLS)) return null; + fs.mkdirSync(SKILLS_CACHE, { recursive: true }); + for (const dir of fs.readdirSync(SKILLS)) { + const srcFile = path.join(SKILLS, dir, "SKILL.md"); + if (!fs.existsSync(srcFile)) continue; + const raw = fs.readFileSync(srcFile, "utf8"); + // Rewrite @-include paths only; namespace conversion is handled at + // Read-time via tool.execute.after for workflow/reference files. + const rewritten = raw + .replace(/@~\/\.claude\/gsd-core\//g, `@${GSD_CORE}/`) + .replace(/~\/\.claude\/gsd-core\//g, `${GSD_CORE}/`); + const destDir = path.join(SKILLS_CACHE, dir); + fs.mkdirSync(destDir, { recursive: true }); + fs.writeFileSync(path.join(destDir, "SKILL.md"), rewritten); + } + return SKILLS_CACHE; +} + +// =========================================================================== +// Plugin entry +// =========================================================================== + +const GsdCorePlugin = async ({ directory } = {}) => { + if (directory) currentCwd = directory; + + return { + // ── Config: register commands / agents / skills paths ────────────── + // Only in package-tree mode (Option 2). In an installed config dir + // (Option 1) GSD's native file copy already registered these, so the + // plugin stays out of registration to avoid double-registering. + config: async (config) => { + if (!IS_PACKAGE_TREE) return; + + // Commands (commands/gsd/*.md → gsd-) + config.command = config.command || {}; + const cmds = loadDir( + COMMANDS, + (f) => "gsd-" + f.slice(0, -3), + (body, fm, name) => ({ + template: rewriteRefs(body.trim()), + description: fm.description || `GSD ${name.slice(0, -3)} command`, + }), + ); + for (const [k, v] of Object.entries(cmds)) { + if (!config.command[k]) config.command[k] = v; + } + + // Agents (agents/*.md) + config.agent = config.agent || {}; + const agents = loadDir( + AGENTS, + (f) => f.slice(0, -3), + (body, fm, name) => ({ + prompt: rewriteRefs(body.trim()), + description: fm.description || `GSD ${name.slice(0, -3)} agent`, + mode: fm.mode || "subagent", + }), + ); + for (const [k, v] of Object.entries(agents)) { + if (!config.agent[k]) config.agent[k] = v; + } + + // Skills — copy SKILL.md files to cache with rewritten @-include paths, + // then register the cache directory. OpenCode's skill loader reads + // SKILL.md from disk and resolves @-includes internally (bypassing our + // tool.execute hooks), so we must pre-process the files. + const skillsCache = prepareSkillsCache(); + config.skills = config.skills || {}; + config.skills.paths = config.skills.paths || []; + const skillsPath = skillsCache || SKILLS; + if (!config.skills.paths.includes(skillsPath)) { + config.skills.paths.push(skillsPath); + } + }, + + // ── shell.env ─────────────────────────────────────────────────────── + "shell.env": async (_input, output) => { + output.env = output.env || {}; + output.env.GSD_DIR = GSD_CORE; + }, + + // ── tool.execute.before — PreToolUse hooks ───────────────────────── + "tool.execute.before": async (input, output) => { + const claudeTool = mapToolName(input.tool); + const toolInput = mapToolInput(output.args || {}); + const cwd = currentCwd; + + // 0. Read path rewrite — redirect ~/.claude/gsd-core/ to actual GSD_CORE + // so the model can read workflow/reference/template files that SKILL.md + // and command templates reference via the canonical Claude path. + if (claudeTool === "Read" && toolInput.file_path) { + const original = toolInput.file_path; + const rewritten = original + .replace(/^~\/\.claude\/gsd-core\//, GSD_CORE + "/") + .replace(/(?:.*)\/\.claude\/gsd-core\//, GSD_CORE + "/"); + if (rewritten !== original) { + const args = output.args || {}; + if (args.filePath) args.filePath = rewritten; + else if (args.path) args.path = rewritten; + else if (args.file_path) args.file_path = rewritten; + else args.filePath = rewritten; + } + } + + const basePayload = { + hook_event_name: "PreToolUse", + cwd, + }; + // NOTE: session_id intentionally omitted for PreToolUse hooks. + // gsd-read-guard.js treats a non-empty session_id as a Claude Code + // session and skips its advisory. On OpenCode we WANT the advisory. + const prePayload = (overrides = {}) => ({ + ...basePayload, + tool_name: claudeTool, + tool_input: toolInput, + ...overrides, + }); + + const isWriteLike = ["Write", "Edit", "MultiEdit"].includes(claudeTool); + + // 1. gsd-prompt-guard.js — injection scan on .planning/ writes + if (claudeTool === "Write" || claudeTool === "Edit") { + const r = runHook("gsd-prompt-guard.js", prePayload()); + handleHookResult(r, output); + } + + // 2. gsd-read-guard.js — read-before-edit advisory + if (claudeTool === "Write" || claudeTool === "Edit") { + const r = runHook("gsd-read-guard.js", prePayload()); + handleHookResult(r, output); + } + + // 3. gsd-worktree-path-guard.js — hard-block edits outside worktree + if (isWriteLike) { + const r = runHook("gsd-worktree-path-guard.js", prePayload()); + handleHookResult(r, output); + } + + // 4. gsd-workflow-guard.js — workflow advisory + git-force-add block + // (covers Write/Edit/MultiEdit AND Bash force-add detection) + if (isWriteLike || claudeTool === "Bash") { + const r = runHook("gsd-workflow-guard.js", prePayload()); + handleHookResult(r, output); + } + }, + + // ── tool.execute.after — PostToolUse hooks ───────────────────────── + "tool.execute.after": async (input, output) => { + const claudeTool = mapToolName(input.tool); + // NOTE: In the `after` hook, `args` lives on `input` (not `output`). + // The `output` object only has { title, output, metadata }. + const toolInput = mapToolInput(input.args || {}); + const cwd = currentCwd; + + // GSD content transform — rewrite paths + namespace in Read results + // BEFORE injection scanning so the scanner sees the final content. + if ( + claudeTool === "Read" && + output.output && + isGsdManagedFile(toolInput.file_path) + ) { + const content = + typeof output.output === "string" + ? output.output + : String(output.output); + output.output = rewriteContent(content); + } + + // gsd-read-injection-scanner.js — scan Read/WebFetch/WebSearch results + if ( + claudeTool === "Read" || + claudeTool === "WebFetch" || + claudeTool === "WebSearch" + ) { + const payload = { + hook_event_name: "PostToolUse", + tool_name: claudeTool, + tool_input: toolInput, + tool_response: output.output, + cwd, + }; + const r = runHook("gsd-read-injection-scanner.js", payload); + handleHookResult(r, output); + return; + } + + // gsd-context-monitor.js — context usage warnings (Bash/Edit/Write/Task/...) + // Only meaningful when a session_id is tracked (writes metrics sentinel). + if (currentSessionId) { + const payload = { + hook_event_name: "PostToolUse", + tool_name: claudeTool, + tool_input: toolInput, + session_id: currentSessionId, + cwd, + }; + const r = runHook("gsd-context-monitor.js", payload); + handleHookResult(r, output); + } + }, + + // ── experimental.session.compacting — PreCompact ─────────────────── + "experimental.session.compacting": async (_input, output) => { + if (!currentSessionId) return; + const payload = { + hook_event_name: "PreCompact", + session_id: currentSessionId, + cwd: currentCwd, + }; + const r = runHook("gsd-context-monitor.js", payload); + handleHookResult(r, output); + + // Also inject a GSD compaction breadcrumb (mirrors the original plugin) + output.context = output.context || []; + output.context.push( + `[GSD] Active session: ${currentSessionId}. Preserve any in-flight phase/plan state.`, + ); + }, + + // ── General event subscriptions ───────────────────────────────────── + event: async ({ event }) => { + // session.created → SessionStart hooks + if (event.type === "session.created") { + // Track session for context-monitor payloads. + // SDK type EventSessionCreated: { properties: { info: Session } } + // Session has `id` and `directory` (not `cwd`). + const info = event.properties?.info; + currentSessionId = + info?.id || event.sessionID || event.session_id || null; + if (info?.directory) currentCwd = info.directory; + + // gsd-ensure-canonical-path.js — no stdin dependency; silent + runHook("gsd-ensure-canonical-path.js", { + hook_event_name: "SessionStart", + session_id: currentSessionId, + cwd: currentCwd, + }); + // gsd-check-update.js — spawns its own background worker; no stdin + runHook("gsd-check-update.js", { + hook_event_name: "SessionStart", + session_id: currentSessionId, + cwd: currentCwd, + }); + return; + } + + // file.edited → FileChanged hook (config.json reload) + if (event.type === "file.edited") { + // SDK type EventFileEdited: { properties: { file: string } } + const filePath = event.properties?.file || event.filePath || ""; + if (!filePath.endsWith("config.json")) return; + const cwd = event.properties?.cwd || currentCwd; + const expected = path.join(cwd, ".planning", "config.json"); + if (path.resolve(filePath) !== path.resolve(expected)) return; + + const payload = { + hook_event_name: "FileChanged", + file_path: filePath, + event: "change", + cwd, + }; + const r = runHook("gsd-config-reload.js", payload); + // Advisory-only (additionalContext); surface to logs + handleHookResult(r); + return; + } + + // session.idle ↔ Claude Stop lifecycle point (#1682 Slice 1b/c). + // OpenCode fires session.idle when the run quiesces. GSD maps it to the + // Stop equivalent — the opencode-subset lifecycle peer of compaction + // (compaction preserves state across context-window summarization; idle + // marks end-of-turn). No-op sentinel today (GSD state is already + // persisted to .planning/), but it MUST be recognized so the declared + // opencode-subset surface is fully wired and a future Stop-class hook can + // attach without a plugin change. + if (event.type === "session.idle") { + return; + } + }, + }; +}; + +// Export shape — verified against OpenCode's plugin loader source +// (packages/opencode/src/plugin). The loader imports this module and runs +// `for (const entry of Object.values(mod)) { getServerPlugin(entry) }`, where +// `getServerPlugin` accepts a bare function OR an object exposing a `.server` +// function, and THROWS `TypeError("Plugin export is not a function")` for +// anything else. So EVERY enumerable value the loader iterates must be a +// function or an object with `.server`. +// +// The subtlety: depending on how OpenCode's runtime (Node or Bun) imports a +// CommonJS file, `mod` may be the raw `module.exports` OR an ESM namespace of +// the form `{ default: module.exports, ...syntheticNamedExports }`. A plain +// `module.exports = { id: "gsd-core", server }` literal risks a string `id` +// appearing in `Object.values(mod)` (as a raw property, or as a lexer- +// synthesized named export) — which would trip the throw. Two defenses: +// 1. `id` is defined NON-ENUMERABLE, so it never appears in Object.values yet +// stays readable (via property access) for the loader's identity/dedup. +// 2. `module.exports` is assigned from a VARIABLE (not an object literal), so +// cjs-module-lexer cannot statically synthesize named exports from it — +// only `default` is exposed under ESM/Bun interop. +// Result: raw-CJS `Object.values` = `[server]`; ESM `Object.values` = +// `[{server, }]` — both fully extractable. Test-only helpers hang +// off the `server` FUNCTION (`server._internals`), never as a sibling export. +GsdCorePlugin._internals = { + REPO_ROOT, + IS_PACKAGE_TREE, + mapToolName, + mapToolInput, + parseFrontmatter, + rewriteContent, + isGsdManagedFile, + handleHookResult, + GsdCorePlugin, +}; + +const gsdCorePluginExport = { server: GsdCorePlugin }; +Object.defineProperty(gsdCorePluginExport, "id", { + value: "gsd-core", + enumerable: false, + writable: false, + configurable: false, +}); +module.exports = gsdCorePluginExport; diff --git a/CHANGELOG.md b/CHANGELOG.md index 3dbc738ca..0a64f2189 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,19 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] +## [1.6.1] - 2026-07-01 + +### Added + +- **Claude Sonnet 5 is now the `standard` (sonnet) tier model.** The model catalog and provider presets resolve the sonnet/standard tier to `claude-sonnet-5` (GA 2026-06-30) across the Anthropic-backed runtimes (`claude`, `copilot`, and the `anthropic`/`anthropic-fable` presets), plus the OpenRouter-style `anthropic/claude-sonnet-5` for `opencode`/`hermes`, replacing the superseded `claude-sonnet-4-6`. Opus and Haiku tiers are unchanged. (#1847) (#1848) + +### Fixed + +- `milestone complete` and `roadmap analyze` now exclude the Phase 0 / Phase 999 backlog sentinels. A milestone whose only directory-less ROADMAP heading is a backlog sentinel can be completed without `--force`, and `roadmap analyze` no longer counts the sentinel in `phase_count` or routes `next_phase` into it. Completes the `^999` exclusion #1445 added to the progress denominators. (#1691) +- **`phase.complete` no longer reports a false `is_last_phase` on a `
`-wrapped checkbox checklist (#1591, #1752)** — when the active milestone's phase checklist was written as `- [ ] Phase N:` checkbox items inside a `
` block and the next phase had no directory on disk yet (still in planning), `phase.complete`'s `isLastPhase` roadmap-enumeration fallback used a heading-only pattern (`/#{2,4}\s*Phase…/`) that never matched checkbox items. It returned `is_last_phase: true, next_phase: null` on a mid-milestone phase and — via the milestone-complete cascade — wrongly flipped STATE.md to `Milestone complete` and decremented `progress.total_phases` (e.g. 8 → 7). The pattern now matches both heading-style (`### Phase N:`) and checkbox-list phases (`- [ ] Phase N:` / `- [x] Phase N:`); `extractCurrentMilestone` already surfaces the `
`-wrapped checklist correctly, so no parser change was needed. Only the reproduced `phase.complete` fallback is changed; the heading-only sibling patterns elsewhere in `phase.cts` are untouched. + (#1819) +- Windows: stop double-quoting $CLAUDE_PROJECT_DIR-anchored managed node hook paths during the #2979 legacy rewrite, which produced "\"$CLAUDE_PROJECT_DIR\"/..." and broke every node managed hook with MODULE_NOT_FOUND (PreToolUse-guard deadlock). (#1746) + ## [1.6.0] - 2026-06-24 ### Added diff --git a/CONTEXT.md b/CONTEXT.md index bbd908ca7..90275b328 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -52,8 +52,8 @@ Module owning projection from dispatch results/errors to CLI `{ exitCode, stdout ### STATE.md Document Module Module owning STATE.md parse, field extraction, field replacement, status normalization, and frontmatter reconstruction. It does not scan `.planning/phases` and does not own persistence or locking; phase/plan/summary counts arrive from inventory/progress Modules as inputs, and read-modify-write paths remain Adapters. Source of truth: `gsd-core/bin/lib/state-document.cjs`. -### STATE.md Transition Module [Planned] -Module owning STATE.md lifecycle/maintenance transitions as intent-based methods (`beginPhase`, `advancePlan`, `completePhase`, `plannedPhase`, `milestoneSwitch`, `milestoneComplete`, `patch`, `sync`, `prune`, `update`). Pure core `(content, intent, deps) → newContent` with injected I/O (file read/write, lock, disk scan); consults a field-classification table that names each STATE.md field's class (`derived-from-body` | `derived-from-disk` | `derived-from-external` | `curated` | `free`) and its preservation policy. Supersedes the 14 scattered RMW callbacks in `state.cts` and the direct `writeStateMd` callers in `milestone.cts:352` and `phase.cts:1770`; verify's `regenerateState` factory-reset primitive stays as a direct `writeStateMd` call. Absorbs `syncStateFrontmatter` + `readModifyWriteStateMd`'s post-sync preservation block; Encoding 3 (`cmdStateBuildFrontmatter`) stays separate — read path concern. Sibling/super-module of the STATE.md Document Module; consumes its `stateReplaceField`/`stateExtractField` primitives. Body section structure (`## Current Position`, `## Session`, etc.) lives as a constants block inside the Module. Append-only transitions (`addDecision`, `addBlocker`, etc.) stay on today's RMW seam for now. Targets the #1760/#1761/#1743/#1695/#1264/#1255/#1257/#3242 bug cluster. Migration per ADR-1372 §T6 sequenced as substrate + `beginPhase` first (PR1), then transition-by-transition with characterization tests first per transition. Source of truth: `gsd-core/bin/lib/state-transition.cjs` (planned, generated from `src/state-transition.cts`). +### STATE.md Transition Module +Module owning STATE.md lifecycle/maintenance transitions as intent-based methods (`beginPhase`, `advancePlan`, `completePhase`, `plannedPhase`, `milestoneSwitch`, `milestoneComplete`, `patch`, `sync`, `prune`, `update`, `rebuild`). Pure core `(content, intent, deps) → newContent` with injected I/O (file read/write, lock, disk scan); consults a field-classification table that names each STATE.md field's class (`derived-from-body` | `derived-from-disk` | `derived-from-external` | `curated` | `free`) and its preservation policy. Supersedes the 14 scattered RMW callbacks in `state.cts` and the direct `writeStateMd` callers in `milestone.cts:352` and `phase.cts:1770`; verify's `regenerateState` factory-reset primitive stays as a direct `writeStateMd` call. Absorbs `syncStateFrontmatter` + `readModifyWriteStateMd`'s post-sync preservation block; Encoding 3 (`cmdStateBuildFrontmatter`) stays separate — read path concern. Sibling/super-module of the STATE.md Document Module; consumes its `stateReplaceField`/`stateExtractField` primitives. Body section structure (`## Current Position`, `## Session`, etc.) lives as a constants block inside the Module. Append-only transitions (`addDecision`, `addBlocker`, etc.) stay on today's RMW seam for now. Targets the #1760/#1761/#1743/#1695/#1264/#1255/#1257/#3242 bug cluster. Migration per ADR-1372 §T6 sequenced as substrate + `beginPhase` first (PR1), then transition-by-transition with characterization tests first per transition. **ADR-1817 adds `rebuild` as the capstone 11th transition — the body-structure derivability contract.** Re-derives `## Current Position` prose from frontmatter and `## By-Phase Progress` table from phase dirs on disk; preserves `## Session` / `## Decisions` / unknown sections verbatim; de-duplicates `## Session Continuity Archive` (keep most-recent N, default 3); appends a structured audit entry to `## Rebuild Log` (`timestamp`, `kind`, `section`, `before`, `after`, `reason`) for every mutation. Hard idempotency guarantee: a no-mutation rebuild appends no log entry, so two successive invocations on a clean file are byte-identical. Non-overlapping with `sync` (3 lightweight frontmatter fields, auto-triggered) and orthogonal to `auto_prune_state` (age-based removal) — `rebuild` reconciles with current canonical sources, `prune` removes by retention policy, the two compose (rebuild first, then prune). Section ordering is invariant: rebuild rewrites content in place, never reorders. Targets the #1776/#1761/#1591 body-drift cluster that survived ADR-1769's per-field transitions. Phased per ADR-1817: Phase 0 = this ADR + predicates (closes #1817), Phase 1 = `rebuildCore` body + `rebuild` dispatch case + drift-class unit tests (#1827), Phase 2 = `cmdStateRebuild` CLI + `--dry-run`/`--verbose` + integration tests + docs + changeset (#1826). Source of truth: `gsd-core/bin/lib/state-transition.cjs` (generated from `src/state-transition.cts`). ### Query Execution Policy Module Module owning query transport routing policy projection (`preferNative`, fallback policy, workstream subprocess forcing) at execution seam. @@ -203,7 +203,7 @@ ADR-1244 D3 fetch-and-stage seam (`gsd-core/bin/lib/capability-source.cjs`). Pri ADR-1244 D4 per-runtime install manifest (`gsd-core/bin/lib/capability-ledger.cjs`). Leaf module (only `node:fs`/`node:path` plus `shell-command-projection`'s `platformWriteSync`). Records `{ id, version, source, integrity, files[], sharedEdits[{file,marker}] }` per installed capability in `.gsd-capabilities.json` at the runtime config dir root. Exports: `readLedger` (structural-validated, never throws), `writeLedger` (atomic via `platformWriteSync`), `recordInstall` (idempotent, prototype-pollution-guarded), `removeEntry`, and `reconcile` (reports orphans whose `files[]` are missing on disk; hardened against non-string/`..` members; never mutates). Serves as the atomic commit point for Phase-4 upgrade/remove and the reconciliation basis for detecting stale entries after out-of-band deletions. ### Capability Consent Store -Issue #1459 user-owned consent seam (`gsd-core/bin/lib/capability-consent.cjs`, generated from `src/capability-consent.cts`). Leaf module (`node:fs`/`node:path`/`node:os`/`node:crypto` + the ledger's shared bounded `readSmallRegularFile`/`readSmallRegularFileBuffer` + the shared `capability-lock` primitive). Stores `{ version:"1", records: { "": { projectRoot, id, scope:'project', integrity, disclosureSignature, contentHash, consentedAt } } }` at `${GSD_HOME||homedir()}/.gsd/consent.json` — a USER-OWNED file OUTSIDE any repository. Exports: `consentStorePath(gsdHome?)`, `readConsentStore(gsdHome?)` (bounded via `readSmallRegularFile` + 8 MiB cap, NON-THROWING — missing/corrupt/oversized/FIFO/wrong-shape → empty `{records:{}}`; caps records at `MAX_RECORDS=4096`), `bundleContentHash(capDir)` (THE security binding — a `sha512-` over a DETERMINISTIC, INJECTIVE, LOSSLESS serialization of EVERY regular file AND directory under the bundle: length-FRAMED entry COUNT + per-entry TYPE tag + uint32 path-byte-len + RAW path bytes from a `{encoding:'buffer'}` dir walk [finding 4] + for files uint64 content-byte-len + RAW content bytes via `readSmallRegularFileBuffer` [finding 1b], plus typed DIR markers binding empty directories [finding 2]; symlinks/non-regular rejected; size+count bounded), `hasProjectConsent({gsdHome,projectRoot,id,contentHash})` (true iff a record for `${realpath(projectRoot)}` exists AND its stored `contentHash` equals the supplied recomputed hash — the binding is `contentHash`, NOT `integrity` and NOT `disclosureSignature` (those remain on the record purely for the human disclosure + re-consent-on-executable-change UX); unsafe ids → false; prototype-pollution-safe NUL-joined keys + `Object.prototype.hasOwnProperty`), `recordProjectConsent({gsdHome,projectRoot,id,integrity,disclosureSignature,contentHash})` (LOCKED, atomic+durable write — tmp `wx`/fsync/rename/dir-fsync mirroring `writeLedger`; enforces the record cap at write time) and `revokeProjectConsent({gsdHome,projectRoot,id})` (LOCKED atomic delete, no-op if absent) — BOTH **THROW** rather than perform an UNLOCKED read-modify-write when the consent-store lock cannot be acquired (finding 3; the lifecycle treats a consent-write failure as non-fatal, and the `trust revoke` CLI catches the throw and emits a clean error). This is the authoritative consent signal the loader recomputes (`bundleContentHash(capDir)`) and checks at load before activating a PROJECT-scope third-party overlay (declarative surfaces AND command dispatch): a forged/cloned in-repo project ledger, OR any post-consent tamper (swapped declarative manifest, edited hook script, empty-integrity local install — all change the recomputed hash), leaves the cap DISCOVERED-BUT-INACTIVE until the user consents on THIS machine to the EXACT bundle (the lifecycle records the consent on a consented project install/upgrade and revokes it on remove; install/lookup/revoke share one canonical `consentProjectRoot` root key). GLOBAL-scope overlays (under the user's own home) need no record; and when `GSD_HOME` resolves (via realpath, defeating symlink aliasing — finding 1) to a genuine project root the in-repo bundle still requires a record. The consent lock is the SHARED hardened primitive (below), so it never stale-steals a slow-but-live writer (finding 4). See `docs/explanation/capability-trust-model.md` "project-scope trust boundary". +Issue #1459 user-owned consent seam (`gsd-core/bin/lib/capability-consent.cjs`, generated from `src/capability-consent.cts`). Leaf module (`node:fs`/`node:path`/`node:os`/`node:crypto` + the ledger's shared bounded `readSmallRegularFile`/`readSmallRegularFileBuffer` + the shared `capability-lock` primitive). Stores `{ version:"1", records: { "": { projectRoot, id, scope:'project', integrity, disclosureSignature, contentHash, consentedAt } } }` at `${GSD_HOME||homedir()}/.gsd/consent.json` — a USER-OWNED file OUTSIDE any repository. Exports: `consentStorePath(gsdHome?)`, `readConsentStore(gsdHome?)` (bounded via `readSmallRegularFile` + 8 MiB cap, NON-THROWING — missing/corrupt/oversized/FIFO/wrong-shape → empty `{records:{}}`; caps records at `MAX_RECORDS=4096`), `bundleContentHash(capDir)` (THE security binding — a `sha512-` over a DETERMINISTIC, INJECTIVE, LOSSLESS serialization of EVERY regular file AND directory under the bundle: length-FRAMED entry COUNT + per-entry TYPE tag + uint32 path-byte-len + RAW path bytes from a `{encoding:'buffer'}` dir walk [finding 4] + for files uint64 content-byte-len + RAW content bytes via `readSmallRegularFileBuffer` [finding 1b], plus typed DIR markers binding empty directories [finding 2]; symlinks/non-regular rejected; size+count bounded), `hasProjectConsent({gsdHome,projectRoot,id,contentHash})` (true iff a record for `${realpath(projectRoot)}\x00` exists AND its stored `contentHash` equals the supplied recomputed hash — the binding is `contentHash`, NOT `integrity` and NOT `disclosureSignature` (those remain on the record purely for the human disclosure + re-consent-on-executable-change UX); unsafe ids → false; prototype-pollution-safe NUL-joined keys + `Object.prototype.hasOwnProperty`), `recordProjectConsent({gsdHome,projectRoot,id,integrity,disclosureSignature,contentHash})` (LOCKED, atomic+durable write — tmp `wx`/fsync/rename/dir-fsync mirroring `writeLedger`; enforces the record cap at write time) and `revokeProjectConsent({gsdHome,projectRoot,id})` (LOCKED atomic delete, no-op if absent) — BOTH **THROW** rather than perform an UNLOCKED read-modify-write when the consent-store lock cannot be acquired (finding 3; the lifecycle treats a consent-write failure as non-fatal, and the `trust revoke` CLI catches the throw and emits a clean error). This is the authoritative consent signal the loader recomputes (`bundleContentHash(capDir)`) and checks at load before activating a PROJECT-scope third-party overlay (declarative surfaces AND command dispatch): a forged/cloned in-repo project ledger, OR any post-consent tamper (swapped declarative manifest, edited hook script, empty-integrity local install — all change the recomputed hash), leaves the cap DISCOVERED-BUT-INACTIVE until the user consents on THIS machine to the EXACT bundle (the lifecycle records the consent on a consented project install/upgrade and revokes it on remove; install/lookup/revoke share one canonical `consentProjectRoot` root key). GLOBAL-scope overlays (under the user's own home) need no record; and when `GSD_HOME` resolves (via realpath, defeating symlink aliasing — finding 1) to a genuine project root the in-repo bundle still requires a record. The consent lock is the SHARED hardened primitive (below), so it never stale-steals a slow-but-live writer (finding 4). See `docs/explanation/capability-trust-model.md` "project-scope trust boundary". ### Capability Lock Issue #1459 finding 4 shared cross-process lock primitive (`gsd-core/bin/lib/capability-lock.cjs`, generated from `src/capability-lock.cts`). Leaf module (`node:fs`/`node:path`/`node:os`/`node:crypto` + the ledger's bounded `readSmallRegularFile` + `shell-command-projection`'s `execTool` for the rare start-time shell-out). THE single hardened lockfile protocol shared by BOTH `capability-lifecycle` (the `.gsd/capabilities/.lock` mutation lock) and `capability-consent` (the consent-store `.consent.lock`) — extracted so the two locks cannot diverge (mirrors the shared-validator / shared bounded-reader lessons). Exports: `acquireLock(lockPath, opts?)` (O_EXCL create with a JSON `{token,pid,hostname,startTime,ts}` body; steal protocol binds age to the body's own `ts`, never stale-steals a VERIFIED-LIVE same-host holder — pid alive AND recorded start-time matches the pid's current start-time, defeating pid-reuse without ever stealing a live holder — and reclaims only a dead/unverifiable holder via the dead-pid fast path or the hard `LOCK_DEADMAN_MS` deadman; `opts.maxAttempts` raises the bounded retry budget and `opts.waitForFresh` makes a contended fresh/live holder be WAITED FOR rather than failed-fast so genuinely-racing consent writers serialize), `releaseLock(handle)` (token + inode owner-safe — never deletes a successor's lock), `getProcessStartTime`, and the `_setLockProbes`/`_resetLockProbes` test seams. Carries the #1462 lifecycle-lock invariants (process-start-time liveness, TOCTOU-safe pre-rename identity recheck, bounded iterative loop). @@ -362,6 +362,9 @@ The canonical lint infrastructure adopted in ADR 452 (`docs/adr/452-eslint-lint- ### External-job-waiting half-state A legal deferred state of an Execute step (`external_job_waiting`): the executor has dispatched a long-running async external job and committed an async-job manifest at `.planning/async-jobs/.json` instead of a SUMMARY.md. Distinct from the synchronous "mid-production-commits" half-state and from an illegal partial-plan state. The core loop's step-completion + safe-resume/pause contract treats a non-terminal manifest as legal and reconciles against it (never re-dispatching the plan, which would duplicate the external job); SUMMARY.md is deferred until the job reaches a terminal state and its `expected_artifacts` are verified. The manifest is a versioned stability contract (`docs/reference/planning-artifacts.md`); core *consumes* it while a default-off scheduler-adapter Capability (#1164) *produces* it at `execute:wave:post` — the contract-is-core / producer-is-capability seam mirrors ADR-857's verification-substrate decision. Status enum is closed and scheduler-agnostic: `submitted`, `running`, `completed-unverified`, `failed`, `cancelled`, `timeout`. +### External-job Capability +The producer half of the async external-job contract (#1164, part of #1105). Default-off Capability (`capabilities/external-job/capability.json`) that *writes* `.planning/async-jobs/.json` manifests — the only thing that does; core never writes them. SLURM is the first backend (`sbatch --parsable` submit, `squeue` poll with `sacct` fallback, terminal-state mapping); the design stays scheduler-pluggable via the `backend` field (LSF/PBS/Kubernetes batch forward-declared, not built). Contributions inject at `execute:wave:post` into the executor (classify runtime budget → externalize `long_compute`, commit manifest + handoff, return `external_job_waiting`, defer SUMMARY.md) and at `plan:post` into the planner (emit `` quick|medium|unknown|long_compute per task). Activation key `external_job.enabled` (default `false`); sibling keys `external_job.backend`, `external_job.artifact_dir` (default `Artifacts/jobs`, per-job dirs — no fixed log paths, no hardcoded cluster/partition/account), `external_job.submit_timeout_ms` / `external_job.poll_timeout_ms` (bounded subprocesses per CLAUDE.md). Pure producer logic — SLURM state→manifest-status mapping, manifest build/validate, `sbatch`/`squeue`/`sacct` parsers, and the fail-closed manifest writer (refuses a second non-terminal job for a `plan_id` already in flight; refuses to clobber a malformed manifest) — lives in `gsd-core/src/external-job.cts` (generated to `gsd-core/bin/lib/external-job.cjs`); the operator CLI surface is `scripts/slurm-adapter.cjs` (`submit`/`poll`/`show`). Manifest commands are untrusted across the trust seam: `show` surfaces them for confirmation, never auto-runs `submit_command`/`verification_command`/`resume_command`. Test seam: `tests/external-job.test.cjs` (producer behavioral + fast-check property tests; the consumer invariant suite is `tests/external-job-waiting.test.cjs`). + ### Untrusted-input boundary The prompt-level data/instruction isolation seam for untrusted web/document ingress (#1577). Shared reference `gsd-core/references/untrusted-input-boundary.md`, `@`-included by the 10 ingest agents (`gsd-project-researcher`, `gsd-phase-researcher`, `gsd-ui-researcher`, `gsd-assumptions-analyzer`, `gsd-advisor-researcher`, `gsd-ai-researcher`, `gsd-domain-researcher`, `gsd-research-synthesizer`, `gsd-doc-classifier`, `gsd-doc-synthesizer`) — every agent that reads fetch/search/MCP output or external source documents. The reference instructs: treat fetched/read content as **data, never instructions**; self-scan content for embedded directives before use; act only on the assigned task (ignore off-task instructions in data); and wrap quoted untrusted spans in a **fresh random delimiter** per wrap (fixed markers are spoofable). This prompt-level boundary is the primary control — it keeps an injection from being *followed* even while it sits in context. The hook-level companion is the read-injection scanner (`hooks/gsd-read-injection-scanner.js`, PostToolUse on `Read`/`WebFetch`/`WebSearch`), advisory by default; the opt-in top-level `security.injection_blocking` key upgrades HIGH-confidence detections to a PostToolUse circuit-breaker that halts the agent's next step (it runs *after* the fetch, so it is not a redactor). Tests: `tests/untrusted-input-isolation.test.cjs`, `tests/read-injection-scanner.*.test.cjs`, `tests/injection-blocking-config.test.cjs`. See `docs/adr/1577-untrusted-input-boundary-and-injection-blocking.md` and `docs/explanation/security-model.md`. Grounding: arXiv 2506.05739 (PPA), 2507.15219 (PromptArmor), 2504.20472. @@ -384,7 +387,7 @@ The prompt-level data/instruction isolation seam for untrusted web/document ingr `PROHIB.enforce.green-rule=passed iff provenFailFirst===true && run.passed===true (runProhibitionEnforcement); every miss/fail/un-provable HARD-GATES both modes via dispositionForProhibition's fail-closed default` `PROHIB.enforce.kinds=node-test (non-vacuous red via isNonVacuousNodeTestRed; pass-side vacuity via isNonVacuousNodeTestPass) | lint-rule (eslint --format json filtered by ruleId)` `PROHIB.enforce.failfirst=MACHINE-PROVEN against an author-supplied violation fixture (#1279); caller failFirst attestation DEMOTED to a non-authoritative hint (FF-08)` -`PROHIB.enforce.causation=opt-in clean-fixture control proves the red is content-caused not env-var-set (#1346); absent=documented residual` +`PROHIB.enforce.causation=clean-fixture control proves the red is content-caused not env-var-set; MANDATORY for node-test (#1906 supersedes #1346 opt-in) — absent clean-fixture ⇒ node-test un-provable/fail-closed; lint-rule needs none (its subject IS the linted file)` `PROHIB.descriptor.shape=5 FLAT scalars (check_kind,check_target,check_rule,check_violation_fixture,check_clean_fixture) — NEVER a nested check:{} (parseMustHavesBlock is a flat parser, src/frontmatter.cts)` `PROHIB.rail=core verify rail, non-toggleable (ADR-857 verification-substrate boundary / decision #6); the verifier<->predicate contract is NOT an off-by-default capability` `PROHIB.judgment-tier=never-silent / never-hard-halt soft gate; autonomous emits "unverified-prohibition — human review recommended" (exogenous grading, ADR-550 D4)` @@ -422,7 +425,7 @@ The prompt-level data/instruction isolation seam for untrusted web/document ingr `RULESET.WORKFLOW_SIZE_BUDGET=workflow size enforcement (#1074; BYTES not lines per #717; LF-normalized per #683) = per-file baseline (PRIMARY anti-creep: tests/workflow-size-baseline.json pins each file's exact size) + loose tier hard caps (outer red lines, NEVER raised on approach: XL<=98304 / LARGE<=61440 / DEFAULT<=40960) + new-file cap (un-baselined files <32768, the Codex anchor) + discuss-phase<32000; a file that grew fails the baseline guard — fix with `npm run size:baseline`, commit the one-line diff, and justify the growth in the PR (or extract LAZILY-loaded content; eager @-imports don't reduce loaded context); crossing a hard cap means EXTRACT, not bump` `RULESET.AGENT_SIZE_BUDGET=agent-size-budget (#1074; sibling of WORKFLOW_SIZE_BUDGET; BYTES not lines per #717/#683, rebased from lines in PR 3/3) = per-file baseline (PRIMARY anti-creep: tests/agent-size-baseline.json pins each agents/gsd-*.md exact byte size) + loose tier hard caps (red lines, never raised on approach: XL<=57344 / LARGE<=49152 / DEFAULT<=24576); net-new agents are DEFAULT-tier (no separate new-file cap). One 'npm run size:baseline' regenerates BOTH workflow and agent baselines via the shared scripts/workflow-size.cjs measureMdFiles(dir,predicate) counter. A grown agent fails the baseline guard — regenerate + justify, or extract LAZILY to gsd-core/references/. DISTINCT from DEFECT.AGENT-FILE-SIZE-CAP-BREACH (a separate 45K-CHAR extraction-evidence threshold on gsd-planner via planner-decomposition/reachability tests): that guard proves mode-sections were extracted; this one bounds total agent bytes. Two guards, two units (chars vs bytes), two purposes` `RULESET.WORKFLOW_FILE_NAMES=workflow files use hyphens; XML attributes must match (extract-learnings not extract_learnings); tests should pin exact hyphenated name` -`RULESET.WORKFLOW_EXECUTION_CONTEXT=@-ref in commands/gsd/*.md must resolve to an existing file on disk; regression test in tests/bug-3135-capture-backlog-workflow.test.cjs; INVENTORY.md row + INVENTORY-MANIFEST.json families.workflows must stay in sync; "Invoked by" attribution must move when a flag absorbs a micro-skill` +`RULESET.WORKFLOW_EXECUTION_CONTEXT=@-ref in commands/gsd/*.md must resolve to an existing file on disk; regression test in tests/docs-update.test.cjs (folds former \`bug-3135-capture-backlog-workflow\`, consolidation epic #1969); INVENTORY.md row + INVENTORY-MANIFEST.json families.workflows must stay in sync; "Invoked by" attribution must move when a flag absorbs a micro-skill` `RULESET.WORKFLOW_EXECUTE_END_TO_END=ADR-0002 standard for single-workflow commands is "Execute end-to-end." (no bolded **Follow the X workflow** fragments); flag-dispatch routing uses "execute the X workflow end-to-end." in routing bullets` `RULESET.WORKFLOW.COVERAGE-METADATA=#1602 SUMMARY frontmatter `coverage:` block (list of {id,description,requirement?,verification:[{kind∈unit|integration|e2e|automated_ui|manual_procedural|other, ref, status∈pass|fail|unknown}],human_judgment:bool,rationale?}) is the per-deliverable RTM consumed DETERMINISTICALLY by verify-work extract_tests via `gsd-tools uat classify-coverage --summary ` (src/coverage.cts → bin/lib/coverage.cjs). AUTHORING: execute-plan create_summary populates it from task results; every deliverable MUST be classified; fail-safe default = human_judgment:true + rationale. CLASSIFY CONTRACT: auto-pass (skip human) ONLY when human_judgment===false (strict boolean) AND verification non-empty AND every status==='pass' AND zero validation errors — else PRESENT to human. mode:legacy (no block) ⇒ byte-identical prose `## Accomplishments` fall-through; `coverage: []` ⇒ mode:coverage, zero entries (single-confirmation). Frozen IR: MODE/PRESENT_REASON/ERROR_CODE enums locked by tests/coverage-metadata-parser.test.cjs. extractFrontmatter CANNOT parse it (scalars-only `-` items) → dedicated parser, sibling of parseMustHavesBlock. Asymmetry by design: false-negative=redundant prompt (status quo); false-positive=shipped bug UAT existed to catch` @@ -487,7 +490,6 @@ The prompt-level data/instruction isolation seam for untrusted web/document ingr `WORKTREE.SEAM.test-policy=cover all decision branches in policy module before changing prune behavior` `WORKTREE.SEAM.test-anchors=[resolveWorktreeContext:has_local_planning|linked_worktree|not_git_repo|main_worktree, planWorktreePrune:git_list_failed|worktrees_present|no_worktrees|parser_throw_fallback, executeWorktreePrunePlan:missing_plan|skip_passthrough|unsupported_action|metadata_prune_only]` `WORKTREE.SEAM.invariant=parser failure must degrade to metadata_prune_only and never escalate to destructive removal` -`WORKTREE.SEAM.execution-rule=prefer node --test tests/worktree-safety-policy.test.cjs for fast seam validation; avoid full npm test loop for seam-only changes` `WORKTREE.SEAM.inventory-interface=[listLinkedWorktreePaths, inspectWorktreeHealth]` `WORKTREE.SEAM.caller-rule=verify.cjs must consume inspectWorktreeHealth for W017 classification; no ad-hoc porcelain parsing in callers` `WORKTREE.SEAM.test-anchor-w017=tests/orphan-worktree-detection.test.cjs + tests/worktree-safety-policy.test.cjs` @@ -850,7 +852,7 @@ Migration plan: Phase 1 (#3465) seam additions complete; Phase 2 (#3466) targets `DEFECT.SHARED-ARTIFACT-MUTATION-IN-CONCURRENT-TEST.examples=#996/88e30d53 — bug-969 hardening tests fs.unlinkSync'd + restored the real gsd-core/bin/lib/core.cjs and set tsBuildInfoFile inside gsd-core/bin/ → next red across the full-test matrix (macOS/Windows) + ubuntu-24 coverage leg, ~40-50 MODULE_NOT_FOUND/ENOENT per leg; reproduced locally on iteration 1; fixed #1001/#1002` `DEFECT.SHARED-ARTIFACT-MUTATION-IN-CONCURRENT-TEST.detect=grep tests for fs.unlinkSync|rmSync|writeFileSync|renameSync|cpSync targeting paths resolved from the repo root (join(__dirname,'..',...)) under gsd-core/bin/lib or a shared committed fixture, instead of a mkdtempSync temp dir; any build helper (e.g. ensureBuiltArtifacts) invoked with real-tree paths during the concurrent test phase; any tsBuildInfoFile / build-cache path that lands inside a copied/shipped dir (gsd-core/bin/)` `DEFECT.SHARED-ARTIFACT-MUTATION-IN-CONCURRENT-TEST.fix-forward=tests mutate ONLY isolated mkdtempSync copies — never delete/rewrite shared real build outputs while node --test runs files concurrently; parameterize build helpers to accept {root,srcDir,outDir,tsBuildInfoPath,tsconfigPath} overrides and point the test at a throwaway temp project (precedent: #1002 ensureBuiltArtifacts(overrides)); keep mutable build state (tsbuildinfo) OUTSIDE copied/shipped trees (repo root, gitignored) + best-effort self-heal of stale bin-local copies; this is the concrete instance of the RULESET.TESTS.delete-bad-tests real-race class` -`DEFECT.SHARED-ARTIFACT-MUTATION-IN-CONCURRENT-TEST.test-anchor=tests/bug-969-test-infra-flake-hardening.test.cjs (hermetic temp-project rewrite); regression gate = 10x concurrent run of that suite + tests/state.test.cjs + tests/install.test.cjs must be clean (reproduces on iter 1 when racy)` +`DEFECT.SHARED-ARTIFACT-MUTATION-IN-CONCURRENT-TEST.test-anchor=tests/run-tests-harness.test.cjs (hermetic temp-project rewrite); regression gate = 10x concurrent run of that suite + tests/state.test.cjs + tests/install.test.cjs must be clean (reproduces on iter 1 when racy)` `DEFECT.STACKED-PR-CANNOT-STAND-ALONE.symptom=patch PR was authored against scaffolding (handler files, lint scripts, generated modules) that exists only on an unmerged upstream feature branch; the PR's "base" on GitHub is the feature branch, not main; merging requires the upstream PR to land first` `DEFECT.STACKED-PR-CANNOT-STAND-ALONE.examples=#3639 + #3637 both targeted base=feat/3575-enforcement-hardening (the Phase 6 PR #3577); #3639 modifies SDK-bridge calls in 6 family-router files that on main do NOT have any SDK-bridge call yet; #3637 patches scripts/lint-shared-module-handsync.cjs which does not exist on main at all` @@ -870,7 +872,7 @@ Migration plan: Phase 1 (#3465) seam additions complete; Phase 2 (#3466) targets `RULESET.HARNESS.test-memory-guard=~/.claude/hooks/test-memory-guard.sh fires on every Bash PreToolUse; if argv[0]∈{node|vitest|jest|mocha|tsx|ts-node|tap|ava|playwright|cypress} OR matches (npm|pnpm|yarn|bun) (run )?(t|test|tests|vitest|jest); blocks via hookSpecificOutput.permissionDecision=deny when sum(RSS of running matching procs, excluding tsserver|*-mcp|claude|Electron|...) ≥ 4 GiB OR when argv[0] basename matches a running process's argv[0]. Exception: node --version|-v|--help|-h|-p|-e are trivial probes and skip the check. Designed for a 24 GB Mac where prior accidental fan-out exhausted RAM` -`RULESET.PR-FLOW.docker-before-push=before ANY git push of any fix to any PR, run gsd-test-summary (docker on the remote, mirrors ubuntu CI) and confirm exit 0. macOS-local node --test is NOT a substitute — many failures are platform-specific (path separators, case sensitivity, locale, fs semantics). Watchdog with Monitor on the output log; never set a sleep/timer and walk away. Source: user feedback 2026-05-16 — "we don't set a timer we actively watch and record results in real time as possible"` +`RULESET.PR-FLOW.docker-before-push=before ANY git push of any fix to any PR, run gsd-test (docker on the remote, mirrors ubuntu CI) and confirm exit 0. macOS-local node --test is NOT a substitute — many failures are platform-specific (path separators, case sensitivity, locale, fs semantics). Watchdog with Monitor on the output log; never set a sleep/timer and walk away. Source: user feedback 2026-05-16 — "we don't set a timer we actively watch and record results in real time as possible"` `RULESET.PR-FLOW.templates-mandatory=every gh pr create|edit|gh issue create|edit MUST first invoke the gh-templates-first skill and Read (Read tool, not Bash cat — k321 read-tracking) the matching template in .github/. Apply ALL required sections; never write freeform bodies. Repo enforces this via gsd-pr-template-policy GitHub Action which flags any non-templated body — the bot allows the PR to stay open only because authors are contributors-or-higher, but the warning is a real complaint that must be cured. Source: user feedback 2026-05-16 (multi-message escalation) — "the whole reason i have that github action is because you fucking blow through and ignore using the templates"` @@ -904,11 +906,11 @@ Migration plan: Phase 1 (#3465) seam additions complete; Phase 2 (#3466) targets `DEFECT.SUBAGENT-LONG-RUNNING-BG-STALL.fix-forward=keep gsd-test-summary --both at the top-level orchestrator; sub-agents either run it foreground with timeout: 1500000 (25min) and block, OR delegate the test step back to the orchestrator (write commits + return); never have a sub-agent fire-and-await a backgrounded long task` `DEFECT.SUBAGENT-LONG-RUNNING-BG-STALL.anchor=project CLAUDE.md "Top-level orchestrator (cross-turn notifications available) vs Sub-agent worker (no cross-turn notifications)" guidance — load-bearing for multi-worktree parallel fix dispatch` `DEFECT.AGENT-RETIRED-SLASH-SYNTAX-DRIFT.symptom=sub-agent writes /gsd- (legacy hyphen syntax) in code comments or doc strings while implementing a fix; lands as part of the implementation diff` -`DEFECT.AGENT-RETIRED-SLASH-SYNTAX-DRIFT.examples=#3541 implementation included a typical /gsd-update path comment in installer-migration-report.cjs; caught by tests/bug-2543-gsd-slash-namespace.test.cjs (#3443 invariant)` -`DEFECT.AGENT-RETIRED-SLASH-SYNTAX-DRIFT.detect=tests/bug-2543-gsd-slash-namespace.test.cjs prints "Found N retired /gsd- reference(s) — use /gsd: instead" with line-number-precise violations` +`DEFECT.AGENT-RETIRED-SLASH-SYNTAX-DRIFT.examples=#3541 implementation included a typical /gsd-update path comment in installer-migration-report.cjs; caught by tests/slash-command-namespace.test.cjs (#3443 invariant)` +`DEFECT.AGENT-RETIRED-SLASH-SYNTAX-DRIFT.detect=tests/slash-command-namespace.test.cjs prints "Found N retired /gsd- reference(s) — use /gsd: instead" with line-number-precise violations` `DEFECT.AGENT-RETIRED-SLASH-SYNTAX-DRIFT.fix-forward=replace /gsd- with /gsd: at the cited file:line; healthy emergent property — project-wide invariant test catches drift agents would never self-correct` `DEFECT.AGENT-RETIRED-SLASH-SYNTAX-DRIFT.lesson=agent-trust-but-verify is load-bearing — sub-agent reporting "done" is not a substitute for running the full suite; the invariant test surfaces drift even in doc-only changes` -`PROC.PARALLEL-FIX-DISPATCH.pattern=bot triage brief → worktree per branch → parallel sub-agents do rubber-duck/RCA/TDD implementation only → top-level orchestrator owns commit + gsd-test-summary --both + push + PR + changeset-pr-backfill` +`PROC.PARALLEL-FIX-DISPATCH.pattern=bot triage brief → worktree per branch → parallel sub-agents do rubber-duck/RCA/TDD implementation only → top-level orchestrator owns commit + gsd-test + push + PR + changeset-pr-backfill` `PROC.PARALLEL-FIX-DISPATCH.rationale=long-running test runs need cross-turn notifications (orchestrator-only); CONTRIBUTING.md gh-templates-first hook requires session-scoped Read calls sub-agents wouldn't otherwise make; sequencing test runs avoids GSD-TEST-CONCURRENT-OUTPUT-COLLISION` `PROC.PARALLEL-FIX-DISPATCH.observed=#3541 + #3542 dispatched simultaneously this session; PRs #3546 #3547 opened green; one syntax slip caught by AGENT-RETIRED-SLASH-SYNTAX-DRIFT and fixed before second PR opened` @@ -932,7 +934,7 @@ Full detail in `~/.claude/skills/gsd-pr-fix-discipline/SKILL.md`. AI agents MUST ### Slash command two-tier confusion -- **Symptom:** `tests/bug-2543-gsd-slash-namespace.test.cjs` or `tests/bug-3584-runtime-slash-emitters.test.cjs` fails +- **Symptom:** `tests/slash-command-namespace.test.cjs` (folds former `bug-2543-gsd-slash-namespace`, consolidation epic #1969) or `tests/init-manager.test.cjs` (folds former `bug-3584-runtime-slash-emitters`, consolidation epic #1969) fails - **Affected this session:** #154 (three passes), #164 (added the authoritative matrix) - **Fix:** Consult `## Slash-command form` section of this file before touching any `/gsd-` or `/gsd:` token — colon for `agents/`/`commands/`, hyphen for runtime emitters diff --git a/VERSIONING.md b/VERSIONING.md index 7c3a53493..1ba485a44 100644 --- a/VERSIONING.md +++ b/VERSIONING.md @@ -131,13 +131,17 @@ match `package.json`: - `.claude-plugin/plugin.json` — Claude Code plugin manifest (issue #766) - `gemini-extension.json` — Gemini CLI extension manifest (issue #775) +- `.claude-plugin/marketplace.json` — Claude plugin marketplace manifest; its + version lives at `plugins[0].version` and is stamped via a nested versionKey + descriptor (issue #1855) The `version` npm lifecycle script (`scripts/sync-manifest-versions.cjs --stage`) stamps these files automatically on every `npm version` call, and stages them so they are included in the release commit alongside `package.json`. -To add a new manifest that must track the package version, register its path in -the `VERSIONED_MANIFESTS` array in `scripts/sync-manifest-versions.cjs`. A +To add a new manifest that must track the package version, register its path +(and, if its version field is not top-level, its dotted `versionKey`) in the +`VERSIONED_MANIFESTS` array in `scripts/sync-manifest-versions.cjs`. A regression test (`tests/issue-844-manifest-version-sync.test.cjs`) enforces this: it scans all committed JSON files for a matching `version` field and fails if any are missing from the registry. diff --git a/agents/gsd-advisor-researcher.md b/agents/gsd-advisor-researcher.md index 13cdd2ec8..b6fcf61c7 100644 --- a/agents/gsd-advisor-researcher.md +++ b/agents/gsd-advisor-researcher.md @@ -19,6 +19,8 @@ Spawned by `discuss-phase` via `Task()`. You do NOT present output directly to t @~/.claude/gsd-core/references/untrusted-input-boundary.md +**agent_skills:** self-load per @~/.claude/gsd-core/references/agent-skills-bootstrap.md + @~/.claude/gsd-core/references/research-documentation-lookup.md diff --git a/agents/gsd-ai-researcher.md b/agents/gsd-ai-researcher.md index 20108ca80..e9df2d44b 100644 --- a/agents/gsd-ai-researcher.md +++ b/agents/gsd-ai-researcher.md @@ -72,7 +72,7 @@ Update AI-SPEC.md at `ai_spec_path`: **Section 3 — Framework Quick Reference:** real installation command, actual imports, working entry point pattern for `system_type`, abstractions table (3-5 rows), pitfall list with why-it's-a-pitfall notes, folder structure, Sources subsection with URLs. -**Section 4 — Implementation Guidance:** specific model (e.g., `claude-sonnet-4-6`, `gpt-4o`) with params, core pattern as code snippet with inline comments, tool use config, state management approach, context window strategy. +**Section 4 — Implementation Guidance:** specific model (e.g., `claude-sonnet-5`, `gpt-4o`) with params, core pattern as code snippet with inline comments, tool use config, state management approach, context window strategy. diff --git a/agents/gsd-assumptions-analyzer.md b/agents/gsd-assumptions-analyzer.md index ccfbd2b76..364baa2b4 100644 --- a/agents/gsd-assumptions-analyzer.md +++ b/agents/gsd-assumptions-analyzer.md @@ -20,6 +20,8 @@ Spawned by `discuss-phase-assumptions` via `Task()`. You do NOT present output d @~/.claude/gsd-core/references/untrusted-input-boundary.md +**agent_skills:** self-load per @~/.claude/gsd-core/references/agent-skills-bootstrap.md + Agent receives via prompt: diff --git a/agents/gsd-code-fixer.md b/agents/gsd-code-fixer.md index 1b89dd79c..0d0470b95 100644 --- a/agents/gsd-code-fixer.md +++ b/agents/gsd-code-fixer.md @@ -24,6 +24,8 @@ Before fixing code, discover project context: **Project instructions:** Read `./CLAUDE.md` if it exists in the working directory. Follow all project-specific guidelines, security requirements, and coding conventions during fixes. **Project skills:** Check `.claude/skills/` or `.agents/skills/` directory if either exists: + +**agent_skills:** self-load per @~/.claude/gsd-core/references/agent-skills-bootstrap.md 1. List available skills (subdirectories) 2. Read `SKILL.md` for each skill (lightweight index ~130 lines) 3. Load specific `rules/*.md` files as needed during implementation diff --git a/agents/gsd-code-reviewer.md b/agents/gsd-code-reviewer.md index 0ecc81549..36220ed4f 100644 --- a/agents/gsd-code-reviewer.md +++ b/agents/gsd-code-reviewer.md @@ -40,6 +40,8 @@ Before reviewing, discover project context: **Project instructions:** Read `./CLAUDE.md` if it exists in the working directory. Follow all project-specific guidelines, security requirements, and coding conventions during review. **Project skills:** Check `.claude/skills/` or `.agents/skills/` directory if either exists: + +**agent_skills:** self-load per @~/.claude/gsd-core/references/agent-skills-bootstrap.md 1. List available skills (subdirectories) 2. Read `SKILL.md` for each skill (lightweight index ~130 lines) 3. Load specific `rules/*.md` files as needed during review diff --git a/agents/gsd-codebase-mapper.md b/agents/gsd-codebase-mapper.md index 4897c3040..5a92d75e7 100644 --- a/agents/gsd-codebase-mapper.md +++ b/agents/gsd-codebase-mapper.md @@ -29,6 +29,8 @@ If the prompt contains a `` block, you MUST use the `Read` too **Context budget:** Load project skills first (lightweight). Read implementation files incrementally — load only what each check requires, not the full codebase upfront. **Project skills:** Check `.claude/skills/` or `.agents/skills/` directory if either exists: + +**agent_skills:** self-load per @~/.claude/gsd-core/references/agent-skills-bootstrap.md 1. List available skills (subdirectories) 2. Read `SKILL.md` for each skill (lightweight index ~130 lines) 3. Load specific `rules/*.md` files as needed during implementation diff --git a/agents/gsd-debugger.md b/agents/gsd-debugger.md index 565ea6508..ab6e8f2d2 100644 --- a/agents/gsd-debugger.md +++ b/agents/gsd-debugger.md @@ -40,6 +40,8 @@ Your job: Find the root cause through hypothesis testing, maintain debug file st - Load `rules/*.md` as needed during **investigation and fix**. - Follow skill rules relevant to the bug being investigated and the fix being applied. +**agent_skills:** self-load per @~/.claude/gsd-core/references/agent-skills-bootstrap.md + @~/.claude/gsd-core/references/debugger-philosophy.md diff --git a/agents/gsd-doc-writer.md b/agents/gsd-doc-writer.md index d594b7bb3..85f26c23e 100644 --- a/agents/gsd-doc-writer.md +++ b/agents/gsd-doc-writer.md @@ -34,6 +34,8 @@ If the prompt contains a `` block, you MUST use the `Read` too **Context budget:** Load project skills first (lightweight). Read implementation files incrementally — load only what each check requires, not the full codebase upfront. **Project skills:** Check `.claude/skills/` or `.agents/skills/` directory if either exists: + +**agent_skills:** self-load per @~/.claude/gsd-core/references/agent-skills-bootstrap.md 1. List available skills (subdirectories) 2. Read `SKILL.md` for each skill (lightweight index ~130 lines) 3. Load specific `rules/*.md` files as needed during implementation diff --git a/agents/gsd-eval-auditor.md b/agents/gsd-eval-auditor.md index 4b0f96282..47d899c39 100644 --- a/agents/gsd-eval-auditor.md +++ b/agents/gsd-eval-auditor.md @@ -39,6 +39,8 @@ Read `~/.claude/gsd-core/references/ai-evals.md` before auditing. This is your s **Context budget:** Load project skills first (lightweight). Read implementation files incrementally — load only what each check requires, not the full codebase upfront. **Project skills:** Check `.claude/skills/` or `.agents/skills/` directory if either exists: + +**agent_skills:** self-load per @~/.claude/gsd-core/references/agent-skills-bootstrap.md 1. List available skills (subdirectories) 2. Read `SKILL.md` for each skill (lightweight index ~130 lines) 3. Load specific `rules/*.md` files as needed during implementation diff --git a/agents/gsd-executor.md b/agents/gsd-executor.md index d86e4a990..d14b7bf7b 100644 --- a/agents/gsd-executor.md +++ b/agents/gsd-executor.md @@ -64,6 +64,8 @@ Before executing, discover project context: - Load `rules/*.md` as needed during **implementation**. - Follow skill rules relevant to the task you are about to commit. +**agent_skills:** self-load per @~/.claude/gsd-core/references/agent-skills-bootstrap.md + **CLAUDE.md enforcement:** If `./CLAUDE.md` exists, treat its directives as hard constraints during execution. Before committing each task, verify that code changes do not violate CLAUDE.md rules (forbidden patterns, required conventions, mandated tools). If a task action would contradict a CLAUDE.md directive, apply the CLAUDE.md rule — it takes precedence over plan instructions. Document any CLAUDE.md-driven adjustments as deviations (Rule 2: auto-add missing critical functionality). @@ -689,7 +691,7 @@ Do NOT skip. Do NOT proceed to state updates if self-check fails. -After SUMMARY.md, update STATE.md using `gsd-tools query` state handlers (positional args; see `sdk/src/query/QUERY-HANDLERS.md`): +After SUMMARY.md, update STATE.md using `gsd-tools query` state handlers (named flags; see `sdk/src/query/QUERY-HANDLERS.md`): ```bash # Advance plan counter (handles edge cases automatically) @@ -700,16 +702,17 @@ gsd_run query state.update-progress # Record execution metrics (phase, plan, duration, tasks, files) gsd_run query state.record-metric \ - "${PHASE}" "${PLAN}" "${DURATION}" "${TASK_COUNT}" "${FILE_COUNT}" + --phase "${PHASE}" --plan "${PLAN}" --duration "${DURATION}" \ + --tasks "${TASK_COUNT}" --files "${FILE_COUNT}" # Add decisions (extract from SUMMARY.md key-decisions) for decision in "${DECISIONS[@]}"; do - gsd_run query state.add-decision "${decision}" + gsd_run query state.add-decision --summary "${decision}" done -# Update session info (timestamp, stopped-at, resume-file) +# Update session info (stopped-at, resume-file; timestamp set automatically) gsd_run query state.record-session \ - "" "Completed ${PHASE}-${PLAN}-PLAN.md" "None" + --stopped-at "Completed ${PHASE}-${PLAN}-PLAN.md" --resume-file "None" ``` ```bash @@ -736,7 +739,7 @@ gsd_run query requirements.mark-complete ${REQ_IDS} **For blockers found during execution:** ```bash -gsd_run query state.add-blocker "Blocker description" +gsd_run query state.add-blocker --text "Blocker description" ``` diff --git a/agents/gsd-integration-checker.md b/agents/gsd-integration-checker.md index ab324b012..8908c5db4 100644 --- a/agents/gsd-integration-checker.md +++ b/agents/gsd-integration-checker.md @@ -35,6 +35,8 @@ Every expected cross-phase connection must resolve to WIRED (verified end-to-end **Context budget:** Load project skills first (lightweight). Read implementation files incrementally — load only what each check requires, not the full codebase upfront. **Project skills:** Check `.claude/skills/` or `.agents/skills/` directory if either exists: + +**agent_skills:** self-load per @~/.claude/gsd-core/references/agent-skills-bootstrap.md 1. List available skills (subdirectories) 2. Read `SKILL.md` for each skill (lightweight index ~130 lines) 3. Load specific `rules/*.md` files as needed during implementation diff --git a/agents/gsd-nyquist-auditor.md b/agents/gsd-nyquist-auditor.md index 862fef52a..c2bdabaab 100644 --- a/agents/gsd-nyquist-auditor.md +++ b/agents/gsd-nyquist-auditor.md @@ -51,6 +51,8 @@ Read ALL files from ``. Extract: **Context budget:** Load project skills first (lightweight). Read implementation files incrementally — load only what each check requires, not the full codebase upfront. **Project skills:** Check `.claude/skills/` or `.agents/skills/` directory if either exists: + +**agent_skills:** self-load per @~/.claude/gsd-core/references/agent-skills-bootstrap.md 1. List available skills (subdirectories) 2. Read `SKILL.md` for each skill (lightweight index ~130 lines) 3. Load specific `rules/*.md` files as needed during implementation diff --git a/agents/gsd-phase-researcher.md b/agents/gsd-phase-researcher.md index c57a853a7..341f95e8a 100644 --- a/agents/gsd-phase-researcher.md +++ b/agents/gsd-phase-researcher.md @@ -50,6 +50,8 @@ Before researching, discover project context: - Load `rules/*.md` as needed during **research**. - Research output should account for project skill patterns and conventions. +**agent_skills:** self-load per @~/.claude/gsd-core/references/agent-skills-bootstrap.md + **CLAUDE.md enforcement:** If `./CLAUDE.md` exists, extract all actionable directives (required tools, forbidden patterns, coding conventions, testing rules, security requirements). Include a `## Project Constraints (from CLAUDE.md)` section in RESEARCH.md listing these directives so the planner can verify compliance. Treat CLAUDE.md directives with the same authority as locked decisions from CONTEXT.md — research should not recommend approaches that contradict them. diff --git a/agents/gsd-plan-checker.md b/agents/gsd-plan-checker.md index 290ec87ce..6109ef96f 100644 --- a/agents/gsd-plan-checker.md +++ b/agents/gsd-plan-checker.md @@ -54,6 +54,8 @@ Before verifying, discover project context: **Project instructions:** Read `./CLAUDE.md` if it exists in the working directory. Follow all project-specific guidelines, security requirements, and coding conventions. **Project skills:** Check `.claude/skills/` or `.agents/skills/` directory if either exists: + +**agent_skills:** self-load per @~/.claude/gsd-core/references/agent-skills-bootstrap.md 1. List available skills (subdirectories) 2. Read `SKILL.md` for each skill (lightweight index ~130 lines) 3. Load specific `rules/*.md` files as needed during verification diff --git a/agents/gsd-planner.md b/agents/gsd-planner.md index b830fa049..538304948 100644 --- a/agents/gsd-planner.md +++ b/agents/gsd-planner.md @@ -46,6 +46,8 @@ Before planning, discover project context: **Project skills:** @~/.claude/gsd-core/references/project-skills-discovery.md - Load `rules/*.md` as needed during **planning**. - Ensure plans account for project skill patterns and conventions. + +**agent_skills:** self-load per @~/.claude/gsd-core/references/agent-skills-bootstrap.md diff --git a/agents/gsd-project-researcher.md b/agents/gsd-project-researcher.md index eb55cc2be..a4fbcbcba 100644 --- a/agents/gsd-project-researcher.md +++ b/agents/gsd-project-researcher.md @@ -34,6 +34,8 @@ Your files feed the roadmap: @~/.claude/gsd-core/references/untrusted-input-boundary.md +**agent_skills:** self-load per @~/.claude/gsd-core/references/agent-skills-bootstrap.md + @~/.claude/gsd-core/references/research-documentation-lookup.md diff --git a/agents/gsd-research-synthesizer.md b/agents/gsd-research-synthesizer.md index d34dfc13e..6d346db8a 100644 --- a/agents/gsd-research-synthesizer.md +++ b/agents/gsd-research-synthesizer.md @@ -34,6 +34,8 @@ If the prompt contains a `` block, you MUST use the `Read` too @~/.claude/gsd-core/references/untrusted-input-boundary.md +**agent_skills:** self-load per @~/.claude/gsd-core/references/agent-skills-bootstrap.md + Your SUMMARY.md is consumed by the gsd-roadmapper agent which uses it to: diff --git a/agents/gsd-roadmapper.md b/agents/gsd-roadmapper.md index 4971cceb7..f2608f0f6 100644 --- a/agents/gsd-roadmapper.md +++ b/agents/gsd-roadmapper.md @@ -26,6 +26,8 @@ If the prompt contains a `` block, you MUST use the `Read` too **Context budget:** Load project skills first (lightweight). Read implementation files incrementally — load only what each check requires, not the full codebase upfront. **Project skills:** Check `.claude/skills/` or `.agents/skills/` directory if either exists: + +**agent_skills:** self-load per @~/.claude/gsd-core/references/agent-skills-bootstrap.md 1. List available skills (subdirectories) 2. Read `SKILL.md` for each skill (lightweight index ~130 lines) 3. Load specific `rules/*.md` files as needed during implementation diff --git a/agents/gsd-security-auditor.md b/agents/gsd-security-auditor.md index 80377d38d..234573fa0 100644 --- a/agents/gsd-security-auditor.md +++ b/agents/gsd-security-auditor.md @@ -51,6 +51,8 @@ Read ALL files from ``. Extract: **Context budget:** Load project skills first (lightweight). Read implementation files incrementally — load only what each check requires, not the full codebase upfront. **Project skills:** Check `.claude/skills/` or `.agents/skills/` directory if either exists: + +**agent_skills:** self-load per @~/.claude/gsd-core/references/agent-skills-bootstrap.md 1. List available skills (subdirectories) 2. Read `SKILL.md` for each skill (lightweight index ~130 lines) 3. Load specific `rules/*.md` files as needed during implementation diff --git a/agents/gsd-ui-auditor.md b/agents/gsd-ui-auditor.md index f30d28a35..9111177f2 100644 --- a/agents/gsd-ui-auditor.md +++ b/agents/gsd-ui-auditor.md @@ -49,6 +49,8 @@ Before auditing, discover project context: **Project instructions:** Read `./CLAUDE.md` if it exists in the working directory. Follow all project-specific guidelines. **Project skills:** Check `.claude/skills/` or `.agents/skills/` directory if either exists: + +**agent_skills:** self-load per @~/.claude/gsd-core/references/agent-skills-bootstrap.md 1. List available skills (subdirectories) 2. Read `SKILL.md` for each skill 3. Do NOT load full `AGENTS.md` files (100KB+ context cost) diff --git a/agents/gsd-ui-checker.md b/agents/gsd-ui-checker.md index b59e4f85e..69c77bd50 100644 --- a/agents/gsd-ui-checker.md +++ b/agents/gsd-ui-checker.md @@ -30,6 +30,8 @@ Before verifying, discover project context: **Project instructions:** Read `./CLAUDE.md` if it exists in the working directory. Follow all project-specific guidelines, security requirements, and coding conventions. **Project skills:** Check `.claude/skills/` or `.agents/skills/` directory if either exists: + +**agent_skills:** self-load per @~/.claude/gsd-core/references/agent-skills-bootstrap.md 1. List available skills (subdirectories) 2. Read `SKILL.md` for each skill (lightweight index ~130 lines) 3. Load specific `rules/*.md` files as needed during verification diff --git a/agents/gsd-ui-researcher.md b/agents/gsd-ui-researcher.md index 93611e340..9b5869f5f 100644 --- a/agents/gsd-ui-researcher.md +++ b/agents/gsd-ui-researcher.md @@ -39,6 +39,8 @@ Before researching, discover project context: **Project instructions:** Read `./CLAUDE.md` if it exists in the working directory. Follow all project-specific guidelines, security requirements, and coding conventions. **Project skills:** Check `.claude/skills/` or `.agents/skills/` directory if either exists: + +**agent_skills:** self-load per @~/.claude/gsd-core/references/agent-skills-bootstrap.md 1. List available skills (subdirectories) 2. Read `SKILL.md` for each skill (lightweight index ~130 lines) 3. Load specific `rules/*.md` files as needed during research diff --git a/agents/gsd-verifier.md b/agents/gsd-verifier.md index 55a4c427c..6e613c782 100644 --- a/agents/gsd-verifier.md +++ b/agents/gsd-verifier.md @@ -52,14 +52,16 @@ Before verifying, discover project context: **Project skills:** @~/.claude/gsd-core/references/project-skills-discovery.md - Load `rules/*.md` as needed during **verification**. - Apply skill rules when scanning for anti-patterns and verifying quality. + +**agent_skills:** self-load per @~/.claude/gsd-core/references/agent-skills-bootstrap.md **Task completion ≠ Goal achievement** -A task "create chat component" can be marked complete when the component is a placeholder. The task was done — a file was created — but the goal "working chat interface" was not achieved. +A "create chat component" task can be complete with a placeholder file — task done, goal "working chat interface" missed. -Goal-backward verification starts from the outcome and works backwards: +Start from the outcome and work backwards: 1. What must be TRUE for the goal to be achieved? 2. What must EXIST for those truths to hold? @@ -197,6 +199,7 @@ For each truth: - A pre-existing test exercises the transition/invariant and passes (confirm via Step 7b's single-named-test path) → ✓ VERIFIED. - No such test exists, or it can't run without a server/state mutation → ⚠️ PRESENT_BEHAVIOR_UNVERIFIED. Emit a human-verification item (Step 8) and do not count it toward the verified score (Step 9). - An accepted override (Step 3b) carries the truth as PASSED (override), exactly as it does for a FAILED truth. +5b. **Non-inferable (`backstop`) truths:** a `verification: backstop` truth (via `truthVerification()`) abstains unless confirmed by explicit evidence — mark `insufficient_spec` -> a human-verification item -> `human_needed`. See `references/honest-verifier.md`. 6. Determine truth status ## Step 3b: Check Verification Overrides diff --git a/bin/gsd-mcp-server.js b/bin/gsd-mcp-server.js new file mode 100644 index 000000000..49a197aed --- /dev/null +++ b/bin/gsd-mcp-server.js @@ -0,0 +1,31 @@ +#!/usr/bin/env node +'use strict'; +/** + * gsd-mcp-server — companion MCP server bin entry (ADR-1239 Phase C-2 / #1681). + * + * Lives at top-level bin/ (alongside install.js) — it is a PACKAGE bin the host + * spawns via `npx gsd-mcp-server` (or the global bin), NOT a per-runtime + * artifact copied into a host's config dir. (Placing it under gsd-core/bin/ + * would leak it into every runtime install + break golden parity.) + * + * A stdio JSON-RPC 2.0 server exposing GSD interface points 1 (command) + 5 + * (state IO) so any MCP-consuming host (Claude/Codex/OpenCode/VS Code/Gemini/ + * Cursor/Cline/Hermes) can drive GSD with no bespoke plugin. Delegates to the + * tested server module (gsd-core/bin/lib/mcp-server.cjs runServer). Reads + * line-delimited JSON-RPC from stdin, writes one response + newline per + * request, exits cleanly when stdin closes. + * + * The protocol logic (handleMessage) + the injectable-stream loop (runServer) + * are unit-tested in tests/gsd-mcp-server.test.cjs; the process lifecycle + * (spawn → JSON-RPC → clean exit) in tests/gsd-mcp-server-bin.test.cjs. + */ +const { runServer } = require('../gsd-core/bin/lib/mcp-server.cjs'); + +runServer({ + input: process.stdin, + output: process.stdout, + ctx: { cwd: process.cwd() }, +}).catch((err) => { + process.stderr.write(String((err && err.message) || err) + '\n'); + process.exit(1); +}); diff --git a/bin/install.js b/bin/install.js index 0f0813a19..536463678 100755 --- a/bin/install.js +++ b/bin/install.js @@ -37,7 +37,7 @@ const { // installer to the runtime-name-policy leaf (ADR-1508 / #1510 Phase 1) so the // conversion module's rewrite engine can consume it without importing // bin/install.js. Re-exported below for back-compat consumers/tests. -const { getDirName } = require('../gsd-core/bin/lib/runtime-name-policy.cjs'); +const { getDirName, getRuntimeLabel, getGlobalConfigHomeFragment, runtimeFlags, getRuntimeNewProjectCommand } = require('../gsd-core/bin/lib/runtime-name-policy.cjs'); const { applyWorktreeBaseRef, readBaseRefFromSettings, @@ -374,6 +374,7 @@ const { } = require(path.join(__dirname, '..', 'gsd-core', 'bin', 'lib', 'legacy-cleanup.cjs')); const { updateCacheFileName, + PACKAGE_NAME, } = require(path.join(__dirname, '..', 'gsd-core', 'bin', 'lib', 'package-identity.cjs')); // ADR-1239 Phase B: runtime-artifact install cluster extracted to install-engine.cjs. @@ -510,18 +511,11 @@ function getConfigDirFromHome(runtime, isGlobal) { // Local installs use the same dir name pattern return `'${getDirName(runtime)}'`; } - // Global installs - OpenCode uses XDG path structure - if (runtime === 'copilot') return "'.copilot'"; - if (runtime === 'opencode') { - // OpenCode: ~/.config/opencode -> '.config', 'opencode' - // Return as comma-separated for path.join() replacement - return "'.config', 'opencode'"; - } - if (runtime === 'gemini') return "'.gemini'"; - if (runtime === 'kilo') return "'.config', 'kilo'"; - if (runtime === 'codex') return "'.codex'"; + // Global installs. antigravity's home is resolved dynamically (env-overridable, + // multi-segment via resolveAntigravityGlobalDir + path.relative) — not a table + // entry. (The prior inner `if (!isGlobal) return "'.agents'"` was unreachable: + // !isGlobal returns at the top of this function.) if (runtime === 'antigravity') { - if (!isGlobal) return "'.agents'"; const antigravityDir = resolveAntigravityGlobalDir(); const rel = path.relative(os.homedir(), antigravityDir); const segments = rel.split(path.sep).filter(Boolean); @@ -532,16 +526,9 @@ function getConfigDirFromHome(runtime, isGlobal) { // stable legacy template so generated path.join() calls remain valid. return "'.gemini', 'antigravity'"; } - if (runtime === 'cursor') return "'.cursor'"; - if (runtime === 'windsurf') return "'.windsurf'"; - if (runtime === 'augment') return "'.augment'"; - if (runtime === 'trae') return "'.trae'"; - if (runtime === 'qwen') return "'.qwen'"; - if (runtime === 'hermes') return "'.hermes'"; - if (runtime === 'codebuddy') return "'.codebuddy'"; - if (runtime === 'cline') return "'.cline'"; - if (runtime === 'kimi') return "'.config', 'agents'"; - return "'.claude'"; + // All other runtimes: single source-of-truth fragment table (ADR-1239 Phase B, + // #1679). claude/unknown fall through to the table's default '.claude'. + return getGlobalConfigHomeFragment(runtime); } /** @@ -6932,19 +6919,7 @@ const GSD_UNINSTALL_HOOKS = [ * @param {string} runtime - Target runtime ('claude', 'opencode', 'gemini', 'codex', 'copilot') */ function uninstall(isGlobal, runtime = 'claude') { - const isOpencode = runtime === 'opencode'; - const isKilo = runtime === 'kilo'; - const isGemini = runtime === 'gemini'; - const isCodex = runtime === 'codex'; - const isCopilot = runtime === 'copilot'; - const isAntigravity = runtime === 'antigravity'; - const isCursor = runtime === 'cursor'; - const isWindsurf = runtime === 'windsurf'; - const isAugment = runtime === 'augment'; - const isTrae = runtime === 'trae'; - const isQwen = runtime === 'qwen'; - const isHermes = runtime === 'hermes'; - const isCodebuddy = runtime === 'codebuddy'; + const { isOpencode, isKilo, isGemini, isCodex, isCopilot, isAntigravity, isCursor, isWindsurf, isAugment, isTrae, isQwen, isHermes, isCodebuddy, isCline, isKimi } = runtimeFlags(runtime); const dirName = getDirName(runtime); // Get the target directory based on runtime and install type. Cline local @@ -6960,21 +6935,9 @@ function uninstall(isGlobal, runtime = 'claude') { ? targetDir.replace(os.homedir(), '~') : targetDir.replace(process.cwd(), '.'); - let runtimeLabel = 'Claude Code'; - if (runtime === 'opencode') runtimeLabel = 'OpenCode'; - if (runtime === 'gemini') runtimeLabel = 'Gemini'; - if (runtime === 'kilo') runtimeLabel = 'Kilo'; - if (runtime === 'codex') runtimeLabel = 'Codex'; - if (runtime === 'copilot') runtimeLabel = 'Copilot'; - if (runtime === 'antigravity') runtimeLabel = 'Antigravity'; - if (runtime === 'cursor') runtimeLabel = 'Cursor'; - if (runtime === 'windsurf') runtimeLabel = 'Windsurf'; - if (runtime === 'augment') runtimeLabel = 'Augment'; - if (runtime === 'trae') runtimeLabel = 'Trae'; - if (runtime === 'qwen') runtimeLabel = 'Qwen Code'; - if (runtime === 'hermes') runtimeLabel = 'Hermes Agent'; - if (runtime === 'kimi') runtimeLabel = 'Kimi CLI'; - if (runtime === 'codebuddy') runtimeLabel = 'CodeBuddy'; + // runtimeLabel is now the single-source getRuntimeLabel lookup (ADR-1239 + // Phase B / #1679) — collapses the prior 15-line assignment chain. + const runtimeLabel = getRuntimeLabel(runtime); console.log(` Uninstalling GSD from ${cyan}${runtimeLabel}${reset} at ${cyan}${locationLabel}${reset}\n`); @@ -7354,6 +7317,22 @@ function uninstall(isGlobal, runtime = 'claude') { } } + // 4z. Remove the OpenCode native plugin adapter (#1914). Only GSD's own + // plugin file is removed; the plugins/ dir is pruned only if it becomes + // empty, preserving any user-authored OpenCode plugins. + if (isOpencode) { + const pluginsDir = path.join(targetDir, 'plugins'); + const pluginPath = path.join(pluginsDir, 'gsd-core.js'); + if (fs.existsSync(pluginPath)) { + try { + fs.unlinkSync(pluginPath); + removedCount++; + console.log(` ${green}✓${reset} Removed OpenCode plugin`); + } catch (_) { /* best-effort */ } + try { fs.rmdirSync(pluginsDir); } catch (_) { /* not empty — user plugins present */ } + } + } + // 4a. Remove scripts/changeset/ and scripts/lib/ (#935) // GSD-managed files only: enumerate the exact set the installer writes. // Any file NOT in this set is user-owned and must survive uninstall. @@ -7398,6 +7377,11 @@ function uninstall(isGlobal, runtime = 'claude') { const fixSlashUninstallPath = path.join(targetDir, 'scripts', 'fix-slash-commands.cjs'); try { fs.unlinkSync(fixSlashUninstallPath); } catch (_) { /* best-effort */ } + // Remove the capability registry generator scripts (#1920) — before the scripts/ rmdir + for (const gen of ['gen-capability-registry.cjs', 'gen-loop-host-contract.cjs']) { + try { fs.unlinkSync(path.join(targetDir, 'scripts', gen)); } catch (_) { /* best-effort */ } + } + // If scripts/ dir is now empty, remove it too const scriptsUninstallDir = path.join(targetDir, 'scripts'); if (fs.existsSync(scriptsUninstallDir)) { @@ -7740,7 +7724,7 @@ function configureOpencodePermissions(isGlobal = true, configDir = null) { modified = true; } - // Configure external_directory permission (the safety guard for paths outside project) + // Configure external_directory permission (the safety guard for paths outside) if (!config.permission.external_directory || typeof config.permission.external_directory !== 'object') { config.permission.external_directory = {}; } @@ -7749,6 +7733,25 @@ function configureOpencodePermissions(isGlobal = true, configDir = null) { modified = true; } + // ADR-1239 Phase D / #1682 — register the companion MCP server (Phase 4) so + // OpenCode connects to GSD's command (point 1) + state-IO (point 5) surface + // with NO bespoke plugin. Idempotent + non-clobbering: only added when + // `mcp.gsd` is absent (a user-defined `mcp.gsd` is respected — Hyrum's Law). + // Local-stdio schema per OpenCode config (packages/core/src/config/mcp.ts). + // `-p @opengsd/gsd-core` resolves the `gsd-mcp-server` bin from this package + // (bin name != package name) regardless of global-install state. + if (!config.mcp || typeof config.mcp !== 'object') { + config.mcp = {}; + } + if (config.mcp.gsd === undefined) { + config.mcp.gsd = { + type: 'local', + command: ['npx', '-y', '-p', PACKAGE_NAME, 'gsd-mcp-server'], + enabled: true, + }; + modified = true; + } + if (!modified) { return; // Already configured } @@ -7941,18 +7944,7 @@ function resolveInstallRelativePath(baseDir, relPath) { * Write file manifest after installation for future modification detection */ function writeManifest(configDir, runtime = 'claude', options = {}) { - const isOpencode = runtime === 'opencode'; - const isKilo = runtime === 'kilo'; - const isGemini = runtime === 'gemini'; - const isCodex = runtime === 'codex'; - const isCopilot = runtime === 'copilot'; - const isAntigravity = runtime === 'antigravity'; - const isCursor = runtime === 'cursor'; - const isWindsurf = runtime === 'windsurf'; - const isTrae = runtime === 'trae'; - const isCline = runtime === 'cline'; - const isKimi = runtime === 'kimi'; - const isHermes = runtime === 'hermes'; + const { isOpencode, isKilo, isGemini, isCodex, isCopilot, isAntigravity, isCursor, isWindsurf, isAugment, isTrae, isQwen, isHermes, isCodebuddy, isCline, isKimi } = runtimeFlags(runtime); const gsdDir = path.join(configDir, 'gsd-core'); // #1367: Claude local now writes flat gsd-*.md files at commands/ (not commands/gsd/). // commandsDir points to the old location for Gemini (which still uses commands/gsd/). @@ -8113,6 +8105,24 @@ function writeManifest(configDir, runtime = 'claude', options = {}) { manifest.files['scripts/fix-slash-commands.cjs'] = fileHash(fixSlashInstallPath); } + // Track the capability registry generator scripts (#1920) — top-level scripts/ files + // not covered by the changeset/lib loops. + for (const gen of ['gen-capability-registry.cjs', 'gen-loop-host-contract.cjs']) { + const genInstallPath = path.join(configDir, 'scripts', gen); + if (fs.existsSync(genInstallPath)) { + manifest.files['scripts/' + gen] = fileHash(genInstallPath); + } + } + + // Track the OpenCode native plugin adapter (#1914) so update/drift detection + // and uninstall can account for it. + if (isOpencode) { + const pluginInstallPath = path.join(configDir, 'plugins', 'gsd-core.js'); + if (fs.existsSync(pluginInstallPath)) { + manifest.files['plugins/gsd-core.js'] = fileHash(pluginInstallPath); + } + } + fs.writeFileSync(path.join(configDir, MANIFEST_NAME), JSON.stringify(manifest, null, 2)); return manifest; } @@ -8439,21 +8449,7 @@ function reportInstallerMigrationResult(result) { } function install(isGlobal, runtime = 'claude', options = {}) { - const isOpencode = runtime === 'opencode'; - const isGemini = runtime === 'gemini'; - const isKilo = runtime === 'kilo'; - const isKimi = runtime === 'kimi'; - const isCodex = runtime === 'codex'; - const isCopilot = runtime === 'copilot'; - const isAntigravity = runtime === 'antigravity'; - const isCursor = runtime === 'cursor'; - const isWindsurf = runtime === 'windsurf'; - const isAugment = runtime === 'augment'; - const isTrae = runtime === 'trae'; - const isQwen = runtime === 'qwen'; - const isHermes = runtime === 'hermes'; - const isCodebuddy = runtime === 'codebuddy'; - const isCline = runtime === 'cline'; + const { isOpencode, isKilo, isGemini, isCodex, isCopilot, isAntigravity, isCursor, isWindsurf, isAugment, isTrae, isQwen, isHermes, isCodebuddy, isCline, isKimi } = runtimeFlags(runtime); const plan = resolveInstallPlan(runtime); const dirName = getDirName(runtime); const src = path.join(__dirname, '..'); @@ -8599,22 +8595,9 @@ function install(isGlobal, runtime = 'claude', options = {}) { homeDir, }); - let runtimeLabel = 'Claude Code'; - if (isOpencode) runtimeLabel = 'OpenCode'; - if (isGemini) runtimeLabel = 'Gemini'; - if (isKilo) runtimeLabel = 'Kilo'; - if (isCodex) runtimeLabel = 'Codex'; - if (isCopilot) runtimeLabel = 'Copilot'; - if (isAntigravity) runtimeLabel = 'Antigravity'; - if (isCursor) runtimeLabel = 'Cursor'; - if (isWindsurf) runtimeLabel = 'Windsurf'; - if (isAugment) runtimeLabel = 'Augment'; - if (isTrae) runtimeLabel = 'Trae'; - if (isQwen) runtimeLabel = 'Qwen Code'; - if (isHermes) runtimeLabel = 'Hermes Agent'; - if (isKimi) runtimeLabel = 'Kimi'; - if (isCodebuddy) runtimeLabel = 'CodeBuddy'; - if (isCline) runtimeLabel = 'Cline'; + // runtimeLabel is now the single-source getRuntimeLabel lookup (ADR-1239 + // Phase B / #1679) — collapses the prior 16-line assignment chain. + const runtimeLabel = getRuntimeLabel(runtime); console.log(` Installing for ${cyan}${runtimeLabel}${reset} to ${cyan}${locationLabel}${reset}\n`); @@ -9075,6 +9058,39 @@ function install(isGlobal, runtime = 'claude', options = {}) { } else { failures.push('skills/gsd-*'); } + + // OpenCode-only: install the native plugin adapter (#1914). OpenCode + // declares hooksSurface: 'none', so GSD's lifecycle hooks are never + // registered as settings.json hooks the way Claude Code does — the hook + // *scripts* ship to /hooks/ but nothing invokes them. This + // plugin bridges OpenCode's event bus onto those existing hook scripts + // (prompt guard, read guard, injection scanner, context monitor, ...), + // spawning them as subprocesses. OpenCode auto-discovers plugin files under + // /plugins/ at startup — no opencode.json registration needed + // (its `plugin` array is for npm packages, not local file paths). + // + // The file MUST land as `.js`: OpenCode's loader globs + // `{plugin,plugins}/*.{ts,js}` (verified against its source) — a `.cjs` + // extension would never be discovered. The config dir carries a + // `{"type":"commonjs"}` package.json (written above), so the `.js` file is + // interpreted as CommonJS, matching the adapter's module.exports/require. + // Kilo has no plugin surface, so this is gated to OpenCode only. + if (isOpencode) { + const pluginSrc = path.join(src, '.opencode', 'plugins', 'gsd-core.js'); + const pluginDestDir = path.join(targetDir, 'plugins'); + const pluginDest = path.join(pluginDestDir, 'gsd-core.js'); + if (fs.existsSync(pluginSrc)) { + fs.mkdirSync(pluginDestDir, { recursive: true }); + fs.copyFileSync(pluginSrc, pluginDest); + if (fs.existsSync(pluginDest)) { + console.log(` ${green}✓${reset} Installed OpenCode plugin (bridges GSD hooks)`); + } else { + failures.push('plugins/gsd-core.js'); + } + } else { + failures.push('plugins/gsd-core.js'); + } + } } else if (isCline) { // Cline local install: rules-based only — commands are embedded in .clinerules (generated below). // No skills/commands directory needed for local installs. @@ -9654,6 +9670,31 @@ function install(isGlobal, runtime = 'claude', options = {}) { } } + // Copy scripts/gen-capability-registry.cjs + scripts/gen-loop-host-contract.cjs — + // required by gsd-core/bin/lib/capability-loader.cjs at overlay-composition time via + // require('../../../scripts/gen-capability-registry.cjs') (which itself requires + // gen-loop-host-contract.cjs). Without these, the loader's never-crash invariant + // discards EVERY third-party capability overlay and silently falls back to the frozen + // first-party registry, so installed capabilities are inert (#1920). Same class of + // gap as #1223 (fix-slash-commands.cjs) and copied unconditionally for the same reason: + // any runtime that installs gsd-core/ needs the capability system to compose. + { + const capGenDestDir = path.join(targetDir, 'scripts'); + fs.mkdirSync(capGenDestDir, { recursive: true }); + for (const gen of ['gen-capability-registry.cjs', 'gen-loop-host-contract.cjs']) { + const genSrc = path.join(src, 'scripts', gen); + const genDest = path.join(capGenDestDir, gen); + if (!fs.existsSync(genSrc)) { + failures.push(`scripts/${gen} (source missing from package — reinstall from npm)`); + } else { + fs.copyFileSync(genSrc, genDest); + if (!verifyFileInstalled(genDest, `scripts/${gen}`)) { + failures.push(`scripts/${gen}`); + } + } + } + } + // Remove legacy get-shit-done-cc artifacts and stale update caches (#607). // cleanupLegacyGsdCc handles both the legacy shared cache and the per-package // cache (formerly an inline unlinkSync here). A cleanup failure must never @@ -10499,14 +10540,7 @@ function install(isGlobal, runtime = 'claude', options = {}) { * Apply statusline config, then print completion message */ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallStatusline, runtime = 'claude', isGlobal = true, configDir = null, bannerOpts = {}) { - const isOpencode = runtime === 'opencode'; - const isKilo = runtime === 'kilo'; - const isCodex = runtime === 'codex'; - const isCopilot = runtime === 'copilot'; - const isCursor = runtime === 'cursor'; - const isWindsurf = runtime === 'windsurf'; - const isTrae = runtime === 'trae'; - const isCline = runtime === 'cline'; + const { isOpencode, isKilo, isGemini, isCodex, isCopilot, isAntigravity, isCursor, isWindsurf, isAugment, isTrae, isQwen, isHermes, isCodebuddy, isCline, isKimi } = runtimeFlags(runtime); const plan = resolveInstallPlan(runtime); if (shouldInstallStatusline && plan.writesSharedSettings && !isOpencode) { @@ -10629,37 +10663,11 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS } } - let program = 'Claude Code'; - if (runtime === 'opencode') program = 'OpenCode'; - if (runtime === 'gemini') program = 'Gemini'; - if (runtime === 'kilo') program = 'Kilo'; - if (runtime === 'codex') program = 'Codex'; - if (runtime === 'copilot') program = 'Copilot'; - if (runtime === 'antigravity') program = 'Antigravity'; - if (runtime === 'cursor') program = 'Cursor'; - if (runtime === 'windsurf') program = 'Windsurf'; - if (runtime === 'augment') program = 'Augment'; - if (runtime === 'trae') program = 'Trae'; - if (runtime === 'cline') program = 'Cline'; - if (runtime === 'qwen') program = 'Qwen Code'; - if (runtime === 'hermes') program = 'Hermes Agent'; - if (runtime === 'kimi') program = 'Kimi CLI'; - - let command = '/gsd-new-project'; - if (runtime === 'opencode') command = '/gsd-new-project'; - if (runtime === 'kilo') command = '/gsd-new-project'; - if (runtime === 'gemini') command = '/gsd:new-project'; - if (runtime === 'codex') command = '$gsd-new-project'; - if (runtime === 'copilot') command = '/gsd-new-project'; - if (runtime === 'antigravity') command = '/gsd-new-project'; - if (runtime === 'cursor') command = 'gsd-new-project (mention the skill name)'; - if (runtime === 'windsurf') command = '/gsd-new-project'; - if (runtime === 'augment') command = '/gsd-new-project'; - if (runtime === 'trae') command = '/gsd-new-project'; - if (runtime === 'cline') command = '/gsd-new-project'; - if (runtime === 'qwen') command = '/gsd-new-project'; - if (runtime === 'hermes') command = '/gsd-new-project'; - if (runtime === 'kimi') command = '/skill:gsd-new-project'; + // program + command are now single-source lookups (ADR-1239 Phase B / #1679): + // program is the runtime display label; command is the per-host /gsd-new-project + // invocation syntax. + const program = getRuntimeLabel(runtime); + const command = getRuntimeNewProjectCommand(runtime); // Claude Code global installs use the skills/ format (CC 2.1.88+). // Restart is required for CC to pick up newly-installed skills, and the diff --git a/capabilities/ai-integration/capability.json b/capabilities/ai-integration/capability.json index 4e7405ba3..89367cc96 100644 --- a/capabilities/ai-integration/capability.json +++ b/capabilities/ai-integration/capability.json @@ -1,7 +1,7 @@ { "id": "ai-integration", "role": "feature", - "version": "1.6.0", + "version": "1.7.0-rc.2", "title": "AI design contract", "description": "AI-SPEC design contract workflow for phases that build AI systems; owns the AI integration command, agents, and workflow.ai_integration_phase activation key.", "tier": "full", diff --git a/capabilities/antigravity/capability.json b/capabilities/antigravity/capability.json index ef7680f2f..cd0736d7c 100644 --- a/capabilities/antigravity/capability.json +++ b/capabilities/antigravity/capability.json @@ -1,7 +1,7 @@ { "id": "antigravity", "role": "runtime", - "version": "1.6.0", + "version": "1.7.0-rc.2", "title": "Antigravity", "description": "Google Antigravity IDE — nested under ~/.gemini/antigravity; probed across 1.x and 2.x layouts; Gemini hook event dialect; flat skill layout; tier-1 support.", "tier": "core", @@ -60,7 +60,14 @@ "hostIntegration": { "embeddingMode": "declarative", "commandSurface": "slash-file", - "dispatch": { "namedDispatch": "undocumented", "nested": "undocumented", "maxDepth": "undocumented", "background": true, "subagentToolkit": "undocumented", "backgroundDispatch": "undocumented" }, + "dispatch": { + "namedDispatch": "undocumented", + "nested": "undocumented", + "maxDepth": "undocumented", + "background": true, + "subagentToolkit": "undocumented", + "backgroundDispatch": "undocumented" + }, "modelMode": "passive", "hookBus": "host", "stateIO": "filesystem", diff --git a/capabilities/assumption-delta/capability.json b/capabilities/assumption-delta/capability.json index 889e01e53..c24ce5c78 100644 --- a/capabilities/assumption-delta/capability.json +++ b/capabilities/assumption-delta/capability.json @@ -1,7 +1,7 @@ { "id": "assumption-delta", "role": "feature", - "version": "1.6.0", + "version": "1.7.0-rc.2", "title": "Assumption-delta architecture checkpoint", "description": "Rarely-firing advisory checkpoint that triggers when a phase makes something plural, optional, or chosen that used to be singular, required, or derived. Surfaces one identity-model question (promote the new general representation to primary, or add it alongside?) so a silent primary-key drift does not accumulate into a later user-facing bug. Non-blocking; fires only on a detected signal.", "tier": "full", diff --git a/capabilities/audit/capability.json b/capabilities/audit/capability.json index 293381326..899fbf89a 100644 --- a/capabilities/audit/capability.json +++ b/capabilities/audit/capability.json @@ -1,7 +1,7 @@ { "id": "audit", "role": "feature", - "version": "1.6.0", + "version": "1.7.0-rc.2", "title": "Audit", "description": "Open-artifact audit and UAT-gap audit for milestone close gates; exposes `gsd-tools audit-uat` (cross-phase UAT outstanding items) and `gsd-tools audit-open` (structured open-artifact scan across debug, tasks, threads, todos, seeds, UAT, verification, context-questions).", "tier": "full", diff --git a/capabilities/augment/capability.json b/capabilities/augment/capability.json index 27f211790..cc4305b6d 100644 --- a/capabilities/augment/capability.json +++ b/capabilities/augment/capability.json @@ -1,7 +1,7 @@ { "id": "augment", "role": "runtime", - "version": "1.6.0", + "version": "1.7.0-rc.2", "title": "Augment Code", "description": "Augment Code CLI — commands + nested-skill artifact layout; settings-json hook surface; Claude hook event dialect; tier-2 support.", "tier": "core", @@ -85,7 +85,14 @@ "hostIntegration": { "embeddingMode": "declarative", "commandSurface": "slash-file", - "dispatch": { "namedDispatch": true, "nested": "undocumented", "maxDepth": "undocumented", "background": true, "subagentToolkit": "full", "backgroundDispatch": "undocumented" }, + "dispatch": { + "namedDispatch": true, + "nested": "undocumented", + "maxDepth": "undocumented", + "background": true, + "subagentToolkit": "full", + "backgroundDispatch": "undocumented" + }, "modelMode": "passive", "hookBus": "host", "stateIO": "filesystem", diff --git a/capabilities/claude/capability.json b/capabilities/claude/capability.json index 712b332f1..e678e318d 100644 --- a/capabilities/claude/capability.json +++ b/capabilities/claude/capability.json @@ -1,7 +1,7 @@ { "id": "claude", "role": "runtime", - "version": "1.6.0", + "version": "1.7.0-rc.2", "title": "Claude Code", "description": "Anthropic Claude Code — primary development runtime; tier-1 support with full hook surface and skills-based global install.", "tier": "core", @@ -66,7 +66,14 @@ "hostIntegration": { "embeddingMode": "imperative", "commandSurface": "slash-file", - "dispatch": { "namedDispatch": true, "nested": true, "maxDepth": 5, "background": true, "subagentToolkit": "full", "backgroundDispatch": false }, + "dispatch": { + "namedDispatch": true, + "nested": true, + "maxDepth": 5, + "background": true, + "subagentToolkit": "full", + "backgroundDispatch": false + }, "modelMode": "passive", "hookBus": "host", "stateIO": "filesystem", diff --git a/capabilities/cline/capability.json b/capabilities/cline/capability.json index 119269871..f9109f2bc 100644 --- a/capabilities/cline/capability.json +++ b/capabilities/cline/capability.json @@ -1,7 +1,7 @@ { "id": "cline", "role": "runtime", - "version": "1.6.0", + "version": "1.7.0-rc.2", "title": "Cline", "description": "Cline (VS Code extension) — global-only nested-skill layout; cline-rules hook surface (.clinerules); no hook events emitted; tier-2 support.", "tier": "core", @@ -43,7 +43,14 @@ "hostIntegration": { "embeddingMode": "imperative", "commandSurface": "slash-file", - "dispatch": { "namedDispatch": true, "nested": false, "maxDepth": 1, "background": true, "subagentToolkit": "read-only", "backgroundDispatch": false }, + "dispatch": { + "namedDispatch": true, + "nested": false, + "maxDepth": 1, + "background": true, + "subagentToolkit": "read-only", + "backgroundDispatch": false + }, "modelMode": "active", "hookBus": "host", "stateIO": "filesystem", diff --git a/capabilities/code-review/capability.json b/capabilities/code-review/capability.json index 153f433ff..4ffd49e4b 100644 --- a/capabilities/code-review/capability.json +++ b/capabilities/code-review/capability.json @@ -1,7 +1,7 @@ { "id": "code-review", "role": "feature", - "version": "1.6.0", + "version": "1.7.0-rc.2", "title": "Code review", "description": "Source-file code review and review-fix workflow support for completed execution work.", "tier": "full", diff --git a/capabilities/codebuddy/capability.json b/capabilities/codebuddy/capability.json index f2de29607..8325b6f40 100644 --- a/capabilities/codebuddy/capability.json +++ b/capabilities/codebuddy/capability.json @@ -1,7 +1,7 @@ { "id": "codebuddy", "role": "runtime", - "version": "1.6.0", + "version": "1.7.0-rc.2", "title": "CodeBuddy", "description": "CodeBuddy (Tencent) — converted commands + skills artifact layout; settings-json hook surface; Claude hook event dialect; tier-2 support.", "tier": "core", @@ -85,7 +85,14 @@ "hostIntegration": { "embeddingMode": "declarative", "commandSurface": "slash-file", - "dispatch": { "namedDispatch": true, "nested": false, "maxDepth": 1, "background": true, "subagentToolkit": "full", "backgroundDispatch": false }, + "dispatch": { + "namedDispatch": true, + "nested": false, + "maxDepth": 1, + "background": true, + "subagentToolkit": "full", + "backgroundDispatch": false + }, "modelMode": "passive", "hookBus": "host", "stateIO": "filesystem", diff --git a/capabilities/codex/capability.json b/capabilities/codex/capability.json index a1ddc49b7..172ff94b4 100644 --- a/capabilities/codex/capability.json +++ b/capabilities/codex/capability.json @@ -1,7 +1,7 @@ { "id": "codex", "role": "runtime", - "version": "1.6.0", + "version": "1.7.0-rc.2", "title": "OpenAI Codex CLI", "description": "OpenAI Codex CLI — shell-var command style; per-agent sandbox tiers; config.toml + hooks.json hook surface; tier-1 support.", "tier": "core", @@ -53,7 +53,14 @@ "hostIntegration": { "embeddingMode": "declarative", "commandSurface": "slash-file", - "dispatch": { "namedDispatch": true, "nested": true, "maxDepth": 1, "background": true, "subagentToolkit": "full", "backgroundDispatch": true }, + "dispatch": { + "namedDispatch": true, + "nested": true, + "maxDepth": 1, + "background": true, + "subagentToolkit": "full", + "backgroundDispatch": true + }, "modelMode": "passive", "hookBus": "host", "stateIO": "filesystem", diff --git a/capabilities/copilot/capability.json b/capabilities/copilot/capability.json index be769009b..e57583756 100644 --- a/capabilities/copilot/capability.json +++ b/capabilities/copilot/capability.json @@ -1,7 +1,7 @@ { "id": "copilot", "role": "runtime", - "version": "1.6.0", + "version": "1.7.0-rc.2", "title": "GitHub Copilot", "description": "GitHub Copilot (VS Code) — markdown config format; copilot-inline hook surface; no hook events emitted; flat skill nesting (unconfirmed recursive loader); tier-2 support.", "tier": "core", @@ -53,7 +53,14 @@ "hostIntegration": { "embeddingMode": "declarative", "commandSurface": "slash-file", - "dispatch": { "namedDispatch": true, "nested": false, "maxDepth": 1, "background": true, "subagentToolkit": "full", "backgroundDispatch": false }, + "dispatch": { + "namedDispatch": true, + "nested": false, + "maxDepth": 1, + "background": true, + "subagentToolkit": "full", + "backgroundDispatch": false + }, "modelMode": "passive", "hookBus": "host", "stateIO": "filesystem", diff --git a/capabilities/cursor/capability.json b/capabilities/cursor/capability.json index 247674009..3d6fd7c66 100644 --- a/capabilities/cursor/capability.json +++ b/capabilities/cursor/capability.json @@ -1,7 +1,7 @@ { "id": "cursor", "role": "runtime", - "version": "1.6.0", + "version": "1.7.0-rc.2", "title": "Cursor", "description": "Cursor IDE — skills + converted commands artifact layout; hooks.json surface; Claude hook event dialect; recursive skill loader (flat nesting); tier-2 support.", "tier": "core", @@ -85,7 +85,14 @@ "hostIntegration": { "embeddingMode": "imperative", "commandSurface": "slash-file", - "dispatch": { "namedDispatch": true, "nested": true, "maxDepth": 2, "background": true, "subagentToolkit": "full", "backgroundDispatch": true }, + "dispatch": { + "namedDispatch": true, + "nested": true, + "maxDepth": 2, + "background": true, + "subagentToolkit": "full", + "backgroundDispatch": true + }, "modelMode": "passive", "hookBus": "host", "stateIO": "filesystem", diff --git a/capabilities/drift/capability.json b/capabilities/drift/capability.json index c69ad521a..66a18b5b9 100644 --- a/capabilities/drift/capability.json +++ b/capabilities/drift/capability.json @@ -1,7 +1,7 @@ { "id": "drift", "role": "feature", - "version": "1.6.0", + "version": "1.7.0-rc.2", "title": "Drift detection gates", "description": "Drift detection gates for the planning loop. At execute:wave:post: a blocking schema drift gate (detects schema files changed without a database push) and a non-blocking codebase drift gate (detects structural additions not reflected in STRUCTURE.md). At plan:pre: a non-blocking, warn-only codebase drift gate (gated on workflow.plan_drift_precheck) that flags a stale codebase map before planning, so plans are authored against a fresh STRUCTURE.md instead of discovering drift mid-execution.", "tier": "full", diff --git a/capabilities/external-job/capability.json b/capabilities/external-job/capability.json new file mode 100644 index 000000000..170294288 --- /dev/null +++ b/capabilities/external-job/capability.json @@ -0,0 +1,81 @@ +{ + "id": "external-job", + "role": "feature", + "version": "1.7.0-rc.2", + "title": "Async external-job scheduler adapter", + "description": "Default-off producer of the async external-job manifest (#1164). At execute:wave:post an executor can externalize long-running compute (SLURM first, scheduler-pluggable), commit a .planning/async-jobs/.json manifest, defer SUMMARY.md, and return external_job_waiting. The core loop (#1165) consumes the manifest; this capability is the only thing that writes it.", + "tier": "full", + "requires": [], + "engines": { + "gsd": ">=1.7.0" + }, + "runtimeCompat": { + "supported": [ + "*" + ], + "unsupported": [] + }, + "skills": [], + "agents": [], + "hooks": [], + "config": { + "external_job.enabled": { + "type": "boolean", + "default": false, + "description": "Master toggle for the async external-job producer capability. Default-off: the core loop consumes manifests whether or not this is on, but no manifest is ever written unless an executor opts in here." + }, + "external_job.backend": { + "type": "enum", + "values": [ + "slurm" + ], + "default": "slurm", + "description": "Scheduler backend. SLURM is the first adapter; the field is the pluggability seam for future backends (LSF, PBS, Kubernetes batch). Core never interprets this value." + }, + "external_job.artifact_dir": { + "type": "string", + "default": "Artifacts/jobs", + "description": "Root for per-job artifact directories (e.g. Artifacts/jobs//). Avoids fixed log paths and hardcoding a cluster/project layout." + }, + "external_job.submit_timeout_ms": { + "type": "number", + "default": 30000, + "description": "Hard timeout (ms) for the scheduler submit subprocess (e.g. sbatch). Bounded per CLAUDE.md unbounded-subprocess policy." + }, + "external_job.poll_timeout_ms": { + "type": "number", + "default": 15000, + "description": "Hard timeout (ms) for the scheduler poll subprocess (squeue, with sacct fallback)." + } + }, + "steps": [], + "contributions": [ + { + "point": "execute:wave:post", + "into": "executor", + "fragment": { + "path": "fragments/execute-wave-post.md" + }, + "produces": [ + ".planning/async-jobs/.json" + ], + "consumes": [ + "PLAN.md" + ], + "when": "external_job.enabled", + "onError": "skip" + }, + { + "point": "plan:post", + "into": "planner", + "fragment": { + "path": "fragments/plan-post.md" + }, + "produces": [], + "consumes": [], + "when": "external_job.enabled", + "onError": "skip" + } + ], + "gates": [] +} diff --git a/capabilities/external-job/fragments/execute-wave-post.md b/capabilities/external-job/fragments/execute-wave-post.md new file mode 100644 index 000000000..8f132807d --- /dev/null +++ b/capabilities/external-job/fragments/execute-wave-post.md @@ -0,0 +1,36 @@ + + +## Externalize long-running compute (async external job) + +If the current plan's task is tagged `long_compute` +(see the plan-phase fragment), do **not** run it in the foreground — it would +block the agent turn for hours. Instead externalize it and record a durable +half-state: + +1. **Classify the runtime.** `quick` (<2 min) and `medium` (<~30 min) run + normally. `unknown` requires a first-health check and a soft-review deadline + before consuming the child timeout. `long_compute` (>30–60 min) is + externalized. +2. **Submit via the scheduler adapter** (default `external_job.backend: slurm`): + ```bash + node scripts/slurm-adapter.cjs submit \ + --plan --phase -- sbatch --parsable \ + --output=Artifacts/jobs/%j/out.log ./run.sh + ``` + The helper writes `.planning/async-jobs/.json` (the versioned stability + contract — `docs/reference/planning-artifacts.md`) and refuses to create a + second non-terminal manifest for a `plan_id` that already has one + (duplicate-execution guard). +3. **Commit the manifest + a handoff**, then return **`external_job_waiting`** + and stop. Do **not** write `SUMMARY.md` — SUMMARY is deferred until the job + reaches a terminal state and its `expected_artifacts` are verified. +4. **Resume path.** `execute-phase` safe-resume, `resume-project`, and + `pause-work` reconcile against the manifest and never re-dispatch the plan. + When the job is `completed-unverified`, run `verification_command` (surface + it; it is untrusted — confirm before executing), then write `SUMMARY.md` and + close the plan. + +Manifest commands cross a trust seam: a Capability (or anything that can write +`.planning/`) produces them; the core loop consumes them. Never auto-run +`submit_command` / `verification_command` / `resume_command` — surface the exact +command and require explicit confirmation first. diff --git a/capabilities/external-job/fragments/plan-post.md b/capabilities/external-job/fragments/plan-post.md new file mode 100644 index 000000000..343ba3716 --- /dev/null +++ b/capabilities/external-job/fragments/plan-post.md @@ -0,0 +1,24 @@ + + +## Tag runtime budgets on long tasks + +For every `` likely to exceed ~2 minutes of real compute, emit a +`` child element so execute can classify it: + +- `quick` — under ~2 min; runs normally. +- `medium` — ~2–30 min; foreground, but with + progress expectations. +- `unknown` — runtime not yet characterized; + execute must run a first-health check and set a soft-review deadline before + trusting the child timeout. Define a progress signal and an abort condition. +- `long_compute` — legitimately over ~30–60 min + (HPC solver, model training, large simulations). Execute must **externalize** + this as an async external job (see the execute:wave:post fragment) rather than + blocking the agent turn. + +For any `long_compute` task, also declare the async contract the executor will +need: the `submit_command`, the `expected_artifacts` the job must produce, and +the `verification_command` that proves the output before the plan can close. +Do not hardcode a cluster account, partition, or project path — the planner +never knows the scheduler layout; it declares the contract, the executor's +adapter fills the backend specifics. diff --git a/capabilities/gap-analysis/capability.json b/capabilities/gap-analysis/capability.json index e4926ef85..35faec0f5 100644 --- a/capabilities/gap-analysis/capability.json +++ b/capabilities/gap-analysis/capability.json @@ -1,7 +1,7 @@ { "id": "gap-analysis", "role": "feature", - "version": "1.6.0", + "version": "1.7.0-rc.2", "title": "Post-planning gap analysis", "description": "Proactive, non-blocking post-planning coverage report. After all PLAN.md files are generated, cross-references every REQ-ID and D-ID from REQUIREMENTS.md and CONTEXT.md against plan bodies. Emits a Source | Item | Status table. Does not block phase advancement.", "tier": "standard", diff --git a/capabilities/gemini/capability.json b/capabilities/gemini/capability.json index ccc67a8d1..393cee428 100644 --- a/capabilities/gemini/capability.json +++ b/capabilities/gemini/capability.json @@ -1,7 +1,7 @@ { "id": "gemini", "role": "runtime", - "version": "1.6.0", + "version": "1.7.0-rc.2", "title": "Gemini CLI", "description": "Google Gemini CLI — commands-only artifact layout (TOML); Gemini hook event dialect; settings-json hook surface; tier-2 support.", "tier": "core", @@ -57,7 +57,14 @@ "hostIntegration": { "embeddingMode": "declarative", "commandSurface": "slash-toml", - "dispatch": { "namedDispatch": true, "nested": false, "maxDepth": 1, "background": "undocumented", "subagentToolkit": "undocumented", "backgroundDispatch": false }, + "dispatch": { + "namedDispatch": true, + "nested": false, + "maxDepth": 1, + "background": "undocumented", + "subagentToolkit": "undocumented", + "backgroundDispatch": false + }, "modelMode": "passive", "hookBus": "host", "stateIO": "filesystem", diff --git a/capabilities/graphify/capability.json b/capabilities/graphify/capability.json index cff231e46..38855ab72 100644 --- a/capabilities/graphify/capability.json +++ b/capabilities/graphify/capability.json @@ -1,7 +1,7 @@ { "id": "graphify", "role": "feature", - "version": "1.6.0", + "version": "1.7.0-rc.2", "title": "Knowledge graph", "description": "Build, query, and inspect the project knowledge graph in `.planning/graphs/`; exposes graphify CLI subcommands (build, query, status, diff) and the /gsd-graphify skill.", "tier": "full", diff --git a/capabilities/hermes/capability.json b/capabilities/hermes/capability.json index 5dd4e4b05..a049eb665 100644 --- a/capabilities/hermes/capability.json +++ b/capabilities/hermes/capability.json @@ -1,7 +1,7 @@ { "id": "hermes", "role": "runtime", - "version": "1.6.0", + "version": "1.7.0-rc.2", "title": "Hermes Agent", "description": "Hermes Agent (NousResearch) — skills nest under skills/gsd/ category bucket; nested skill layout; settings-json hook surface; Claude hook event dialect; tier-2 support.", "tier": "core", @@ -53,7 +53,14 @@ "hostIntegration": { "embeddingMode": "imperative", "commandSurface": "slash-programmatic", - "dispatch": { "namedDispatch": false, "nested": true, "maxDepth": 1, "background": true, "subagentToolkit": "read-only", "backgroundDispatch": false }, + "dispatch": { + "namedDispatch": false, + "nested": true, + "maxDepth": 1, + "background": true, + "subagentToolkit": "read-only", + "backgroundDispatch": false + }, "modelMode": "active", "hookBus": "host", "stateIO": "filesystem", diff --git a/capabilities/intel/capability.json b/capabilities/intel/capability.json index 69c14bcc7..b33f8499a 100644 --- a/capabilities/intel/capability.json +++ b/capabilities/intel/capability.json @@ -1,7 +1,7 @@ { "id": "intel", "role": "feature", - "version": "1.6.0", + "version": "1.7.0-rc.2", "title": "Codebase intelligence", "description": "Code-intelligence store for codebase querying, diff, snapshot, and API-surface extraction; exposes `gsd-tools intel` subcommands (query, status, update, diff, snapshot, patch-meta, validate, extract-exports, api-surface) and backs `/gsd-map-codebase` and `gsd-intel-updater`.", "tier": "full", diff --git a/capabilities/kilo/capability.json b/capabilities/kilo/capability.json index 032f9be2e..53e29f73c 100644 --- a/capabilities/kilo/capability.json +++ b/capabilities/kilo/capability.json @@ -1,7 +1,7 @@ { "id": "kilo", "role": "runtime", - "version": "1.6.0", + "version": "1.7.0-rc.2", "title": "Kilo Code", "description": "Kilo Code — XDG-based config dir; global skills at ~/.kilo/skills (separate from XDG config); flat command/ + skills artifact layout; no lifecycle hook registration; tier-2 support.", "tier": "core", @@ -75,7 +75,14 @@ "hostIntegration": { "embeddingMode": "imperative", "commandSurface": "slash-file", - "dispatch": { "namedDispatch": true, "nested": true, "maxDepth": -1, "background": true, "subagentToolkit": "undocumented", "backgroundDispatch": false }, + "dispatch": { + "namedDispatch": true, + "nested": true, + "maxDepth": -1, + "background": true, + "subagentToolkit": "undocumented", + "backgroundDispatch": false + }, "modelMode": "active", "hookBus": "host", "stateIO": "filesystem", diff --git a/capabilities/kimi/capability.json b/capabilities/kimi/capability.json index d9bf8da7d..f025709ee 100644 --- a/capabilities/kimi/capability.json +++ b/capabilities/kimi/capability.json @@ -1,7 +1,7 @@ { "id": "kimi", "role": "runtime", - "version": "1.6.0", + "version": "1.7.0-rc.2", "title": "Kimi CLI", "description": "Kimi CLI (Moonshot AI) — generic agents root at ~/.config/agents; skills + kimi-agents artifact layout; no hook surface; no hook events; tier-2 support.", "tier": "core", @@ -56,7 +56,14 @@ "hostIntegration": { "embeddingMode": "imperative", "commandSurface": "slash-file", - "dispatch": { "namedDispatch": true, "nested": false, "maxDepth": 1, "background": true, "subagentToolkit": "undocumented", "backgroundDispatch": false }, + "dispatch": { + "namedDispatch": true, + "nested": false, + "maxDepth": 1, + "background": true, + "subagentToolkit": "undocumented", + "backgroundDispatch": false + }, "modelMode": "passive", "hookBus": "host", "stateIO": "filesystem", diff --git a/capabilities/mempalace/capability.json b/capabilities/mempalace/capability.json index a2e80c04a..0ff3ee460 100644 --- a/capabilities/mempalace/capability.json +++ b/capabilities/mempalace/capability.json @@ -1,7 +1,7 @@ { "id": "mempalace", "role": "feature", - "version": "1.6.0", + "version": "1.7.0-rc.2", "title": "MemPalace memory", "description": "Cross-session, cross-project memory: deliberate recall before discuss/plan and verbatim capture + temporal-KG sync at phase boundaries, via the MemPalace MCP server and CLI.", "tier": "full", diff --git a/capabilities/nyquist/capability.json b/capabilities/nyquist/capability.json index 589402fa3..f6d75a96e 100644 --- a/capabilities/nyquist/capability.json +++ b/capabilities/nyquist/capability.json @@ -1,7 +1,7 @@ { "id": "nyquist", "role": "feature", - "version": "1.6.0", + "version": "1.7.0-rc.2", "title": "Nyquist validation", "description": "Validation coverage audit that maps executed work back to tests and manual-only evidence.", "tier": "full", diff --git a/capabilities/opencode/capability.json b/capabilities/opencode/capability.json index 21987141b..f123fa28a 100644 --- a/capabilities/opencode/capability.json +++ b/capabilities/opencode/capability.json @@ -1,7 +1,7 @@ { "id": "opencode", "role": "runtime", - "version": "1.6.0", + "version": "1.7.0-rc.2", "title": "OpenCode", "description": "OpenCode — XDG-based config dir; flat command/ + skills artifact layout; settings-json config format; no lifecycle hook registration; tier-2 support.", "tier": "core", @@ -61,6 +61,7 @@ }, "commandStyle": "slash-hyphen", "hooksSurface": "none", + "extensionEvents": "opencode", "sandboxTier": "none", "supportTier": 2, "installSurface": "settings-json", @@ -70,7 +71,14 @@ "hostIntegration": { "embeddingMode": "imperative", "commandSurface": "slash-file", - "dispatch": { "namedDispatch": true, "nested": "undocumented", "maxDepth": "undocumented", "background": false, "subagentToolkit": "full", "backgroundDispatch": "undocumented" }, + "dispatch": { + "namedDispatch": true, + "nested": "undocumented", + "maxDepth": "undocumented", + "background": false, + "subagentToolkit": "full", + "backgroundDispatch": "undocumented" + }, "modelMode": "active", "hookBus": "host", "stateIO": "filesystem", diff --git a/capabilities/pattern-mapper/capability.json b/capabilities/pattern-mapper/capability.json index 259f48d84..ac0027951 100644 --- a/capabilities/pattern-mapper/capability.json +++ b/capabilities/pattern-mapper/capability.json @@ -1,7 +1,7 @@ { "id": "pattern-mapper", "role": "feature", - "version": "1.6.0", + "version": "1.7.0-rc.2", "title": "Pattern mapping", "description": "Optional codebase-pattern mapping before planning; owns the pattern mapper agent and workflow.pattern_mapper activation key.", "tier": "full", diff --git a/capabilities/profile-pipeline/capability.json b/capabilities/profile-pipeline/capability.json index cc0b58c34..2603254c8 100644 --- a/capabilities/profile-pipeline/capability.json +++ b/capabilities/profile-pipeline/capability.json @@ -1,7 +1,7 @@ { "id": "profile-pipeline", "role": "feature", - "version": "1.6.0", + "version": "1.7.0-rc.2", "title": "Developer profiling pipeline", "description": "Developer behavioral profiling from Claude Code session history; scans session JSONL files, extracts and samples user messages, and generates profile artifacts (USER-PROFILE.md, dev-preferences.md, CLAUDE.md sections). Exposes eight `gsd-tools` commands: scan-sessions, extract-messages, profile-sample (pipeline phase) and write-profile, profile-questionnaire, generate-dev-preferences, generate-claude-profile, generate-claude-md (output phase). Backs the /gsd-profile-user skill and gsd-user-profiler agent.", "tier": "full", diff --git a/capabilities/qwen/capability.json b/capabilities/qwen/capability.json index 2199c8a5e..5d5cf46ff 100644 --- a/capabilities/qwen/capability.json +++ b/capabilities/qwen/capability.json @@ -1,7 +1,7 @@ { "id": "qwen", "role": "runtime", - "version": "1.6.0", + "version": "1.7.0-rc.2", "title": "Qwen Code", "description": "Qwen Code (Alibaba) — nested-skill artifact layout; settings-json hook surface; Claude hook event dialect; tier-2 support.", "tier": "core", @@ -57,7 +57,14 @@ "hostIntegration": { "embeddingMode": "imperative", "commandSurface": "slash-file", - "dispatch": { "namedDispatch": true, "nested": false, "maxDepth": 1, "background": true, "subagentToolkit": "full", "backgroundDispatch": false }, + "dispatch": { + "namedDispatch": true, + "nested": false, + "maxDepth": 1, + "background": true, + "subagentToolkit": "full", + "backgroundDispatch": false + }, "modelMode": "passive", "hookBus": "host", "stateIO": "filesystem", diff --git a/capabilities/research/capability.json b/capabilities/research/capability.json index 63ecab84d..1cbc891a6 100644 --- a/capabilities/research/capability.json +++ b/capabilities/research/capability.json @@ -1,7 +1,7 @@ { "id": "research", "role": "feature", - "version": "1.6.0", + "version": "1.7.0-rc.2", "title": "Phase research", "description": "Optional phase research before planning; owns the phase researcher agent and workflow.research activation key.", "tier": "standard", diff --git a/capabilities/schema-gate/capability.json b/capabilities/schema-gate/capability.json index 254a160cf..626122a43 100644 --- a/capabilities/schema-gate/capability.json +++ b/capabilities/schema-gate/capability.json @@ -1,7 +1,7 @@ { "id": "schema-gate", "role": "feature", - "version": "1.6.0", + "version": "1.7.0-rc.2", "title": "Schema push detection gate", "description": "Detects ORM schema-relevant files in the phase scope during planning 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.", "tier": "full", diff --git a/capabilities/security/capability.json b/capabilities/security/capability.json index e9599ed0e..d4d5fc61a 100644 --- a/capabilities/security/capability.json +++ b/capabilities/security/capability.json @@ -1,7 +1,7 @@ { "id": "security", "role": "feature", - "version": "1.6.0", + "version": "1.7.0-rc.2", "title": "Security enforcement", "description": "Threat mitigation verification and ship-time security blocking for phases with security enforcement enabled.", "tier": "full", diff --git a/capabilities/tdd/capability.json b/capabilities/tdd/capability.json index c2f99c57b..03b77f394 100644 --- a/capabilities/tdd/capability.json +++ b/capabilities/tdd/capability.json @@ -1,7 +1,7 @@ { "id": "tdd", "role": "feature", - "version": "1.6.0", + "version": "1.7.0-rc.2", "title": "Test-driven development", "description": "Injects TDD heuristics into the planner and enforces RED/GREEN gate compliance on type:tdd plans after execution. Owns workflow.tdd_mode; the --tdd CLI flag is the ephemeral override.", "tier": "full", diff --git a/capabilities/trae/capability.json b/capabilities/trae/capability.json index 87dc2f5cd..02fcda713 100644 --- a/capabilities/trae/capability.json +++ b/capabilities/trae/capability.json @@ -1,7 +1,7 @@ { "id": "trae", "role": "runtime", - "version": "1.6.0", + "version": "1.7.0-rc.2", "title": "Trae IDE", "description": "Trae IDE — nested-skill artifact layout; no hook surface (profile-marker-only config); tier-2 support.", "tier": "core", @@ -68,7 +68,14 @@ "hostIntegration": { "embeddingMode": "imperative", "commandSurface": "slash-file", - "dispatch": { "namedDispatch": true, "nested": "undocumented", "maxDepth": "undocumented", "background": true, "subagentToolkit": "undocumented", "backgroundDispatch": "undocumented" }, + "dispatch": { + "namedDispatch": true, + "nested": "undocumented", + "maxDepth": "undocumented", + "background": true, + "subagentToolkit": "undocumented", + "backgroundDispatch": "undocumented" + }, "modelMode": "passive", "hookBus": "engine", "stateIO": "filesystem", diff --git a/capabilities/ui/capability.json b/capabilities/ui/capability.json index 903477c89..fc1266934 100644 --- a/capabilities/ui/capability.json +++ b/capabilities/ui/capability.json @@ -1,7 +1,7 @@ { "id": "ui", "role": "feature", - "version": "1.6.0", + "version": "1.7.0-rc.2", "title": "UI design contracts", "description": "UI-SPEC design contract + retrospective UI audit for frontend phases.", "tier": "full", diff --git a/capabilities/windsurf/capability.json b/capabilities/windsurf/capability.json index 98933d374..a20ad08c8 100644 --- a/capabilities/windsurf/capability.json +++ b/capabilities/windsurf/capability.json @@ -1,7 +1,7 @@ { "id": "windsurf", "role": "runtime", - "version": "1.6.0", + "version": "1.7.0-rc.2", "title": "Windsurf", "description": "Windsurf (Codeium) — workspace workflow artifact layout for slash commands; no hook surface; no hook events; tier-2 support.", "tier": "core", @@ -61,7 +61,14 @@ "hostIntegration": { "embeddingMode": "declarative", "commandSurface": "slash-file", - "dispatch": { "namedDispatch": "undocumented", "nested": "undocumented", "maxDepth": "undocumented", "background": "undocumented", "subagentToolkit": "undocumented", "backgroundDispatch": "undocumented" }, + "dispatch": { + "namedDispatch": "undocumented", + "nested": "undocumented", + "maxDepth": "undocumented", + "background": "undocumented", + "subagentToolkit": "undocumented", + "backgroundDispatch": "undocumented" + }, "modelMode": "passive", "hookBus": "host", "stateIO": "filesystem", diff --git a/commands/gsd/next.md b/commands/gsd/next.md new file mode 100644 index 000000000..8ca19c1a1 --- /dev/null +++ b/commands/gsd/next.md @@ -0,0 +1,29 @@ +--- +name: gsd:next +description: Smart entry — detect project state and route to the right next GSD action. +effort: low +allowed-tools: + - Read + - Bash + - Glob + - SlashCommand + - AskUserQuestion +--- + +GSD smart entry — the state-aware front door. Detect what's going on in this project, then present a short menu of the right next actions and dispatch to one. + +This is a launcher/router only. It never does the work itself. It reads project + workflow state via `gsd-tools smart-entry --json`, shows a situation-appropriate menu, and hands off to an existing GSD command. + + + +@~/.claude/gsd-core/workflows/smart-entry.md +@~/.claude/gsd-core/references/ui-brand.md + + + +Arguments: $ARGUMENTS + + + +Follow ~/.claude/gsd-core/workflows/smart-entry.md. Detect the situation, present the menu, and dispatch exactly one command. Then stop. + diff --git a/commands/gsd/ns-workflow.md b/commands/gsd/ns-workflow.md index a5cca73fe..cbadbe1de 100644 --- a/commands/gsd/ns-workflow.md +++ b/commands/gsd/ns-workflow.md @@ -5,13 +5,15 @@ argument-hint: "" allowed-tools: - Read - Skill -requires: [discuss-phase, spec-phase, plan-phase, execute-phase, verify-work, phase, progress, ultraplan-phase, plan-review-convergence, add-tests, ai-integration-phase, autonomous, fast, mvp-phase, quick] +requires: [discuss-phase, spec-phase, plan-phase, execute-phase, verify-work, phase, progress, next, ultraplan-phase, plan-review-convergence, add-tests, ai-integration-phase, autonomous, fast, mvp-phase, quick] --- Route to the appropriate phase-pipeline skill based on the user's intent. Sub-skill names below are post-#2790 consolidated targets — `gsd-phase` absorbs the former add/insert/remove/edit-phase commands and `gsd-progress` -absorbs the former next/do commands. +absorbs the former next/do workflow-advance commands. The reclaimed +`gsd-next` target is the state-aware smart-entry launcher, not the retired +workflow-advance command. | User wants | Invoke | |---|---| @@ -22,6 +24,7 @@ absorbs the former next/do commands. | Verify built features through UAT | gsd-verify-work | | Add / insert / remove / edit a phase | gsd-phase | | Advance to the next logical step | gsd-progress | +| Open the state-aware smart-entry launcher | gsd-next | | Offload planning to the ultraplan cloud | gsd-ultraplan-phase | | Cross-AI plan review convergence loop | gsd-plan-review-convergence | | Generate tests for a completed phase | gsd-add-tests | diff --git a/docs/CLI-TOOLS.md b/docs/CLI-TOOLS.md index c698a986f..392656603 100644 --- a/docs/CLI-TOOLS.md +++ b/docs/CLI-TOOLS.md @@ -93,6 +93,17 @@ node gsd-tools.cjs state-snapshot Returns JSON with: current position, phase, plan, status, decisions, blockers, metrics, last activity. +### Smart Entry + +Read-only situation classifier used by `/gsd:next`. + +```bash +node gsd-tools.cjs smart-entry # Human summary + recommended route +node gsd-tools.cjs smart-entry --json # Machine-readable result for workflows +``` + +The JSON result contains `situation`, `recommended`, `summary`, `signals`, and ordered `actions[]`. Detection reads `.planning/STATE.md`, `ROADMAP.md`, latest verification/summary artifacts, and git status; it does not write files or dispatch commands. + --- ## Phase Commands @@ -444,7 +455,7 @@ if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi ```bash # Archive milestone -node gsd-tools.cjs milestone complete [--name ] [--archive-phases] +node gsd-tools.cjs milestone complete [--name ] [--no-archive-phases] # Mark requirements as complete node gsd-tools.cjs requirements mark-complete diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md index 8bfd35fae..5e3672a92 100644 --- a/docs/COMMANDS.md +++ b/docs/COMMANDS.md @@ -28,7 +28,7 @@ Six namespace routers ship as the first-stage entry points in v1.40. They keep t | Command | Routes to | |---------|-----------| -| `/gsd-workflow` | Phase pipeline — discuss / plan / execute / verify / phase / progress | +| `/gsd-workflow` | Phase pipeline — discuss / plan / execute / verify / phase / progress / next | | `/gsd-project` | Project lifecycle — milestones, audits, summary | | `/gsd-quality` | Quality gates — code review, debug, audit, security, eval, ui | | `/gsd-context` | Codebase intelligence — map, graphify, docs, learnings | @@ -315,6 +315,8 @@ For browser-backed UAT, use a configured browser MCP server. The current Open GS **Coverage-aware UAT routing (#1602).** When a SUMMARY.md carries a `coverage:` frontmatter block, `verify-work` classifies each deliverable deterministically instead of prompting for every prose bullet: deliverables proven by passing tests are auto-passed (recorded with `source: automated`, no prompt) and only judgment-dependent deliverables are presented for human sign-off. SUMMARYs without a `coverage:` block fall back to the previous prose-based extraction unchanged. See the [`coverage:` block reference](#summary-coverage-block) below. +**Honest verifier — `insufficient_spec` abstention (#1154).** A `must_haves.truths` item carrying the `verification: backstop` marker (a *non-inferable* check the edge-probe surfaced at spec time) is graded specially: if the verifier cannot confirm it with **explicit evidence** (a passing wired held-out/property-based test, or a directly-observed behavior), it **abstains** — the item is reported `unverified — held-out test recommended` and the phase verdict becomes `human_needed` (with reason `insufficient_spec`, distinct from ordinary manual-UAT `human_needed`), **never a silent `passed`**. Autonomous runs complete with "N unverified non-inferable checks" rather than hard-halting; interactive runs route the item to the end-of-phase human checkpoint. Abstention is exogenous (driven by the `backstop` tag, never a self-judged "abstain if unsure") and an inferable truth is never abstained. Reliable on capable verifier tiers (`sonnet`+); the budget `haiku` tier degrades toward current behavior. See [Honest Verifier](../gsd-core/references/honest-verifier.md). + #### SUMMARY `coverage:` block A SUMMARY.md may carry an optional `coverage:` frontmatter block — a list of per-deliverable entries that joins requirements → tests → verification status: @@ -576,9 +578,21 @@ node gsd-tools.cjs phase uat-passed 3 --raw # Machine-readable ## Navigation Commands +### `/gsd:next` + +Open the state-aware smart-entry launcher. It reads `.planning/STATE.md`, `ROADMAP.md`, verification artifacts, and git status, classifies the current situation, shows a short menu, then dispatches exactly one existing GSD command. + +This is a launcher/router only — it never performs project work directly. Detection is handled by `gsd-tools smart-entry --json`; the markdown workflow presents the menu with `AskUserQuestion` or a numbered `--text` fallback. + +**Situations detected:** no project, paused work, blockers, failed verification, first-phase setup, planning, executing, pending verification, idle stranded work, complete milestone, or unknown state. + +```bash +/gsd:next # Detect state and route to the best next action +``` + ### `/gsd-progress` -Show status, next steps, and automatically advance to the next logical workflow step. Reads project state and determines the appropriate action. +Show status, next steps, and automatically advance to the next logical workflow step. Reads project state and determines the appropriate action. Use `/gsd:next` when you want an interactive smart-entry menu before dispatch; use `/gsd-progress --next` when you want GSD to advance directly. | Flag | Description | |------|-------------| @@ -1417,6 +1431,7 @@ Reviewers are prompted to verify the plan's claims against the actual repository **Default reviewer behavior (no flags):** - If `review.default_reviewers` is **unset**, `/gsd-review` runs all detected reviewers (current default behavior). - If `review.default_reviewers` is **set**, `/gsd-review` runs only that subset (for example `["gemini","codex"]`). +- `review.default_reviewers` may include names from `review.reviewer_instances`; each instance runs as its own reviewer identity using its configured adapter/model. Instance names are not CLI flags. - `--all` always overrides config and runs the full detected set. - Explicit flags (for example `--cursor`) override both `--all` and config defaults for that run. @@ -1639,6 +1654,28 @@ node gsd-tools.cjs state sync --verify # Dry-run: show changes without writin --- +### `state rebuild [--dry-run] [--verbose]` + +Re-derive STATE.md body structure from canonical sources (frontmatter + `.planning/phases/` disk scan). Reconciles `## Current Position` prose with frontmatter, drops orphaned rows from the `**By Phase:**` table, clears template-placeholder field values, and de-duplicates `## Session Continuity Archive` blocks down to the 3 most-recent entries. Every mutation is recorded in a structured `## Rebuild Log` audit section appended to STATE.md (ADR-1817 §3). + +Heavier and manual counterpart to the lightweight, auto-triggered `state sync`. The two compose non-overlappingly: `sync` patches three frontmatter fields; `rebuild` reconciles body structure. Per ADR-1817 §4, `rebuild` is idempotent — running it twice on a clean file produces no change. + +| Flag | Description | +|------|-------------| +| `--dry-run` | Compute the rebuild and emit a structured preview, write nothing | +| `--verbose` | Tee the audit-log entries to stderr in addition to writing them to STATE.md | + +**Prerequisites:** `.planning/STATE.md` exists +**Produces:** Reconciled `STATE.md` with a `## Rebuild Log` audit entry (only when drift was reconciled) + +```bash +node gsd-tools.cjs state rebuild # Reconcile body structure +node gsd-tools.cjs state rebuild --dry-run # Preview the diff without writing +node gsd-tools.cjs state rebuild --verbose # Emit audit-log entries to stderr +``` + +--- + ### `state planned-phase` Record state transition after plan-phase completes (Planned/Ready to execute). @@ -1694,7 +1731,7 @@ A lint gate enforces the budget: npm run lint:descriptions ``` -The check is also run as part of `npm test` via `tests/enh-2789-description-budget.test.cjs`. +The check is also run as part of `npm test` via `tests/skill-frontmatter-contract.test.cjs`. --- diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index b3c8d066e..4a1669ede 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -82,6 +82,7 @@ GSD stores project settings in `.planning/config.json`. Created during `/gsd-new }, "review": { "default_reviewers": null, + "reviewer_instances": {}, "models": {} }, "parallelization": { @@ -171,6 +172,10 @@ GSD stores project settings in `.planning/config.json`. Created during `/gsd-new | `brave_search` | boolean | `true`/`false` | auto-detected | Override auto-detection of Brave Search API availability. When unset, GSD checks for `BRAVE_API_KEY` env var or `~/.gsd/brave_api_key` file | | `firecrawl` | boolean | `true`/`false` | auto-detected | Override auto-detection of Firecrawl API availability. When unset, GSD checks for `FIRECRAWL_API_KEY` env var or `~/.gsd/firecrawl_api_key` file | | `exa_search` | boolean | `true`/`false` | auto-detected | Override auto-detection of Exa Search API availability. When unset, GSD checks for `EXA_API_KEY` env var or `~/.gsd/exa_api_key` file | +| `tavily_search` | boolean | `true`/`false` | auto-detected | Override auto-detection of Tavily Search API availability. When unset, GSD checks for `TAVILY_API_KEY` env var or `~/.gsd/tavily_api_key` file | +| `ref_search` | boolean | `true`/`false` | auto-detected | Override auto-detection of Ref search API availability. When unset, GSD checks for `REF_API_KEY` env var or `~/.gsd/ref_api_key` file | +| `perplexity` | boolean | `true`/`false` | auto-detected | Override auto-detection of Perplexity API availability. When unset, GSD checks for `PERPLEXITY_API_KEY` env var or `~/.gsd/perplexity_api_key` file | +| `jina` | boolean | `true`/`false` | `true` | Override auto-detection of Jina API availability. Jina is a terminal fallback in the docs waterfall and defaults to available (`true`); GSD checks for `JINA_API_KEY` env var or `~/.gsd/jina_api_key` file when an explicit override is needed | | `search_gitignored` | boolean | `true`/`false` | `false` | Legacy top-level alias for `planning.search_gitignored`. Prefer the namespaced form; this alias is accepted for backward compatibility | > **Note:** `granularity` was renamed from `depth` in v1.22.3. Existing configs are auto-migrated. @@ -190,6 +195,10 @@ API key fields accept a string value (the key itself). They can also be set to t | `brave_search` | string \| boolean \| null | `null` | Brave Search API key used for web research. Displayed as `****` in all UI / `config-set` output; never echoed plaintext | | `firecrawl` | string \| boolean \| null | `null` | Firecrawl API key for deep-crawl scraping. Masked in display | | `exa_search` | string \| boolean \| null | `null` | Exa Search API key for semantic search. Masked in display | +| `tavily_search` | string \| boolean \| null | `null` | Tavily Search API key used in the web-discovery waterfall. Masked in display | +| `ref_search` | string \| boolean \| null | `null` | Ref search API key used in the docs-discovery waterfall. Masked in display | +| `perplexity` | string \| boolean \| null | `null` | Perplexity API key used in the web-discovery waterfall. Masked in display | +| `jina` | string \| boolean \| null | `null` | Jina API key (docs / scrape fallback). Masked in display | **Masking convention (`gsd-core/bin/lib/secrets.cjs`):** keys 8+ characters render as `****`; shorter keys render as `****`; `null`/empty renders as `(unset)`. Plaintext is written as-is to `.planning/config.json` — that file is the security boundary — but the CLI, confirmation tables, logs, and `AskUserQuestion` descriptions never display the plaintext. This applies to the `config-set` command output itself: `config-set brave_search ` returns a JSON payload with the value masked. @@ -212,7 +221,7 @@ Use `review.default_reviewers` to scope the no-flag `/gsd-review` run to a subse | Setting | Type | Default | Description | |---------|------|---------|-------------| -| `review.default_reviewers` | string[] \| null | `null` (all detected reviewers) | Optional default subset for no-flag `/gsd-review`, e.g. `["gemini","codex"]`. Precedence is: explicit reviewer flags > `--all` > `review.default_reviewers` > all detected. Unknown slugs are ignored with a warning; known-but-undetected slugs are ignored with an info note; empty arrays are rejected by `config-set`. | +| `review.default_reviewers` | string[] \| null | `null` (all detected reviewers) | Optional default subset for no-flag `/gsd-review`, e.g. `["gemini","codex"]`. Entries may be built-in reviewer slugs or configured `review.reviewer_instances` names. Precedence is: explicit reviewer flags > `--all` > `review.default_reviewers` > all detected. Unknown slugs are ignored with a warning when no instances are configured; with `review.reviewer_instances` present, unknown entries are hard errors to catch typoed instance names. Known-but-undetected slugs are ignored with an info note; empty arrays are rejected by `config-set`. | Example: @@ -520,7 +529,13 @@ The `Skill` tool is granted to consumer agents deliberately and is instruction-b ### How It Works -At spawn time, workflows call `gsd-tools query agent-skills ` (or legacy `node gsd-tools.cjs agent-skills `) to load configured skills. If skills exist for the agent type, they are injected as an `` block in the Task() prompt. +Skills reach a consumer agent through **two cooperating seams** (dual injection — see [ADR 1866](adr/1866-agent-skills-dual-injection-contract.md)): + +1. **Orchestrator-side (primary on Claude Code).** At spawn time, workflows call `gsd-tools query agent-skills ` (or legacy `node gsd-tools.cjs agent-skills `) in their bash init and interpolate the resulting `` block into the `Task()`/`Agent()` prompt. This is established across ~25 workflows. + +2. **Agent-side self-load (durable fallback).** Each of the 22 consumer agents also self-loads in its own mandatory init step — it runs `gsd_run query agent-skills ` and `Read`s every listed `SKILL.md` per [`gsd-core/references/agent-skills-bootstrap.md`](../gsd-core/references/agent-skills-bootstrap.md). This is the path that works on every runtime, including **Cursor**, where `Skill()`-delegated workflow bash init does not reliably execute and `/gsd-autonomous` delegates via flat `Skill()` calls. + +**Dedup guard.** If an agent's prompt already contains an `` block (orchestrator already injected one), the agent skips self-load — so on runtimes where both seams run (Claude Code), the prompt never carries two copies. `query agent-skills` is read-only and idempotent: it exits 0 with an empty block when nothing is configured for the type, so self-load is zero-overhead for unconfigured agents. For project-relative and global personal skills, entries appear as `@`-includes: @@ -958,7 +973,7 @@ Configure per-CLI model selection for `/gsd-review`. When set, overrides the CLI | `review.models.ollama` | string | (server default) | Model name passed to Ollama when `--ollama` reviewer is invoked. If unset, the first available model reported by the server is used (e.g. `llama3`). Set to a specific tag: `gsd config-set review.models.ollama codellama` | | `review.models.lm_studio` | string | (server default) | Model name passed to LM Studio when `--lm-studio` reviewer is invoked. If unset, the first available model reported by the server is used. | | `review.models.llama_cpp` | string | (server default) | Model name passed to llama.cpp when `--llama-cpp` reviewer is invoked. If unset, the first model reported by `/v1/models` is used. | -| `review.default_reviewers` | string[] \| null | (all detected reviewers) | Default reviewer subset for no-flag `/gsd-review`. Example: `["gemini","codex"]`. Explicit flags and `--all` override this setting. | +| `review.default_reviewers` | string[] \| null | (all detected reviewers) | Default reviewer subset for no-flag `/gsd-review`. Example: `["gemini","codex"]`. May include configured `review.reviewer_instances` names. Explicit flags and `--all` override this setting. | | `review.max_prompt_tokens` | number\|null | null | Default maximum estimated tokens for the assembled review prompt. When set, the prompt is deterministically trimmed before being sent to each reviewer. Per-reviewer overrides via `review.max_prompt_tokens_per_reviewer` take precedence. null = no trim (current behavior). | | `review.max_prompt_tokens_per_reviewer` | object | {} | Per-reviewer token budget overrides. Keys are reviewer slugs (ollama, llama_cpp, lm_studio, gemini, claude, codex, opencode, qwen, cursor). Values override `review.max_prompt_tokens` for that reviewer. Recommended for local model servers. | | `review.ollama_host` | string | `http://localhost:11434` | Base URL of the Ollama server. Override when running Ollama on a non-default port or remote host: `gsd config-set review.ollama_host http://192.168.1.10:11434` | @@ -1399,13 +1414,13 @@ When `runtime` is set, profile tiers (`opus`/`sonnet`/`haiku`) resolve to runtim | Runtime | `opus` | `sonnet` | `haiku` | reasoning_effort | |---------|--------|----------|---------|------------------| -| `claude` | `claude-opus-4-8` | `claude-sonnet-4-6` | `claude-haiku-4-5` | (not used) | +| `claude` | `claude-opus-4-8` | `claude-sonnet-5` | `claude-haiku-4-5` | (not used) | | `codex` | `gpt-5.5` | `gpt-5.4` | `gpt-5.4-mini` | `xhigh` / `medium` / `medium` | | `gemini` | `gemini-3.1-pro-preview` | `gemini-3-flash` | `gemini-2.5-flash-lite` | (not used) | | `qwen` | `qwen3-max-2026-01-23` | `qwen3-coder-plus` | `qwen3-coder-next` | (not used) | -| `opencode` | `anthropic/claude-opus-4-8` | `anthropic/claude-sonnet-4-6` | `anthropic/claude-haiku-4-5` | (not used) | -| `copilot` | `claude-opus-4-8` | `claude-sonnet-4-6` | `claude-haiku-4-5` | (not used) | -| `hermes` | `anthropic/claude-opus-4-8` | `anthropic/claude-sonnet-4-6` | `anthropic/claude-haiku-4-5` | (not used) | +| `opencode` | `anthropic/claude-opus-4-8` | `anthropic/claude-sonnet-5` | `anthropic/claude-haiku-4-5` | (not used) | +| `copilot` | `claude-opus-4-8` | `claude-sonnet-5` | `claude-haiku-4-5` | (not used) | +| `hermes` | `anthropic/claude-opus-4-8` | `anthropic/claude-sonnet-5` | `anthropic/claude-haiku-4-5` | (not used) | | Group B (`kilo`, `cline`, `cursor`, `windsurf` (alias: `devin-desktop`), `augment`, `trae`, `codebuddy`, `antigravity`) | (no built-in default — your runtime handles model selection) | | | | > **How these model IDs are sourced.** The catalog (`bin/shared/model-catalog.json`) pins each runtime's tier defaults to that provider's current frontier IDs, and may intentionally carry forward-dated IDs ahead of a provider's public docs. To verify an ID is live before changing it, check the provider's own source/API — e.g. Gemini: gemini-cli `packages/core/src/config/models.ts` or `gemini --model --prompt ping`; Codex: `codex debug models` or the OpenAI Codex models page; Qwen: Alibaba Model Studio model list. Only change an ID that the provider actually rejects — absence from documentation alone is not proof of invalidity. diff --git a/docs/FEATURES.md b/docs/FEATURES.md index 1409b652c..1bd42dd0d 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -1223,6 +1223,7 @@ When verification returns `human_needed`, items are persisted as a trackable HUM **User configuration note:** - Set `review.default_reviewers` in `.planning/config.json` (or via `gsd config-set`) to control no-flag `/gsd-review` fan-out. +- `review.default_reviewers` may include configured `review.reviewer_instances` names; each instance runs as an independent reviewer identity backed by its configured adapter/model. Instance names are not CLI flags. - Use `--all` for a full pre-merge sweep without changing project defaults. - For local model servers with small context windows, set `review.max_prompt_tokens_per_reviewer` to auto-trim prompts per reviewer — see [Prompt budgets for small-context reviewers](../docs/CONFIGURATION.md#prompt-budgets-for-small-context-reviewers) in CONFIGURATION.md. @@ -1834,7 +1835,7 @@ Test suite that scans all agent, workflow, and command files for embedded inject - REQ-CTXRED-01: System MUST truncate oversized markdown artifacts to fit within context budgets - REQ-CTXRED-02: System MUST order prompts for cache-friendly assembly (stable prefixes first) - REQ-CTXRED-03: Reduction MUST preserve essential information (headings, requirements, task structure) -- REQ-CTXRED-04: Skill `description:` fields MUST be ≤ 100 chars; enforced by `npm run lint:descriptions` (see `scripts/lint-descriptions.cjs` and `tests/enh-2789-description-budget.test.cjs`) +- REQ-CTXRED-04: Skill `description:` fields MUST be ≤ 100 chars; enforced by `npm run lint:descriptions` (see `scripts/lint-descriptions.cjs` and `tests/skill-frontmatter-contract.test.cjs`) **Process:** 1. **Measure** — Calculate total prompt size for the workflow @@ -2638,7 +2639,7 @@ Users who run a memory / knowledge-base MCP server (for example, ExoCortex-style ### 122. Skill Surface Consolidation -**Purpose:** Cut the eager skill-listing overhead by folding 31 micro-skills into 4 new grouped parents and 6 existing parents that absorb sub-operations as flags. Zero functional loss — every removed micro-skill's behavior survives via a flag on a consolidated parent. After consolidation, `commands/gsd/*.md` ships 59 sub-skills (plus 6 namespace meta-skills, see #123). +**Purpose:** Cut the eager skill-listing overhead by folding 31 micro-skills into 4 new grouped parents and 6 existing parents that absorb sub-operations as flags. Zero functional loss — every removed micro-skill's behavior survives via a flag on a consolidated parent. After consolidation, `commands/gsd/*.md` ships 60 sub-skills (plus 6 namespace meta-skills, see #123). **Requirements:** - REQ-CONSOLIDATE-01: Four new grouped skills replace clusters of micro-skills: @@ -2647,8 +2648,9 @@ Users who run a memory / knowledge-base MCP server (for example, ExoCortex-style - `/gsd-config` — folds settings-advanced (`--advanced`), settings-integrations (`--integrations`), set-profile (`--profile`) - `/gsd-workspace` — folds new-workspace (`--new`), list-workspaces (`--list`), remove-workspace (`--remove`) - REQ-CONSOLIDATE-02: Six existing parents absorb wrap-up / sub-operations as flags: `/gsd-update --sync`, `/gsd-update --reapply`, `/gsd-sketch --wrap-up`, `/gsd-spike --wrap-up`, `/gsd-map-codebase --fast`, `/gsd-map-codebase --query`, `/gsd-code-review --fix`, `/gsd-progress --do`, `/gsd-progress --next`. -- REQ-CONSOLIDATE-03: Deleted micro-skill slash forms (the bare `gsd-add-todo`, `gsd-add-backlog`, `gsd-plant-seed`, `gsd-check-todos`, `gsd-add-phase`, `gsd-insert-phase`, `gsd-remove-phase`, `gsd-edit-phase`, `gsd-new-workspace`, `gsd-list-workspaces`, `gsd-remove-workspace`, `gsd-settings-advanced`, `gsd-settings-integrations`, `gsd-set-profile`, `gsd-sketch-wrap-up`, `gsd-spike-wrap-up`, `gsd-reapply-patches`, `gsd-code-review-fix`, …) MUST resolve to "Unknown command" — no shadow stubs. -- REQ-CONSOLIDATE-04: `autonomous.md` invokes `/gsd-code-review --fix` (was previously calling the deleted `gsd-code-review-fix`). +- REQ-CONSOLIDATE-03: `/gsd:next` is not the retired workflow-advance command; it is reserved for the state-aware smart-entry launcher. Workflow advancement remains under `/gsd-progress --next`. +- REQ-CONSOLIDATE-04: Deleted micro-skill slash forms (the bare `gsd-add-todo`, `gsd-add-backlog`, `gsd-plant-seed`, `gsd-check-todos`, `gsd-add-phase`, `gsd-insert-phase`, `gsd-remove-phase`, `gsd-edit-phase`, `gsd-new-workspace`, `gsd-list-workspaces`, `gsd-remove-workspace`, `gsd-settings-advanced`, `gsd-settings-integrations`, `gsd-set-profile`, `gsd-sketch-wrap-up`, `gsd-spike-wrap-up`, `gsd-reapply-patches`, `gsd-code-review-fix`, …) MUST resolve to "Unknown command" — no shadow stubs. +- REQ-CONSOLIDATE-05: `autonomous.md` invokes `/gsd-code-review --fix` (was previously calling the deleted `gsd-code-review-fix`). **Reference issue:** [#2790](https://github.com/open-gsd/gsd-core/issues/2790) @@ -2659,7 +2661,7 @@ Users who run a memory / knowledge-base MCP server (for example, ExoCortex-style **Purpose:** Replace the flat eager skill listing with a two-stage hierarchical routing layer. The model sees 6 namespace routers instead of 86 entries, selects a namespace, then routes to the sub-skill. Descriptions use pipe-separated keyword tags (≤ 60 chars) for routing density. **Commands:** -- `/gsd-workflow` — phase pipeline router (discuss / plan / execute / verify / phase / progress) +- `/gsd-workflow` — phase pipeline router (discuss / plan / execute / verify / phase / progress / next) - `/gsd-project` — project lifecycle (milestones, audits, summary) - `/gsd-quality` — quality gates (code review, debug, audit, security, eval, ui) - `/gsd-context` — codebase intelligence (map, graphify, docs, learnings) @@ -2677,11 +2679,13 @@ Users who run a memory / knowledge-base MCP server (for example, ExoCortex-style - REQ-NS-01: Six `commands/gsd/ns-*.md` namespace routers ship with pipe-separated keyword-tag descriptions (≤ 60 chars). - REQ-NS-02: Existing sub-skills are unchanged and still invocable directly — namespace skills are additive, not a replacement for direct slash forms. - REQ-NS-03: The body of each namespace router contains a routing table that maps user intent to the correct concrete sub-skill on the post-#2790 consolidated surface. +- REQ-NS-04: Tests validate namespace files exist, include matching command `requires`, and reference only existing sub-skill files. **Reference issue:** [#2792](https://github.com/open-gsd/gsd-core/issues/2792) --- + ### 124. Context-Window Utilization Guard **Command:** `/gsd-health --context` @@ -3115,7 +3119,9 @@ When a requirement's prose matches **no** shape cue, the probe does not silently The resolved edges populate a `## Edge Coverage` section in `SPEC.md`. Unresolved *applicable* edges trigger a soft gate (Resolve / Write-anyway-flagged / Keep-probing) rather than a hard block. Under `--auto`, the probe **never auto-dismisses** — it auto-covers where a defensible criterion exists, otherwise auto-backstops, and logs `[auto] edge coverage: C covered, B backstop, U unresolved`. The one exception is an `unclassified` candidate: `--auto` leaves it **`unresolved`** (surfaced as a flagged assumption), never auto-`backstop` — a missing shape is not evidence an edge exists, so minting a held-out edge obligation would be a false claim. -The load-bearing wire is the `plan-phase` lift: `covered` and `backstop` edges become `must_haves.truths` the verifier can check, so the section is not merely documentation. +The load-bearing wire is the `plan-phase` lift: `covered` and `backstop` edges become `must_haves.truths` the verifier can check, so the section is not merely documentation. A `backstop` edge is lifted as a **structured non-inferable marker** (`{ statement, verification: backstop }`, a flat scalar — not a prose note), which the **honest verifier** then consumes (see below) — closing the loop the edge-probe opened. + +**Honest verifier — abstention on non-inferable checks (#1154).** A non-inferable (`backstop`) truth is one whose correct behavior is not derivable from the spec alone, so the verifier cannot self-detect the gap and would confidently false-pass it (~100% of the time). At verify time, a `backstop` truth the verifier cannot confirm with **explicit evidence** (a passing wired held-out/property-based test, or a directly-observed behavior) **abstains** → `human_needed` with reason `insufficient_spec` (reported as `unverified — held-out test recommended`), **never a silent `passed`**. This is the verify-time, truth-axis mirror of the prohibition judgment-tier disposition (ADR-550 D4): exogenous (driven by the `backstop` tag, never a self-judged "abstain if unsure"), routing-not-diagnosis (the held-out test carries the omitted rule), and capable-tier dependent (reliable on `sonnet`+; the budget `haiku` tier degrades toward current behavior). An inferable truth is never abstained (the over-abstention guard). Reference: [Honest Verifier](../gsd-core/references/honest-verifier.md). **Requirements:** - REQ-EDGE-01: The edge pass MUST run after the ambiguity gate and emit a `## Edge Coverage` SPEC section. @@ -3125,6 +3131,8 @@ The load-bearing wire is the `plan-phase` lift: `covered` and `backstop` edges b - REQ-EDGE-05: `--auto` MUST never auto-dismiss — auto-cover or auto-backstop only. - REQ-EDGE-06: `plan-phase` MUST lift `covered` criteria and `backstop` notes into `must_haves.truths`. - REQ-EDGE-07: A requirement whose prose matches no shape cue MUST surface an `unclassified — review manually` candidate (never silently dropped); `--auto` MUST leave it `unresolved`, never auto-`backstop`. +- REQ-EDGE-08: `plan-phase` MUST lift a `backstop` edge into `must_haves.truths` as a structured flat-scalar marker (`{ statement, verification: backstop }`), never a prose parenthetical. +- REQ-HONEST-01: At verify time a `backstop` truth that cannot be confirmed with explicit evidence MUST abstain → `human_needed` (reason `insufficient_spec`), never `passed`; an inferable truth MUST never be abstained (over-abstention guard); abstention MUST be exogenous (driven by the `backstop` tag, not self-judgment). **Reference:** [Edge Probe](../gsd-core/references/edge-probe.md) @@ -3188,6 +3196,7 @@ The load-bearing wire is the `plan-phase` lift into `must_haves.prohibitions`, s **Reference:** [Prohibition Probe](../gsd-core/references/prohibition-probe.md) + ### 147. Capability Management Command **Command:** `gsd capability install | update | remove | list | outdated | disable | enable` @@ -3205,3 +3214,23 @@ The load-bearing wire is the `plan-phase` lift into `must_haves.prohibitions`, s **Trust boundary:** install never executes capability code (copy-only staging); executable surfaces require explicit consent; sources are gated by the **project-scoped** `capabilities.strict_known_registries` policy (fail-closed on a malformed/unparseable value); every shared-config write/delete is realpath-confined to the scope root, and a name collision with a user's `mcpServers` entry is never clobbered. **Reference:** [`gsd capability` command reference](reference/gsd-capability-command.md) · [ADR-1244](adr/1244-capability-ecosystem.md) +### 148. Smart Entry Launcher + +**Command:** `/gsd:next` + +**Tool:** `gsd-tools smart-entry [--json]` + +**Purpose:** Provide a state-aware front door that reads project/workflow state, classifies the user's situation, presents a short menu, and dispatches exactly one existing GSD command. + +**Requirements:** +- REQ-SMART-ENTRY-01: Detection MUST be read-only and deterministic; classification lives in `gsd-tools smart-entry`. +- REQ-SMART-ENTRY-02: The launcher MUST never perform project work directly; it only displays a menu and dispatches one command. +- REQ-SMART-ENTRY-03: The workflow MUST fall back to `/gsd-progress` if detection fails. +- REQ-SMART-ENTRY-04: Each classified situation MUST provide exactly one recommended action and valid slash commands. +- REQ-SMART-ENTRY-05: Text-mode runtimes MUST receive a numbered-list fallback instead of being stranded by interactive UI assumptions. + +**Situations:** no project, paused, blocked, verify failed, needs first phase, planning, executing, verify pending, idle stranded, complete, unknown. + +**Reference:** [Smart Entry Design](superpowers/specs/2026-06-27-gsd-smart-entry-design.md) + +--- diff --git a/docs/INVENTORY-MANIFEST.json b/docs/INVENTORY-MANIFEST.json index 68c20baf9..1f84431ea 100644 --- a/docs/INVENTORY-MANIFEST.json +++ b/docs/INVENTORY-MANIFEST.json @@ -71,6 +71,7 @@ "/gsd-mvp-phase", "/gsd-new-milestone", "/gsd-new-project", + "/gsd-next", "/gsd-ns-context", "/gsd-ns-ideate", "/gsd-ns-manage", @@ -182,6 +183,7 @@ "ship.md", "sketch-wrap-up.md", "sketch.md", + "smart-entry.md", "spec-phase.md", "spike-wrap-up.md", "spike.md", @@ -200,6 +202,7 @@ ], "references": [ "agent-contracts.md", + "agent-skills-bootstrap.md", "ai-evals.md", "ai-frameworks.md", "artifact-types.md", @@ -222,6 +225,7 @@ "gates.md", "git-integration.md", "git-planning-commit.md", + "honest-verifier.md", "ios-scaffold.md", "loop-hook-dispatch.md", "mandatory-initial-read.md", @@ -279,6 +283,8 @@ ], "cli_modules": [ "active-workstream-store.cjs", + "adapter-declarative.cjs", + "adapter-imperative.cjs", "adr-parser.cjs", "agent-command-router.cjs", "agent-install-check.cjs", @@ -322,8 +328,11 @@ "docs.cjs", "drift.cjs", "edge-probe.cjs", + "embedding-adapter.cjs", "eval-command-router.cjs", "eval.cjs", + "external-descriptor-trust.cjs", + "external-job.cjs", "fallow-runner.cjs", "federated-config.cjs", "frontmatter.cjs", @@ -332,6 +341,9 @@ "graphify-command-router.cjs", "graphify.cjs", "gsd2-import.cjs", + "handshake-serialized.cjs", + "hook-bus.cjs", + "host-integration-sdk.cjs", "host-integration.cjs", "init-command-router.cjs", "init.cjs", @@ -348,7 +360,9 @@ "loop-host-contract.cjs", "loop-resolver.cjs", "markdown-sectionizer.cjs", + "mcp-server.cjs", "milestone.cjs", + "model-adapter.cjs", "model-catalog.cjs", "model-profiles.cjs", "model-resolver.cjs", @@ -391,9 +405,12 @@ "security.cjs", "semver-compare.cjs", "shell-command-projection.cjs", + "smart-entry.cjs", "stale-bake-guard.cjs", "state-command-router.cjs", "state-document.cjs", + "state-io.cjs", + "state-transition.cjs", "state.cjs", "surface.cjs", "task-command-router.cjs", diff --git a/docs/INVENTORY.md b/docs/INVENTORY.md index 71cdcd970..aa41acbdf 100644 --- a/docs/INVENTORY.md +++ b/docs/INVENTORY.md @@ -67,7 +67,7 @@ These six routers are descriptor-only entries that the model picks first; the bo | Command | Role | Source | |---------|------|--------| -| `/gsd-workflow` | Phase pipeline router — discuss / plan / execute / verify / phase / progress. | [commands/gsd/ns-workflow.md](../commands/gsd/ns-workflow.md) | +| `/gsd-workflow` | Phase pipeline router — discuss / plan / execute / verify / phase / progress / next. | [commands/gsd/ns-workflow.md](../commands/gsd/ns-workflow.md) | | `/gsd-project` | Project lifecycle router — milestones, audits, summary. | [commands/gsd/ns-project.md](../commands/gsd/ns-project.md) | | `/gsd-quality` | Quality-gate router — code review, debug, audit, security, eval, ui. | [commands/gsd/ns-review.md](../commands/gsd/ns-review.md) | | `/gsd-context` | Codebase-intelligence router — map, graphify, docs, learnings. | [commands/gsd/ns-context.md](../commands/gsd/ns-context.md) | @@ -123,6 +123,7 @@ These six routers are descriptor-only entries that the model picks first; the bo | Command | Role | Source | |---------|------|--------| +| `/gsd:next` | State-aware smart-entry launcher — reads project state, shows a contextual menu, and dispatches one existing GSD command. | [commands/gsd/next.md](../commands/gsd/next.md) | | `/gsd-progress` | Check project progress, show context, and route to next action; use `--next` to advance automatically or `--do` to run a freeform task. | [commands/gsd/progress.md](../commands/gsd/progress.md) | | `/gsd-capture` | Capture ideas, tasks, notes, and seeds — todo (default), `--note`, `--backlog`, `--seed`, or `--list` pending todos. | [commands/gsd/capture.md](../commands/gsd/capture.md) | | `/gsd-stats` | Display project statistics — phases, plans, requirements, git metrics, timeline. | [commands/gsd/stats.md](../commands/gsd/stats.md) | @@ -292,6 +293,7 @@ Full roster at `gsd-core/references/*.md`. References are shared knowledge docum | `common-bug-patterns.md` | Common bug patterns for code review and verification. | | `debugger-philosophy.md` | Evergreen debugging disciplines loaded by `gsd-debugger`. | | `mandatory-initial-read.md` | Shared required-reading boilerplate injected into agent prompts. | +| `agent-skills-bootstrap.md` | Shared agent_skills self-load contract (query + Read + dedup guard) injected into all 22 consumer agents. | | `project-skills-discovery.md` | Shared project-skills-discovery boilerplate injected into agent prompts. | | `research-documentation-lookup.md` | Shared documentation-lookup protocol (Context7 MCP + guarded CLI fallback) injected into all researcher agents. | | `research-philosophy.md` | Shared research philosophy (training-as-hypothesis, honest reporting, investigation-not-confirmation) injected into researcher agents. | @@ -308,6 +310,7 @@ Full roster at `gsd-core/references/*.md`. References are shared knowledge docum | `domain-probes.md` | Domain-specific probing questions for discuss-phase. | | `edge-probe.md` | Spec-phase edge-completeness probe — 8-category edge taxonomy, shape classification, and the `requirements → checks → verifier` resolution model (Step 5.5). | | `prohibition-probe.md` | Spec-phase prohibition-completeness probe — the two-stage adversarial-recall → precision protocol that surfaces the unwritten *must-NOT* constraints (values/safety/ethics), with status×verification (`test`/`judgment`) tiering and canon-referral breadcrumbs (Step 5.6); second adapter of the `probe-core` resolution model. | +| `honest-verifier.md` | Verify-time abstention on non-inferable (`backstop`) truths — the truth-axis mirror of the prohibition judgment-tier disposition (ADR-550 D4): a `backstop` truth the verifier can't confirm with explicit evidence abstains → `human_needed` (reason `insufficient_spec`), never a silent pass (#1154). | | `gate-prompts.md` | Gate/checkpoint prompt templates. | | `loop-hook-dispatch.md` | Generic dispatch contract for consuming `gsd_run loop render-hooks --raw` output in any host-loop workflow — envelope shape, per-kind dispatch rules (contribution/step/gate), and liveness banner. | | `scout-codebase.md` | Phase-type→codebase-map selection table for discuss-phase scout step (extracted via the discuss-phase/modes progressive-disclosure split, #717). | diff --git a/docs/TESTING-SUITES.md b/docs/TESTING-SUITES.md index 6cd890305..b58c8d22a 100644 --- a/docs/TESTING-SUITES.md +++ b/docs/TESTING-SUITES.md @@ -221,8 +221,8 @@ node scripts/ci-test-scope.cjs --base origin/next --head HEAD ## Test strategy: #443 effort + fast_mode engine > Feature: unified cross-provider effort and fast_mode knobs (issue #443). -> Test files: `tests/feat-443-effort-fast-mode.test.cjs` (unit), -> `tests/feat-443-effort-fast-mode.integration.test.cjs` (integration). +> Test files: `tests/model-resolver.test.cjs` (unit), +> `tests/model-resolver.test.cjs` (integration). ### Testing pyramid diff --git a/docs/adr/0002-command-contract-validation-module.md b/docs/adr/0002-command-contract-validation-module.md index 093bfd86a..fac60d04c 100644 --- a/docs/adr/0002-command-contract-validation-module.md +++ b/docs/adr/0002-command-contract-validation-module.md @@ -18,8 +18,8 @@ The command file contract defines what makes a valid `commands/gsd/*.md`: ## Context Before this ADR, the command contract was enforced inconsistently: -- `tests/enh-2790-skill-consolidation.test.cjs` checked existence and frontmatter of specific post-consolidation commands -- `tests/bug-3135-capture-backlog-workflow.test.cjs` checked `execution_context` @-ref resolution (added 2026-05-05) +- `tests/skill-frontmatter-contract.test.cjs` (folds former `enh-2790-skill-consolidation`, consolidation epic #1969) checked existence and frontmatter of specific post-consolidation commands +- `tests/docs-update.test.cjs` (folds former `bug-3135-capture-backlog-workflow`, consolidation epic #1969) checked `execution_context` @-ref resolution (added 2026-05-05) - No test checked `allowed-tools` validity, `name:` convention, or `description:` non-emptiness across all commands simultaneously This meant any PR touching a command file could break the contract without a single test catching it. The `add-backlog.md` gap (#3135) is a concrete example: the workflow file was missing for the full consolidation cycle before a targeted regression test was written. diff --git a/docs/adr/0009-shell-command-projection-module.md b/docs/adr/0009-shell-command-projection-module.md index 98f92d8cf..e1ecf9b89 100644 --- a/docs/adr/0009-shell-command-projection-module.md +++ b/docs/adr/0009-shell-command-projection-module.md @@ -52,11 +52,11 @@ These call sites should migrate behind the Shell Command Projection Module: ### Tests expected to move with the seam -- `tests/bug-2979-hook-absolute-node.test.cjs` +- `tests/install.test.cjs` (folds former `bug-2979-hook-absolute-node`, consolidation epic #1969) - `tests/sh-hook-paths.test.cjs` - `tests/bug-3011-sdk-path-diagnostic.test.cjs` -- `tests/bug-3017-codex-hook-absolute-node.test.cjs` -- `tests/bug-2376-opencode-windows-home-path.test.cjs` +- `tests/codex-config.test.cjs` (folds former `bug-3017-codex-hook-absolute-node`, consolidation epic #1969) +- `tests/path-replacement.test.cjs` (folds former `bug-2376-opencode-windows-home-path`, consolidation epic #1969) - `tests/bug-3020-install-shell-path-probe.test.cjs` - `tests/bug-3359-stale-gsd-sdk-path-version.test.cjs` diff --git a/docs/adr/1235-descriptor-driven-agent-conversion-migration.md b/docs/adr/1235-descriptor-driven-agent-conversion-migration.md index 319d75d20..efff11bc5 100644 --- a/docs/adr/1235-descriptor-driven-agent-conversion-migration.md +++ b/docs/adr/1235-descriptor-driven-agent-conversion-migration.md @@ -10,7 +10,7 @@ Agent installation is the last major artifact class in `install()` that does **not** flow through the descriptor-driven path established by ADR-3660 (`installRuntimeArtifacts` → `_copyStaged`, with each runtime's artifacts declared in the capability registry's `artifactLayout`). Skills and kimi-agents already flow through it. The claude-**local** `agents` kind is *declared* in the descriptor (raw-copy, `converter: null`), but production `install()` still produces claude-local agent output via the inline loop — the skills-runtime gate routes Claude through `installRuntimeArtifacts` only when `isGlobal` (`bin/install.js` ~9822/9829), and claude-local commands/agents go through the back-compat copy path. So agents for **every** runtime (claude included) are, in practice, converted and written by a **separate inline loop** in `bin/install.js` (currently lines ~10102–10249). -This was surfaced while attempting #1173: #1227 added the `convertedAgentsKind` plumbing to the layout module, but it is **inert in production** — no runtime descriptor declares an `agents` kind with a non-null `converter`, so `dispatchKindEntry` always routes to the raw-copy `agentsKind`. The plumbing is exercised only by `tests/feat-1173-agent-converters-descriptor.test.cjs` against a synthetic registry. +This was surfaced while attempting #1173: #1227 added the `convertedAgentsKind` plumbing to the layout module, but it is **inert in production** — no runtime descriptor declares an `agents` kind with a non-null `converter`, so `dispatchKindEntry` always routes to the raw-copy `agentsKind`. The plumbing is exercised only by `tests/runtime-converters.test.cjs` (folds former `feat-1173-agent-converters-descriptor`, consolidation epic #1969) against a synthetic registry. A naive cutover (point descriptors at the extracted converters, delete the inline loop) **regresses installs for every runtime**, because the inline loop performs cross-cutting and runtime-specific pipeline steps that the descriptor agents path does not replicate, and because the extracted converters' signature cannot receive the context those steps require. Two of those behaviors have no converter function at all today. Because the migration touches install correctness across ~15 runtimes in the most-edited install module, the cutover strategy and the parity-test contract are architectural decisions worth recording before code. diff --git a/docs/adr/1239-gsd-embeddable-orchestration-engine.md b/docs/adr/1239-gsd-embeddable-orchestration-engine.md index 5ba27d05d..0bc95130c 100644 --- a/docs/adr/1239-gsd-embeddable-orchestration-engine.md +++ b/docs/adr/1239-gsd-embeddable-orchestration-engine.md @@ -1,6 +1,6 @@ # ADR-1239: GSD as an Embeddable Orchestration Engine -- **Status:** Proposed +- **Status:** Accepted - **Date:** 2026-06-14 - **Issue:** [#1239](https://github.com/open-gsd/gsd-core/issues/1239) - **Epic:** [#857](https://github.com/open-gsd/gsd-core/issues/857) (Capability system) @@ -87,13 +87,14 @@ Each phase is its own `approved-*` issue + PR with equivalence/parity proof. ### Amendment — Phase A implemented (#1684, v1.7.0) -Phase A is **implemented** (the ADR itself remains `Proposed` overall until Phases B–E land). The negotiated capability schema is materialized as a pure, additive, no-I/O module — the **Host-Integration Interface** (`src/host-integration.cts` → `gsd-core/bin/lib/host-integration.cjs`): +Phase A is **implemented** and Phases B–E have landed, so this ADR is **Accepted** (Status flipped from `Proposed` once Phase E shipped the published SDK + serialized handshake + versioning policy + Diátaxis docs). The negotiated capability schema is materialized as a pure, additive, no-I/O module — the **Host-Integration Interface** (`src/host-integration.cts` → `gsd-core/bin/lib/host-integration.cjs`): - **The eight negotiated axes** are carried under `capability.json` `runtime.hostIntegration` (extending, not replacing, the ADR-1016 axes), validated by `validateRuntimeBody` (`capability-validator.cjs`) across all 16 runtime descriptors, with the closed vocabulary kept in lock-step by a parity guard. - **`PROTOCOL_VERSION`** is an integer starting at `1`, **distinct** from the package `version` / `engines.gsd` semver (the `version`/`protocolVersion` overlap, resolved). - **`negotiateHostCapabilities(host, engine?)`** performs the in-process `initialize` exchange and enforces the trust-boundary invariant `effective ⊆ host-declared ∩ engine-known`: an undeclared axis or an unknown / higher-`protocolVersion` value is **never** trusted — it degrades to the most-restrictive known value (fail-closed), never throws. - **`degradationFor`** is the typed Full/Degraded/Absent ladder table; **`profileOf` + `PROFILE_BASELINES`** classify each descriptor into `programmatic-cli` (9 hosts: claude, opencode, cursor, cline, hermes, qwen, kilo, trae, kimi), `declarative-cli` (7 hosts: codex, gemini, antigravity, augment, codebuddy, copilot, windsurf), or `ide` (defined as a baseline; no installed host yet — VS Code lands in Phase D). -- **Overlap resolutions (explicit):** `commandStyle` (GSD emission style, retained) ⊥ `commandSurface` (host surface type); `hookEvents` dialect ⊥ `hookBus` ownership (a host with `hooksSurface:none` may still be `hookBus:host` — e.g. opencode); the `opencode-subset` `hookEvents` value remains reserved for the Phase D OpenCode hook-dialect consumer; `runtimeCompat` (feature→host) stays an independent override, orthogonal to these runtime→engine axes. +- **Overlap resolutions (explicit):** `commandStyle` (GSD emission style, retained) ⊥ `commandSurface` (host surface type); `hookEvents` dialect ⊥ `hookBus` ownership (a host with `hooksSurface:none` may still be `hookBus:host` — e.g. opencode); `runtimeCompat` (feature→host) stays an independent override, orthogonal to these runtime→engine axes. +- **`extensionEvents` vocabulary (amendment, #1943).** The OpenCode extension-system event subset is a SEPARATE descriptor field + closed vocabulary, **not** a `hookEvents` value. `hookEvents` is the *managed-hook* dialect only (`claude`/`gemini`) — the event names GSD writes into a declarative host's settings.json. `extensionEvents` is the *plugin/extension-system* event surface an imperative host exposes: `{ opencode, pi, none }` (OpenCode ~25 plugin events; pi ~30 fine-grained events; `none` = the host exposes no extension surface and the engine owns the bus, e.g. VS Code). The former `opencode-subset` `hookEvents` value was this concept misfiled; it is now `extensionEvents: opencode`. Keeping them separate preserves the `hooksSurface:"none" ⇔ no-hookEvents` invariant (OpenCode declares `extensionEvents`, not `hookEvents`). Resolved by `extensionEventSurfaceFor` in `src/host-integration.cts`, validated by `VALID_EXTENSION_EVENTS` in `capability-validator.cjs`. **Every per-host axis value is documentation-sourced, with citations.** Each of the 8 axes for all 16 installed CLIs was determined from that CLI's authoritative documentation (Context7 + the official dev docs/source), never inferred. The full per-CLI, per-axis matrix — value, source, and an evidence quote — is recorded in [`docs/reference/host-integration-capability-matrix.md`](reference/host-integration-capability-matrix.md), the deployment source-of-truth that Phases B–E build on. Where a CLI's docs genuinely do not state an axis, the descriptor carries the explicit `undocumented` sentinel (which `negotiateHostCapabilities` fail-closes on) rather than a guessed value — 22 such markers exist today, each with its search trail in the matrix. Two findings corrected this ADR's original appendix matrix: (1) current OpenAI **Codex** docs document slash-commands, so its `commandSurface` is `slash-file`, not `prose-only`; (2) several hosts run non-Node runtimes (opencode & kilo on **bun**; hermes & kimi on **python**; antigravity on **go**), so the `runtime` axis vocabulary was widened to `node|bun|sandboxed-web|python|go|rust|electron|other`. The documented `embeddingMode` split (9 imperative / 7 declarative, above) likewise reflects each CLI's real plugin/extension API, not a profile assumption. @@ -120,7 +121,7 @@ A plugin is a JS/TS module exporting an `async` function that returns a **hooks | 1 Command | slash-file commands projected to the xdg command dir (`gsd:`-namespaced); plugin may also surface entrypoints as custom `tool()`s and drive `tui.command.execute` | `commandSurface: slash-file` | none (full) | | 2 Dispatch | `mode: subagent` / `@`-mention; `subtask` is **synchronous-only** | `dispatch: { namedDispatch:true, nested:true, background:false, subagentToolkit:'full' }` | no background → waves run inline (the #853 flatten rule) | | 3 Model | per-agent `model` field on the agent `.md`; no provider `sendRequest` | `modelMode: passive` | tier routing degrades to per-agent model field | -| 4 Hooks | host `event` bus (~25 events) | `hookBus: host`; ADR-1016 dialect = **`opencode-subset`** | session/tool-scoped only — see gap below | +| 4 Hooks | host `event` bus (~25 events) | `hookBus: host`; `extensionEvents: opencode` (Phase D / #1943) | session/tool-scoped only — see gap below | | 5 State | filesystem `.planning/` + config under xdg `~/.config/opencode`; `opencode-jsonc` permissions sidecar (`permissionWriter: 'opencode'`) | `stateIO: filesystem` | `configHome` write-confinement applies | | 6 Artifact | native Agent Skills + `@agent` subagents + slash commands | — | none (full) | @@ -128,7 +129,7 @@ A plugin is a JS/TS module exporting an `async` function that returns a **hooks ### The load-bearing gap: the loop is phase-scoped, the bus is session-scoped -OpenCode's bus fires on **sessions, tools, files, and permissions** — never on **workflow phases**. GSD's 12 loop extension points (`plan:pre`, `verify:post`, `ship:post`…) have **no event on this bus**. So the imperative adapter for OpenCode cannot drive the loop *from host events*; the engine must own phase sequencing internally and treat OpenCode's bus as a **subset hook surface** (exactly what the ADR-1016 `opencode-subset` dialect already encodes). Concretely: +OpenCode's bus fires on **sessions, tools, files, and permissions** — never on **workflow phases**. GSD's 12 loop extension points (`plan:pre`, `verify:post`, `ship:post`…) have **no event on this bus**. So the imperative adapter for OpenCode cannot drive the loop *from host events*; the engine must own phase sequencing internally and treat OpenCode's bus as a **subset extension-event surface** — exactly what the `extensionEvents: opencode` vocabulary encodes (amendment #1943; formerly misfiled as a `hookEvents` value `opencode-subset`). Concretely: - **Steps, gates, and most contributions fire from GSD's own workflow/command invocation (point 1), engine-side** — not from the host bus. The plugin invokes `gsd-tools.cjs` (via `$` or the companion MCP server) and the engine runs the loop resolver. - **Only the contributions that align with a real host event bind to the bus.** The clean case is memory: a MemPalace-style capability's capture/recall already keys on `discuss:post`/`plan:post`/`verify:post`; those can *additionally* bind to `experimental.session.compacting` so memory persists across OpenCode's compaction — a concrete win the host gives us for free. diff --git a/docs/adr/1606-prohibition-enforcement-verify-seam.md b/docs/adr/1606-prohibition-enforcement-verify-seam.md index 028735507..4330ea5ca 100644 --- a/docs/adr/1606-prohibition-enforcement-verify-seam.md +++ b/docs/adr/1606-prohibition-enforcement-verify-seam.md @@ -99,11 +99,13 @@ to be made that are not derivable from the contract alone: optional `cleanFixture` runs the same negative test a second time with `GSD_PROHIB_SUBJECT=` and requires **non-vacuous GREEN** (`isNonVacuousNodeTestPass`) — so fail-first is proven only when the check is **RED on the - violation AND GREEN on the clean subject** (content-dependent red). Opt-in, not mandatory: + violation AND GREEN on the clean subject** (content-dependent red). ~~Opt-in, not mandatory: absent `cleanFixture`, behaviour matches the pre-#1346 zero-authoring compose path and the "reds because the env var is set" case stays a documented residual for that one author's - check. The lint-rule kind needs no analog (its subject *is* the linted file; no env-var - indirection). + check.~~ **MANDATORY for the node-test kind as of #1906 (2026-07-03) — absent `cleanFixture` + the node-test is un-provable (fail-closed), never proven on the violation alone; see the #1906 + addendum below.** The lint-rule kind needs no analog (its subject *is* the linted file; no + env-var indirection). 5. **Deterministic locate via five flat scalars — never a nested object.** The wired-check descriptor is authored at spec-phase and projected onto the `must_haves.prohibitions` @@ -174,7 +176,46 @@ belong to the spec-phase contract and are recorded in ADR-550's "Alternatives co - **Mandatory causation control — REJECTED in favour of opt-in.** Requiring every node-test prohibition to ship a `cleanFixture` would regress the zero-authoring compose path and hard-gate every existing descriptor without one; the control is opt-in (Decision 4), leaving - one documented residual rather than breaking working checks. + one documented residual rather than breaking working checks. **↳ SUPERSEDED 2026-07-03 (#1906) + — see the addendum below.** The blast-radius premise no longer holds: there is **no in-tree + `node-test` consumer** (Consequences, "Open conventions"), so making the control mandatory + hard-gates *zero* existing checks. Decision 4 is now mandatory for the node-test kind. + +## Addendum (2026-07-03, #1906) — node-test causation control is now MANDATORY (supersedes Decision 4's opt-in) + +Decision 4 made the `cleanFixture` causation control **opt-in** to avoid regressing the #1314 +zero-authoring compose path. That trade-off left a Goodhart hole open **by default**: a node-test +that omits `cleanFixture` is proven fail-first on the violation alone, so a deceptive +content-independent negative test — one that reds merely *because* `GSD_PROHIB_SUBJECT` is set, +ignoring the subject's CONTENT (`assert.ok(!process.env.GSD_PROHIB_SUBJECT)`) — passes the proof. +The proof's observed signal (RED) thus diverges from its target (RED *caused by content*), and the +divergence is the **default**, not an edge case an author opts into. + +**Decision (owner ruling on #1906, 2026-07-03):** for the `node-test` kind the causation control is +**required**. A node-test descriptor that omits `cleanFixture` is treated as **un-provable** +(fail-closed) — never accepted under the weaker violation-only proof. When a clean fixture is +present, fail-first is proven exactly as before (RED on the violation subject **AND** non-vacuous +GREEN on the clean subject); a deceptive content-independent test reds on the clean subject too, so +the control fails and the proof does not pass. + +- **Why the opt-in's rationale no longer applies.** Decision 4 / the rejected-alternative above kept + the control opt-in to avoid hard-gating "every existing descriptor without one." Per this ADR's own + Consequences ("Open conventions … no in-tree `node-test` consumer"), **there are none** — the only + live dogfood is the lint-rule `local/no-source-grep`; node-test fail-first is exercised only by + synthetic temp fixtures. So the mandatory control hard-gates **zero** real checks today; its cost is + paid only by future node-test authors, who now must supply a clean control subject to earn a green. +- **Scope — node-test only.** The `lint-rule` kind is unchanged (byte-identical): its subject *is* the + linted file, with no `GSD_PROHIB_SUBJECT` indirection, so the "reds because the env var is set" gap + cannot exist there. Net effect on D4's disposition is unchanged — every miss/fail/**un-provable** + still hard-gates; this only *tightens* what counts as proven. +- **Breaking change (Hyrum).** A previously-green node-test prohibition with no clean fixture now + hard-gates. Disclosed as breaking with the ~zero in-tree blast radius noted above. `cleanFixture` + stays optional at the *type* level (it rides both kinds; the lint-rule kind never uses it) but is + *required by the node-test prover*. +- **Mechanism.** `defaultProveFailFirst`'s node-test branch in `src/prohibition-enforcement.cts`: + `const clean = check.cleanFixture; if (!clean) return { provenFailFirst: false, … }` before the + existence + non-vacuous-GREEN control. Compiled by `build:lib`. ADR-550's 2026-06-21 (#1346) + addendum "Why opt-in, not required" is superseded to match. ## Cross-references @@ -186,4 +227,4 @@ belong to the spec-phase contract and are recorded in ADR-550's "Alternatives co - **`gsd-core/references/prohibition-probe.md`** — the portable runtime reference. - **`docs/how-to/resolve-prohibition-findings.md`** — user-facing resolution guide. - Code: `src/prohibition-enforcement.cts`, `src/probe-core.cts` (`projectProhibitions`), - `gsd-core/workflows/verify-phase.md`. Issues: #644, #1259, #1278, #1279, #1346. + `gsd-core/workflows/verify-phase.md`. Issues: #644, #1259, #1278, #1279, #1346, #1906. diff --git a/docs/adr/1610-workflow-agent-size-budget-ratchet.md b/docs/adr/1610-workflow-agent-size-budget-ratchet.md new file mode 100644 index 000000000..0f4035038 --- /dev/null +++ b/docs/adr/1610-workflow-agent-size-budget-ratchet.md @@ -0,0 +1,118 @@ +# ADR 1610: workflow & agent size-budget ratchet (per-file byte baseline + tier hard caps) [Proposed] + +- **Status:** Proposed +- **Date:** 2026-06-22 + +> **Provenance.** Drafted 2026-06-22 to give an already-shipped architectural governance +> decision its first ADR. The decision landed across three PRs — #1089 (additive per-file +> workflow baseline guard), #1096 (swap enforcement to baseline + loose hard caps), #1097 +> (agent-size baseline + line→byte rebase) — under epic #1074, building on #717 (bytes rebase) +> and #683 (LF-normalized byte count) and superseding the #597 tier-max ratchet. Authored by +> the implementer. Verified against `tests/{workflow,agent}-size-budget.test.cjs`, +> `tests/{workflow,agent}-size-baseline.json`, `scripts/workflow-size.cjs`, and +> `scripts/lib/allowlist-ratchet.cjs` on `next`. The rationale here is lifted from those +> tests' own doc comments (the decision was documented in-code but never as an ADR). + +## Context + +`gsd-core/workflows/*.md` and `agents/*.md` are loaded **verbatim into agent context** every +time the corresponding command/agent runs. Unbounded growth is paid on every invocation across +every session, and — more importantly — degrades quality: larger context erodes recall and +reasoning ("context rot" / finite attention budget). With prompt caching the per-invocation +*cost* premise is weak (cache reads are ~10% of input), so the **caching-independent quality +argument is the load-bearing one**: lean, high-signal instructions produce better plans. + +The prior mechanism (#597) was a **tier-max tighten-only ratchet**: it bound only the single +largest file per tier, leaving the other ~85 files free to grow silently. That left the actual +risk — broad, quiet creep across many files — unguarded. + +No ADR governs how GSD bounds instruction-document size. The decision currently lives only in +test doc comments, so it is invisible to anyone browsing `docs/adr/` and at risk of being +weakened (e.g. "just bump the baseline") without encountering its rationale. + +## Decision + +1. **Measure in BYTES, not lines (#717).** Line count is a poor proxy — markdown tables and + fenced code are token-dense, so a line budget over-penalizes prose and under-catches dense + additions. Bytes are cheap, deterministic, and need no tokenizer; they are also the unit + vendors bound on (Codex caps instruction docs at 32,768 bytes, `project_doc_max_bytes`, and + truncates past it). We adopt the **unit**, not the exact number. + +2. **Count LF-normalized bytes (#683).** Normalize CRLF→LF (`content.replace(/\r\n/g, '\n')`) + before counting so a CRLF (Windows) checkout yields the same byte count as an LF checkout — + a raw on-disk count would add one byte per line on Windows and make the guard + platform-dependent. + +3. **Two complementary guards, neither a tier-max ceiling (#1074):** + - **Per-file baseline (the anti-creep).** Every workflow/agent file is pinned to its exact + byte size in `tests/{workflow,agent}-size-baseline.json`. Any growth fails with the file + name and delta. A deliberate change is recorded via `npm run size:baseline` as a one-line + reviewable diff. This is the day-to-day guard and it covers **every** file, not just the + largest-per-tier. + - **Tier hard caps (the outer bound).** XL / LARGE / DEFAULT are absolute red lines with + real headroom (`XL_CAP = 98304` / 96 KiB, `LARGE_CAP = 61440` / 60 KiB, + `DEFAULT_CAP = 40960` / 40 KiB), a few KB above the largest file in each tier — the largest + XL orchestrators (`plan-phase.md`, `execute-phase.md`) currently sit in the low-90s KB, a + few KB under `XL_CAP`. (Exact per-file sizes are not duplicated here: they live in + `tests/workflow-size-baseline.json`, the enforced source of truth, and an absolute byte + count quoted in prose drifts with every edit.) Hard caps are never raised in normal work; + crossing + one is a signal to do **lazy extraction**, not a `+N` bump. New workflow files default to + the Codex 32 KiB anchor (`NEW_FILE_CAP = 32768`) unless explicitly tiered in the same PR. + +4. **The metric is a proxy for bounded *loaded* context — do not game it.** The real target is + total context loaded at runtime. Because `@~/.claude/gsd-core/references/...` imports are + loaded **eagerly**, moving prose into an eagerly @-imported reference shrinks the measured + file while leaving (or growing) total loaded context — that is gaming the proxy (Goodhart's + Law: the moment the byte count is the target, the cheapest way to satisfy it is to relocate + bytes, not remove them). **Legitimate extraction is LAZY**: content `Read` only at the step + that needs it (the `workflows/discuss-phase/modes/` progressive-disclosure pattern). The + per-file baseline + tier-cap pair is itself a Goodhart hedge — two guards pulling in + different directions are harder to game than one. + +5. **Shared measurement path.** The guard and the baseline generator both measure via + `scripts/workflow-size.cjs` (`lfByteCount`/`measureWorkflows`, re-exported to + `scripts/update-size-baseline.cjs` and asserted via + `scripts/lib/allowlist-ratchet.cjs` `assertFileBaseline`), so the recorded baseline and the + enforced size can never drift apart (#1074). + +## Alternatives considered (rejected & deferred) + +- **Tier-max tighten-only ratchet (#597) — SUPERSEDED.** Bound only the largest file per tier; + the other ~85 files could grow silently. Replaced by the per-file baseline, which guards + every file. *Re-open only if* the per-file baseline proves too noisy in practice (it has not). +- **A line-count budget — REJECTED (#717).** Token-dense tables/code make lines a poor proxy; + bytes are the vendor-bound, tokenizer-free unit. *Re-open only if* a cheap token count + becomes portably available and measurably better than bytes. +- **Eager `@`-import extraction to "fit" a file — REJECTED as proxy-gaming (#717).** Shrinks the + measured file without shrinking loaded context. Only **lazy** (Read-at-step) extraction + counts. Not re-openable — it defeats the goal the metric proxies. +- **Codex 32 KiB as a hard ceiling for the grandfathered orchestrators — REJECTED.** The XL/ + LARGE tiers sit *above* 32 KiB because they are top-level orchestrators loaded by Claude, not + Codex `AGENTS.md` docs; 32 KiB is the **new-file anchor**, not a universal cap. + +## Consequences + +- **Positive:** broad, quiet size creep is caught per-file across the whole surface; deliberate + growth is an explicit, reviewable one-line baseline diff; the byte unit is deterministic and + cross-platform (LF-normalized); the "bounded loaded context" goal and its anti-gaming rule + are recorded where a contributor will meet them; the quality (context-rot) rationale is no + longer caching-dependent hand-waving. +- **Costs:** every legitimate edit to a workflow/agent file must regenerate the baseline + (`npm run size:baseline`) or the guard reds — intentional friction that makes growth visible. + The baseline JSON is a merge-conflict surface on concurrent edits (resolved by regenerating). +- **Boundary:** this governs **instruction-document size** (workflow/agent `.md` loaded into + context). It is distinct from the **install-time skill-surface budget** of ADR-0010/0011 + (how many skills are eagerly registered) — different lever, different file, do not conflate. + +## Cross-references + +- Epic #1074 (per-file baseline + hard caps); #717 (bytes rebase + rationale); #683 (LF byte + count); #597 (superseded tier-max ratchet); PRs #1089 / #1096 / #1097. +- Code: `scripts/workflow-size.cjs`, `scripts/update-size-baseline.cjs`, + `scripts/lib/allowlist-ratchet.cjs` (`assertFileBaseline`), + `tests/{workflow,agent}-size-budget.test.cjs`, `tests/{workflow,agent}-size-baseline.json`. +- Related but distinct: **ADR-0010/0011** (skill-surface budget — install-time, not file size); + **ADR-456** (test-rigor architecture — the test these guards are authored under). +- External anchors: Codex `project_doc_max_bytes` (32 KiB); Anthropic "effective context + engineering for AI agents" (the context-rot / attention-budget argument). diff --git a/docs/adr/1769-state-md-transition-module.md b/docs/adr/1769-state-md-transition-module.md index 1fa4a4b53..429bb55eb 100644 --- a/docs/adr/1769-state-md-transition-module.md +++ b/docs/adr/1769-state-md-transition-module.md @@ -1,7 +1,7 @@ # ADR-1769: STATE.md Transition Module — intent-based transitions over scattered RMW callbacks -- **Status:** Proposed (Phase 0); **Accepted** at Phase 7 closeout -- **Date:** 2026-06-27 (Phase 0) +- **Status:** Accepted (Phase 7 closeout — all 10 lifecycle/maintenance transitions migrated; bug cluster #1760/#1761/#1743/#1695/#1264/#1255/#1257/#3242 retired) +- **Date:** 2026-06-27 (Phase 0); Phase 7 closeout 2026-06-27 - **Issue:** [#1769](https://github.com/open-gsd/gsd-core/issues/1769) — epic - **Supersedes:** the policy portions of `syncStateFrontmatter` (`src/state.cts:1667–1743`), `readModifyWriteStateMd`'s post-sync preservation block (`src/state.cts:2008–2119`), and @@ -208,10 +208,45 @@ alongside, leave callbacks — parallel worlds don't converge (ADR-857's failure | Phase | Scope | Closes issue | Bug coverage | |---|---|---|---| | 0 | ADR + CONTEXT.md update | #1769 | — | -| 1 | Substrate + `beginPhase` | TBD | #1255, #1257, #3242 | -| 2 | `advancePlan` | TBD | — | -| 3 | `completePhase` + `phase.cts:1770` | TBD | — | -| 4 | `plannedPhase` + `milestoneSwitch` | TBD | — | -| 5 | `milestoneComplete` + `milestone.cts:352` | TBD | — | -| 6 | `patch` | TBD | #1743, #1695 | -| 7 | `sync`, `prune`, `update` | TBD | #1760, #1761 | +| 1 | Substrate + `beginPhase` | #1771 | #1255, #1257, #3242 | +| 2 | `advancePlan` | #1782 | — | +| 3 | `completePhase` + `phase.cts:1770` | #1784 | — | +| 4 | `plannedPhase` + `milestoneSwitch` | #1786 | — | +| 5 | `milestoneComplete` + `milestone.cts:352` | #1789 | — | +| 6 | `patch` | #1791 | #1743, #1695 | +| 7 | `sync`, `prune`, `update` | #1793 | #1760, #1761 | + +## Amendments + +### #1796 — Finish the preservation consolidation (Path A) + +**Date:** 2026-06-28 · **Status:** Accepted + +Surfaced by an `/adr-phase-coverage` audit of this ADR (issue #1796): the +Consequences claim that the module *"Absorbs … `readModifyWriteStateMd`'s +post-sync preservation block"* was **not** realized by Phases 0–7. The block +stayed inline in `readModifyWriteStateMd` (`state.cts`); only +`current_phase_name`'s preservation was table-driven. `#1264` was fixed by a +call-site `shouldResync` guard rather than the field-classification table, so the +bug *class* was not "killed structurally" as the Consequences (line 161) claimed. + +**Resolution — Path A ("finish the consolidation"):** the post-sync preservation +block is now the pure, field-classification-table-driven `applyStatePreservation` +in `src/state-transition.cts`, consulted via `getFieldClassification` for **all +four** preserved fields — `progress`, `status`, `stopped_at`, `current_phase_name` +(previously only the last was table-driven). `readModifyWriteStateMd` calls it. +This makes the CONTEXT.md "Absorbs … post-sync preservation block" claim accurate +and routes the `#1264` preservation policy through the single field-classification +table (one policy source, not three drifting encodings). + +Behavior is byte-identical to the pre-amendment inline block (Hyrum-safe — 15 +callers' observable preservation is unchanged); the full state / frontmatter / +transition regression suite (847 tests, including the `#1264`, `#1743`, `#1695`, +`#1760`, `#1761`, `#3242`, `#1230` characterization blocks) passes unmodified. +A codex (gpt-5.5 / high) adversarial review returned CLEAN — no behavior drift +across a 58,564-case equivalence sweep, no security surface introduced. + +The `shouldResync` call-site guard remains — it is the transition's declaration +of "am I re-deriving from disk?" What changed is that the *preservation policy* +it feeds is now centralized and table-driven rather than re-encoded per writer, +which is the consolidation this ADR originally specified. diff --git a/docs/adr/1787-gsd-next-smart-entry.md b/docs/adr/1787-gsd-next-smart-entry.md new file mode 100644 index 000000000..a03d88a8c --- /dev/null +++ b/docs/adr/1787-gsd-next-smart-entry.md @@ -0,0 +1,123 @@ +# ADR 1787: `/gsd:next` smart-entry front door delegates advancement to `/gsd:progress --next` + +- **Status:** Accepted +- **Date:** 2026-07-03 +- **Issue:** #1787 +- **Implementation:** PR #1798 (`feat(#1787): add /gsd:next smart entry workflow`) +- **Supersedes context:** the removal of the flat `gsd-next` command (#3054) + +## Context + +gsd-core has no state-aware front door. A user must already know whether to +reach for `/gsd:progress`, `/gsd:plan-phase`, `/gsd:execute-phase`, `/gsd:quick`, +or `/gsd:new-project`. gsd-pi ships a `/gsd` smart-entry wizard (a state-aware +menu with one recommended action) that users run first; gsd-core wants the same +"run one command, get told what to do next" feel without gsd-pi's TUI, since +gsd-core is a markdown prompt framework installed into AI agents, not a Node app. + +Two facts constrain the design: + +1. **A `gsd-next` command already existed and was deliberately removed (#3054),** + with `/gsd:progress --next` established as the canonical "advance to the next + logical step" engine. `tests/bug-3054-stale-gsd-next-references.test.cjs` + guards against user-facing surfaces re-referencing the removed flat command. + Re-introducing a `next` entry point re-opens a settled question: it must not + recreate the duplication that justified the removal. + +2. **`/gsd:progress` is the "unified GSD situational command."** Its `--next` + mode (`gsd-core/workflows/next.md`) is a *gated* advancement engine: + - **Route 0** — the resume-incomplete-phase invariant (#160): if a session + died mid-execution and `STATE.md`'s `current_phase` advanced past a phase + that still has `PLAN.md` files without matching `SUMMARY.md`, `--next` + resumes the *incomplete earlier* phase rather than the recorded current one. + - **Gates 1–3** — unresolved checkpoint, error/failed state, and unchecked + verification failures each hard-stop advancement. + +The initial implementation of the new `smart-entry` classifier +(`src/smart-entry.cts`) re-derived in-project forward routing itself. For the +`executing` situation it recommended dispatching `/gsd:execute-phase` **directly**, +bypassing Route 0 and Gates 1–3. That reproduced exactly the duplication that got +`gsd-next` removed — two front doors that can route the *same* in-project state +to *different* phases — and introduced a correctness hazard (executing the +recorded current phase while an earlier phase is silently incomplete). A +maintainer (davesienkowski) flagged the overlap on PR #1798. + +## Decision + +Ship `/gsd:next` as a **menu front door only**, with a hard boundary against +re-implementing advancement: + +1. **Detection + classification** live in Node as `gsd-tools smart-entry [--json]` + (`src/smart-entry.cts`): a pure, unit-tested classifier over `.planning/STATE.md`, + `ROADMAP.md`, and read-only git signals, producing one of 11 situations with a + recommended action and an action menu. **Presentation + dispatch** live in the + markdown layer (`commands/gsd/next.md` → `gsd-core/workflows/smart-entry.md`), + using `AskUserQuestion` with a `--text` numbered-list fallback for non-Claude + runtimes. The command carries no `requires` field so it works pre-project. + +2. **In-project forward motion delegates to the single gated engine.** For the + `planning`, `executing`, and `verify-pending` situations, the *recommended* + action is `/gsd:progress --next`. smart-entry never re-derives forward routing; + it hands linear advancement to `workflows/next.md` so Route 0 and Gates 1–3 are + always honored. The specific command (`/gsd:plan-phase`, `/gsd:execute-phase`, + `/gsd:verify-work`) remains available as an explicit secondary menu option for a + user who deliberately wants to bypass advancement gating. + +3. **smart-entry's distinct value is the states `--next` cannot reach.** For + situations *off* the linear advance path it keeps direct recommendations, since + these are precisely what `/gsd:progress --next` does not (or cannot, given its + `requires: [phase]`) handle: `no-project` → `/gsd:new-project`, `paused` → + `/gsd:resume-work`, `blocked` → `/gsd:debug`, `verify-failed` → + `/gsd:verify-work`, `needs-first-phase` → `/gsd:discuss-phase`, `idle-stranded` + → `/gsd:ship`, `complete` → `/gsd:new-milestone`, `unknown` → `/gsd:progress`. + +This makes the spec's stated decision #4 ("complementary, not redundant") true in +the implementation, not just the prose: there is exactly one advancement engine, +and `/gsd:next` is a menu over it plus the off-path states. + +## Consequences + +### Positive + +- **One advancement engine.** `/gsd:next` and `/gsd:progress --next` can never + disagree about the next in-project step, and Route 0 / Gates 1–3 cannot be + bypassed through the new front door. The #3054 duplication does not return. +- **Genuine new value, no overlap.** The front door adds pre-project, remediation, + and lifecycle-exit routing that `--next` structurally cannot serve. +- **Testable boundary.** `tests/smart-entry.unit.test.cjs` asserts that every + forward-motion situation recommends `/gsd:progress --next` and every off-path + situation keeps its direct recommendation — the delegation is a regression-locked + contract, not a convention. + +### Negative / trade-offs + +- One extra indirection hop for the common "just continue" case (`/gsd:next` → + `/gsd:progress --next` → dispatched command) versus dispatching the phase command + directly. Accepted: the hop is what buys gate-safety and single-engine behavior. +- The classifier reads STATE.md via shared primitives (`frontmatter.cjs`, + `state-document.cjs`, `phase-id.cjs`) rather than through `workflows/next.md`'s + own detection, so detection logic exists in two places. Accepted: they share + parsing primitives and only the *routing decision* is centralized (in `--next`), + which is where divergence would actually harm the user. + +## Alternatives considered + +1. **Keep smart-entry as an independent in-project router (as first implemented).** + Rejected: reproduces the #3054 duplication and the Route 0 / Gates 1–3 bypass + hazard. +2. **Fold everything into `/gsd:progress` (add a menu mode) and ship no new + command.** Rejected: `/gsd:progress` carries `requires: [phase]` and cannot + serve the pre-project `no-project` front-door case, which is a primary goal. +3. **Replace `workflows/next.md`'s inline detection with the new classifier so + there is one detection *and* routing engine.** Rejected for this PR: `--next` + couples detection to safety gates and convergence flags the classifier does not + model; swapping its detection wholesale would risk regressing those invariants. + Left as possible future consolidation. + +## References + +- Spec: `docs/superpowers/specs/2026-06-27-gsd-smart-entry-design.md` +- Removed flat command guard: `tests/bug-3054-stale-gsd-next-references.test.cjs` +- Gated engine: `gsd-core/workflows/next.md` (Route 0 = resume-incomplete-phase, #160) +- Classifier: `src/smart-entry.cts` → `gsd-core/bin/lib/smart-entry.cjs` +- Command contract naming (`gsd:*`): ADR-0002 diff --git a/docs/adr/1817-state-md-rebuild-derivability-contract.md b/docs/adr/1817-state-md-rebuild-derivability-contract.md new file mode 100644 index 000000000..08cd472fb --- /dev/null +++ b/docs/adr/1817-state-md-rebuild-derivability-contract.md @@ -0,0 +1,279 @@ +# ADR-1817: STATE.md rebuild — derivability contract (capstone transition) + +- **Status:** Accepted (Phase 0 — ADR + CONTEXT.md update; lands ahead of Phases 1–2) +- **Date:** 2026-06-29 +- **Issue:** [#1817](https://github.com/open-gsd/gsd-core/issues/1817) — epic +- **Builds on:** [ADR-1769](1769-state-md-transition-module.md) (STATE.md Transition Module). Adds the 11th transition (`rebuild`) on top of ADR-1769's 10 lifecycle/maintenance intents. +- **Supersedes:** nothing. Extends ADR-1769's transition set; does not revisit its design. + +## Context + +ADR-1769 landed the STATE.md Transition Module with 10 intent-based transitions +(`beginPhase`, `advancePlan`, `completePhase`, `plannedPhase`, `milestoneSwitch`, +`milestoneComplete`, `patch`, `sync`, `prune`, `update`) and a field-classification +table that killed the per-call-site preservation-policy bug cluster (#1760, #1761, +#1743, #1695, #1264, #1255, #1257, #3242). Each transition touches **individual +fields**. + +A second bug class survived ADR-1769: **body-structure drift** that no current +command can reconcile. `syncCore` (the lightest-weight transition) only patches +three frontmatter fields — `Total Plans in Phase`, `Progress`, `Last Activity` — +and intentionally does not re-derive body structure. `buildStateFrontmatter` +re-derives all frontmatter from body + disk scan but does not touch the body +itself. The result: **the body can diverge from ground truth indefinitely while +`gsd-tools state sync` reports `synced: true`.** + +Observed drift signatures (full list in epic #1817): + +- `## Current Position` prose fields (Phase, Status, Current Plan) contradict + frontmatter after a milestone switch or prune. +- `## By-Phase Progress` table has orphaned rows for phases from a prior + milestone or rows with zero-padded phase IDs that were renamed. +- Template-placeholder field values (`[phase name]`, `[date]`) left in place + when an AI agent wrote partial state. +- Duplicate `## Session Continuity Archive` blocks from repeated + `state record-session` calls on a corrupt file. +- `stopped_at` in frontmatter sourced from the wrong section (an archive block + rather than the current `## Session` block) — bug #2444's guard only applies + in `buildStateFrontmatter`, not to the body itself. +- `progress.total_phases` in frontmatter correct, but `Phase: [N] of [M]` prose + still shows the old milestone's count. + +Three open issues sit in this defect class: **#1776** (`cmdStatePrune` phase +fallback matches any `| Phase | N |` table cell, not `## Current Position` +prose → requires scoped body extraction), **#1761** (`state sync` writes wrong +progress when ROADMAP lacks versioned milestone headings → a rebuild would +re-derive from disk + ROADMAP together), **#1591** (`phase.complete` mis-parses +`
`-wrapped roadmaps and garbles counters → a rebuild can reconcile +from canonical disk sources rather than parsing ROADMAP mid-transition). + +The deeper shape: **every body-level drift bug today requires a per-bug regex +fix.** Ten-plus closed issues (#1658, #1659, #1668, #1446, #1230, #948, #549, +#500, #363, #316) each added a narrow guard that didn't prevent the next +variant. A `rebuild` transition that re-derives the **canonical body sections** +from ROADMAP + phase dirs + session history would resolve the entire class +without per-bug patches. + +## Decision + +Add `rebuild` as the **11th intent** in `transitionCore` (ADR-1769 §6 "Core +scope: writes only"). Six design decisions, resolved via `/grilling`: + +### 1. The `rebuild` intent is a maintenance transition, same tier as the other 10 + +`rebuild` is a STATE.md Transition Module method, dispatched from +`transitionCore`'s switch alongside `sync`, `prune`, `update`, etc. Pure core +`(content, intent, deps) → newContent`, same shape as ADR-1769 §3. + +**Capability tier (ADR-857 analog):** `rebuild` is **core substrate**, not a +Feature Capability. It is non-toggleable, lives inside the transition module, +and is not subject to ADR-857 capability consent/overlay. This mirrors ADR-550 +§83's "verifier↔predicate contract is core/non-toggleable" rule for the +verification seam: the derivability contract documented here is the state-seam +equivalent. + +*Rejected:* (B) Implement `rebuild` as a Feature Capability that users opt into +— rejected because the bug class is core STATE.md correctness, not optional +behavior. (C) Keep `rebuild` outside the Transition Module (a sibling +utility) — rejected: it must own the same lock→read→apply→preserve→write +transaction ADR-1769 §1 specified for the other 10 transitions; a sibling +utility would re-encapsulate that machinery and drift. + +### 2. Section taxonomy: derived vs preserved + +Each STATE.md body section is classified as **re-derivable** or **preserved**. +`rebuild` consults the taxonomy; it never re-derives a preserved section and +never preserves a re-derivable one verbatim when the canonical source +disagrees. + +| Section | Class | Source of truth | Rebuild behavior | +|---|---|---|---| +| `## Current Position` prose | re-derivable | Frontmatter (which `buildStateFrontmatter` already derives correctly from disk + ROADMAP) | Re-derive each prose field from the corresponding frontmatter field; replace verbatim. | +| `## By-Phase Progress` table | re-derivable | Phase dirs on disk (same source as `buildStateFrontmatter`'s disk scan) | Re-derive the entire table from disk; drop orphaned rows. | +| `## Session` block | preserved | Human-curated current-session data | Preserve verbatim. (Only the *current* `## Session` block; archived sessions are de-duplicated per §4.) | +| `## Decisions` | preserved | Human-curated decision log | Preserve verbatim. Staleness is `pruneCore`'s concern, not rebuild's. | +| `## Session Continuity Archive` | preserved, de-duplicated | Prior session snapshots | Keep the most-recent N (configurable; default 3); drop duplicates; preserve kept entries verbatim. | +| `## Rebuild Log` | appended (new section) | The rebuild transition itself | Append one entry per rebuild that mutated the file; never re-derive or edit prior entries. | +| Any other `## …` section | preserved (unknown) | Human-curated | Preserve verbatim. Rebuild does not recognize or rewrite sections outside the taxonomy. | + +**Section ordering is invariant.** Rebuild rewrites the *content* of +re-derivable sections in place; it does not reorder sections, insert new +sections (other than `## Rebuild Log` if absent), or remove sections. + +*Principle:* derive what is derivable, preserve what is curated, log what is +dropped. (Postel's Law applied to a state file: be liberal in what you accept +— any drifted input — and conservative in what you send — canonical form for +derived, verbatim for preserved.) + +*Rejected:* (α) Treat all sections as re-derivable — rejected: destroys +human-curated content (session notes, decisions). (β) Treat all sections as +preserved — rejected: this is the status quo; the drift class survives. + +### 3. Orphaned data: log + drop (audit trail mandatory) + +When `rebuild` encounters canonical-source-disagreement that requires dropping +data, it MUST append a structured entry to `## Rebuild Log` recording: + +- `timestamp` (ISO-8601, from the injected `clock`) +- `kind` — one of `orphaned-row`, `placeholder-removed`, `archive-deduplicated`, + `wrong-section-source`, `milestone-count-stale`, or `section-rewritten` +- `section` — which body section was mutated +- `before` / `after` — the dropped/changed content (truncated to 512 chars per + entry to bound log growth) +- `reason` — short structured string explaining the canonical source that won + +`## Rebuild Log` is itself **preserved** (per §2). Rebuild never rewrites or +truncates prior log entries; it only appends. A separate prune step (out of +scope here) governs log retention. + +**Why log everything:** dropping user-adjacent data without a trace is hostile +even when the drop is correct. The log gives the user an undo path (manual +re-add) and gives the maintainer a debugging signal when rebuild drops +something it shouldn't have. (Hyrum's Law mitigation: the drop is observable, +the audit trail is the contract.) + +*Rejected:* silent drop — rejected: violates the ADR-1411 resolution-provenance +principle that mutation decisions report what they did, not fall open silently. + +### 4. Idempotency is a hard guarantee + +`rebuild` is **idempotent**: invoking it twice in succession on the same file +produces no change on the second invocation. This is testable and tested. + +The idempotency contract has two parts: + +1. **Body content idempotency:** re-running rebuild on a file rebuild just + canonicalized produces byte-identical `## Current Position` and + `## By-Phase Progress` sections. +2. **Rebuild Log idempotency:** a rebuild that mutates nothing appends no log + entry. (This is what makes the second-invocation case truly byte-identical + — without it, the second run would always append a no-op log entry and + violate idempotency.) + +The log-appends-only-on-mutation rule is the load-bearing constraint. If +rebuild wrote a log entry unconditionally on every invocation, idempotency +would break. + +### 5. Interaction with `sync` — non-overlapping scopes + +`sync` and `rebuild` compose; they do not compete. + +| Transition | Scope | Trigger | Latency | +|---|---|---|---| +| `sync` | 3 frontmatter fields (`Total Plans in Phase`, `Progress`, `Last Activity`) | Auto-triggered on every state transition | Lightweight, runs on every transition | +| `rebuild` | Body structure (`## Current Position`, `## By-Phase Progress`, archive dedup) | Manual (`gsd-tools state rebuild`) | Heavier; reads disk + ROADMAP; explicit user invocation | + +Running `sync` after `rebuild` is safe: `sync`'s 3 fields are a strict subset +of what `rebuild` reconciled (in canonical form), so sync's derivation will +produce the same values rebuild just wrote. Running `rebuild` after `sync` is +also safe: rebuild re-derives body from canonical sources; sync's just-written +frontmatter is one of those sources. + +`sync` stays as the auto-triggered lightweight path; `rebuild` is the +user-invoked heavy reconciliation. Neither subsumes the other. + +### 6. Interaction with `auto_prune_state` — orthogonal concerns + +`rebuild` does NOT prune. Pruning (removing data that is no longer relevant, +e.g. decisions older than N sessions, prior-milestone state) is a separate +concern governed by `auto_prune_state` and `pruneCore`. + +The distinction: + +- **Rebuild reconciles** body with current canonical sources. A `## By-Phase + Progress` row for a phase that no longer exists on disk is dropped because + it is *canonical-mismatched*, not because it is *old*. +- **Prune removes** data based on age/staleness policy. A `## Decisions` + entry from 6 months ago stays under rebuild (preserved) but may be removed + by prune based on retention policy. + +The two compose: rebuild first (reconcile with canonical sources), then prune +(remove per policy). Rebuild never makes pruning decisions; prune never +re-derives structure. + +## Consequences + +**Positive:** + +- The body-structure drift bug class is killed structurally. #1776, #1761, + and #1591 each become either directly fixable by `rebuild` or indirectly + addressable (the rebuild provides the scoped body extraction those bugs + need). +- The derivability contract is a new correctness invariant: STATE.md body + is **derivable from canonical sources at any time**, not just incrementally + updatable. This is the capstone property ADR-1769's per-field transitions + couldn't deliver alone. +- The audit log gives the maintainer a debugging signal when STATE.md editing + (manual or AI-driven) produces drift that rebuild later reconciles. +- Future drift classes (anything not in the §2 taxonomy today) can be added + by extending the taxonomy + a new `kind` in the log enum, without + re-touching the transition core's dispatch shape (Gall's Law: extend, don't + rewrite). + +**Negative:** + +- A new body section (`## Rebuild Log`) is added to STATE.md. Older GSD + versions reading the file ignore the section (preserved verbatim by + `readModifyWriteStateMd`'s post-sync block — the section name is not in + the field-classification table, so it falls through as "unknown, preserved"). +- The derivability contract is a new shared artifact: any future STATE.md + body section must declare its taxonomy class. Adding a new re-derivable + section is a non-trivial change (rebuild must learn the derivation rule); + adding a new preserved section is mechanical. +- The first invocation of `rebuild` on a long-lived project will produce a + substantial audit log entry (the project's accumulated drift is reconciled + in one pass). This is honest — the drift existed; rebuild surfaces it — + but users may be surprised by the log size on first run. Mitigation: + `--dry-run` flag (Phase 2) previews the diff before writing. + +**Neutral:** + +- `rebuildCore` is a pure function over `(content, intent, deps)`, callable + inside any orchestration shape (single-file write, multi-file transaction, + dry-run preview). Same property that let ADR-1769 §3 run `completePhase` + inside `writePlanningFileSet`. +- The existing 10 transitions are unchanged. `transitionCore`'s switch grows + from 10 cases to 11; the missing-case-compile-time-error guarantee + (ADR-1769 §1) extends to the new case. + +## Alternatives considered + +1. **Fix each body-level bug individually (status quo).** Rejected: already + done for 10+ closed issues. Each fix is a narrow regex guard that doesn't + prevent the next variant. Does not scale; the open issues (#1776, #1761, + #1591) are evidence. +2. **`state sync` expansion — extend `syncCore` to cover body structure.** + Rejected: `sync` is intentionally lightweight and auto-triggers on every + transition. Making it re-derive body structure would make every state + transition pay the disk-scan + ROADMAP-read cost, and would couple + auto-triggered behavior to a heavier and riskier code path. The + manual/auto split (§5) is the right factoring. +3. **Regenerate STATE.md from scratch (nuke-and-rebuild).** Rejected: loses + curated human content (session notes, decisions, archives). The + derivability contract is *selective* — derived sections re-derive, + preserved sections survive — which is exactly what a nuke-and-rebuild + cannot do. +4. **A standalone `state-doctor` workflow outside the transition module.** Rejected: + same drift-from-canonical-shape risk that motivated ADR-1769's + consolidation. A workflow that bypasses the transition module re-imports + the lock/scan/preservation machinery and re-creates the bug class. +5. **Defer until ADR-1769's amendments (#1796) finish independently.** + Rejected: ADR-1769 is closed (Phase 7 closeout + #1796 amendment landed). + There is no consumer-driven sequencing constraint; the rebuild transition + composes cleanly with the existing 10. + +## Phases + +This epic (#1817) is implemented in three phases, each its own PR. Phase 0 +closes this issue (the epic); Phases 1 and 2 close their own sub-issues. + +| Phase | Scope | Closes issue | Bug coverage | +|---|---|---|---| +| 0 | ADR + CONTEXT.md update (derivability contract, preserved-vs-derived taxonomy, idempotency, sync/prune interaction) | #1817 | — | +| 1 | `rebuildCore` body + `rebuild` intent dispatch case + drift-class unit tests | #1827 | surfaces the class; #1776, #1761, #1591 become directly addressable | +| 2 | `cmdStateRebuild` CLI + `--dry-run` / `--verbose` + integration tests + `docs/commands/state.md` + changeset | #1826 | end-to-end reconciliation available to users | + +Per-transition discipline (inherited from ADR-1769 §7): characterization tests +first (capture the drift signatures we want to reconcile), then implement +`rebuildCore`, then verify existing `pruneCore` / `syncCore` tests still pass, +then add idempotency tests. diff --git a/docs/adr/1866-agent-skills-dual-injection-contract.md b/docs/adr/1866-agent-skills-dual-injection-contract.md new file mode 100644 index 000000000..1e5e45464 --- /dev/null +++ b/docs/adr/1866-agent-skills-dual-injection-contract.md @@ -0,0 +1,60 @@ +# ADR 1866: agent_skills dual injection — orchestrator-side + agent-side self-load + +- **Status:** Accepted +- **Date:** 2026-07-01 +- **Issue:** [#1866](https://github.com/open-gsd/gsd-core/issues/1866) + +## Context + +The documented `agent_skills` injection contract ([Agent Skills Injection](../CONFIGURATION.md#agent-skills-injection)) is **orchestrator-side**: at spawn time, the *workflow* calls `gsd-tools query agent-skills ` in its bash init and interpolates the resulting `` block into the `Task()`/`Agent()` prompt. This is established across ~25 workflows and was extended to the review family by #991 / PR #1005. + +Three gaps make orchestrator-side injection insufficient on its own: + +1. **`/gsd-autonomous`** delegates plan/execute/review via flat `Skill()` invocations and resolves no `agent_skills` itself — it relies on the delegated workflow's bash init running. On runtimes where `Skill()`-delegated workflow bash does not reliably execute (Cursor; assessed in #1600 / #1601 — "Phase C+D per-platform plugin skill model assessment, all N/A"), the configured skills never reach the consumer agent. +2. **GSD agent definitions** (`agents/gsd-*.md`) discover project skills via `project-skills-discovery.md` but never call `query agent-skills` for the `agent_skills` config map. +3. The gap is **invisible**: output still looks successful, so users who configure `agent_skills` in `.planning/config.json` see no error — the skills are silently ignored (same failure class as #991). + +`cmdAgentSkills` (`src/init.cts`) is read-only and idempotent — it exits 0 with an empty block when nothing is configured for a type. So an agent re-invoking it at its own init is safe and zero-overhead for unconfigured types. + +## Decision + +Adopt a **dual injection** contract. The orchestrator-side path stays as-is (no removal from the ~25 workflows); each of the 22 consumer agents *additionally* self-loads in its mandatory init step. + +- A shared reference, [`gsd-core/references/agent-skills-bootstrap.md`](../../gsd-core/references/agent-skills-bootstrap.md), owns the contract: the query, the `Read` step, the per-runtime coverage matrix, and the **dedup guard**. +- Each consumer agent file carries one self-load line naming its own type (e.g. `query agent-skills gsd-executor`) and `@`-including the shared reference. +- **Dedup guard (load-bearing):** if the agent's prompt already contains an `` block, self-load is skipped. On runtimes where orchestrator-side injection also runs (Claude Code), this prevents the prompt from carrying two copies — a real context-cost regression that the guard closes. + +### Why not orchestrator-side only (Lens A) + +Extending `autonomous.md` to resolve skills itself (the #991 pattern) fixes the *autonomous-orchestrator* gap but not the deeper one: on Cursor, the agent *definition* is what the runtime loads, and the workflow bash init is the unreliable seam. Agent-side self-load moves the resolution to the agent definition, which is the surface Cursor actually consumes. We retain the autonomous.md documentation note (the workflow no longer needs per-delegation injection) but do not duplicate injection code there. + +### Why not remove orchestrator-side injection + +The ~25 workflows' injection is the proven, tested path on Claude Code and remains the primary channel there. Removing it would regress the runtime that works today. The dual contract keeps both; the dedup guard makes coexistence cost-free. + +## Coverage + +| Skill form | Orchestrator-side | Agent self-load | +|---|---|---| +| Project-relative path | all runtimes | all runtimes | +| `global:` | all runtimes | all runtimes | +| `global::` | Claude only (Skill-tool directive) | Claude only — **not closeable on Cursor** (#1601); no plugin/Skill-tool model exists there | + +The `global:plugin:skill` gap on Cursor is **out of scope** for this ADR — it is a runtime limitation, not an injection-contract one. + +## Drift guard + +Three surfaces share the consumer-agent set: the `CONSUMER_AGENTS` list in `tests/agent-skills.test.cjs`, the agents carrying the bootstrap, and the `query agent-skills ` calls in workflows. `tests/agent-skills-bootstrap.test.cjs` asserts the bijection between `CONSUMER_AGENTS` and agents-with-bootstrap (Generative-Fix-Divergence, `CLAUDE.md`), with a `fast-check` property test over the agent file set. Any new consumer agent added without the bootstrap (or vice versa) fails CI. + +## Consequences + +- **Positive:** `agent_skills` config reaches consumer agents on every runtime regardless of orchestrator bash discipline; the `/gsd-autonomous` + Cursor path works without project-level workarounds. +- **Negative:** Two injection seams to maintain. Mitigated by the dedup guard (no double-load) and the bijection drift-guard test (no silent desync). +- **Neutral:** The documented contract in `CONFIGURATION.md` is updated to describe both paths. + +## References + +- #991 / PR #1005 — orchestrator-side injection precedent (review family) +- #1258 / #1243 / PR #1261 — cross-runtime skill mapping epic & Claude plugin-skill consumption +- #1366 / #1374 / #1400 / #1410 / #1415 / #1424 / #1425 — `agent_skills` resolver hardening (cwd-drift, diagnostics, provenance, flush) +- #1600 / #1601 — per-platform plugin skill model assessment (Cursor N/A) diff --git a/docs/adr/3524-cjs-sdk-hard-seam.md b/docs/adr/3524-cjs-sdk-hard-seam.md index 5206a736c..2ad3b5989 100644 --- a/docs/adr/3524-cjs-sdk-hard-seam.md +++ b/docs/adr/3524-cjs-sdk-hard-seam.md @@ -137,7 +137,7 @@ parsing from `sdk/dist/query/validate.js`, the same technique used for `escapeRe `gen-phase-lifecycle-policy.mjs`. The function is deterministic and pure: no closures over external state, no side effects. -**Parity tests:** `tests/6-validate-cjs-drift-regression.test.cjs` — 5 tests (all GREEN after fix, +**Parity tests:** `tests/health-validation.test.cjs` (folds former `6-validate-cjs-drift-regression`, consolidation epic #1969) — 5 tests (all GREEN after fix, all RED on pre-fix `origin/main`). Covers each drift item with concrete fixtures: - Drift 1: two milestone archives (v1.0 old, v1.1 active); v1.0 phase absent from ROADMAP. - Drift 2: ROADMAP "01A", disk "1A-foo" — padding mismatch. @@ -192,7 +192,7 @@ No new behavioral fix is required — the generator pattern extension is the del - `extractTopLevelFunction()` (new in `gen-validate.mjs`) — brace-balanced parser for top-level named function declarations. Used for `canonicalPlanStem`. -**Parity tests:** `tests/26-w005-w006-i001-cjs-drift-regression.test.cjs` — 7 tests. +**Parity tests:** `tests/health-validation.test.cjs` (folds former `26-w005-w006-i001-cjs-drift-regression`, consolidation epic #1969) — 7 tests. - W005: no false positive for `999.1-foo`; W005 still fires for single-digit prefix. - W006-archived: no false W006 for phase archived under `milestones/v1.0-phases/`; unit tests for `MILESTONE_ARCHIVE_DIR_RE` and `PHASE_TOKEN_FROM_DIR_RE` export and behavior. diff --git a/docs/adr/443-opus48-unified-effort-and-fast-mode-routing.md b/docs/adr/443-opus48-unified-effort-and-fast-mode-routing.md index f3c61c44a..f28704ebc 100644 --- a/docs/adr/443-opus48-unified-effort-and-fast-mode-routing.md +++ b/docs/adr/443-opus48-unified-effort-and-fast-mode-routing.md @@ -86,7 +86,7 @@ Common core: `low`, `medium`, `high`, `xhigh`. - Tracking issue: #443 - Prior art (inert reasoning_effort): #2517; `tests/issue-2517-runtime-aware-profiles.test.cjs` -- dynamic_routing escalation: #3024; `tests/feat-3024-dynamic-routing.test.cjs` +- dynamic_routing escalation: #3024; `tests/model-profiles.test.cjs` (folds former `feat-3024-dynamic-routing`, consolidation epic #1969) - phase-type tiers: #3023 - Anthropic effort API: `output_config.effort` (`low`/`medium`/`high`/`xhigh`/`max`); fast mode: `speed` (`standard`/`fast`) - Claude Code effort: `/effort`, `--effort`, `CLAUDE_CODE_EFFORT_LEVEL`, `effortLevel` setting, subagent `effort` frontmatter (anthropics/claude-code #31536, completed); fast mode: `/fast` (interactive only) diff --git a/docs/adr/550-spec-phase-probe-contract.md b/docs/adr/550-spec-phase-probe-contract.md index 8389b1245..54a79b66c 100644 --- a/docs/adr/550-spec-phase-probe-contract.md +++ b/docs/adr/550-spec-phase-probe-contract.md @@ -127,6 +127,8 @@ This addendum ratifies one contract point: **Why opt-in, not required:** making the control mandatory would regress the #1314 zero-authoring compose path — every existing node-test prohibition (which carries no clean fixture) would suddenly hard-gate. So **absent `cleanFixture` → no control runs and behavior is byte-identical to post-#1314**; the residual remains a documented permanent constraint *only* for checks whose author did not supply a clean control. An author opts into the stronger machine guarantee by supplying one. The lint-rule kind needs no analog: its "subject" *is* the linted file (no `GSD_PROHIB_SUBJECT` indirection), so the "reds because the env var is set" gap does not exist there. Net effect on D4 is unchanged — every miss/fail/un-provable still hard-gates; this only *tightens* what counts as proven. The mechanism lives in `src/prohibition-enforcement.cts` (`defaultProveFailFirst` node-test branch + the `runNodeTestWithSubject` helper) and `src/probe-core.cts` (`projectProhibitions`), compiled by `build:lib`. +> **SUPERSEDED 2026-07-03 (#1906) — the node-test causation control is now MANDATORY, not opt-in.** The "why opt-in" rationale above rested on avoiding a regression of "every existing node-test prohibition." That premise no longer holds: there is **no in-tree `node-test` consumer** (ADR-1606 Consequences, "Open conventions"), so requiring the control hard-gates *zero* existing checks — while leaving the Goodhart hole open by default let a deceptive content-independent test pass the proof. Per the owner ruling on #1906, a `node-test` descriptor that omits `cleanFixture` is now **un-provable (fail-closed)**, never proven on the violation alone; when a clean fixture is present, fail-first is proven exactly as before (RED on violation AND non-vacuous GREEN on clean). The `lint-rule` kind is unchanged. Disclosed as breaking (Hyrum) with the ~zero blast radius noted. The decision-of-record lives in **ADR-1606's 2026-07-03 (#1906) addendum**; this note supersedes the "Why opt-in, not required" paragraph immediately above. + ## Addendum (2026-06-15): optional `check` descriptor on the prohibition item — D3 shape extension (#1278) This ratifies the **deterministic SOURCE** for the test-tier `CheckDescriptor` that #1259 (PR #1273) left caller/verifier-supplied. #1259 shipped the PRODUCER (`check prohibition-enforcement`) that *runs* a wired check given a `{kind, target, rule?}` descriptor, but the descriptor itself was invented by the verify-phase LLM each run (the "locate" half). #1278 makes that locate half **deterministic**: an optional `check` descriptor is authored at spec-phase on the resolved `test`-tier prohibition, projected by `projectProhibitions`, and read back by verify-phase — so a wired, passing test closes the gap with **zero manual authoring**. This extends the **Decision 3 prohibition-item shape** (it adds optional keys to that item), so it is ratified here rather than rewriting D3 in place. @@ -143,6 +145,22 @@ This ratifies the **deterministic SOURCE** for the test-tier `CheckDescriptor` t Net effect on D3: the prohibition-item shape is extended with three optional, backward-compatible flat-scalar keys that give the test-tier locate a deterministic spec-phase source; the contract's CI-testable surface (D5) gains the projection round-trip parity (CHK-03), the fail-closed guard (CHK-06), and the byte-stable backward-compat fixture (CHK-07). The decision also lives in `src/probe-core.cts` / `src/prohibition-enforcement.cts` comments, the `verify-phase.md` / `spec-phase.md` prose, and the #1278 changeset. +## Addendum (2026-06-25, #1154) — honest verifier: the truth-axis disposition mirror of D4 + +This records the **truth-axis half of Decision 4** that the original ADR deliberately scoped out. D3 left `truths` untouched (no `polarity` field — that is a prohibition concern), and the Lineage note parked the N17 abstention experiment as the verify-time half of the **prohibition** judgment-tier only. D7a, however, already gives the **edge** axis an orthogonal `verification` tier (`explicit | backstop`), and `plan-phase` already lifts a `backstop` edge into `must_haves.truths`. Until now that tier was flattened to a prose parenthetical at the projection, so the verifier had nothing structured to branch on and graded a `backstop` truth `passed` like any inferable one — confidently false-passing a non-inferable check ~100% of the time (the exact "verifier reach = spec reach" failure ADR-857 names). This addendum closes that gap by giving the `backstop` **truth** tier the same abstain-and-flag disposition D4 gave the prohibition `judgment` tier — **opposite polarity (must-HAVE under-specified vs must-NOT irreducible), same never-silent-pass machinery.** It cross-references **ADR-857** (the verifier↔predicate contract this rides is core, non-toggleable substrate, graded exogenously — `:65`); this completes the already-endorsed edge branch of that rail and adds no parallel mechanism. + +1. **The D3 truth-item shape gains an OPTIONAL flat-scalar `verification` marker.** A `must_haves.truths` item is normally a plain string (an inferable truth — today's shape, unchanged). A **non-inferable** truth MAY instead be an object item carrying `statement` + a flat scalar `verification: backstop`. The marker is additive and default-absent: a string truth, or an object with no marker, behaves byte-identically to today (Hyrum's Law backward-compat). This extends the **Decision 3 truth shape** the same way #1278 extended the prohibition shape — it does **not** add a `polarity` field (D3's "truths untouched" holds); `verification` is D7a's pre-existing orthogonal axis, now carried through to the truth projection. + +2. **Flat scalars — NOT a nested object (load-bearing, #1278 precedent).** The marker is a flat `verification:` continuation key on the truth item, never a nested object — the shared flat `parseMustHavesBlock` round-trips it with **no parser change** (the round-trip parity test is the proof; the untouched frontmatter suite confirms `truths`/`artifacts`/`key_links`/`prohibitions` readers stay regression-free). + +3. **Deterministic disposition + projection.** `projectTruths` (`src/probe-core.cts`) emits the flat-scalar marker ONLY for a `backstop` truth and collapses every inferable truth to a bare string (conservative serializer). `dispositionForUnverifiableTruth(truth, { evidence })` is the pure, fail-closed verdict: a `backstop` truth with no **explicit evidence** (a passing wired held-out/property-based test, or a directly-observed behavior) → `{ status: 'unverified', flagged: true, reason: 'insufficient_spec' }`, **never green**; with evidence → green; any non-`backstop` truth → green (the over-abstention guard). No LLM judgment is tested (D5) — the helper owns routing once evidence-existence is known; the LLM verifier's only job is to decide whether explicit evidence exists. + +4. **`insufficient_spec` feeds the EXISTING `human_needed` outcome — no new verifier status (maintainer Decision 1).** Abstention reuses the locked 3-value `VERIFIER_STATUSES` (`['passed','gaps_found','human_needed']`, `src/verification.cts`) with **zero change** and no new downstream routing in `ship`/`execute-phase`. The abstain cause rides as a **distinguishable report reason** (`human_needed` + `reason: insufficient_spec`) so it is never conflated with an ordinary manual-UAT `human_needed` — asserted in a test (review condition-1 caveat). *Interactive:* the item routes to the end-of-phase human checkpoint. *Autonomous (AFK):* a prominent `unverified — held-out test recommended` flag; completion reads "complete with N unverified non-inferable checks" — never a silent pass, never a hard halt (the D4 guarantee, now on the truth axis). + +5. **Two measured properties define the design (maintainer Decision 2; caveats to record).** *Exogenous, not endogenous:* abstention is triggered by the external `backstop` tag, never a self-judged "abstain if unsure" — endogenous abstention was measured near-useless on true blind spots (100% → 67% vs exogenous 100% → 17%; N17). *Routing, not diagnosis:* the verdict does not name the omitted rule (the held-out test carries it). **Evidence honesty:** N17 is n=27, 1 rep — **direction-finding, not powered**; the effect is large and monotone but real-world precision depends on the edge-probe's *true* `backstop` recall/precision (the experiment modeled a perfect tagger), which is why the over-abstention guard and the capable-tier requirement are load-bearing acceptance criteria. **Model-tier coupling:** abstention is reliable on the default `gsd-verifier` tier (`sonnet`+); the budget tier (`haiku`) heeds the tag only inconsistently and degrades toward current behavior — captured as a documented cost (and a test) so a tier regression is caught, not discovered in production. + +Net effect: the truth-axis `backstop` tier gains the verify-time disposition D4 gave the prohibition judgment tier; the contract's CI-testable surface (D5) gains the truth-axis projection round-trip parity and the abstain-on-unconfirmed-backstop regression. The decision also lives in `src/probe-core.cts` comments, `gsd-core/references/honest-verifier.md`, the `plan-phase.md` / `verify-phase.md` / `agents/gsd-verifier.md` prose, and the #1154 changeset. + ## Addendum (2026-06-22) — Alternatives considered (recall / representation / packaging side) This consolidates the spec-phase-side rejected and deferred alternatives for the probe family, diff --git a/docs/adr/README.md b/docs/adr/README.md index a1a9ee823..afe84e846 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -62,6 +62,7 @@ See **[CONTRIBUTING.md — "Proposing an ADR or PRD"](../../CONTRIBUTING.md#prop | [1508-runtime-artifact-conversion-module.md](1508-runtime-artifact-conversion-module.md) | Runtime Artifact Conversion Module owns per-runtime content rewriting | Accepted | | [1593-skill-mapping-converter-methodology.md](1593-skill-mapping-converter-methodology.md) | Skill mapping & converter methodology across runtimes | Accepted | | [1769-state-md-transition-module.md](1769-state-md-transition-module.md) | STATE.md Transition Module — intent-based transitions over scattered RMW callbacks | Proposed | +| [1817-state-md-rebuild-derivability-contract.md](1817-state-md-rebuild-derivability-contract.md) | STATE.md rebuild — derivability contract (capstone 11th transition) | Accepted | ## Seam map diff --git a/docs/design/verifier-reach.md b/docs/design/verifier-reach.md new file mode 100644 index 000000000..028f23ba2 --- /dev/null +++ b/docs/design/verifier-reach.md @@ -0,0 +1,111 @@ +# Design note: "Verifier reach = spec reach" (the probe family's organizing principle) + +> **What this is.** A design-rationale doc — *not* an ADR. It records the organizing principle +> behind the spec-completeness probe family and orients a reader to the subsystem. The +> point-in-time decisions it references are owned by ADR-550, ADR-857, and ADR-1606. +> +> **Attribution.** The calibration evidence below is *the author's own research* (the +> verifier-reach experiment program). It is cited here as the **motivation** for a GSD design +> principle — it is not claimed as a novel GSD contribution, and the principle's *framing* is +> the author's. External prior art exists for individual pillars (clarification-questioning, +> omission-decay, conformal abstention); specific citations are deliberately omitted pending +> primary-source verification before being written down. + +## The principle + +> **A goal-backward verifier can only check assertions that exist. An assertion only exists +> for a requirement that was written down. Therefore the verifier's reach is bounded by the +> spec's reach — and you raise reliability by widening the spec, not by sharpening the +> verifier.** + +This is the single idea that ties the spec-completeness probe family together. It explains +*why* the probes live at the front of the pipeline (spec-phase), why enforcement is +fail-closed, and why the verifier is graded **exogenously** against explicit predicates +rather than trusted to grade itself. The principle is already stated in ADR-857's +*"Verification substrate vs. plug-in tier (the predicate boundary)"* section ("the load-bearing +finding: *verifier reach = spec reach*"), and is surfaced as the `PROBE.principle` predicate in +`CONTEXT.md` by sibling PR #1608 (epic #1605); this note expands the rationale and the evidence +behind it. + +## Why sharpening the verifier alone does not work + +The author's calibration experiments found the verifier is **most confidently wrong on +exactly the cases that matter**: + +- **Non-inferable corpus.** On non-inferable edge defects, a held-out check caught 100% + while the verifier caught **0/12** — and the verifier was overconfident doing it + (ECE ≈ 0.81). Confidence-gating cannot rescue this: the model is ~0.93-confident and wrong, + so there is no threshold that separates its hits from its misses. The only honest move on an + irreducible case is **abstain-and-flag**, not a higher bar. +- **Self-grading drifts.** A verifier asked to grade its own work against a restated goal + rationalizes a pass (the prose-drift / self-graded-review failure modes reproduced + independently on #664). Grading must be **against the spec's explicit predicates**, never a + self-restatement. + +The conclusion is structural: reliability is **not** a verifier-tuning problem. The verifier +is fine *when the predicate exists*; it fails when the predicate was never written. So the +leverage is upstream — make the missing predicate exist. + +## How GSD operationalizes the principle + +Three moves, each a member of the probe family or its enforcement seam: + +1. **Widen reach on the shape axis — the Edge-Probe.** A deterministic shape taxonomy + surfaces the boundary/adjacency/encoding/ordering edges an author omits. The author's + residual experiment (n=121) found that **once a surfaced edge is resolved into the spec, + the verifier then catches it 94–100%** — i.e. resolving the omission closes the gap the + verifier could not. The remaining miss is a *recall* gap in the probe, not a verifier gap. + +2. **Widen reach on the must-NOT axis — the Prohibition Probe.** A shape taxonomy is the + wrong instrument for "must not shame the user" / "must not proxy on protected attributes" + (the edge-probe caught 0/8 of these). Adversarial elicitation ("what could this silently + become that the author would not want?") is **model-robust recall** (N18: 17/17 holistic + surfacing including small models), and a one-pass precision classifier collapses the raw + list to the ~2–3 genuine bespoke prohibitions. + +3. **Close the residual honestly — tiered enforcement + exogenous abstention.** A surfaced + prohibition is only as good as what verify-phase does with it: + - **test-tier** → the deterministic `check prohibition-enforcement` producer machine-proves + the wired check is fail-first and runs it; green only on a proven, non-vacuous pass, else + hard-gate. The verifier *consumes* a contract-shaped predicate; it does not judge. + - **judgment-tier** (irreducible values) → **exogenous abstention**: record a + non-authoritative LLM-judge verdict and emit an "unverified — human review recommended" + flag. The author's N17 experiment showed this drops the false-pass rate from **100% → + 17%**, where endogenous (self-confidence) gating barely moved it. Never a silent pass; + never a hard halt of an AFK run. + +## Design implications (what this principle forbids) + +- **Do not** try to buy reliability by making the verifier stricter or more confident — it is + overconfident precisely on the non-inferable cases. Spend the effort on spec reach. +- **Do not** let the verifier grade a restated goal; grade against explicit predicates + (test-tier checks, judgment-tier flags). +- **Do not** put the verifier↔predicate contract behind an off-by-default capability. Because + predicates *are* the verifier's reach, gating them would make core reliability a function of + an optional plug-in — settled core/non-toggleable by ADR-857 (the *"Verification substrate + vs. plug-in tier"* section, decision #6). +- **Do** keep CI honest: test the **contract** the verifier consumes (parse/validate, + projection round-trip, fail-closed guards), never assert the LLM's judgment as green + (ADR-550 D5). A test that grades the model's judgment is vacuous. + +## The three failure modes this defends against + +| Failure mode | Without the principle | With it | +|--------------|----------------------|---------| +| Omitted **edge** (boundary/encoding/ordering) | ships; verifier never had an assertion | edge-probe surfaces → verifier catches 94–100% | +| Unwritten **must-NOT** (values/safety) | passes a literal spec while violating intent | prohibition-probe surfaces → test-tier enforced / judgment-tier flagged | +| Verifier **overconfidence / self-grading** | confident green on a real miss (ECE 0.81) | exogenous grading + abstention (false-pass 100%→17%) | + +## Cross-references + +- **ADR-550** — spec-phase probe contract (states the principle in its ADR-857 cross-reference + section); its "Alternatives considered" holds the recall-side rejections (why not a general + classifier / deterministic recall engine). +- **ADR-1606** *(proposed)* — prohibition-enforcement verify-time seam (the + test-tier mechanism); its "Alternatives considered" holds the enforcement-side rejections. +- **ADR-857** — *"Verification substrate vs. plug-in tier (the predicate boundary)"* section + (decision #6): the verifier↔predicate contract is core, non-toggleable. +- Refs: `gsd-core/references/{edge-probe,prohibition-probe}.md`; CONTEXT.md predicate + `PROBE.principle=verifier-reach-equals-spec-reach`. +- Author's research (internal): non-inferable corpus; edge-probe residual (n=121); N17 + verifier-abstention; N18 prohibition-elicitation. diff --git a/docs/explanation/interface-versioning-policy.md b/docs/explanation/interface-versioning-policy.md new file mode 100644 index 000000000..8c5ebfd2f --- /dev/null +++ b/docs/explanation/interface-versioning-policy.md @@ -0,0 +1,59 @@ +# Interface versioning and deprecation policy + +This explains how the GSD Host-Integration Interface (ADR-1239) evolves over +time — what is an additive change, what is breaking, how `PROTOCOL_VERSION` +bumps, and what deprecation window host-plugin authors can rely on. The policy +exists because a published SDK is a long-term compatibility commitment (Hyrum's +Law: once external hosts depend on it, the observable behavior is the contract). + +--- + +## The version knob: `PROTOCOL_VERSION` + +`PROTOCOL_VERSION` is a positive integer carried by the engine and exchanged in +every negotiation (in-process and serialized). It governs the negotiated +capability **set** — which axes, values, and adapter shapes exist. A host +declares the version it targets; the engine negotiates down to +`min(host, engine)` and warns when a host declares a newer version than the +engine (capabilities beyond the engine's version are not trusted). + +## Additive vs. breaking + +- **Additive (no version bump required):** adding a new *optional* axis value, a + new adapter option, or a new reference profile. Existing host-plugins keep + working — the engine negotiates to their declared version and ignores + unknowns. Additive changes are the default; the interface prefers growing the + vocabulary over changing it. + +- **Breaking (requires a `PROTOCOL_VERSION` bump):** removing or renaming an + axis, changing an adapter's call/return shape, changing the negotiation result + shape, or narrowing a value set. A breaking change is never silent — it lands + behind a version bump so a host that declares the old version negotiates down + and gets a warning, not a crash. + +## Deprecation window + +A value or adapter shape is **deprecated**, not removed, for at least **one +minor release cycle**. During the window: + +1. The old form still works at its declared version. +2. The reference docs mark it deprecated with the successor + the removal version. +3. The negotiation emits a `warnings` entry when a host uses a deprecated value. + +Removal is a breaking change and follows the version-bump rule above. The +`undocumented` sentinel is never deprecated — it is the permanent fail-closed +fallback for a host that omits an axis. + +## What this means for host-plugin authors + +- Pin to a `PROTOCOL_VERSION` and assert on it in your host-plugin's test. +- Treat unknown axis values / adapter options as additive (ignore, don't crash). +- Watch negotiation `warnings` — they surface both "host declared a newer + version" and "host used a deprecated value." +- The serialized handshake's consistency with the in-process negotiation is + itself part of the contract: a change that breaks that consistency is breaking. + +## See also + +- [Reference: the Host-Integration Interface](../reference/host-integration-interface.md) +- ADR-1239 (Status: Accepted) diff --git a/docs/how-to/async-external-jobs.md b/docs/how-to/async-external-jobs.md new file mode 100644 index 000000000..30deb2b49 --- /dev/null +++ b/docs/how-to/async-external-jobs.md @@ -0,0 +1,115 @@ +# Run a long-running job asynchronously with the SLURM adapter + +> **How-To** (Diátaxis). When a GSD execute task is legitimately long-running +> (HPC solver, model training, large simulation — over ~30–60 min), externalize +> it instead of blocking the agent turn. This guide uses the default-off +> `external-job` Capability's SLURM adapter (#1164 / #1105). + +## When to use this + +Use this path when a task is tagged `long_compute` +by the planner. For `quick`, `medium`, and `unknown` budgets, run normally — +see the [operation policy reference](../reference/long-running-operations.md). + +## Prerequisites + +- The `external-job` capability is enabled (`external_job.enabled: true` in + `.planning/config.json`). It is **default-off**. +- A SLURM cluster is reachable (`sbatch`, `squeue`, `sacct` on PATH). +- You are inside a GSD project (a `.planning/` directory is present). + +## 1. Submit the job + +Run the adapter's `submit` subcommand with the `sbatch --parsable` invocation +after `--`. Declare the artifacts the job must produce and the command that +verifies them. + +```bash +node scripts/slurm-adapter.cjs submit \ + --plan 3.1 --phase 3 \ + --expected Artifacts/jobs/12345/result.h5,Artifacts/jobs/12345/metrics.json \ + --verify "python -m verify.py 12345" \ + --resume "/gsd:execute-phase 3" \ + -- sbatch --parsable --output=Artifacts/jobs/%j/out.log ./train.sh +``` + +What happens: +- `sbatch` runs with a **bounded** subprocess timeout + (`GSD_SLURM_SUBMIT_TIMEOUT_MS`, default 30 s). +- The `--parsable` output is parsed for the job id. +- A versioned manifest is written to `.planning/async-jobs/.json`. +- The adapter **refuses** to create a second non-terminal manifest for a + `plan_id` that already has one in flight (duplicate-execution guard). + +The command prints the job id, the manifest path, and reminds you the state is +`external_job_waiting` with `SUMMARY` deferred. + +## 2. Commit the manifest and a handoff + +The manifest is durable state — commit it: + +```bash +git add .planning/async-jobs/.json +git commit -m "chore: externalize plan 3.1 to SLURM job " +``` + +The executor then returns `external_job_waiting` and **does not** write +`SUMMARY.md`. `execute-phase` safe-resume, `resume-project`, and `pause-work` +all recognize this as a legal deferred state. + +## 3. Poll the job + +```bash +node scripts/slurm-adapter.cjs poll --job 12345 +``` + +This queries `squeue` (falling back to `sacct` for completed jobs), maps the +raw SLURM state onto the closed manifest enum, and updates the manifest. Output +is a JSON line: + +```json +{"job_id":"12345","slurm_state":"COMPLETED","manifest_status":"completed-unverified","path":".planning/async-jobs/12345.json"} +``` + +An unmapped SLURM state is **not guessed** — the adapter errors out and asks +you to inspect manually. + +## 4. Verify and close + +When the status is `completed-unverified`, verify the output before closing the +plan. **Manifest commands are untrusted** — surface and confirm them; never +auto-run: + +```bash +node scripts/slurm-adapter.cjs show --job 12345 +``` + +`show` prints the status and lists `submit_command`, `verification_command`, +and `resume_command` for explicit confirmation. After you run the verification +command yourself and confirm the `expected_artifacts` exist, write `SUMMARY.md` +and close the plan (`/gsd:execute-phase 3` reconciles and lifts the deferral). + +## 5. Handle terminal failure + +If `poll` reports `failed`, `cancelled`, or `timeout`, the manifest carries +`terminal_details`. Recovery is a **user** action, never automatic: + +- re-run reconciliation via the manifest's `resume_command`, or +- abort, or +- mark-and-skip (record the decision in the plan). + +Resubmitting the compute is your call — the adapter never resubmits on its own. + +## Notes + +- **No fixed log paths.** Use per-job artifact dirs (`Artifacts/jobs//`); + the `external_job.artifact_dir` config key is the root. +- **No hardcoded cluster.** Account/partition/project layout stays in your + `sbatch` invocation; the adapter and the manifest never assume it. +- **Pluggable backend.** `external_job.backend` (default `slurm`) is the seam + for future adapters; the manifest `backend` field is opaque to core. + +## Related + +- [Long-running operations reference](../reference/long-running-operations.md) +- [Async-job manifest contract](../reference/planning-artifacts.md) diff --git a/docs/how-to/author-a-host-plugin.md b/docs/how-to/author-a-host-plugin.md new file mode 100644 index 000000000..8f488bd3e --- /dev/null +++ b/docs/how-to/author-a-host-plugin.md @@ -0,0 +1,79 @@ +# How to author a host-plugin for GSD + +This guide is for **external tool authors** who want to embed GSD's orchestration +loop (dispatch + hooks + model + state) into a new host — a CLI, IDE, or agent +runtime GSD does not special-case — by writing a **host-plugin** against the +published Host-Integration SDK, **without modifying gsd-core source**. + +The contract is the SDK entry (`src/host-integration-sdk.cts` → +`gsd-core/bin/lib/host-integration-sdk.cjs`): import only from it. Everything it +re-exports is public and versioned (`PROTOCOL_VERSION`); everything else in +gsd-core is internal and may change. A smoke test +(`tests/sdk-smoke.test.cjs`) proves a third-party host can be built from this +surface alone. + +--- + +## 1. Decide your host's profile + +Classify your host into one of three profiles (the SDK's `profileOf` does this +from your axes): + +| Profile | Example hosts | Binding | +|---|---|---| +| `programmatic-cli` | OpenCode, pi | imperative adapter (`createImperativeAdapter`) | +| `declarative-cli` | Antigravity, Codex | declarative adapter (`createDeclarativeAdapter`) | +| `ide` | VS Code | imperative adapter + engine-owned hook bus + active model + sandboxed state | + +## 2. Declare your host's integration axes + +Declare the eight negotiated axes (`embeddingMode`, `commandSurface`, `dispatch`, +`modelMode`, `hookBus`, `stateIO`, `transport`, `runtime`) from your host's +**authoritative documentation** — never infer. Where the docs are silent, use the +`undocumented` sentinel (the SDK degrades it fail-closed). See +`docs/reference/host-integration-interface.md` for the closed vocabulary. + +## 3. Compose the engine adapters + +```js +const SDK = require('@opengsd/gsd-core/sdk'); // the published entry + +// Engine-owned hook bus (for hosts with no event bus, e.g. VS Code): +const hookBus = SDK.createHookBus({ bus: 'engine' }); + +// Active model via a host provider (e.g. vscode.lm); or 'passive' for CLIs: +const model = SDK.createModelAdapter({ modelMode: 'active' }, { sendRequest: hostSendRequest }); + +// State IO — filesystem for CLIs, sandboxed-storage for IDEs/web: +const stateIO = SDK.createStateIO({ io: 'filesystem' }); + +// The engine-as-library adapter bound to your runtime id: +const adapter = SDK.createImperativeAdapter({ runtime: 'my-host' }); +``` + +## 4. Negotiate capabilities (serialized, for out-of-process hosts) + +If your host runs out-of-process (can't share object refs with the engine), use +the serialized handshake instead of the in-process `negotiateHostCapabilities`: + +```js +const req = SDK.buildHandshakeRequest({ ...myAxes }); +const { effective, points, warnings } = SDK.handleHandshakeRequest(req); +``` + +The result is identical to the in-process negotiation for the same axes — that +consistency is the contract. + +## 5. Bind your host primitives + ship + +Map the six interface points (command, dispatch, model, hooks, state, artifact) +to your host's primitives (palette/chat, plugin API, provider calls, etc.). The +reference host-plugins (OpenCode, Antigravity, pi, VS Code — see Phase 5) are +canonical examples to mirror. Your host-plugin is now a self-contained artifact; +no gsd-core source change is required. + +## See also + +- **Tutorial:** `docs/tutorials/embed-gsd-in-a-new-host.md` — a complete end-to-end embedding. +- **Reference:** `docs/reference/host-integration-interface.md` — every axis, adapter, and the handshake. +- **Versioning:** `docs/explanation/interface-versioning-policy.md` — what evolves additively vs. breaking. diff --git a/docs/how-to/connect-gsd-mcp-server.md b/docs/how-to/connect-gsd-mcp-server.md new file mode 100644 index 000000000..a036419cd --- /dev/null +++ b/docs/how-to/connect-gsd-mcp-server.md @@ -0,0 +1,75 @@ +# How to connect a host to the GSD companion MCP server + +This guide shows you how to make a MCP-capable host (Claude Code, Codex, +OpenCode, VS Code, Gemini CLI, Cursor, Cline, Hermes) drive GSD — run GSD +commands and read/write `.planning/` state — through the companion MCP server, +with no bespoke plugin. + +Once connected, three tools appear in the host alongside its others: +`gsd_invoke_command`, `gsd_read_state`, `gsd_write_state`. (For the tool +contracts, see the reference section below; for *why* this server exists and +its trust model, see [ADR-1239](../adr/1239-gsd-embeddable-orchestration-engine.md) +and the [capability trust model](../explanation/capability-trust-model.md).) + +## 1. Add the server to your host's MCP config + +The entry shape is the same everywhere; only the config file and key differ by +host. + +```jsonc +{ + "gsd": { + "command": "npx", + "args": ["-y", "@opengsd/gsd-core", "gsd-mcp-server"], + "cwd": "/abs/path/to/your/project" + } +} +``` + +- **Claude Code / Codex / OpenCode / Cursor / Cline / Hermes** — under the + host's `mcpServers` object (project or user config). +- **VS Code** — in the workspace MCP servers list. +- **Gemini CLI** — under its `mcpServers` block. + +Set `cwd` to the project whose `.planning/` you want GSD to manage — the server +resolves state paths against it. + +## 2. Restart the host + +On startup the host performs the MCP `initialize` handshake, lists tools, and +the three GSD tools become callable. + +## 3. Verify + +Ask the host to read an existing planning file: + +```jsonc +{ "name": "gsd_read_state", "arguments": { "path": "/abs/path/to/your/project/.planning/STATE.md" } } +``` + +It returns the file's contents. `gsd_invoke_command` takes +`{family, subcommand, args}` and returns the command-routing hub's structured +result (the same shape `gsd-tools` produces). + +## If something does not work + +- **`command not found: gsd-mcp-server`** — invoke via `npx` as shown above, or + install the package globally first (`npm i -g @opengsd/gsd-core`). +- **`gsd_read_state` fails with ENOENT** — the path is resolved literally; pass + an absolute path under the project's `.planning/`. +- **The host lists no GSD tools** — confirm the server starts in isolation: + `npx @opengsd/gsd-core gsd-mcp-server` then send an `initialize` request on + stdin; it writes a `protocolVersion` response and exits on EOF. +- **You manage multiple projects** — register one `gsd` entry per project with a + distinct name and `cwd`; the server is stateless across projects. + +## Reference — the three tools + +| Tool | Arguments | Returns | +|------|-----------|---------| +| `gsd_invoke_command` | `{family: string, subcommand: string, args?: unknown[]}` | the command-routing hub result (`{ok, …}`) as JSON text | +| `gsd_read_state` | `{path: string}` | the file contents as text | +| `gsd_write_state` | `{path: string, content: string}` | `{ok: true, path}` as JSON text | + +Errors from a tool are returned as MCP tool errors (`isError: true`), not as +JSON-RPC protocol errors — the host surfaces them in its normal tool-failure UX. diff --git a/docs/how-to/install-on-your-runtime.md b/docs/how-to/install-on-your-runtime.md index daed18174..d97430123 100644 --- a/docs/how-to/install-on-your-runtime.md +++ b/docs/how-to/install-on-your-runtime.md @@ -102,6 +102,15 @@ The `gsd-tools` binary (installed as part of the `@opengsd/gsd-core` npm package Node.js (`node`) must also be available on your `PATH`. The plugin's always-on guard hooks (wired in `hooks/hooks.json`) are invoked as `node "${CLAUDE_PLUGIN_ROOT}/hooks/