Merge remote-tracking branch 'origin/next' into fix/1477-surface-source-marker

# Conflicts:
#	tests/golden-install-parity.test.cjs
This commit is contained in:
Joe Slitzker
2026-07-04 08:22:49 -05:00
824 changed files with 114332 additions and 78962 deletions

View File

@@ -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.

View File

@@ -0,0 +1,6 @@
---
type: Fixed
pr: 1819
---
**`phase.complete` no longer reports a false `is_last_phase` on a `<details>`-wrapped checkbox checklist (#1591, #1752)** — when the active milestone's phase checklist was written as `- [ ] Phase N:` checkbox items inside a `<details>` 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 `<details>`-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.

View File

@@ -0,0 +1,6 @@
---
type: Added
pr: 1929
---
<!-- docs-exempt: Phase 5 (#1682) Slice 1a; consolidated Phase 5 (Embeddable Orchestration) docs land with phase completion. -->
**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)

View File

@@ -0,0 +1,6 @@
---
type: Added
pr: 1930
---
<!-- docs-exempt: Phase 5 (#1682) Slice 1b/c; consolidated Phase 5 docs land with phase completion. -->
**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)

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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 <file> <field> <value>`), 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 <file> --field <field> --value <value>`).

View File

@@ -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`.

View File

@@ -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)
<!-- docs-exempt: Sonnet 5 operator docs (CONFIGURATION.md model-tier tables, settings-advanced.md) already landed on next via #1851; this fragment only records the already-shipped 1.6.1 change so the 1.7.0 changelog render reflects current shipping code. -->

View File

@@ -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)

View File

@@ -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.

View File

@@ -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.
<!-- docs-exempt: internal refactor; no user-facing doc surface -->

View File

@@ -0,0 +1,7 @@
---
type: Changed
pr: 1811
---
**Internal: the installer's per-function `is<Runtime>` 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.
<!-- docs-exempt: internal refactor; no user-facing doc surface -->

View File

@@ -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.
<!-- docs-exempt: internal loader hardening; no user-facing doc surface until slice 3's MCP server -->

View File

@@ -0,0 +1,6 @@
---
type: Added
pr: 1965
---
<!-- docs-exempt: pi extension is a reference host-plugin module; installation instructions are in the #1944 issue body + ADR-1239 Phase D context. No standalone docs/ file needed. -->
**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)

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.
<!-- docs-exempt: internal adapter infrastructure; no user-facing command/flag/config/schema/doc surface -->

View File

@@ -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)

View File

@@ -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)

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.
<!-- docs-exempt: internal server module; user-facing doc lands with slice 3b's bin entry -->

View File

@@ -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.

View File

@@ -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)

View File

@@ -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.
<!-- docs-exempt: internal adapter infrastructure; no user-facing command/flag/config/schema/doc surface -->

View File

@@ -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)

View File

@@ -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)

View File

@@ -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.
<!-- docs-exempt: internal adapter seam; no user-facing command/flag/config/schema/doc surface -->

View File

@@ -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`.

View File

@@ -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.

View File

@@ -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/<version>-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)

View File

@@ -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)

View File

@@ -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 <name>` are unchanged. (#1912)

View File

@@ -0,0 +1,6 @@
---
type: Added
pr: 1966
---
<!-- docs-exempt: VS Code extension is a repo-local reference module; installation instructions are in the #1942 issue body + ADR-1239 Phase D context. No standalone docs/ file needed. -->
**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)

View File

@@ -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).

View File

@@ -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.
<!-- docs-exempt: the changed file gsd-core/references/model-profiles.md IS the documentation; it lives under gsd-core/references/, not docs/ -->

View File

@@ -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.

View File

@@ -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)

View File

@@ -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)

View File

@@ -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.
<!-- docs-exempt: internal adapter seams; no user-facing command/flag/config/schema/doc surface -->

View File

@@ -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.

View File

@@ -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.

View File

@@ -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)

View File

@@ -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)
<!-- docs-exempt: internal workflow structure refactor; byte-invariant lazy extraction (ADR-1610), no user-facing behavior or doc change -->

View File

@@ -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)

View File

@@ -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)

View File

@@ -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.<agent-type>` 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)

View File

@@ -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'.
<!-- docs-exempt: internal refactor; only cosmetic console-label normalization with no doc surface to update -->

View File

@@ -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.

View File

@@ -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.
<!-- docs-exempt: internal security module; no user-facing doc surface until the loader wiring in slice 2 -->

View File

@@ -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.
<!-- docs-exempt: internal refactor; hook codegen path-fragment table, no user-facing doc surface -->

View File

@@ -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"
}
}
]
}

View File

@@ -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",

View File

@@ -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

View File

@@ -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:

View File

@@ -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

11
.gitignore vendored
View File

@@ -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

View File

@@ -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 <HOOKS_DIR>/<hook>.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 <opencodeConfigDir>/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: <root>/.opencode/plugins/gsd-core.js → <root>
// • global file-copy: ~/.config/opencode/plugins/gsd-core.js → ~/.config/opencode
// • local file-copy: <proj>/.opencode/plugins/gsd-core.js → <proj>/.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/ → @<REPO_ROOT>/
// 2. plain-text paths: ~/.claude/gsd-core/ → <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-<name>)
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, <id non-enum>}]` — 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;

View File

@@ -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 `<details>`-wrapped checkbox checklist (#1591, #1752)** — when the active milestone's phase checklist was written as `- [ ] Phase N:` checkbox items inside a `<details>` 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 `<details>`-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

View File

@@ -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: { "<JSON disk key {r:realpath(projectRoot),i:id}>": { 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-<base64>` 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)}<id>` 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: { "<JSON disk key {r:realpath(projectRoot),i:id}>": { 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-<base64>` 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<id>` 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/<job>.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/<job>.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 `<runtime_budget>` 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; <step name="..."> 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 <f>` (src/coverage.cts → bin/lib/coverage.cjs). AUTHORING: execute-plan create_summary populates it from task <verify> 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-<cmd> (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-<cmd> reference(s) — use /gsd:<cmd> 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-<cmd> reference(s) — use /gsd:<cmd> instead" with line-number-precise violations`
`DEFECT.AGENT-RETIRED-SLASH-SYNTAX-DRIFT.fix-forward=replace /gsd-<cmd> with /gsd:<cmd> 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

View File

@@ -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.

View File

@@ -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
<documentation_lookup>
@~/.claude/gsd-core/references/research-documentation-lookup.md
</documentation_lookup>

View File

@@ -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.
</step>
<step name="write_section_4b">

View File

@@ -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
<input>
Agent receives via prompt:

View File

@@ -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

View File

@@ -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

View File

@@ -29,6 +29,8 @@ If the prompt contains a `<required_reading>` 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

View File

@@ -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
<philosophy>
@~/.claude/gsd-core/references/debugger-philosophy.md

View File

@@ -34,6 +34,8 @@ If the prompt contains a `<required_reading>` 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

View File

@@ -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

View File

@@ -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).
</project_context>
@@ -689,7 +691,7 @@ Do NOT skip. Do NOT proceed to state updates if self-check fails.
</self_check>
<state_updates>
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"
```
</state_updates>

View File

@@ -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

View File

@@ -51,6 +51,8 @@ Read ALL files from `<required_reading>`. 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

View File

@@ -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.
</project_context>

View File

@@ -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

View File

@@ -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
</project_context>
<context_fidelity>

View File

@@ -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
<documentation_lookup>
@~/.claude/gsd-core/references/research-documentation-lookup.md
</documentation_lookup>

View File

@@ -34,6 +34,8 @@ If the prompt contains a `<required_reading>` 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
<downstream_consumer>
Your SUMMARY.md is consumed by the gsd-roadmapper agent which uses it to:

View File

@@ -26,6 +26,8 @@ If the prompt contains a `<required_reading>` 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

View File

@@ -51,6 +51,8 @@ Read ALL files from `<required_reading>`. 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

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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
</project_context>
<core_principle>
**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

31
bin/gsd-mcp-server.js Normal file
View File

@@ -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);
});

View File

@@ -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 <configDir>/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
// <configDir>/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

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

Some files were not shown because too many files have changed in this diff Show More