### README # GSD Core documentation Documentation is organised into four quadrants: **tutorials** help you learn by doing, **how-to guides** solve specific tasks, **reference** states authoritative facts, and **explanation** explores concepts and design decisions. Language versions: [English](README.md) · [Português (pt-BR)](pt-BR/README.md) · [日本語](ja-JP/README.md) · [简体中文](zh-CN/README.md) --- ## Tutorials - [Your first project](tutorials/your-first-project.md) — install to first shipped phase, one guaranteed path - [Onboarding an existing codebase](tutorials/onboarding-an-existing-codebase.md) — bring GSD Core to a brownfield repo - [Build your first capability](tutorials/build-your-first-capability.md) — author a tiny declarative capability and watch it act in the loop - [Install your first capability](tutorials/install-your-first-capability.md) — install a third-party capability end-to-end: consent, verify, check for updates, remove --- ## How-to guides - [Install on your runtime](how-to/install-on-your-runtime.md) — runtime-specific install steps for all 16 supported runtimes - [Install a minimal GSD and add skills later](how-to/install-minimal-and-add-skills.md) — install only the core skills, then grow the surface with profiles and `/gsd-surface` - [Attach a plugin-provided skill to a GSD agent](how-to/attach-a-plugin-skill-to-a-gsd-agent.md) — use the `global:plugin:skill` entry form to load Claude Code plugin skills into agent prompts - [Discuss a phase](how-to/discuss-a-phase.md) — capture implementation decisions before planning begins - [Resolve edge-coverage findings](how-to/resolve-edge-coverage-findings.md) — turn the spec phase's surfaced domain-boundary edges into covered, dismissed, or backstopped spec decisions - [Resolve prohibition findings](how-to/resolve-prohibition-findings.md) — turn the spec phase's surfaced must-NOT constraints into resolved, dismissed, or deferred spec decisions - [Plan a phase](how-to/plan-a-phase.md) — run research, decompose work, and verify plan quality - [Execute a phase](how-to/execute-a-phase.md) — run plans in parallel waves with fresh-context subagents - [Verify and ship](how-to/verify-and-ship.md) — walk through completed work, diagnose failures, and create the PR - [Run phases autonomously](how-to/run-phases-autonomously.md) — use autonomous mode for unattended phase execution - [Handle quick and fast tasks](how-to/handle-quick-and-fast-tasks.md) — use `/gsd-quick` and `/gsd-fast` for ad-hoc work outside the phase loop - [Configure model profiles](how-to/configure-model-profiles.md) — switch between quality, balanced, and budget model tiers - [Set up cross-AI review](how-to/set-up-cross-ai-review.md) — configure a second AI to review code produced by the primary agent - [Work in parallel with workstreams](how-to/work-in-parallel-with-workstreams.md) — run independent lines of work simultaneously using workstreams - [Isolate work with workspaces](how-to/isolate-work-with-workspaces.md) — use workspaces to sandbox experimental or risky changes - [Debug a failed execution](how-to/debug-a-failed-execution.md) — diagnose and recover from broken or incomplete phase execution - [Spike and sketch](how-to/spike-and-sketch.md) — use `/gsd-spike` and `/gsd-sketch` for exploratory work before committing to a plan - [Design a UI phase](how-to/design-a-ui-phase.md) — use the UI phase loop for frontend and visual work - [Develop a Capability for GSD 1.5+](how-to/develop-a-capability.md) — add feature Capabilities, hook fragments, and registry entries - [Ship a reviewer lane in your capability](how-to/ship-a-reviewer-lane.md) — declare a `reviewer` body so `/gsd-review` discovers, invokes, and renders your external review CLI or model endpoint - [List your reviewer lane in the registry](how-to/list-your-reviewer-lane.md) — publish a lane you have built to the Reviewer Lane Registry so other people can find and install it - [Take over a capability or EoS integration](how-to/take-over-a-capability-or-eos.md) — assume maintainership of an existing third-party capability, reviewer lane, or EoS host integration through a handoff, an adoption fork, first-party absorption, or a de-listing - [Add or update a host's integration](how-to/add-or-update-a-host-integration.md) — set a host's documentation-sourced `runtime.hostIntegration` axes (ADR-1239 Phase A), with the `undocumented` sentinel rule - [Turn a capability off (and keep it off)](how-to/turn-a-capability-off.md) — disable a capability via the surface, or gate individual hooks off without removing the capability - [Drive GSD from a tracker issue](how-to/drive-gsd-from-a-tracker-issue.md) — start a phase from a GitHub, Linear, or Jira issue - [Migrate from GSD 2](how-to/migrate-from-gsd-2.md) — upgrade an existing GSD 2 project to GSD Core - [Update GSD](how-to/update-gsd.md) — re-run the installer to pick up the latest release - [Clean up get-shit-done-cc](cleanup-get-shit-done-cc.md) — remove leftover old-package artifacts that cause a spurious `⬆ /gsd-update` indicator after migrating to `@opengsd/gsd-core` - [Fix the worktree base-mismatch (exit 42) error](how-to/fix-worktree-base-mismatch.md) — resolve the branch-divergence condition that halts parallel phase execution - [Recover and troubleshoot](how-to/recover-and-troubleshoot.md) — fix common problems, rebuild context, and uninstall --- ## Reference - [Commands](COMMANDS.md) — every command with flags and examples - [Configuration](CONFIGURATION.md) — full config schema, model profiles, git branching strategies - [CLI tools](CLI-TOOLS.md) — `gsd-tools.cjs` programmatic API for workflows and agents - [Features](FEATURES.md) — complete feature index - [Inventory](INVENTORY.md) — installed skills and surface map - [STATE.md schema](reference/state-md.md) — field-by-field reference for `.planning/STATE.md` - [CONTEXT.md schema](reference/context-md.md) — field-by-field reference for `.planning/phases//CONTEXT.md` - [PLAN.md schema](reference/plan-md.md) — field-by-field reference for `.planning/phases//PLAN.md` - [Planning artifacts](reference/planning-artifacts.md) — all `.planning/` files and their roles - [Review and verification capabilities](reference/review-verification-capabilities.md) — code review, security, and Nyquist capability ownership and hook contracts - [Gate predicates](reference/gate-predicates.md) — canonical specification of the phase-gate predicate vocabulary - [Capability matrix](reference/capability-matrix.md) — generated catalogue of every capability's role, tier, extension points, hook kinds, and `engines.gsd` - [Capability manifest](reference/capability-manifest.md) — the full `capability.json` schema and validation rules - [`gsd capability` command](reference/gsd-capability-command.md) — install / update / remove / list reference for third-party capabilities - [Workflow fragments](reference/workflow-fragments.md) — in-file `` marker grammar for fragmentizing workflow markdown at emission time - [Reviewer Lane Registry](registries/reviewer-registry.md) — generated catalogue of third-party reviewer lanes, with their flags, transport, and install commands --- ## Explanation - [Context engineering](explanation/context-engineering.md) — how context rot forms and how GSD Core prevents it - [The phase loop](explanation/the-phase-loop.md) — design rationale for the Discuss → Plan → Execute → Verify → Ship cycle - [Multi-agent orchestration](explanation/multi-agent-orchestration.md) — how subagents are spawned, scoped, and coordinated - [Security model](explanation/security-model.md) — trust boundaries, permissions, and safe automation - [The capability trust model](explanation/capability-trust-model.md) — why third-party capabilities are gated by consent + integrity + reversibility, not a sandbox - [How overlay capabilities compose](explanation/capability-overlay-model.md) — why first-party always wins and how the loader resolves precedence, conflicts, and fail-open load-failure warnings - [Architecture](ARCHITECTURE.md) — system architecture, agent model, and data flow - [The Embeddable Orchestration System](explanation/embeddable-orchestration-system.md) — one public, versioned contract for embedding GSD across many hosts - [Discuss modes](workflow-discuss-mode.md) — assumptions mode vs interview mode for `/gsd-discuss-phase` - [Context monitoring](context-monitor.md) — context window monitoring hook architecture - [Issue-driven orchestration](issue-driven-orchestration.md) — recipe for driving GSD from a tracker issue using existing primitives --- ## Related - [What's new in 1.7.0](whats-new-1.7.0.md) — curated highlights of the 1.7.0 release - [Root README](../README.md) — landing page, quickstart, and documentation overview - [Changelog](../CHANGELOG.md) — release history --- ### Adr/0001 Dispatch Policy Module # Dispatch policy module as single seam for query execution outcomes - **Status:** Accepted - **Date:** 2026-05-03 We decided to centralize query dispatch outcomes in one Dispatch Policy Module that returns a structured union result (`ok` success or failure with typed `kind`, `details`, and final `exit_code`) instead of mixing throws and ad-hoc error mapping across CLI and SDK paths. This keeps fallback policy, timeout classification, and exit mapping in one place for better locality, prevents drift between native and fallback behavior, and makes callers thin adapters over a stable interface. ## Amendment (2026-05-03): query seam deepening completion To complete the query architecture pass, we deepened adjacent seams around the Dispatch Policy Module: - Extracted **Query Runtime Context Module** to own `projectDir` + `ws` resolution policy. - Extracted **Native Dispatch Adapter Module** so Dispatch Policy consumes a stable native dispatch Interface (not closure-wired call sites). - Extracted **Query CLI Output Module** to own projection from dispatch results/errors to CLI output contract. - Converged internal command-resolution and policy imports onto canonical modules and removed dead wrapper modules. - Added **Command Topology Module** as dispatch-facing seam that resolves commands, projects command policy, binds handler Adapters, and emits no-match diagnosis consumed by Dispatch Policy. - Locked **pre-project query config policy** for parity-sensitive query Interfaces: when `.planning/config.json` is absent, use built-in defaults and parity-aligned empty model ids for model-resolution surfaces. - Gated real-CLI SDK E2E suites behind explicit opt-in (`GSD_ENABLE_E2E=1`) to keep default CI/local verification deterministic while preserving full-path validation when requested. ### Dead-wrapper convergence Removed wrapper Modules after call-site convergence: - `normalize-query-command.ts` - `command-resolution.ts` - `policy-convergence.ts` - `query-policy-snapshot.ts` - `query-registry-capability.ts` This amendment preserves the original ADR direction: keep policy depth high, adapters thin, and locality concentrated in explicit modules. ## Amendment (2026-05-05): SDK Runtime Bridge seam deepening To make SDK dispatch a cleaner publishable seam, we deepened `GSDTools` dispatch behind one **SDK Runtime Bridge Module** (`sdk/src/query-runtime-bridge.ts`) and converged policy wiring into that seam: - `GSDTools` callers now route through one runtime bridge Interface for command resolution, execution, and hotpath dispatch. - Added explicit fallback policy at the seam (`allowFallbackToSubprocess`) instead of implicit transport behavior. - Added strict native-only enforcement mode (`strictSdk`) so SDK consumers can fail fast when a command lacks a native adapter. - Added structured bridge observability (`onDispatchEvent`) for dispatch mode, fallback reason, latency, outcome, and error kind. - Kept transport and command callers as thin adapters over the bridge seam. This continues the dispatch-policy design goal: deep policy Modules, thin Adapters, and high locality for behavior changes. --- ### Adr/0002 Command Contract Validation Module # Command Contract Validation Module - **Status:** Accepted - **Date:** 2026-05-05 We decided to centralize the `commands/gsd/*.md` file contract into a single validation seam enforced at two layers: a fast lint script (`scripts/lint-command-contract.cjs`) that runs as a pre-test CI step, and a behavioral regression test (`tests/command-contract.test.cjs`) that validates the full contract against the live filesystem. ## Decision The command file contract defines what makes a valid `commands/gsd/*.md`: - `name:` field present, non-empty, matches `gsd:*` or `gsd-*` (ns- commands use `gsd-`) - `description:` field present and non-empty - `allowed-tools:` block present and non-empty, all entries from the canonical tool set - Every `@`-reference inside `` blocks resolves to an existing file on disk - `@`-references inside `` blocks appear on their own line (no trailing prose) ## Context Before this ADR, the command contract was enforced inconsistently: - `tests/skill-frontmatter-contract.test.cjs` (folds former `enh-2790-skill-consolidation`, consolidation epic #1969) checked existence and frontmatter of specific post-consolidation commands - `tests/docs-update.test.cjs` (folds former `bug-3135-capture-backlog-workflow`, consolidation epic #1969) checked `execution_context` @-ref resolution (added 2026-05-05) - No test checked `allowed-tools` validity, `name:` convention, or `description:` non-emptiness across all commands simultaneously This meant any PR touching a command file could break the contract without a single test catching it. The `add-backlog.md` gap (#3135) is a concrete example: the workflow file was missing for the full consolidation cycle before a targeted regression test was written. Additionally, 40 of 65 command files contained redundant prose @-references — the same path appearing once in `` (which loads the file) and again in `` body text (inert). This added ~900 tokens of dead weight per invocation and created a drift seam where prose refs could go stale independently of the executable `execution_context` ref. The two largest commands (`debug.md`, `thread.md`) embedded their full implementation inline rather than delegating to workflow files, causing ~4,400 tokens of implementation detail to load as part of the skills index description on every session regardless of whether those commands are used. ## Consequences - A single `lint-command-contract.cjs` script enforces frontmatter invariants across all 65 commands in milliseconds, runs before the test suite in CI - `tests/command-contract.test.cjs` replaces the scattered contract coverage in `enh-2790` and `bug-3135`, becoming the authoritative behavioral contract test for the entire command surface - Redundant prose @-refs removed from 40 command files (~900 tokens/invocation recovered) - `debug.md` and `thread.md` refactored to the workflow-delegation pattern (~4,400 tokens removed from eager system-prompt load) - `workflows/extract_learnings.md` renamed to `workflows/extract-learnings.md` to align with the hyphen convention used by all other workflow files - The `execution_context` block is the single authoritative declaration of what a command loads — no duplication in prose --- ### Adr/0003 Model Catalog Module # Model Catalog Module as single source of truth for agent profiles and runtime tier defaults - **Status:** Accepted - **Date:** 2026-05-07 We decided to centralize model-selection data in one Model Catalog Module so the SDK, the CLI/CJS layer, and the docs do not maintain separate agent lists, profile maps, or runtime tier defaults. ## Problem Before this ADR there were four drifting sources: 1. `gsd-core/bin/lib/model-profiles.cjs` — agent → profile alias map, phase-type map, dynamic-routing default tiers 2. `sdk/src/query/config-query.ts` — stale 18-agent copy of `MODEL_PROFILES` 3. `gsd-core/workflows/settings-advanced.md` — runtime → built-in model-id table 4. `sdk/src/session-runner.ts` — hardcoded Claude-only profile → model-id map This caused issue #3229: the SDK knew only 18 agents while 33 agent files existed on disk, so ~15 agents silently fell back to Sonnet with `unknown_agent: true`. ## Decision Create one machine-readable catalog and derive everything else from it. The catalog owns: - supported runtime names - runtime tier defaults (`opus` / `sonnet` / `haiku`) and runtime capabilities (e.g. `reasoning_effort` support) - the full agent registry for model resolution - the canonical per-agent **golden** alias (quality intent) - derived profile aliases for `balanced`, `budget`, and `adaptive` - agent → phase-type mapping - agent → dynamic-routing default tier mapping The canonical file lives in a location both packages ship: - repo root package (`@opengsd/get-shit-done-redux`) includes it - standalone SDK package (`@opengsd/gsd-sdk`) includes it Both CJS and SDK load this exact file. Neither package keeps its own independent list. ## Golden profile The catalog stores a `golden` alias per agent. `quality` is defined as the golden profile exactly. Other profiles (`balanced`, `budget`, `adaptive`) are explicit views over the same agent registry. This keeps the highest-quality intent in one place while allowing lower-cost profiles to differ per agent where needed. ## Consequences - `resolve-model` in SDK and CJS read the same registry, so missing-agent drift disappears - `settings-advanced.md` runtime tier table must stay in parity with the catalog (enforced by test) - `sdk/src/query/helpers.ts` runtime list comes from the catalog, fixing drift like the missing `hermes` runtime - `sdk/src/session-runner.ts` uses the catalog's Claude runtime tier defaults instead of a private hardcoded profile map - tests validate: - every `agents/gsd-*.md` file exists in the catalog - SDK and CJS resolve the same aliases for all known agents - unknown-agent fallback follows profile semantics (`quality`→`opus`, `budget`→`haiku`, etc.), not a hardcoded `sonnet` - docs/runtime tables stay aligned with the catalog --- ### Adr/0004 Worktree Workstream Seam Module # Planning Workspace Module as single seam for worktree and workstream state - **Status:** Accepted - **Date:** 2026-05-08 We decided to treat planning/worktree behavior as one explicit Planning Workspace Module Interface rather than spread policy across ad-hoc call sites. The Module owns `.planning` path resolution, active workstream pointer policy, workstream-name invariants, and lock semantics, while a focused Worktree Root Resolution Adapter owns linked-worktree root mapping and metadata prune behavior. This raises depth at the seam, increases leverage for callers, and improves locality for bug fixes in the worktree/workstream loop. ## Decision - The Planning Workspace Module Interface is authoritative for: - `planningDir` / `planningRoot` / `planningPaths` - active workstream pointer policy (`session-scoped > shared`) - pointer self-heal behavior (invalid/stale pointers clear to null) - planning lock semantics (`withPlanningLock`) - Worktree root detection stays behind one Worktree Root Resolution Adapter (`resolveWorktreeRoot`), so callers do not re-derive git-dir/common-dir logic. - Worktree metadata cleanup remains non-destructive by default: `pruneOrphanedWorktrees` runs `git worktree prune` only and does not remove linked worktree directories. - Workstream naming is one invariant across create/migrate/set/get/env-pointer paths: values must be canonical slugs that remain addressable by all workstream commands. ## Consequences - Tests can pin behavior through one Interface instead of source-grep fragments, improving regression quality for worktree/workstream bugs. - Bug classes caused by contract drift (for example migration names accepted in one path but rejected in another) are fixed once in the Module and propagate to all callers. - Callers become thin Adapters over a deeper seam; future policy changes (session identity strategy, lock recovery, worktree prune behavior) stay localized. --- ### Adr/0005 Sdk Architecture Seam Map # SDK Architecture seam map for query/runtime surfaces - **Status:** Superseded by [ADR-0174](0174-retire-gsd-sdk-package-boundary.md) (2026-05-23); originally Accepted (2026-05-09) - **Date:** 2026-05-09 We decided to keep SDK architecture explicitly module-seamed rather than allow feature logic to spread across query handlers, runtime adapters, and compatibility shims. This ADR is the top-level map for SDK seams and their ownership boundaries. ## Decision - Treat the SDK as a composition of explicit seam Modules with thin call-site Adapters. - Keep compatibility policy isolated behind the **SDK Package Seam Module** (see `0007-sdk-package-seam-module.md`). - Keep dispatch transport/outcome policy behind the **Dispatch Policy Module** and **SDK Runtime Bridge Module** (see `0001-dispatch-policy-module.md` amendment). - Keep model/runtime profile resolution behind the **Model Catalog Module** (see `0003-model-catalog-module.md`). - Keep planning/worktree/workstream path-state policy behind the **Planning Workspace Module** (see `0004-worktree-workstream-seam-module.md`). - Keep planning path projection policy explicit and centralized (detailed in `0006-planning-path-projection-module.md`). ## Consequences - SDK callers (`init*`, query handlers, runtime entry points) remain thin Adapters over stable interfaces. - Changes to package layout compatibility, dispatch transport, model policy, and planning path policy are localized to owning Modules. - Architecture reviews can classify drift quickly: if behavior changes outside owning seam Module, it is a design violation. --- ### Adr/0006 Planning Path Projection Module # Planning Path Projection Module for SDK query handlers - **Status:** Accepted - **Date:** 2026-05-09 We decided to centralize SDK planning-path projection behind one Module interface instead of reconstructing `.planning` paths in each handler with ad-hoc joins. This deepens the planning seam and prevents path-policy drift between helper and caller layers. ## Decision - `helpers.planningPaths(projectDir, workstream?)` is the canonical SDK projection interface for planning paths. - `helpers.planningPaths` delegates to `workspacePlanningPaths` + `resolveWorkspaceContext` for policy, not duplicate local path composition. - Policy precedence is explicit and stable: `explicit workstream > env workstream > env project > root`. - Query/init handlers (`initExecutePhase`, `initPlanPhase`, `initPhaseOp`, `initMilestoneOp`) must consume `planningPaths(...).planning` rather than direct `relPlanningPath` joins. - SDK project scope for planning is `.planning/` (never `.planning/projects/`), aligned with CJS planning workspace behavior. ## Consequences - One fix in planning path policy updates all handlers and reduces regression surface. - Tests can target seam behavior (`workspace.test.ts`, `helpers.test.ts`, init handler tests) instead of source-grep heuristics. - Cross-package parity bugs between SDK and CJS planning path resolution become easier to detect and correct. --- ### Adr/0007 Sdk Package Seam Module # SDK Package Seam Module owns SDK-to-get-shit-done-redux compatibility - **Status:** Superseded by [ADR-0174](0174-retire-gsd-sdk-package-boundary.md) (2026-05-23); originally Accepted (2026-05-07) - **Date:** 2026-05-07 We decided to define one explicit SDK Package Seam Module for the `@opengsd/gsd-sdk` → `@opengsd/get-shit-done-redux` transition. During this transition, install-layout probing, legacy `gsd-tools.cjs` discovery, legacy `core.cjs` discovery, and compatibility-only missing-asset diagnostics must live behind one seam instead of leaking across SDK Modules. This keeps callers thin, raises leverage for standalone-SDK testing, and improves locality by making package-readiness bugs land in one place. First tracer-bullet slice: add one compatibility Adapter Module at this seam and migrate current legacy asset callers onto it before broader native replacement work. Runtime-global skills directory resolution is explicitly out of scope for this seam. That policy varies by runtime (`claude`, `codex`, `cline`, etc.) rather than by legacy package/install layout, so it now lives in a separate Runtime-Global Skills Policy Module consumed by `agent-skills` and `skill-manifest`. --- ### Adr/0008 Installer Migration Module # Installer Migration Module owns install-time upgrade safety - **Status:** Accepted - **Date:** 2026-05-11 We decided to introduce an explicit Installer Migration Module for install-time file moves, removals, config rewrites, and user-data preservation. Installer upgrade behavior must be represented as versioned migration records that produce a dry-run plan before applying changes. ## Decision - Add an Installer Migration Module as the owner for upgrade migrations. - Keep the existing installer materialization pipeline, but move cleanup and feature-retirement behavior into migration records over time. - Track applied migrations in an install-state file next to the existing file manifest. - Treat the existing file manifest as the managed-file ownership baseline. - Treat user-owned artifacts as a single shared policy consumed by preservation and manifest writing. - Require migrations to plan first, then apply through a shared executor that owns backup, rollback, and reporting. - Default ambiguous or unknown files to preserve; destructive changes need managed-file evidence or explicit user choice. - Support dry-run output using the same planner used by apply mode. - Include a first-time baseline scanner for legacy installs that need classification before destructive migrations can be trusted. - Treat the runtime configuration contract registry in `docs/installer-migrations.md` as the source of truth for migrations that touch host runtime config. ## Runtime Contract Decision Every migration that rewrites runtime config, moves an invocation surface, or retires a generated runtime artifact must cite the registry row in `docs/installer-migrations.md`. If the migration changes where a runtime loads commands, skills, agents, hooks, or rules, the PR must update both the registry and `docs/ARCHITECTURE.md`. The registry records what GSD installs, where it installs it, when migrations may touch it, who owns the surrounding config, and why the shape matches the host runtime. When upstream docs do not publish an API or docs version, the checked date is the drift sentinel. A later upstream docs or CLI release that changes command, skill, agent, hook, or rule loading requires a new registry snapshot before migration work proceeds. ## Consequences - Retiring features requires an explicit migration instead of a hidden cleanup block. - The installer can remove stale GSD-owned artifacts without guessing about user files. - Locally modified managed files get a consistent backup path before removal or replacement. - Future rollback work can become runtime-neutral instead of Codex-specific. - Migration authors must define ownership evidence, conflict behavior, runtime scope, and non-interactive behavior. - Migration authors must also define which runtime contract they are relying on and whether the upstream documentation is versioned. - The installer gains another state file, so tests must cover missing, legacy, and checksum-mismatch state. ## Scope The first implementation should extract manifest/user-owned helpers, add install-state persistence, add migration planning, and port one existing orphan cleanup into the migration runner. It should not rewrite every runtime installer branch in the first pass. The detailed module contract lives in `docs/installer-migrations.md`. ## Amendment (2026-05-11): Authoring guard enforcement The Installer Migration Authoring Guard Module validates migration records and planned actions before planning can proceed. Records must declare title, description, introduction version, explicit install scopes, destructive status, and a plan function. Destructive or config-rewrite actions must include ownership evidence, and runtime config rewrites must cite the runtime configuration contract registry. ## Amendment (2026-08-07): Non-recursive empty-directory removal primitive Migration 003's docblock records, as an intentional consequence of this ADR, that the framework has no *recursive* directory-removal primitive: every action targets a single file by `relPath`, and an emptied directory shell is left behind for the user (or a future migration) to clean up. #3023 exposed a case where that is not enough: pi reserves the directory NAME `hooks/` for its own deprecated-extension check, which warns on the path's mere existence regardless of contents. Leaving an emptied `hooks/` shell behind would keep the warning firing forever, defeating the retirement. We added `remove-empty-dir`, a new action type, rather than relaxing the "never remove directories" posture generally: - It calls `fs.rmdirSync` only — never `fs.rmSync`, `{ recursive: true }`, or `{ force: true }`. A non-empty directory fails the underlying syscall and is treated as a successful no-op (`skipped-not-empty`), not swept. - Emptiness is re-checked immediately before the call, not trusted from planning time, so a file that survived an earlier action in the same run (a failed removal, or a legitimately preserved unknown file) keeps the directory alive. - The target must not be a symlink, and its realpath must resolve strictly inside — and never equal — the config directory's own realpath. - Any unexpected failure degrades to `left-in-place`, matching every sibling action type's non-throwing posture. **Recursive directory removal remains deliberately absent.** This primitive only retires a directory NODE once every file inside it has already been individually classified and actioned by other, ordinary file-level actions in the same migration — it is not a shortcut for sweeping a subtree in one step, and a migration author who wants that should still enumerate files individually per migration 003's and 009's pattern. See `docs/installer-migrations.md#action-types` (`remove-empty-dir`) and `src/installer-migrations/009-pi-retire-reserved-hooks-dir.cts`. --- ### Adr/0009 Shell Command Projection Module # Shell Command Projection Module owns runtime-aware OS command rendering - **Status:** Accepted - **Supersedes:** [ADR-0010](0010-file-operation-engine-module.md) (File Operation Engine Module) — absorbed into this seam's Phases 3–4 (`#3467`–`#3468`), 2026-05-13 - **Date:** 2026-05-12 We propose introducing a Shell Command Projection Module that owns projection from typed command intent to concrete shell/runtime-specific command text. GSD currently hand-builds hook commands, PATH repair commands, shim scripts, and other serialized OS-facing command strings across installer call sites. That drift has repeatedly produced cross-shell regressions (`#2376`, `#2979`, `#3002`, `#3011`, `#3181`, `#3393`, `#3413`). The proposed seam concentrates quoting, path-style, and runtime-wrapper policy in one module while keeping real subprocess execution on array-arg/non-shell paths. ## Decision - Add a **Shell Command Projection Module** under `gsd-core/bin/lib/` as the single owner for runtime-aware command-text rendering. - Feed the module typed inputs (`platform`, `shell`, `runtime`, executable token, args, path policy) instead of prebuilt shell strings. - Keep callers as thin Adapters that request projected text for: - managed hook commands in `settings.json` - managed hook commands in runtime config TOML/JSON surfaces - user-facing PATH repair / setup instructions - generated shim / wrapper script text written to disk - Keep internal subprocess execution (`spawnSync`, `execFileSync`, SDK query dispatch) outside this seam. The module does **not** become a generic command runner. - Make runtime-specific wrappers explicit policy at the seam (for example, emit PowerShell call-operator prefixes only for shells/runtimes that require them). - Make path-style projection explicit policy at the seam (`native Windows`, `POSIX slash`, `$HOME`-relative, `project-dir-relative`, etc.). - Prefer typed IR outputs that tests can assert against directly, then render text at the final Adapter. ## Initial Scope First migration slice should cover installer/runtime surfaces already proving this bug class: 1. managed JS and `.sh` hook command construction 2. managed hook rewrite / normalization on reinstall 3. Codex hook block command rendering 4. PATH diagnostic action-line rendering 5. Windows shim / wrapper script text builders 6. local-install hook command rendering (`$CLAUDE_PROJECT_DIR` vs cwd-relative runtime paths) It should **not** in the first pass expand into workflow markdown ` ``` /* Detailed source-code truncated for AI context efficiency. */ ```js projectShellCommand({ platform: 'linux' | 'darwin' | 'win32', shell: 'bash' | 'zsh' | 'cmd' | 'pwsh', runtime: 'claude' | 'gemini' | 'codex' | 'opencode' | 'copilot' | 'antigravity' | 'generic', executable: { kind: 'node' | 'bash' | 'pwsh' | 'literal', token: '...' }, args: ['...'], pathStyle: 'native' | 'posix' | 'home-relative' | 'project-relative', }) ``` For user-facing multi-line guidance, the seam should return typed action IR first, then let the installer print it: ```js projectShellActions({ intent: 'prepend-path', platform, targetDir, runtime }) ``` For generated shim/wrapper files, the seam should own script text rendering too: ```js projectShellScript({ shell: 'cmd' | 'pwsh' | 'sh', executable, argsTemplate }) ``` ## Consequences - Quoting, slash-direction, wrapper-prefix, and variable-expansion policy become local to one module. - Installer/runtime call sites become thinner and stop inventing sibling string builders. - Windows runtime-specific regressions become easier to classify as seam bugs instead of one-off installer bugs. - Tests can assert against typed projection IR instead of source-grepping ad-hoc string concatenation sites. - The first implementation will move a broad installer surface, so scope discipline matters: start with installer/runtime projection only, not every shell string in the repo. - If accepted, `CONTEXT.md` should gain a canonical **Shell Command Projection Module** entry and future architecture reviews should treat out-of-seam command rendering as drift. ## Open questions - Whether `hooks.shell_preference` from `#3082` should become an input policy consumed by this module or remain a higher-level runtime config concern. - Whether Windows Git Bash should be modeled as explicit `shell: 'bash'` + `platform: 'win32'` or as a distinct shell target. - Whether existing shim/script builders should migrate in the first pass or follow immediately after the hook/diagnostic path is stable. - Whether the seam should live entirely in installer land or later become shared with other runtime-output surfaces outside `bin/install.js`. ## References - Feature issue: `#3439` - Related bug history: `#2376`, `#2979`, `#3002`, `#3011`, `#3017`, `#3020`, `#3082`, `#3181`, `#3393`, `#3413` - See `0005-sdk-architecture-seam-map.md` - See `0008-installer-migration-module.md` ## Update — 2026-05-13 (Phases 1–4 expansion, `#3465`–`#3468`) The seam grew beyond the original "rendering only" scope. The "does not become a generic command runner" and "does not replace safe internal subprocess APIs" constraints (Decision §17, Initial Scope §33) were intentionally superseded. **Scope now owned by `shell-command-projection.cjs`:** - runtime-aware command-text rendering (original ADR scope) - subprocess dispatch — `execGit`, `execNpm`, `execTool`, `probeTty` (Phase 2, `#3466`) - platform file I/O — `platformWriteSync`, `platformReadSync`, `platformEnsureDir`, `normalizeContent` (Phase 3, `#3467`) - legacy wrappers `atomicWriteFileSync` / `safeReadFile` / `normalizeMd` removed from `core.cjs` (Phase 4, `#3468`) **Result-shape invariant:** all `exec*` return `{ exitCode, stdout, stderr }` and never throw on non-zero exit. Platform-conditional logic (`shell: process.platform === 'win32'`, `probeTty` Windows null return, `.md`-aware normalization) lives only at the seam. **Open question resolutions:** - Q4 (installer-only vs shared seam): **resolved — shared.** The seam lives in `gsd-core/bin/lib/`, consumed by installer, planning workflow, and every fs/subprocess call site across the tool. - Q1, Q2, Q3 (`hooks.shell_preference`, Windows Git Bash modeling, shim/script builder migration timing): unresolved, carried forward as projection-design concerns independent of the I/O expansion. See CONTEXT.md "Shell Command Projection Module" entry for the canonical current-state description. --- ### Adr/0010 File Operation Engine Module # File Operation Engine Module owns safe runtime/config file mutations - **Status:** Superseded by [ADR-0009](0009-shell-command-projection-module.md) (Shell Command Projection Module expansion, Phases 3–4, `#3467`–`#3468`) - **Date:** 2026-05-12 - **Superseded:** 2026-05-13 > **Supersession note.** Rather than build a separate File Operation Engine, the file-mutation safety policy this ADR proposed was absorbed into the **Shell Command Projection Module** (ADR-0009). Phase 3 (`#3467`) added `platformWriteSync` / `platformReadSync` / `platformEnsureDir` / `normalizeContent` to that seam, owning atomic write (tmp+rename), `.md` normalization, and directory creation as a single platform-conditional surface. Phase 4 (`#3468`) removed the duplicated `atomicWriteFileSync` / `safeReadFile` / `normalizeMd` wrappers from `core.cjs`. The `applyFileMutationPlan` / typed plan IR design proposed below was not built — the simpler per-call seam proved sufficient for the actual drift sites. Lock-file lifecycle (Track B item 3) remains owned by `withPlanningLock` in `planning-workspace.cjs` because its `{ flag: 'wx' }` exclusive-create semantics differ from atomic-write rename semantics. --- We propose introducing a File Operation Engine Module that owns policy for managed file reads, writes, deletes, locks, backups, and rollbacks across installer, migration, and planning surfaces. Today, file mutation behavior is duplicated across `bin/install.js`, `gsd-core/bin/lib/installer-migrations.cjs`, and multiple planning modules, with drift in atomic-write guarantees, path safety checks, and ownership classification. This ADR also captures where Shell Command Projection Module policy should be consumed or expanded for hook-command-specific file mutations, so shell command drift and file mutation drift do not evolve as separate bug classes. ## Decision - Add a **File Operation Engine Module** under `gsd-core/bin/lib/` as the single seam for file mutation safety policy. - Keep command-text projection in the Shell Command Projection Module (ADR-0009), but route projection-adjacent hook file mutations through shared managed-hook ownership policy. - Move file operation adapters to the new seam in two tracks: - **Track A (projection-adjacent):** runtime config hook-command detection/rewrite/delete paths consume shared managed-hook policy from the projection seam. - **Track B (solution-wide):** shared file operation engine owns atomic write, path containment, lock behavior, rollback bookkeeping, and best-effort cleanup policy. - Keep internal subprocess execution out of this seam (same boundary as ADR-0009): this is a file operation seam, not a command runner. ## Initial Scope 1. Unify managed-hook ownership classification used by install/uninstall/migration hook config rewrites. 2. Unify atomic write behavior currently duplicated in installer/core/migration paths. 3. Unify lock-file lifecycle policy used by planning workspace and installer migration journal flows. 4. Expose typed file mutation plan IR for tests (`rewrite-json`, `rewrite-text` with format (`toml`/`markdown`/`plain`), `delete-file`, `backup-file`, `restore-file`, `ensure-dir`). ## Migration Inventory ### Projection-adjacent file mutation drift (Track A) - `bin/install.js` - hook cleanup command detection (`isGsdHookCommand`) - stale Codex hook strip basenames (`STALE_HOOK_BASENAMES`) - settings/config hook entry prune/rewrite paths - `gsd-core/bin/lib/installer-migrations/002-codex-legacy-hooks-json.cjs` - `isManagedCodexHookCommand` regex/path detection duplicated from installer-owned hook policy - `gsd-core/bin/lib/shell-command-projection.cjs` - `isManagedHookBasename` already owns part of this policy and should become the canonical owner ### Solution-wide file operation drift (Track B) - `bin/install.js` - local `atomicWriteFileSync` and temp cleanup registry - large inlined read/modify/write + backup/rollback logic for runtime config and hooks - `gsd-core/bin/lib/core.cjs` - `atomicWriteFileSync` helper diverges in fallback behavior from installer/migration variants - `gsd-core/bin/lib/installer-migrations.cjs` - separate `writeFileAtomicSync`, rollback journaling, lock handling, and containment checks - `gsd-core/bin/lib/planning-workspace.cjs` and `gsd-core/bin/lib/state.cjs` - duplicated lock-file create/release/remove patterns and best-effort cleanup semantics - `gsd-core/bin/lib/roadmap.cjs`, `phase.cjs`, `milestone.cjs`, `frontmatter.cjs`, `drift.cjs` - direct read/modify/write flows with inconsistent atomicity and normalization policy application ## Interface sketch The File Operation Engine Module should expose typed mutation planning and execution helpers: ```js planFileMutations({ rootDir, operations: [ { type: 'rewrite-json', relPath, mutate }, { type: 'rewrite-text', relPath, mutate, format: 'toml' | 'markdown' | 'plain' }, { type: 'delete-file', relPath }, { type: 'ensure-dir', relPath }, ], ownership: { mode: 'managed-only' | 'allow-user', classifier }, }) ``` ```js applyFileMutationPlan({ plan, atomic: true, rollback: true, lock: { scope: 'config' | 'planning', id: '...' }, }) ``` For projection-adjacent paths, adapters should consume projection policy: ```js isManagedHookCommand(commandText, { surface, configDir }) ``` ## Consequences - File mutation safety policy becomes local to one module, reducing drift across installer/migration/planning paths. - Shell command projection and hook ownership classification stay aligned at one seam family. - Tests can assert typed mutation IR and reason codes instead of source-grep and duplicated predicate mirrors. - Initial migration is broad; sequencing should prioritize projection-adjacent hook config paths first, then converge atomic-write and lock semantics. ## Open questions - Whether lock semantics should be one shared policy for installer + planning, or two adapters over one lock primitive. - Whether SDK query write paths should consume the same engine in the first pass or follow after CJS convergence. - Whether file mutation telemetry (per-op reason codes and rollback events) should be required for all engine adapters. ## References - ADR-0008: `0008-installer-migration-module.md` - ADR-0009: `0009-shell-command-projection-module.md` - Related bug history: `#1755`, `#2866`, `#2979`, `#3002`, `#3017`, `#3439` --- ### Adr/0010 Skill Surface Budget Module # Skill Surface Budget Module owns install-time skill listing curation - **Status:** Superseded by [ADR-0011](0011-skill-surface-budget-module.md) (Skill Surface Budget Module — install-time profile staging and runtime surface control); originally Proposed (2026-05-12) - **Date:** 2026-05-12 > **Provenance of this status (2026-07-16).** This file said `Proposed` while the hand-maintained index in `README.md` recorded it as *"Skill Surface Budget Module — earlier draft superseded by ADR-0011"*, status *"Superseded by 0011"*. The index was right and the file was stale. When the index became a generated artifact (derived from these files), that assertion would have been silently dropped and this superseded draft would have reappeared as a live `Proposed` decision — so it is recorded here, at its source, instead. This is the one status corrected from the old index rather than left for ratification, because leaving it would have *lost* a decision the maintainer had already made. We propose extending the existing install profile seam (`gsd-core/bin/lib/install-profiles.cjs`) into a **Skill Surface Budget Module** that owns which subset of GSD's 66 skills is written to the runtime config dirs, and that owns the per-skill `requires:` dependency manifest used to keep that subset closed under cross-skill references. GSD currently ships a binary `--minimal` / full toggle; runtimes that enumerate skills (Claude Code, OpenCode, etc.) cap the `` system-prompt block at `skillListingBudgetFraction` of the context window (default 1% = ~2k tokens at 200k), and GSD alone consumes ~60% of that cap (#3408). Further description shrinkage is unavailable — `scripts/lint-descriptions.cjs` already enforces a hard 100-char ceiling and the mean is 72.5 chars. The remaining lever is surfacing fewer skills, which requires a typed profile model plus a dependency manifest, not more ad-hoc allowlists. ## Decision - Add a **Skill Surface Budget Module** by extending `gsd-core/bin/lib/install-profiles.cjs` as the single owner for which `commands/gsd/*.md` and `agents/gsd-*.md` files are staged into the per-runtime copy pipeline. - Replace the single `MINIMAL_SKILL_ALLOWLIST` constant with a typed `PROFILES` map keyed by profile name. Each profile is a *base set* of skills; the module computes the **transitive closure** over each skill's declared `requires:` set before staging. - Add a `requires:` frontmatter field to every skill whose body references another GSD skill. The dependency graph in the research memo (`docs/research/2026-05-12-skill-surface-budget.md` §3.1) is the migration spec for this pass. - Extend `bin/install.js` argument parsing to accept `--profile=` and `--profile=,` (composable). Preserve `--minimal` / `--core-only` as aliases for `--profile=core`. Default install (no flag) remains `full` for back-compat. - Persist the active profile to `~/.claude/skills/.gsd-profile` (and runtime-equivalent locations) so `gsd update` re-applies the same profile instead of expanding silently to full. - Add `scripts/lint-skill-deps.cjs` and wire it into the existing `npm run lint:descriptions` pretest gate. The lint fails if: - a skill body references another skill not in its `requires:` set, or - any profile would ship a skill whose `requires:` closure is not satisfied. - Keep the **interactive install picker** behind the same `AskUserQuestion`-style flow already used for runtime/location selection. Non-interactive installs (CI, `npx --yes`) fall back to `--profile=full` unless overridden. ## Initial Scope First migration slice should land the profile model and one new tier above `core`: 1. Profile map (typed): `core` (current minimal, 7 skills including `phase`), `standard` (~13 skills covering the audit + main-loop + utility floor), `full` (current default, 66 skills). 2. `requires:` frontmatter added to the **hot nodes** of the dependency graph first: `phase` (38 callers), `review` (11), `config` (7), `progress` (5), `update` (5). These are the skills whose absence silently breaks others, so they need explicit `required_by` audit before any profile narrows them out. 3. Confirm-and-lock the latent bug fix surfaced by the audit: **`phase` is referenced by 38 skills and now belongs in `MINIMAL_SKILL_ALLOWLIST` / `PROFILES.core`.** Keep explicit coverage in minimal/core tests so this cannot regress. 4. CLI surface: `--profile=`, comma-composed profiles, `--profile=help` listing each profile's contents and token cost. 5. Profile marker persistence + `gsd update` re-application. It should **not** in the first pass: - Build a runtime enable/disable surface (`/gsd:surface`). Track as a follow-up ADR (see "Open questions"). - Split GSD into multiple npm packages. The packaging-level alternative was considered and rejected — see research memo §4 Option F. - Consolidate further skills (e.g. collapsing `*-phase` into a dispatcher). Track separately as IA cleanup; orthogonal to surface curation. ## Migration Inventory ### `gsd-core/bin/lib/install-profiles.cjs` - Replace `MINIMAL_SKILL_ALLOWLIST` Object.freeze constant with `PROFILES` Object.freeze map of profile-name → base skill set. - Replace `isMinimalMode(mode)` with `resolveProfile(mode)` returning a typed `{name, skills: Set, agents: Set}` after transitive-closure computation. - Replace `shouldInstallSkill(name, mode)` with `shouldInstallSkill(name, resolvedProfile)`. - Replace `stageSkillsForMode(srcDir, mode)` with `stageSkillsForProfile(srcDir, resolvedProfile)`. Add a sibling `stageAgentsForProfile` since this module now owns agent staging too (current `--minimal` skips agents wholesale; tiered profiles need finer control). - Keep the existing exit-cleanup machinery (`STAGED_DIRS`, `ensureExitCleanup`) unchanged — the bug surface it covers is the same. ### `bin/install.js` These call sites should migrate behind the Skill Surface Budget Module: - `--minimal` / `--core-only` flag parsing — `bin/install.js:123-124` - `_effectiveInstallMode` plumbing + `isMinimalMode()` checks — `bin/install.js:7634-8465` (passes through to per-runtime copy fns) - minimal-agent skip block — `bin/install.js:8167-8207` (becomes "skip agents not in profile") - runtime-specific copy entry points that consume `stageSkillsForMode` — 13 sites per the existing comment in `install-profiles.cjs` - usage help block — `bin/install.js:508` (add `--profile=` documentation) ### Frontmatter changes - Add `requires:` field to every skill in `commands/gsd/*.md` whose body references another GSD skill. Audit data lists the full set (`docs/research/2026-05-12-skill-surface-budget.md` §3.1). Estimate: 25-30 files touched in Phase 1. - Field is optional. Absence = "no GSD-skill dependencies." `lint-skill-deps.cjs` enforces consistency, not presence. ### New: `scripts/lint-skill-deps.cjs` - Walks `commands/gsd/*.md`, parses `requires:`, walks the body for `gsd:` or `\b\b` references to other skills (same matching rules documented in `docs/research/2026-05-12-skill-surface-budget.md` §3.1). - Fails CI if `requires:` set ≠ actual references (modulo ignore-list for prose mentions that aren't actual dispatches). - Walks `PROFILES` from `install-profiles.cjs`, fails if any profile's transitive closure references a skill not in the profile. - Wires into `npm run lint:descriptions` (or as a sibling `lint:skill-deps`) and `pretest`. ### Profile marker - New `~/.claude/skills/.gsd-profile` (and per-runtime equivalents under `.codex/`, `.cursor/`, etc. as enumerated in `install.js`) containing the active profile name. - Installer Migration Module (ADR-0008) gains a one-shot migration: if marker absent and skills dir matches `core` exactly, write `core`; otherwise write `full`. Migrations are idempotent per existing module contract. ### Tests expected to move with the seam - `tests/install-profiles-*.test.cjs` (any existing) — extend to assert profile resolution, transitive closure, and `--profile=core,standard` composition. - New `tests/skill-surface-budget-*.test.cjs` covering: - profile closure: a profile that lists `discuss-phase` must transitively include `phase` if `discuss-phase` requires it - lint failures: a skill body that references an un-required skill makes `lint:skill-deps` fail - marker persistence: `gsd install --profile=standard` followed by `gsd update` preserves `standard` - minimal back-compat: `--minimal` resolves to `--profile=core` and emits the same file set as today (modulo the `phase`-inclusion bug fix) ## Interface sketch The module should accept typed profile intent and return a typed resolved profile: ```js // install-profiles.cjs (extended) resolveProfile({ modes: ['core' | 'standard' | 'full'], skillsManifest: ManifestMap, // parsed `requires:` graph }) // → { name: 'standard', skills: Set, agents: Set } ``` Profile composition: `--profile=core,standard` resolves to `union(closure(core), closure(standard))`. `--profile=full` is the identity profile (every skill). ```js stageSkillsForProfile(srcDir, resolvedProfile) // returns staged dir path stageAgentsForProfile(srcAgentsDir, resolvedProfile) // new ``` Profile marker IO is typed too, not stringly: ```js readActiveProfile(runtimeConfigDir) // → 'core' | 'standard' | 'full' | null writeActiveProfile(runtimeConfigDir, profileName) ``` Per-skill frontmatter contract: ```yaml --- name: gsd:plan-phase description: ... requires: [phase, discuss-phase] # GSD skills only; not Claude Code primitives --- ``` `requires:` lists *GSD* skills (file stems). It does not include Claude Code built-ins (`Read`, `Bash`, etc.) — those continue to live in `allowed-tools:` per existing convention. ## Consequences - The skill-set written by the installer becomes a typed first-class artifact, not a side effect of file copies + an allowlist constant. ADR-0008 (Installer Migration Module) gains a clean handle for safe profile migrations on upgrade. - `gsd update` stops silently re-expanding a `--minimal` install to full — a current foot-gun documented inline in `install-profiles.cjs` (its module-level comment recommends `gsd update` without `--minimal` to "expand to the full surface"; that path remains available, but the default `gsd update` now respects the recorded profile). - The `requires:` manifest creates a new authoring obligation (~30 files in Phase 1), enforced by CI. Skill authors who add a `/gsd:phase` reference in a new skill body have to update `requires:`. The lint script keeps drift low-cost. - The `phase`-in-minimal latent gap (research memo §3.1) gets resolved as a side effect of adopting closure-based profile resolution — `phase` is auto-included whenever any minimal-loop skill `requires:` it. - First-time install UX gains a profile picker. The default remains `full` for non-interactive (`npx --yes`) installs, so back-compat for CI scripts is preserved. - The module becomes the canonical place to land future Anthropic platform features (lazy descriptions, per-plugin budgets, `.disabled` toggles — see Open Questions). It does not, in this ADR, *use* those features. - If accepted, `CONTEXT.md` should gain a canonical **Skill Surface Budget Module** entry alongside the existing seam entries, and future architecture reviews should treat ad-hoc `commands/gsd/` filtering outside this seam as drift. ## Open questions - Whether the Phase-2 runtime `/gsd:surface` command (research memo §4 Option B) should be its own ADR or an amendment to this one. Leaning **separate ADR** because it introduces persistent runtime state outside the install pipeline. - Profile naming bikeshed. `core / standard / full` is the working proposal. Alternatives surveyed: `minimal / recommended / everything`, functional names (`planning, audit, research`). Settle in the implementation PR after a contributor poll. - Whether the `requires:` field should also be consumed by `/gsd:help` to render a "skills you have installed and what depends on what" graph. Likely yes, but out of scope for this ADR. - Whether to keep `phase` explicitly listed in `core` forever vs relying purely on closure semantics. Current recommendation: keep explicit listing because minimal mode has a back-compat allowlist path. - Whether telemetry (opt-in) is worth proposing to inform where the `standard` profile line goes. Without it, the cut points are author-intuition. Track separately; not a blocker. - Whether the Anthropic platform asks (research memo §6 — lazy descriptions, per-plugin budgets, dependency-aware listing, `.disabled` toggles) should be filed before or after this ADR ships. Recommendation: file as a feedback bundle when ADR is accepted, so we ship Phase 1 unilaterally and platform improvements compose on top. ## References - Feature issue: `#3408` - Research input: `docs/research/2026-05-12-skill-surface-budget.md` - Existing seam being extended: `gsd-core/bin/lib/install-profiles.cjs` - Description budget enforcement: `scripts/lint-descriptions.cjs` - Installer dispatch site: `bin/install.js:123-124`, `:8167-8207` - See `0008-installer-migration-module.md` (the migration that records the profile marker lives here) - See `0005-sdk-architecture-seam-map.md` (the seam map this module joins) --- ### Adr/0011 Review Default Reviewers # `review.default_reviewers` config key scopes the no-flag `/gsd-review` fan-out - **Status:** Accepted — ratified 2026-07-17 (originally Proposed 2026-05-13); see "Ratification" below - **Date:** 2026-05-13 We propose adding a `review.default_reviewers` key to `.planning/config.json` that scopes the no-flag default of `/gsd-review` to a user-chosen subset of detected CLI reviewers. Today the no-flag branch of `workflows/review.md` (line 52) invokes **all available** CLIs, which for multi-CLI users plus local model servers (ollama, lm-studio, llama.cpp) means probing up to ~10 backends per review, paying timeout costs on servers that aren't running and burning tokens on reviewers the user doesn't want for routine work (`#3079`). The only workaround today is patching `workflows/review.md` in place; that patch is wiped on every `/gsd-update` and requires `/gsd-update --reapply` to restore, with no machine-readable record of intent. The proposed key sits inside the existing `review.*` namespace (alongside `review.models.` and `review.*_host`), follows GSD's **absent = enabled** config philosophy, and is implementable as a one-line config read plus an intersection on the detected reviewer set. ## Ratification (2026-07-17): Proposed → Accepted Ratified by explicit maintainer directive; the Status field had sat stale at "Proposed" for roughly 65 days after the decision actually shipped. **Evidence the decision shipped:** - Landing commit `245d5f66a` ("feat: add review.default_reviewers config for /gsd-review defaults (#3464)", 2026-05-13) added the schema, resolution logic, workflow wiring, docs, and three test files in one change. - `src/review-reviewer-selection.cts` (329 lines) exports `KNOWN_REVIEWER_SLUGS` (line 51) and `normalizeConfiguredDefaultReviewers` (line 105), implementing the ADR's precedence order (explicit flags > `--all` > `review.default_reviewers` > all detected). - `src/config.cts:878` handles `kp === 'review.default_reviewers'` for `config-get`/`config-set`, running values through `normalizeConfiguredDefaultReviewers` and surfacing schema errors. - `gsd-core/workflows/review.md` (no-flag branch, ~lines 55-70) intersects detected reviewers with `review.default_reviewers` exactly as specified, including unknown-slug warnings and undetected-slug info notes. - `docs/CONFIGURATION.md:219-225` documents the key, type, default, and precedence; `docs/COMMANDS.md:1451-1461` documents usage with a `gsd config-set` example. - Four test files are present and current: `tests/review-default-reviewers-config.test.cjs`, `tests/review-default-reviewers-resolution.test.cjs`, `tests/review-default-reviewers-workflow.test.cjs`, `tests/review-reviewer-instances.test.cjs`. - `.changeset/archived/daring-badgers-munch.md` (type: Added, pr: 3464) is archived, confirming release tooling already processed it. Governance state: the owning issue (`#3079`, referenced above) and its landing PR (`#3464`) both 404 against the current `open-gsd/gsd-core` tracker — their numbering belongs to a predecessor repo whose issue space predates this repo's 2026-05 range (which topped out near `#540`), consistent with known predecessor-repo numbering rather than a fabricated reference. No in-tracker close event is directly checkable; the shipped-code evidence above substitutes for it. **Known gaps at ratification:** two of the ADR's own non-blocking open questions remain genuinely unresolved — Q-2 (`--no-default` flag) and Q-3 (`review.profiles.*` namespace) — exactly as the ADR itself scoped them as future/non-blocking, so this is expected rather than a regression. ## Decision - Add **`review.default_reviewers`** to the `config.json` schema as `string[]`, validated against the existing CLI slug pattern `^[a-zA-Z0-9_-]+$` (the same pattern used for `review.models.` slugs). - When the key is **present**, the no-flag branch of `/gsd-review` invokes only the reviewers listed in the key, intersected with the host's `detect_clis` result. - When the key is **absent**, today's behavior is preserved: every detected reviewer runs. This matches the **absent = enabled** pattern documented in `docs/CONFIGURATION.md`. - **`--all`** continues to mean "every detected reviewer" and ignores the config key. - **Individual reviewer flags** (`--gemini`, `--codex`, `--cursor`, `--claude`, `--opencode`, …) continue to win over both config and `--all`. - Resolution lives in the `detect_clis` step of `workflows/review.md`: detect first, then filter by `review.default_reviewers` only on the no-flag branch. - Unknown slugs in the key emit a warning and are dropped; valid-but-undetected slugs emit an info-level note and are dropped; an all-undetected post-filter set emits an actionable error (see "Open questions" Q-1 for empty-array semantics). - Slug comparison is lowercase-normalized on read. The schema pattern already accepts mixed case, so normalization is forgiving without making the pattern itself stricter. - No new top-level config namespace, no new command, no new flag in v1. **Precedence (highest first):** 1. Individual reviewer flags 2. `--all` 3. `review.default_reviewers` 4. No config, no flags → today's behavior (all detected) ## Initial Scope First slice should land the config plumbing and the no-flag branch behavior without expanding into adjacent reviewer-selection design: 1. Schema addition for `review.default_reviewers` in the config loader; validation as `string[]` with slug pattern; lowercase normalization; clear schema errors for non-array / non-string-element values. 2. Filter step inside `workflows/review.md` `detect_clis` no-flag branch: - intersect `detected ∩ default_reviewers` - emit a single-line "selection source" log identifying which path was taken (default config / `--all` / explicit flags / no config) - warn on unknown slugs; info on valid-but-undetected slugs; error if the post-filter selection is empty 3. Docs update: extend `docs/CONFIGURATION.md` with a `review.*` subsection documenting the key, allowed values, defaults, and override precedence; add the key to the schema block at the top of that file; update `workflows/review.md` to reference the key in the no-flag branch. 4. Tests: config parsing (valid / empty / malformed); `detect_clis` intersection; integration coverage of no-flag honors config, `--all` overrides, individual flags override, unknown slug warns, only-undetected slugs errors. 5. Release notes entry calling out the new key with the two-line config example. It should **not** in the first pass: - Add `--no-default` or any new CLI flag (track in "Open questions" Q-2 — likely equivalent to `--all`). - Add per-phase or per-file-type reviewer profiles (`review.profiles.*`). The namespace is left open for this as a future ADR (see "Open questions" Q-3). - Add reviewer "groups" or aliases (`review.groups.cheap = [...]`). Same — namespace deliberately left open. - Auto-suggest defaults from usage history. Different design philosophy (silent behavior drift); explicitly out of scope. - Extend `/gsd-config --integrations` to set the key interactively. Track as a fast follow (see "Open questions" Q-4). - Change `--all` semantics or the individual flag set. ## Migration Inventory ### `workflows/review.md` - `detect_clis` step, no-flag branch (current line 52: "No flags → include all available") — replace with: "No flags → if `review.default_reviewers` is set, intersect detected with the listed slugs; otherwise include all detected." - Verbose / debug output path — emit one line identifying the selection source. ### Config loader - Add `review.default_reviewers: string[]` to the JSON schema for `.planning/config.json`. Pattern per element: `^[a-zA-Z0-9_-]+$`. Slug list de-dup on read; lowercase-normalize on read. - Surface schema errors at config load (file path + line number where the parser supports it), matching the existing handling for other malformed `review.*` keys. ### `docs/CONFIGURATION.md` - Add `review.default_reviewers` to the **Full Schema** code block at the top of the file as an optional array key under a `"review": { ... }` object. - Add a new **Reviewer Selection** subsection (or extend the existing `review.*` section if one exists) covering: purpose, type, default, precedence vs. `--all` and individual flags, edge-case behavior (unknown slugs, undetected slugs, empty result). - Cross-reference `workflows/review.md` for how the key is consumed at review time. ### Tests expected to move with the seam - New `tests/review-default-reviewers-config.test.cjs`: - valid `["gemini", "codex"]` parses; lowercase normalization works - `[]` schema decision per Q-1 (proposed: parse-time error) - non-array → schema error - non-string element → schema error - element failing slug pattern → schema error - New `tests/review-default-reviewers-resolution.test.cjs`: - no flags + key set, both slugs detected → exactly those reviewers invoked - no flags + key set, one slug undetected → info logged, remaining slug invoked - no flags + key set, all slugs undetected → actionable error, nothing invoked - no flags + key set, unknown slug present → warning logged, unknowns dropped, rest invoked - `--all` + key set → every detected reviewer invoked, key ignored - `--gemini` + key set to `["codex"]` → only Gemini invoked - no flags + key absent → every detected reviewer invoked (back-compat) - Extend existing `/gsd-review` integration tests to cover the new selection-source log line. ### Schema doc cross-reference - Update the schema example at the top of `docs/CONFIGURATION.md` to include `"review": { "default_reviewers": ["gemini", "codex"] }` so the key is discoverable from the canonical schema view. ## Example config ```json { "review": { "default_reviewers": ["gemini", "codex"] } } ``` With this set, `/gsd-review` (no flags) invokes only Gemini and Codex. `/gsd-review --all` invokes every detected reviewer. `/gsd-review --cursor` invokes only Cursor. Today's behavior is preserved by simply omitting the key. ## Resolution pseudocode ```text detected = detect_clis() # unchanged if any individual flag passed: selected = flags_to_set(flags) ∩ detected elif --all: selected = detected elif config.review.default_reviewers is set: valid = filter(config.review.default_reviewers, is_known_slug) # warn on each invalid slug selected = valid ∩ detected # info on each valid-but-undetected slug if selected is empty: error with actionable message # see Q-1 else: selected = detected # today's behavior log_selection_source(selected, source) ``` ## Consequences - Multi-CLI users can stop patching `workflows/review.md`; the patch class that `/gsd-update` wipes goes away for this case. - Teams can commit `.planning/config.json` and share a default reviewer set across machines and contributors, without forking the workflow file. - `/gsd-review` wall-clock time drops on machines where detection probes idle local model servers — the timeout cost on stopped daemons is no longer paid on every routine review. - Schema surface grows by one optional key; doc maintenance and the test matrix grow by a small fixed amount. - The `review.*` namespace stays internally consistent. Future `review.profiles.*` or `review.groups.*` can coexist with `review.default_reviewers` without renaming. - Cross-runtime impact is minimal: the change operates on the detection layer in `detect_clis`, not on any per-runtime adapter. The existing `resolve_model_ids: "omit"` path used by non-Claude runtimes (Codex, OpenCode, Gemini CLI, Kilo) is unaffected. - One additional surface area for bug reports — primarily edge interactions between the key and `--all` / individual flags, which the test plan covers. - If telemetry is ever opt-in for `.planning/config.json` shape, adoption of this key becomes a useful signal for whether to invest in the richer profiles design (see Q-3). ## Open questions - **Q-1.** Should `review.default_reviewers: []` be a schema error, or should it fall back to "all detected"? *Proposal: schema error.* Rationale: users who want "all detected" can simply omit the key (more readable); `[]` looks like a typo or programmatic mistake; surfacing the ambiguity is more helpful than silently swallowing it. **Blocking** — affects schema validation and tests. - **Q-2.** Is a new `--no-default` flag warranted, or is it equivalent to `--all` for this use case? *Proposal: drop unless a concrete difference surfaces during implementation.* Non-blocking. - **Q-3.** Do we want to commit now to leaving `review.profiles.*` open as a future namespace, or is that premature? *Proposal: leave open; document the intent in `docs/CONFIGURATION.md` so the next contributor doesn't pick a conflicting key.* Non-blocking. - **Q-4.** Should `/gsd-config --integrations` learn the new key in this pass, or as a fast follow once the schema + resolution land? *Proposal: fast follow.* Non-blocking; depends on contributor bandwidth. - **Q-5.** Should the verbose-mode "selection source" line ship in the first pass, or only behind `--verbose`? *Proposal: behind `--verbose`.* Non-blocking. - **Q-6.** Slug normalization: lowercase-on-read (proposed) vs. exact-match enforcement at the schema layer. *Proposal: normalize.* Non-blocking; document either way. ## References - Feature issue: `#3079` - Configuration reference: `docs/CONFIGURATION.md` — `review.models.`, `review.*_host`, and the **absent = enabled** pattern - Workflow file owning the no-flag branch: `workflows/review.md` (line 52) - Existing slug validation pattern: `^[a-zA-Z0-9_-]+$` (used for `review.models.` keys) - Related PRD: `0011-review-default-reviewers-prd.md` --- ### Adr/0011 Review Default Reviewers Prd # PRD — `review.default_reviewers` config key for `/gsd-review` reviewer selection - **Status:** Legacy — frozen historical record; not a pattern to follow (see the note below) - **Date:** 2026-05-13 - **Issue:** `#3079` - **Related ADR:** [`0011-review-default-reviewers.md`](0011-review-default-reviewers.md) > **Note (2026-07-16).** This PRD's original note said "the repo does not yet have a `docs/prd/` directory; if maintainers prefer one, this file can move there." That directory **now exists**, and [`docs/prd/README.md`](../prd/README.md) records this file's disposition: it *"predates this directory and is preserved as immutable historical record. It is not a pattern to follow. New PRDs live here."* It is therefore kept in place, and its status is `Legacy` — the decision is frozen for provenance, not superseded by a specific successor. New PRDs go in `docs/prd/`. ## TL;DR `/gsd-review` with no flags fans out to **every** detected CLI reviewer (Claude, Codex, Cursor, Gemini, OpenCode, plus local model servers such as ollama, lm-studio, llama.cpp). For users with many backends installed, this wastes wall-clock on timeouts and burns tokens on reviewers they don't want for routine work. Add a `review.default_reviewers` key under the existing `review.*` namespace in `.planning/config.json` that scopes the no-flag default to a user-chosen subset. Absent key preserves today's behavior. `--all` and individual flags continue to work unchanged. Follows GSD's **absent = enabled** convention. ## Problem Statement GSD's `/gsd-review` workflow treats "no flags" as "invoke every CLI we can detect" (`workflows/review.md` line 52). That default is fine at install time — it makes the feature discoverable — but it's the wrong default for any user who has accumulated multiple reviewer CLIs plus local model servers. Each review probes up to ~10 backends, including ones that are slow, expensive, redundant for the change at hand, or not actually running (timeout waits on ollama, lm-studio, llama.cpp when the daemon is off). The only existing workaround is editing `workflows/review.md` in place. That patch gets clobbered on every `/gsd-update`, requiring `/gsd-update --reapply` to restore. There is no machine-readable record of the user's intent — every machine the user works on needs the same patch reapplied. The issue reporter (`#3079`) and presumably others are paying a "tax" on every review that is purely a default-selection problem. This is a small change with broad reach: it lands in a hot-path workflow that power users run many times per day. ## Goals - Eliminate the recurring local-patch tax for multi-CLI users. A user who consistently wants only Gemini + Codex for routine reviews should be able to set that once and forget it. - Cut median wall-clock time of a no-flag `/gsd-review` on multi-CLI machines (target: ≥40% reduction for users with ≥4 detected CLIs). - Keep the change non-breaking. Absent config = today's behavior; nothing changes for the install-day experience. - Match GSD's established config conventions (`review.models.*`, `review.*_host`, **absent = enabled**, namespacing under `review.*`) so users don't have to learn a new pattern. - Stay one-line-shaped. The implementation should be a config read plus an intersection with the detected set — no new commands, no schema overhaul, no migration. ## Non-Goals - **Per-phase or per-task reviewer routing** (e.g., "use Codex on Rust phases, Gemini on docs"). Useful, but a separate, larger design — track as a future ADR. - **Reviewer scoring, weighting, or ensemble logic.** This is about *which* reviewers run, not *how* their output is aggregated. - **Auto-detecting the "best" default reviewers based on usage history.** Out of scope; we want explicit user intent, not silent behavior drift. - **Changing the `--all` semantics or the individual flag set** (`--gemini`, `--codex`, `--cursor`, …). They keep their current meaning. - **A new top-level config namespace.** Reviewers belong under the existing `review.*` namespace. - **GUI / TUI editing of the key in v1.** Editing the JSON file directly (or via `/gsd-settings` / `/gsd-config --integrations` if maintainers choose to support it later) is sufficient. ## Users & Use Cases ### Primary persona: "Multi-CLI power user" A developer who has installed multiple coding CLIs (e.g., Claude Code, Codex, Gemini CLI, Cursor, OpenCode, Kilo) plus one or more local inference servers. They run `/gsd-review` frequently — sometimes dozens of times per day during a sprint — and have a stable mental model of which 1–3 reviewers actually add signal for their day-to-day work. ### Secondary persona: "Single-CLI user with a sometimes-on local server" Has Claude + ollama installed. Wants reviews from Claude every time, and from ollama only when explicitly asked. Today, every `/gsd-review` pays the ollama timeout cost when ollama isn't running. ### Secondary persona: "Cost-sensitive team lead" Routine reviews should hit cheap/local reviewers; pre-merge reviews should hit the expensive ones via `--all` or explicit flags. Wants a config-level expression of "the cheap subset is my default." ### Use cases this enables - "I only want Gemini + Codex for routine reviews." → set `review.default_reviewers: ["gemini", "codex"]`. - "I want Claude only by default, and I'll opt into the others with flags." → set `["claude"]`. - "I want today's behavior." → leave the key absent. - "I want today's behavior just this once" on a configured project → `/gsd-review --all`. ## User Stories Grouped by persona, ordered roughly by frequency. **Multi-CLI power user** - As a multi-CLI user, I want to declare which reviewers run by default so that `/gsd-review` doesn't probe backends I don't use. - As a multi-CLI user, I want my preference to survive `/gsd-update` so I don't have to keep re-patching `workflows/review.md`. - As a multi-CLI user, I want `--all` to still work so I can opt into a full review pre-merge without un-setting my config. - As a multi-CLI user, I want individual flags (`--gemini`, `--cursor`, …) to keep working regardless of my default so ad-hoc runs aren't constrained by the default. **Single-CLI user with sometimes-on local server** - As a user with intermittent local servers, I want my default to exclude them so a stopped daemon doesn't cost me a 30-second timeout on every review. **Cost-sensitive team lead** - As a team lead, I want to commit `.planning/config.json` to the repo so everyone on the team gets the same review defaults. **New user** - As a new user, I want today's behavior preserved so the feature still "just works" out of the box without config. **Edge cases** - As a user with a typo in my default list, I want a clear warning that names an unknown reviewer rather than a silent skip. - As a user whose configured reviewer is no longer installed, I want a clear note that it was dropped from this run. - As a user who lists only reviewers that aren't installed, I want a clear error or a documented fallback (see Open Questions). ## Requirements ### Must-Have (P0) - **P0-1. New config key.** `review.default_reviewers` is `string[]`. Each element validates against the existing slug pattern `^[a-zA-Z0-9_-]+$`. Schema parser accepts the key; rejects non-array or non-string-element values with a clear error. Empty array `[]` behavior is decided per Q-1. - **P0-2. No-flag honors the key.** Given the key is `["gemini", "codex"]` and both are detected, running `/gsd-review` invokes only Gemini and Codex. - **P0-3. Absent key preserves current behavior.** Given the key is unset, running `/gsd-review` runs every detected reviewer, identical to today. - **P0-4. `--all` overrides the config.** Given the key is `["gemini"]`, running `/gsd-review --all` invokes every detected reviewer. Verbose mode shows which reviewers came from `--all` vs. the default. - **P0-5. Individual flags override the config.** Given the key is `["gemini"]`, running `/gsd-review --cursor` invokes only Cursor. Running `/gsd-review --gemini --codex` invokes exactly those two regardless of the default. - **P0-6. Graceful slug handling.** Unknown slug → start-of-run warning naming the offending slug; run continues with valid entries. Known slug but undetected → info-level note; run continues. Zero post-filter selections → error per Q-1. - **P0-7. Docs updated.** `docs/CONFIGURATION.md` gets a `review.*` subsection (or an extension of an existing one) covering the new key. `workflows/review.md` references the key in the no-flag branch. Schema example at the top of `docs/CONFIGURATION.md` includes the key. - **P0-8. Tests.** Unit and integration coverage per the test list in the ADR's **Tests expected to move with the seam** section. ### Nice-to-Have (P1) - **P1-1.** `/gsd-config --integrations` extends to set `review.default_reviewers` interactively, aligning with the existing interactive config flow for reviewers. - **P1-2.** `--no-default` flag that runs the full detected set without `--all` semantics — slightly different intent expression. Drop if equivalent to `--all` (Q-2). - **P1-3.** Verbose-mode "selection source" line in `/gsd-review` output: `Running reviewers (default): gemini, codex (set in .planning/config.json)`. - **P1-4.** Per-command override env var (`GSD_REVIEW_DEFAULT=...`) for CI scenarios where mutating `config.json` is undesirable. ### Future Considerations (P2) - **P2-1.** Per-phase or per-file-type reviewer profiles (`review.profiles.frontend: ["claude", "cursor"]`). The shape of `default_reviewers` is deliberately chosen not to foreclose this — a future `review.profiles.*` map can coexist. - **P2-2.** Reviewer "groups" or aliases (`review.groups.cheap = ["ollama", "gemini-flash"]`). Same — leave room under `review.*`. - **P2-3.** Auto-suggestion that detects repeated flag patterns and offers to persist them. Natural follow-up but explicitly out of scope here. ## Behavior Specification ### Precedence (highest first) 1. Individual reviewer flags (`--gemini`, `--codex`, `--cursor`, …) — always win. 2. `--all` — full detected set, ignores config. 3. `review.default_reviewers` in config — subset, intersected with detected set. 4. No config, no flags — full detected set (today's behavior). This matches the principle of least surprise: explicit user input (flags) always wins over persisted preference (config), and persisted preference only fills the gap when the user hasn't said anything else. ### Resolution pseudocode ```text detected = detect_clis() # unchanged if any individual flag passed: selected = flags_to_set(flags) ∩ detected elif --all: selected = detected elif config.review.default_reviewers is set: valid = filter(config.review.default_reviewers, is_known_slug) # warn on each invalid slug selected = valid ∩ detected # info on each valid-but-undetected slug if selected is empty: error with actionable message # see Q-1 else: selected = detected # today's behavior ``` ### Validation - Slug pattern: `^[a-zA-Z0-9_-]+$` (already in use for `review.models.`). - Type: JSON array of strings; anything else → schema error at config load. - Empty array: see Q-1 (proposed: schema error). - Slug case normalization: lowercase-on-read. - Duplicates: de-dup silently. ### Logging - One line per `/gsd-review` start identifying the source of selection (default config / `--all` / explicit flags / no config). Surfaced under `--verbose` per Q-5. - Slug warnings/infos as described in P0-6. ## Success Metrics ### Leading indicators (1–4 weeks post-release) - **Adoption proxy.** Count of `.planning/config.json` files containing `review.default_reviewers` (only countable if/when GSD ever ships opt-in telemetry; otherwise qualitative via Discussions). - **Issue echo.** Closure of `#3079` and zero new issues reporting the same wipe-on-update problem within 60 days. - **Patch-removal proxy.** Maintainer observes no further PRs or Discussion threads about patching `workflows/review.md` defaults within 60 days. ### Lagging indicators (1–3 months post-release) - **Median wall-clock per `/gsd-review`** on machines with ≥4 detected CLIs (self-reported or telemetry). Target: ≥40% reduction for users who opt in. - **User-perceived signal-to-noise** on review output (qualitative; gather via GitHub Discussions or a single follow-up question on the issue). ### Measurement notes GSD doesn't ship usage telemetry today. Most of these metrics rely on qualitative signal: issue activity, Discussion threads, and a follow-up on `#3079`. That's appropriate for a config addition of this size — we don't need a metrics pipeline to validate it. ## Edge Cases & Error Handling - **Config key missing** → today's behavior (all detected). - **Config key is `[]`** → schema error per proposed Q-1 resolution. Message: `review.default_reviewers is empty; remove the key to use the default-all behavior or list at least one reviewer.` - **Config key contains an unknown slug** → warn, drop the unknown entry, continue with the rest. - **Config key contains a known slug not detected on this host** → info, drop, continue. - **Config key contains only undetected slugs** → error with actionable message: `All configured default reviewers are missing on this host: [...]. Install at least one, or pass --all / specific flags.` - **Config key is malformed (e.g., string instead of array)** → schema error at config load, with file path and line number where the parser supports it. - **Slug case sensitivity** → lowercase-normalize on read; document this. - **Duplicates in the array** → de-dup silently. - **User passes `--all` and individual flags together** → existing behavior preserved; this change does not alter that interaction. Confirm in tests. ## Open Questions - **Q-1. Empty-array semantics.** Should `review.default_reviewers: []` be a schema error, or should it fall back to "all detected"? *Proposal: schema error.* **Blocking.** Affects schema validation and tests. - **Q-2. `--no-default` flag.** Is this meaningfully different from `--all`? *Proposal: drop unless implementation surfaces a concrete difference.* - **Q-3. `/gsd-config --integrations` integration.** Land in this pass or as a fast follow? *Proposal: fast follow; depends on contributor bandwidth.* - **Q-4. Slug case handling.** Lowercase-on-read (proposed) or exact-match enforcement at the schema layer? - **Q-5. Verbose-mode "selection source" line.** Always-on or only under `--verbose`? *Proposal: `--verbose` only.* - **Q-6. Cross-runtime sanity.** Any cross-runtime concerns for Codex, OpenCode, Gemini CLI, or Kilo given the existing `resolve_model_ids: "omit"` pattern? *Proposal: none expected — the change operates on detection, not runtime; add at least one non-Claude integration test to confirm.* ## Rollout Plan This is a small, additive, non-breaking change. No migration is required. 1. **Implementation** (one PR) — schema addition, resolution logic, unit + integration tests per P0-8, docs update per P0-7. 2. **Pre-release sanity** — dogfood on a multi-CLI setup; confirm `--all` and individual flags still behave. 3. **Release** in the next minor version (no semver-major bump needed — additive). 4. **Changelog & announcement** — call out in release notes; link to `#3079`; show the two-line config example. 5. **Monitor** `#3079` and any new issues mentioning "default reviewers" or "review.md patch" for 60 days. 6. **Optional fast follow** — P1-1 (`/gsd-config --integrations` integration) if there's contributor bandwidth. ## Timeline Considerations - No hard deadlines. Quality-of-life fix, not contractual or compliance-driven. - No dependencies on other in-flight work. - Size: estimated ≤1 day of engineering for implementation + tests + docs. ## Out-of-Scope (Restated) - No new commands. - No new top-level config namespaces. - No changes to `--all` or individual flag semantics. - No reviewer-output aggregation changes. - No per-phase reviewer profiles in v1 — the namespace is left open. - No GUI/TUI editing of the key in v1. ## Appendix A: Example config ```json { "review": { "default_reviewers": ["gemini", "codex"] } } ``` With this config, `/gsd-review` invokes only Gemini and Codex. `/gsd-review --all` invokes every detected reviewer. `/gsd-review --cursor` invokes only Cursor. ## Appendix B: Glossary - **Reviewer / CLI / backend.** Any code-review-capable CLI or model server GSD can invoke (Claude, Codex, Cursor, Gemini, OpenCode, Kilo, ollama, lm-studio, llama.cpp, …). - **Detected set.** The list of reviewers `detect_clis` finds on the current host at review time. - **Slug.** The lowercase short name of a reviewer used in flags and config (e.g., `gemini`, `codex`). - **"Absent = enabled" pattern.** GSD's convention that missing config keys default to a sensible enabled state. Here, missing `review.default_reviewers` means "all detected." ## References - Feature issue: `#3079` - Configuration reference: `docs/CONFIGURATION.md` — `review.models.`, `review.*_host`, and the **absent = enabled** pattern - Workflow file owning the no-flag branch: `workflows/review.md` (line 52) - Companion ADR: `0011-review-default-reviewers.md` --- ### Adr/0011 Skill Surface Budget Module # Skill Surface Budget Module owns install-time profile staging and runtime surface control - **Status:** Accepted - **Date:** 2026-05-12 - **Decision date:** 2026-05-12 - **Supersedes:** [ADR-0010](0010-skill-surface-budget-module.md) (Skill Surface Budget Module — earlier draft, install-time skill listing curation) - **Subsumed by:** [ADR-857](857-capability-system.md) (Capability system) — generalizes this module; this seam remains live at `src/surface.cts:348` (`applySurface`) - **Implementation:** feat/3408-skills-description-dropped-due-to-size, PR Every installed `gsd-*` skill costs eager system-prompt tokens: runtimes (Claude Code, opencode, and others) enumerate all skill descriptions in `` on every turn. With 66 skills and 33 agents, GSD alone consumes roughly 60% of the default 1%-of-context skill-listing budget, causing descriptions to drop when users stack multiple plugins (#3408). The root problem is an absence of a profile/surface seam: the installer wrote every skill unconditionally, and no runtime-side control existed for enabling or disabling a cohesive group of skills without a full reinstall. ## Decision - Add a **Skill Surface Budget Module** under `gsd-core/bin/lib/install-profiles.cjs` as the single owner for which skills and agents are written to runtime config directories. - Define three named profiles: `core` (six skills covering the main loop), `standard` (core + phase management and workspace skills), and `full` (all skills — the previous default). - Compute each profile's effective skill set as the transitive closure over the `requires:` dependency graph extracted from skill frontmatter, so partial installs never break cross-skill dependencies. - Persist the chosen profile in a `.gsd-profile` marker file in each runtime config directory; `gsd update` reads the marker to honor the profile on re-install. - When multiple runtimes are configured, resolve disagreement to the most-restrictive profile (smallest effective skill set). - Allow profile composition: `--profile=core,audit` resolves to `union(closure(core), closure(audit))`. - Preserve back-compat aliases: `--minimal` and `--core-only` map to `--profile=core`; `MINIMAL_SKILL_ALLOWLIST`, `isMinimalMode`, `shouldInstallSkill`, and `stageSkillsForMode` remain exported for existing callers. - Add a CI gate (`scripts/lint-skill-deps.cjs`, wired into `pretest`) that verifies every skill's `requires:` entries resolve against real skill stems — prevents the profile closure from silently over-installing or breaking. ## Phase 2 — Runtime Surface Module The Phase 2 decision, previously listed as an open question, is recorded here as an amendment to this ADR. ### Decision - Add a `/gsd:surface` slash command with the following sub-commands: - `list` — show all clusters and their enabled/disabled status for the active runtime - `status` — show the active profile, effective skill count, and any dropped-description warnings - `profile ` — switch the active profile and re-stage skills/agents for the current runtime - `disable ` — mark a cluster disabled; re-stage to remove its skills from the runtime config dir - `enable ` — mark a cluster enabled; re-stage to add its skills back - `reset` — clear surface state and re-apply the active profile from `.gsd-profile` - Implement the runtime surface engine in `gsd-core/bin/lib/surface.cjs`, consuming `stageSkillsForProfile` and `stageAgentsForProfile` from the Phase 1 module without duplicating staging logic. - Persist per-runtime surface state in `/.gsd-surface.json`, independent from `.gsd-profile`. The profile marker owns install-time identity; the surface JSON owns session-scope cluster toggles. - Source cluster taxonomy from the research memo §3.2 (2026-05-12-skill-surface-budget.md). Define clusters in `gsd-core/bin/lib/clusters.cjs` — a separate module so the surface engine and future SDK callers can import cluster definitions without loading the full profile module. - Cluster taxonomy: `core_loop`, `audit_review`, `milestone`, `research_ideate`, `workspace_state`, `docs`, `ui`, `ai_eval`, `ns_meta`, `utility`. Membership may overlap; every installed skill stem must appear in at least one cluster (enforced by `tests/surface-clusters.test.cjs`). - Relationship to Anthropic platform asks: Asks D (native per-skill toggle API) and E (budget-fraction negotiation) remain filed separately. The `/gsd:surface` command is a unilateral GSD-side workaround that does not depend on those platform changes. ## Status — Phase 1 shipped Phase 1 artifacts landed on `feat/3408-skills-description-dropped-due-to-size`: - `gsd-core/bin/lib/install-profiles.cjs` — `PROFILES` map, `resolveProfile`, `loadSkillsManifest`, `stageSkillsForProfile`, `stageAgentsForProfile`, `readActiveProfile`, `writeActiveProfile`, `mostRestrictiveProfile`, `resolveEffectiveProfile` - `requires:` frontmatter added to 64 skills in `commands/gsd/*.md` - `scripts/lint-skill-deps.cjs` — CI gate for `requires:` integrity, wired into `pretest` - `bin/install.js` — `--profile=` flag (composable); `--minimal`/`--core-only` as aliases; `.gsd-profile` marker write on install; `gsd update` re-reads marker - Tests: `tests/install-profiles-manifest.test.cjs`, `tests/install-profiles-marker.test.cjs`, `tests/install-profiles-resolve.test.cjs`, `tests/install-profiles-stage.test.cjs`, `tests/lint-skill-deps.test.cjs` Phase 2 shipped on the same branch: - `commands/gsd/surface.md` — `/gsd:surface` slash command runbook (sub-commands: `list`, `status`, `profile `, `disable `, `enable `, `reset`) - `gsd-core/bin/lib/surface.cjs` — runtime engine (`readSurface`, `writeSurface`, `resolveSurface`, `applySurface`, `listSurface`); reuses `stageSkillsForProfile` / `stageAgentsForProfile` from Phase 1 - `gsd-core/bin/lib/clusters.cjs` — 10-cluster taxonomy covering all installed skill stems - Tests: `tests/surface-state.test.cjs`, `tests/surface-clusters.test.cjs`, `tests/surface-resolve.test.cjs`, `tests/surface-apply.test.cjs`, `tests/surface-list.test.cjs` - Persistent surface state: `/.gsd-surface.json` (independent from `.gsd-profile`) ## Open questions - Whether the `requires:` field should also be consumed by `/gsd:help` to annotate dependency chains in help output (follow-up). - Whether telemetry (per-profile install counts, cluster-disable events) should be added to the surface engine or deferred. - Whether Anthropic platform asks D and E (native skill toggle API, budget-fraction negotiation) should block any future work in this module. ## Consequences - Users with constrained context budgets can install `--profile=core` and expand incrementally via `/gsd:surface enable ` without a full reinstall. - The `requires:` closure ensures partial installs never silently break skill cross-references. - Future skills must declare `requires:` dependencies to participate in profile resolution; the lint gate enforces this at CI time. - `CONTEXT.md` gains a canonical **Skill Surface Budget Module** entry; future architecture reviews should treat out-of-seam skill staging as drift. - Cluster definitions in `clusters.cjs` are the authoritative taxonomy for runtime surface control; additions must be reflected there and in tests. ## References - Feature issue: `#3408` - Research memo: `docs/research/2026-05-12-skill-surface-budget.md` (§3.2 cluster taxonomy) - See `0008-installer-migration-module.md` - See `0009-shell-command-projection-module.md` - See `0010-file-operation-engine-module.md` --- ### Adr/0012 Command Routing Hub # CommandRoutingHub as single dispatch seam for CJS command families - **Status:** Superseded by [ADR-0174](0174-retire-gsd-sdk-package-boundary.md) (2026-05-23); originally Accepted (2026-05-20) - **Date:** 2026-05-20 ## Context Seven `*-command-router.cjs` files (`phase`, `phases`, `roadmap`, `state`, `verify`, `validate`, `init`) each duplicate the same three-part dispatch pattern: (1) check `GSD_WORKSTREAM` + `tryLoadSdk()` to decide whether to use the SDK or CJS handler, (2) invoke the selected path, (3) map errors to the `error()` callback. The duplicated mode-selection logic means a policy change (e.g., adding a new fallback condition) must be applied in eight places. Tests for these routers are mock-heavy — they stub `tryLoadSdk`, stub `getExecuteForCjs`, and assert on internal call shapes rather than observable dispatch outcomes. The SDK-vs-CJS fallback decision is smeared across every router, making it impossible to reason about or test the policy in isolation. ## Decision Introduce `CommandRoutingHub` (`gsd-core/bin/lib/command-routing-hub.cjs`) as the single dispatch seam for all CJS command family routers. The hub contract: ``` createHub({ mode: 'sdk' | 'cjs', sdkLoader, cjsRegistry, manifest }) -> hub hub.dispatch({ family, subcommand, args, cwd, raw }) -> Result Result = { ok: true, data } | { ok: false, errorKind, message, details? } ``` Load-bearing design properties: - **Pure result**: the hub never prints to stdout/stderr, never calls `process.exit`, and never throws. All internal throws are caught and converted to `{ ok: false, errorKind: 'HandlerFailure' }`. - **Mode fixed at construction**: `mode` is set once when `createHub` is called; it is never re-evaluated per dispatch call. Each adapter (caller) computes mode based on its own env/sdk-load context before constructing the hub. - **No transparent fallback**: an SDK-mode hub that encounters an SDK crash or load failure returns `{ ok: false, errorKind: 'SdkDispatchFailed' }` or `'SdkLoadFailed'` respectively. It does not silently retry via the CJS registry. - **Closed `errorKind` enum**: the six error kinds (`UnknownCommand`, `InvalidArgs`, `HandlerRefusal`, `HandlerFailure`, `SdkLoadFailed`, `SdkDispatchFailed`) are exported as a frozen `ERROR_KINDS` object. Callers switch on `ERROR_KINDS` values, not bare string literals. Adding a new error kind requires amending this ADR. The router adapter's responsibilities shrink to: determine mode from env, build stubs/registry, construct hub, dispatch, translate the pure Result to `output()`/`error()` calls. Each adapter remains a thin CLI-facing translation layer. `phase-command-router.cjs` is migrated as the proof-of-concept for this PR. Remaining routers migrate in follow-up issues. ## Consequences - **Positive**: policy (mode decision, no-throw contract, error taxonomy) is concentrated in one module rather than duplicated across eight. Testing the policy requires only the hub unit tests; adapter tests verify translation correctness (args → dispatch, Result → output/error). - **Positive**: future routers can be onboarded by wiring `cjsRegistry` entries rather than hand-replicating the SDK/CJS conditional block. - **Constraint**: adding a new `errorKind` value requires updating `ERROR_KINDS` in `command-routing-hub.cjs` AND amending this ADR. The closed enum is the drift-prevention property; the amendment requirement makes scope of impact explicit. - **Constraint**: each adapter must compute mode before hub construction (no lazy re-evaluation). This is intentional — mode ambiguity at dispatch time is a prior source of subtle test flakiness. ## Known limitation: SDK-incomplete subcommands The hub's mode is fixed at construction (`'sdk'` or `'cjs'`). This works cleanly only when every subcommand in a family has an implementation in the active mode. Today some phase subcommands have divergent CJS and SDK implementations. `phase.mvp-mode` is present in the SDK catalog (`command-static-catalog-domain.ts`) but its CJS-native implementation (`phase.cmdPhaseMvpMode`) differs in ROADMAP scan behaviour and error reason codes from the SDK query layer. Routing `mvp-mode` through the SDK hub would silently change observable CLI behaviour (exit codes, JSON error shape). The proof-of-concept adapter (`phase-command-router.cjs`) handles this with an early-return bypass: `mvp-mode` is intercepted before the dispatch call so it never reaches the hub. This preserves observable behavior but introduces a hub-level abstraction leak — the adapter now carries per-subcommand routing policy that the hub was meant to own. Future direction (deferred): the hub should consult `manifest` to detect per-subcommand SDK coverage and route to CJS automatically for subcommands not present in the SDK manifest. That refinement stays inside the global-mode decision — the mode still applies to the family as a whole — and avoids the per-command policy ladder that was explicitly rejected during design. This work is tracked alongside SDK-CJS migration #3524 closure. ## References - Extends ADR-0001 (Dispatch Policy Module) — the hub implements the no-throw + structured-result contract ADR-0001 established for the SDK query layer, applying it to the CJS adapter layer. - Issue: [#3788](https://github.com/open-gsd/get-shit-done-redux/issues/3788) --- ### Adr/0174 Retire Gsd Sdk Package Boundary # ADR-0174: Retire @opengsd/gsd-sdk package boundary — single-runtime collapse - **Status:** Accepted (2026-05-23); amended #1642 (2026-06-23) — §5 reconciled to as-built Result type + `exitReason?` field added on `InvalidArgs` - **Date:** 2026-05-23 - **Tracking issue:** [#174](https://github.com/open-gsd/get-shit-done-redux/issues/174) — sub-issues #175–#197 ## Supersedes | ADR | What it said | Why it is superseded | |-----|-------------|----------------------| | [ADR-0005](0005-sdk-architecture-seam-map.md) | Established the SDK as a composition of explicit seam Modules with thin Adapters, with the SDK package itself as the composition root. | The SDK package boundary is being retired; the seam-Module vocabulary survives intact under a single `src/` tree inside `get-shit-done-cc`. | | [ADR-0007](0007-sdk-package-seam-module.md) | Defined one explicit SDK Package Seam Module for the `@opengsd/gsd-sdk` → `@opengsd/get-shit-done-redux` compatibility transition. | The transition scaffolding this Module owned (install-layout probing, legacy-asset discovery, compatibility diagnostics) is deleted when the SDK package boundary is retired. | | [ADR-0012](0012-command-routing-hub.md) | Introduced `CommandRoutingHub` with a `mode: 'sdk' \| 'cjs'` parameter, `sdkLoader`, `cjsRegistry`, and the `SdkDispatchFailed` / `SdkLoadFailed` errorKinds as the dispatch seam for CJS command families. | The Hub survives but is simplified: `mode`, `sdkLoader`, `cjsRegistry`, and SDK-failure errorKinds are deleted because there is no second runtime to select. The four surviving cross-cutting concerns (errors, manifest, args, observability) remain in the Hub and are now unambiguously load-bearing. | | [ADR-3524](3524-cjs-sdk-hard-seam.md) | Hardened the CJS↔SDK seam with a generator-based Shared-Module Source Policy (one source of truth per Module, generated artifacts, freshness checks, hand-sync pair lint) as the canonical Phase 5 engine. | The generator pattern solved drift within the dual-runtime world. Collapsing onto a single TypeScript source tree in `src/` eliminates the seam these generators bridged; tsc replaces every `.generated.cjs` artifact. | ## Context The following forced-decisions explain why this consolidation is happening now rather than continuing the ADR-3524 trajectory: 1. **Lived cost of dual-runtime exceeded the value of a separately-installable typed package.** ADR-3524 was correct about the drift problem and correct that a generator pattern was better than hand-sync. But the generator pattern is infrastructure to maintain parity between two runtimes, not value delivered to users. The total scaffolding footprint reached ~120 files: worker pool, per-Module generators, freshness checks, parity tests, transition shims, two release pipelines, and the bridge that ran the async SDK handler synchronously via `synckit` for CJS callers. None of that scaffolding was visible to users as a feature. 2. **CJS already provides the feature set in-process; the SDK package boundary added cost without user-visible capability.** Every capability the SDK was being built to expose — typed dispatch, structured results, observability, command metadata — can be compiled out of a single TypeScript source tree and required directly. The separately-installable `@opengsd/gsd-sdk` package had no external programmatic API user requirement driving it. It was scaffolding for scaffolding. 3. **The deletion test concentrated complexity rather than spreading it.** Removing the SDK package as a whole eliminates an entire surface: the bridge, the generators, the release pipeline, the parity tests. Keeping the SDK package and merging only the bridge would have preserved parity tests, generators, and the second release pipeline indefinitely. 4. **ADR-0012's `mode` parameter was the contested part of the Hub design.** Review comments during the ADR-0012 PR identified mode-selection, sdkLoader injection, and the SdkDispatchFailed errorKind as the fragile parts of the Hub contract. These concerns disappear when there is one runtime. The four surviving concerns — uniform error contract, manifest-backed resolution, arg shape coercion, observability — are clearly load-bearing and uncontested. Stripping mode makes the Hub stronger, not weaker. 5. **External programmatic API surface is not a user requirement.** The user does not need `@opengsd/gsd-sdk` to be installable by external consumers. The typed contract is internal. Retaining the package boundary would mean paying for SDK scaffolding in perpetuity to support a use case that does not exist. ## Decision ### 1. Single npm package `get-shit-done-cc` is the sole npm package. `@opengsd/gsd-sdk` is retired. No external programmatic API is exposed. The typed contract is internal to `get-shit-done-cc`. ### 2. Source shape — TypeScript canonical in `src/`, compiled to CJS in `dist/` `src/` is the hand-authored source of truth for all Modules. `tsc` on `prepublishOnly` compiles `src/` to CJS in `dist/`. `bin/gsd-tools.cjs` becomes a thin shim that `require`s from `dist/`. No `synckit`, no worker pool, no generator scripts replacing tsc. The `.generated.cjs` artifacts in `bin/lib/` are deleted; their callers are updated to require from `dist/`. ### 3. Source tree — seam-aligned subdirectories under `src/` Each subdirectory maps 1:1 to an architectural concern, preserving the seam-Module vocabulary from ADR-0005 under a single tree: | Directory | Concern | |-----------|---------| | `src/dispatch/` | The simplified Hub | | `src/handlers/` | Command implementations grouped by family (`phase/`, `roadmap/`, `state/`, `init/`, …) | | `src/errors/` | `GSDError`, `errorKind` enum, error classification | | `src/manifest/` | Command metadata, alias resolution | | `src/config/` | Configuration Module (was Shared CJS/SDK) | | `src/state/` | STATE.md Document Module | | `src/workstream/` | Workstream Inventory Module | | `src/runtime/` | Runtime Name Policy, project-root resolution | | `src/cli/` | `gsd-tools` entrypoint code | | `src/observability/` | DispatchLogger, redaction | ### 4. Hub simplified — four cross-cutting concerns, no mode parameter `CommandRoutingHub` is retained with its no-throw contract and closed `errorKind` enum, but the following are deleted: `mode: 'sdk' | 'cjs'` parameter, `sdkLoader`, `cjsRegistry`, `SdkDispatchFailed` errorKind, `SdkLoadFailed` errorKind. The Hub owns exactly four cross-cutting concerns: 1. **Uniform error contract** — all internal throws are caught and converted to structured `Result` values; the Hub never calls `process.exit` or prints to stdout/stderr. 2. **Manifest-backed resolution** — command lookup and alias resolution are routed through `src/manifest/`. 3. **Arg shape coercion** — incoming arg shapes are normalized before handler invocation. 4. **Observability** — DispatchLogger is injected; the Hub emits `DispatchEvent` records on every dispatch path. ### 5. Sync dispatch with tight-typed `Result` per `errorKind` variant Dispatch is synchronous: `dispatch(req: DispatchRequest): Result`. Rationale: continuous stack traces, no async-boundary races in the logger, no orphaned side effects, SIGINT shows what is actually running. `synckit` dependency is removed. The `Result` type is a discriminated union per `errorKind` variant, not a flat string field. The as-built type (in `src/command-routing-hub.cts`) is: ```ts type Result = | { ok: true; data: T } | { ok: false; kind: 'UnknownCommand'; command: string } | { ok: false; kind: 'InvalidArgs'; arg: string; reason: string; exitReason?: string } | { ok: false; kind: 'HandlerRefusal'; reason: string } | { ok: false; kind: 'HandlerFailure'; message: string; cause?: Error }; ``` > **Drift note (amendment #1642, 2026-06-23):** the original §5 text specified a different planned shape — `'Unknown'` / `'BadArgs'` / `'ValidationFailed'` / `'NotImplemented'` / `'HandlerFailed'`. The SDK retirement migration kept the ADR-0012 names (`UnknownCommand` / `InvalidArgs` / `HandlerFailure`) and never added the planned `ValidationFailed` or `NotImplemented` variants; `HandlerRefusal` was added during implementation but never back-filled into this ADR. This amendment reconciles the ADR to the as-built code so the contract documented here matches what consumers actually depend on. The drift was caught during architecture review (parent #1641). **Factories** (`src/command-routing-hub.cts`): ```ts makeUnknownCommand(command: string) → Readonly makeInvalidArgs(arg: string, reason: string, exitReason?: string) → Readonly makeHandlerRefusal(reason: string) → Readonly makeHandlerFailure(message: string, cause?: unknown) → HandlerFailureResult ``` **The `exitReason?` field on `InvalidArgs`** (added by this amendment) carries an `ERROR_REASON` enum value (e.g. `ERROR_REASON.USAGE`) separately from the existing `reason` explanation text. This lets routers that today call `error(msg, ERROR_REASON.USAGE)` directly — bypassing the Hub — preserve `ERROR_REASON` granularity when they migrate to returning `makeInvalidArgs(...)` Results through the Hub. The field is optional and additive; existing callers are unaffected. **Dispatcher translation contract:** when an adapter translates an `InvalidArgs` Result whose `exitReason` is present, it passes `exitReason` as the second argument to `error(message, exitReason)` so the JSON-error envelope (`GSD_JSON_ERRORS=1`) preserves the typed reason for downstream consumers (CLI tests, integration harnesses). Adding a new variant **or adding a field to an existing variant** requires amending this ADR (preserving the drift-prevention property from ADR-0012). ### 6. Observability seam — silent on success, structured JSON on error, opt-in audit - **Silent on success** — no stdout/stderr output from the Hub on a successful dispatch. - **Structured JSON to stderr on error** — every `Result` with `ok: false` emits a structured JSON line to stderr with `traceId`, `kind`, and the variant's typed payload. - **Opt-in file audit** — `GSD_AUDIT=1` env var or `config.audit.enabled: true` writes `DispatchEvent` records to `.planning/.gsd-trace.jsonl`. Args are excluded by default (privacy); `GSD_AUDIT_ARGS=1` opts in. - **Trace identity** — every `DispatchEvent` carries a `traceId`. Composed dispatches set `parentTraceId` to link children to the parent invocation. - **Injected logger** — the Hub accepts a `DispatchLogger` interface. The default implementation writes per the rules above. The test implementation is in-memory and carries no I/O side effects. ### 7. `Init.*` family stays as a composer module `src/handlers/init/composer.ts` composes N atomic dispatches. Each child dispatch receives the parent's `traceId` as its `parentTraceId`, linking the full init tree in the audit trail. The composer is the only caller that sets `parentTraceId`; all other dispatches are leaf dispatches. ## Consequences ### Positive - **Locality up.** Composition concerns are concentrated in one place per seam. The seam-Module vocabulary from ADR-0005 is preserved; only the package boundary is retired. - **Leverage up.** One build pipeline, one `npm publish`, one CI release workflow. The `release-sdk.yml` workflow and all SDK steps in `release.yml`, `hotfix.yml`, and `install-smoke.yml` are deleted. - **Test surface down ~35 files; character shifts from ~40% infrastructure / 60% behavior to ~5% / 95%.** The parity tests, generator freshness checks, hand-sync pair lint, bridge unit tests, and worker-pool integration tests are deleted outright. Tests that were testing infrastructure (does the generator emit the right bytes?) become irrelevant. Tests that verify observable dispatch outcomes survive and are the dominant surface. - **Debuggability up.** Continuous stack traces from the caller through the Hub to the handler. `traceId` trees link composed dispatches in the audit log. No async-boundary gaps in log entries. SIGINT shows the actual call in progress. - **`synckit` dependency removed.** The in-process event-loop bridge that ran async SDK handlers synchronously for CJS callers is deleted with it. ### Negative - **External programmatic API surface retires.** Accepted by the user — it is not a requirement. Any future external API surface would be a new design decision, not a reversion. - **CONTEXT.md and ~20 doc files require updating across Phase 6 PRs.** The "Shared CJS/SDK Module" qualifier, SDK seam descriptions, and references to the `sdk/` directory structure are updated in Phase 6 (deferred; this ADR does not touch CONTEXT.md). - **~15–18 PRs of implementation work across 7 phases.** See Migration Plan below. - **`tsc` build step added to `prepublishOnly`.** `get-shit-done-cc` currently has no TypeScript compilation at the root. The build step is net-simpler than the existing generator infrastructure, but it is a new step in the publish path. ## Alternatives considered ### Shape A — pure CJS + JSDoc, zero build step Rejected. Loses TypeScript authoring ergonomics: no structural type checking, no discriminated-union narrowing, no intra-repo cross-Module type errors caught at compile time. The SDK was correct that TypeScript authoring is worth the compilation step; this alternative gives up the wrong thing. ### Shape C — CJS + `.d.ts` overlays Rejected. Recreates the parity problem at a smaller scale: hand-authored `.d.ts` files drift from the `.cjs` implementations unless a freshness check is added, which is the generator pattern again at half the scale. The root cause (two artifacts for one behavior surface) is not addressed. ### Keep SDK package, merge only the bridge Rejected. Deleting `synckit` without deleting the SDK package preserves parity tests, generator scripts, and the second release pipeline indefinitely. The bridge was not the sole source of complexity — it was the most visible symptom. Merging only the bridge trades a runtime dependency for an ongoing infrastructure maintenance burden without eliminating the dual-runtime cost. ### Keep dual runtime indefinitely (status quo) Rejected. Lived experience showed the cost ratio is wrong: ~120 files of scaffolding for a feature set CJS provides in-process, with no external programmatic API user requirement to justify the separately-installable package. The incremental fix cycle (each drift class fixed once in CJS, once in SDK, once in the generator) compounds with every new Module added to the Shared-Module table. ## Migration plan Seven phases, ~15–18 PRs total. Each phase is a coherent slice that leaves the codebase in a working state. | Phase | Description | PRs | |-------|-------------|-----| | 1 — Simplify the Hub in place | Drop `mode`, introduce tight-typed `Result`, add observability seam, add `traceId`. | ~4 | | 2 — Move TS source from `sdk/` to `src/` | Per-Module migration: config, state, workstream, runtime, manifest, errors, observability, dispatch, cli, handlers. | ~8 | | 3 — Retire parity layer | Delete parity tests; replace `.generated.cjs` generator scripts with tsc output in `dist/`; update callers. | ~3 | | 4 — Collapse bridge | Inline bridge logic into Hub; delete `synckit`; delete `bin/lib/cjs-sdk-bridge.cjs` and `sdk/src/runtime-bridge-sync/`. | ~1 | | 5 — Retire SDK package | Delete `sdk/` directory; delete `bin/gsd-sdk.js` shim and `bin/gsd-sdk` wrapper; delete `release-sdk.yml` and SDK steps in `release.yml`, `hotfix.yml`, `install-smoke.yml`. | ~2 | | 6 — Docs cleanup | Update CONTEXT.md, `docs/`, workflow markdown, and localized docs to remove SDK references. | ~4 | | 7 — Land this ADR's PR | The PR for this ADR closes the umbrella tracking issue. | 1 (this PR) | Implementation is tracked in [#174 — sub-issues #175–#197](https://github.com/open-gsd/get-shit-done-redux/issues/174). --- ### Adr/0656 Research Module Seam # ADR-0656: Research Module — L2-hybrid seam for cached, curated-first research - **Status:** Accepted - **Date:** 2026-06-03 ## Context Research in GSD was entirely prose-duplicated. Seven researcher agents each carried their own copy of the provider waterfall (Context7, Ref, Jina, Exa, Tavily, Perplexity, Brave, Firecrawl, websearch), their own confidence-tier definitions, and their own fallback policy. Every time a new provider was added or the ordering changed, all seven files drifted independently — the exact failure mode `META.RULE.brief-no-paraphrase` exists to prevent. There was no research cache. Agents checked for an existing `RESEARCH.md` file but had no TTL, no content-addressing, and no notion of staleness. Identical queries re-fetched from live providers across phases and projects. Package legitimacy was a pip-install `slopcheck` bolt-on. When the `slopcheck` binary was absent or crashed, every package was silently downgraded to `[ASSUMED]`, removing the legitimacy gate entirely rather than degrading gracefully. Context7 was prompt-only: agents mentioned it in prose but there was no code-level integration, no cache, and no structured verdict returned to the orchestrator. ## Decision Introduce an **L2-hybrid seam**: code owns cache, provider policy, legitimacy verdicts, and confidence classification; MCP owns the actual network fetch (a `.cjs` module cannot call MCP tools directly). Three modules are introduced under `src/` compiled to `gsd-core/bin/lib/*.cjs` per ADR-457 (generated-single-source): **Research Store** (`src/research-store.cts`): content-addressed cache keyed by `sha256(ecosystem + library + version + query + kind)`. `getResearch()` never throws — it returns `{ hit, stale }` mirroring the graphify staleness tri-state pattern. TTL is per-source: curated-doc providers get 30 days (HIGH), medium-quality sources get 7 days (MED), web/synthesis gets 1 day (LOW). Two storage tiers: curated-doc kinds write to `~/.gsd/research-cache` (cross-project reuse); web and synthesis results write to `.planning/research/.cache` (project-local, gitignored). **Research Provider** (`src/research-provider.cts`): single source of truth for `PROVIDER_WATERFALL`. Docs waterfall: Context7 → Ref → Jina → websearch. Web waterfall: Exa → Tavily → Perplexity → Brave → websearch. Scrape: Firecrawl → Jina (Firecrawl is scrape-only, not in docs/web discovery). `planResearch()` returns cache hits plus a fetch plan for misses. `classifyConfidence()` stamps `HIGH | MEDIUM | LOW` by provider authority + verification evidence — the tier set is unchanged (ADR-consistent), but HIGH now requires code-computed ground-truth corroboration (e.g. `legitimacyVerdict: 'OK'`); provider authority alone caps at MEDIUM; `SLOP` caps at LOW. Provider availability is driven by config flags and `_API_KEY` env vars; `context7`, `jina`, and `websearch` are always available as the terminal fallback. **Package Legitimacy** (`src/package-legitimacy.cts`): registry-API verdicts via injectable adapters for npm, PyPI, and crates.io. Thresholds: `{ minAgeDays: 30, minWeeklyDownloads: 1000, requireRepo: true }`. Verdict per package: `OK | SUS | SLOP`. `slopcheck` is an optional escalate-only adapter — it can only raise a verdict, never lower it — and is not the install-or-degrade gate. Absence of `slopcheck` leaves registry-API verdicts intact rather than downgrading everything to `[ASSUMED]`. All three modules are reachable via `gsd-tools query research-plan | research-store | package-legitimacy`. Agents return a `RESEARCH.md` path; they never return raw fetched content. This enforces context discipline: subagent isolation, compact provider output, fetches-to-disk, cache-returns-digest. ## Consequences **Positive:** - Provider policy lives in one tested module. Adding or reordering a provider is a one-line change that propagates to all researcher agents. - Content-addressed cache eliminates redundant fetches across phases and projects. - Package legitimacy is registry-API-first and degrades gracefully; `slopcheck` enriches without gating. - The `gsd-tools query` interface is the test surface — behavioral tests can assert typed JSON output without source-grep. **Deferred to #657:** - Collapsing the seven researcher agent `.md` files into generated-from-profiles agents (the prose waterfall duplication in those files is the primary `DEFECT.RESEARCH-PROVIDER-PROSE-DRIFT` site). - The `install.js` MCP tool-mapping for tavily, ref, and jina (those land where the agents declare the tools they need). **Known constraint:** API context-editing primitives (`clear_tool_uses`, memory tool) are the conceptual model for context discipline, but they are not configurable through the Claude Code harness today. The current implementation achieves context discipline through subagent isolation and fetch-to-disk patterns. --- ### Adr/15 Autonomous Cross Ai Convergence # Cross-AI Plan Convergence via Existing Orchestration Commands - **Status:** Accepted — ratified 2026-07-17 (originally Proposed 2026-05-24); see "Ratification" below - **Date:** 2026-05-24 - **Issue:** #15 Current orchestration commands (`/gsd-autonomous` and `/gsd-progress --next --auto`) route planning through `gsd-plan-phase` and only use local/Claude subagent review paths. The cross-AI convergence path already exists (`/gsd-plan-review-convergence`, `/gsd-review`, `review.default_reviewers`, `review.models.*`) but is not wired into these orchestrators. This creates a gap: users can configure cross-AI reviewers yet still get local-only planning in autonomous/auto-chain execution. ## Ratification (2026-07-17): Proposed → Accepted Ratified by explicit maintainer directive after the shipped implementation was independently re-verified; the Status field had read "Proposed" for roughly 8 weeks after the underlying decision had already landed. **Evidence the decision shipped:** - Primary, parity, and alias surfaces are present verbatim: `commands/gsd/progress.md:4,28` (`--next --converge`, `--cross-ai` alias, reviewer flags, `--max-cycles N`) and `commands/gsd/autonomous.md:4,40-41` (`--converge`, `--cross-ai` alias). - The `plan_strategy=local|converge` seam is implemented in `gsd-core/workflows/next.md:260-313` (`PLAN_STRATEGY` parsing, `CONVERGENCE_ARGS` build, feature-gate check, Route-3 override) and mirrored in `gsd-core/workflows/autonomous.md:19-90,378-419`. - Fail-fast-on-disabled-gate behavior matches the ADR's Failure Policy exactly: `next.md:279-292` and the equivalent block in `autonomous.md` check `workflow.plan_review_convergence` via `config-get` and abort with the exact `gsd config-set workflow.plan_review_convergence true` instruction — no silent downgrade to `local`. - The config contract is shipped: `gsd-core/bin/shared/config-schema.manifest.json:36` (`workflow.plan_review_convergence`), `:54` (`review.default_reviewers`), `:123,141` (`review.models.*`); documented identically in `docs/CONFIGURATION.md:225,316` and `docs/COMMANDS.md:620-622,850-852`. - Dedicated regression tests exist: `tests/adr-15-progress-converge.test.cjs` (179 lines, describe block titled `'ADR-15: /gsd:progress --next --auto --converge (#1190)'`) and `tests/autonomous-converge.test.cjs` (225 lines, covering the parity surface under `'autonomous --converge flag (#711)'` — this file does not itself reference ADR-15 by name). - Landing commits: `092340d18` (`fix(#711): wire autonomous convergence flag`, 2026-06-10, parity surface) and `0b3a2e5f9` (`feat(#1190): wire --converge primary surface into /gsd:progress --next (ADR-15) (#1237)`, 2026-06-14) — the latter's commit body states "ADR-15 designates /gsd-progress --next --auto --converge as the PRIMARY plan-convergence surface" and confirms the wiring gap the ADR called out is closed. - No later ADR references or supersedes ADR-15: `grep -rl 'ADR-15' docs/adr/*.md` returns only `docs/adr/README.md`'s own index row (line 158), which still lists it as "Proposed" — the stale bookkeeping entry this ratification corrects. **Governance state:** Issue #15 CLOSED — stateReason COMPLETED (closed 2026-05-25T03:12:26Z). Follow-up test-coverage issue #1190 ("test(coverage): fill Proposed-ADR test gaps") also CLOSED — stateReason COMPLETED (closed 2026-06-14T19:52:24Z). ## Decision Do not add a new command. Add convergence as an orchestration policy in existing commands, with `/gsd-progress` as the primary operator surface. 1. Add a shared **plan strategy seam** for orchestration workflows: - `plan_strategy=local|converge` - `local` maps to `gsd-plan-phase` - `converge` maps to `gsd-plan-review-convergence` 2. Expose the strategy via existing entry points: - `/gsd-progress --next --auto --converge` (primary) - `/gsd-autonomous --converge` (parity path for users who prefer autonomous directly) - keep `--cross-ai` as a compatibility alias for `--converge` 3. Reuse existing reviewer selection semantics from `/gsd-review` and `/gsd-plan-review-convergence`: - explicit reviewer flags (`--codex`, `--gemini`, `--claude`, `--opencode`, `--ollama`, `--lm-studio`, `--llama-cpp`) - `--all` - `review.default_reviewers` and `review.models.*` config 4. Add pass-through flags (no new command surface): - `--converge` (primary) - `--cross-ai` (alias) - reviewer selector flags listed above - `--max-cycles N` (forwarded per phase) 5. Keep convergence behind existing feature gate: - if `workflow.plan_review_convergence=false` and `--converge` (or alias) is requested, fail fast with actionable enable instructions. 6. Keep post-execution review behavior unchanged in this slice (`gsd-code-review` and `gsd-ui-review` stay as-is). Cross-AI code-review fanout is deferred. 7. Define convergence eligibility and allowed AIs via config (no new command): - enable gate: `workflow.plan_review_convergence=true` - allowed reviewer set: `review.default_reviewers` (for no-flag converge runs) - per-reviewer model selection: `review.models.*` ## Interface Contract ### Existing CLI Surfaces (No New Command) - `/gsd-progress --next --auto [--converge|--cross-ai] [reviewer flags] [--max-cycles N]` - `/gsd-autonomous [existing flags] [--converge|--cross-ai] [reviewer flags] [--max-cycles N]` ### Planning Step Routing - `plan_strategy=local`: - orchestrator step uses `gsd-plan-phase` (current behavior). - `plan_strategy=converge`: - orchestrator step uses `gsd-plan-review-convergence`. - convergence workflow remains owner of HIGH counting (`CYCLE_SUMMARY`), stall detection, and escalation. ### Failure Policy - If `--converge` (or `--cross-ai`) is requested but convergence gate is disabled: - stop before planning dispatch - emit exact enable command: - `gsd config-set workflow.plan_review_convergence true` - no silent downgrade to `local` strategy. ### Configuration Contract (Enable + Allowed AIs) Convergence is configurable without introducing new config namespaces. 1. Enable convergence: - `workflow.plan_review_convergence: true` 2. Define which AIs are allowed by default for convergence runs: - `review.default_reviewers: ["codex", "gemini"]` (example) 3. Optionally pin models per allowed reviewer: - `review.models.codex`, `review.models.gemini`, etc. Precedence for reviewer selection in converge mode: 1. Explicit CLI reviewer flags (`--codex`, `--gemini`, `--all`, etc.) 2. `review.default_reviewers` 3. If neither resolves to any reviewer, fail fast with actionable message. Example config: ```json { "workflow": { "plan_review_convergence": true }, "review": { "default_reviewers": ["codex", "gemini"], "models": { "codex": "gpt-5.4", "gemini": "gemini-2.5-pro" } } } ``` ## Flag Naming Issue #15 asks for a flag such as `--converge` or `--cross-ai` on autonomous execution. `--converge` is the better primary term because it names the behavior (plan-review convergence loop), not the transport (external AI) or mode label (`autonomous`). 1. Primary: `--converge` 2. Alias: `--cross-ai` 3. Avoid: introducing `--autonomous-*` variants (the command already defines that mode) ## Options Considered 1. **Autonomous-only flag (`/gsd-autonomous --cross-ai`)** - Files: `commands/gsd/autonomous.md`, `workflows/autonomous.md` - Problem: solves issue #15 directly but leaves `/gsd-progress --next --auto` inconsistent. - Benefit: smallest blast radius. - Drawback: two orchestration modes diverge in behavior. 2. **Progress-primary + autonomous parity (Chosen)** - Files: `commands/gsd/progress.md`, `workflows/progress.md`, `workflows/next.md`, plus autonomous wiring - Problem: must keep two orchestrators aligned. - Solution: one shared plan-strategy seam consumed by both commands. - Benefit: better locality; users who already drive from `progress --next --auto` get convergence without switching workflows. 3. **Config-only global toggle (no per-run flag)** - Files: config schema + both orchestrators - Benefit: minimal CLI syntax expansion. - Drawback: less control per run; harder to do targeted high-cost convergence only when needed. - Decision: defer; keep explicit runtime flag. ## Rubber-Duck Design Notes Expected behavior: the two existing orchestration entry points should be able to opt into cross-AI plan convergence without adding another top-level command. Actual behavior: both orchestration entry points always take the local planning route, so external reviewers are never reached unless the user abandons orchestration flow and runs convergence manually. Wrong assumptions surfaced: 1. "Enabling `workflow.plan_review_convergence` changes orchestration behavior." It does not unless the convergence command is explicitly routed. 2. "Cross-AI config propagates automatically into autonomous/next flows." It only applies where convergence/review workflows are invoked. 3. "Adding a separate command is required." Existing orchestration commands are sufficient if they expose a strategy seam and clear flag naming. Root architectural gap: orchestration flows lack a plan strategy seam (`local` vs `converge`). ## Scope ### In scope - Plan strategy seam shared by existing orchestration commands. - `--cross-ai` pass-through contract on existing commands. - `--converge` primary flag naming and `--cross-ai` compatibility alias. - Feature-gate behavior contract for convergence strategy. - Config contract for enabling convergence and selecting allowed AIs. - Documentation updates tied to command/config behavior. ### Out of scope - New top-level command creation. - Reworking `gsd-code-review` into cross-AI convergence loop. - New reviewer config schema (reuse existing `review.*` keys). - Changing default planning strategy without explicit opt-in. - Altering `gsd-plan-review-convergence` internal loop semantics. ## Consequences - No new command tax on docs, routing, and long-term maintenance. - Existing orchestration habits (`progress --next --auto` and autonomous) can opt into convergence consistently. - Existing review configuration gets leverage without new schema. - Backward compatibility is preserved by default. - Explicit failure on disabled gate avoids silent false-confidence automation. ## References - Issue: #15 - `commands/gsd/progress.md` - `gsd-core/workflows/progress.md` - `gsd-core/workflows/next.md` - `commands/gsd/autonomous.md` - `gsd-core/workflows/autonomous.md` - `commands/gsd/plan-review-convergence.md` - `gsd-core/workflows/plan-review-convergence.md` - `commands/gsd/review.md` - `docs/COMMANDS.md` (`/gsd-plan-review-convergence`, `/gsd-review`) - `docs/CONFIGURATION.md` (`workflow.plan_review_convergence`, `review.default_reviewers`, `review.models.*`) --- ### Adr/22 Plan Drift Guard # Plan-vs-codebase drift guard: defaults and symbol-resolver seam - **Status:** Accepted — ratified 2026-07-17 (originally Proposed 2026-05-29); see "Ratification" below - **Date:** 2026-05-29 - **Issue:** open-gsd/gsd-core#22 ## Ratification (2026-07-17): Proposed → Accepted Ratified by explicit maintainer directive; the Status field sat at Proposed for roughly 14 months against a decision that in fact shipped and closed within a day of the ADR being written (issue closed 2026-05-30, one day after the 2026-05-29 ADR date). **Evidence the decision shipped** - `src/plan-drift-guard.cts` implements the ADR's authority ladder and severity table as a pure decision module: `AUTHORITY_RUNGS` (grep=0…scip=4), `getEffectiveAuthority()` (auto-upgrades `grep`→`intel` when `intel.enabled`), and `classifyDriftSeverity()` producing the exact table (VERIFIED→none, MISSING@rung<3→needs-acknowledgement, MISSING@rung>=3→HIGH/hardBlock, AMBIGUOUS→MEDIUM, UNCHECKABLE→INFO); compiled to `gsd-core/bin/lib/plan-drift-guard.cjs` (gitignored generated artifact, `.gitignore:101`). - `gsd-core/bin/shared/config-defaults.manifest.json:94-97` sets `plan_review.source_grounding` default `true` and `plan_review.source_grounding_authority` default `'grep'` — the default-on verification pass from Part 1 point 1. - `capabilities/intel/capability.json` keeps `intel.enabled` default `false` and wires its `plan:pre` step (`intel api-surface`) with `onError: "skip"` — `intel.enabled` stays opt-in and the injection never blocks, per Part 1 points 2-3. - `gsd-core/workflows/plan-review-convergence.md`'s "Source-grounding pass" section (~lines 184-208) implements the four-valued resolver contract (VERIFIED/MISSING/AMBIGUOUS/UNCHECKABLE), excludes plan-declared "Artifacts this phase produces," delegates severity to the `drift-guard` CLI seam rather than inline reviewer reasoning, and appends a "Verification coverage" block to REVIEWS.md. - `gsd-core/workflows/plan-phase.md` §7.9 ("Regenerate API-SURFACE.md (intel gate)") regenerates the surface only when the intel step hook is active and injects it into the planner prompt labeled "HINT ONLY... MAY BE INCOMPLETE... Never treat the surface as exhaustive" — matching Part 1 point 2 verbatim. - `gsd-core/workflows/settings.md` and `gsd-core/workflows/new-project.md` surface `plan_review.source_grounding` as a "Drift Guard" toggle/setup question; `docs/CONFIGURATION.md` documents both config keys, explicitly marking authority rungs 2-4 (treesitter/lsp/scip) as reserved with no effect in the current release. Governance: owning issue open-gsd/gsd-core#22 — CLOSED, stateReason COMPLETED, closed 2026-05-30T21:08:13Z, labeled `enhancement` + `approved-feature`. ## Context The planner regularly cites symbols that do not exist in the codebase — invented decorators, wrong dataclass fields, renamed CLI flags, mismatched signatures. The phenomenon is measured, not anecdotal: the *Practical Code Generation* hallucination taxonomy (arXiv:2409.20550) reports Dependency Conflicts (11.26%) and API Knowledge Conflicts (20.41%), which together describe exactly this failure. Today the drift is caught only at execution time by the executor (ImportError/AttributeError), at roughly 10–15 min/fix, a dozen per multi-wave phase. `/gsd:plan-review-convergence` does not catch it because planner and reviewer both read the same channel (other plan files); the drift originates in that channel. The fix must introduce an out-of-band source of truth: the project's own source code. GSD ships `intel.cjs` (`api-map.json` et al.), but: enabling `intel.enabled` populates nothing (the `gsd-intel-updater` LLM agent is never auto-spawned; population is a manual `/gsd:map-codebase --query refresh`); extraction is regex, JS/CJS/ESM only; intel is stale after 24h with no auto-refresh (`intelUpdate()` is a stub); useful intel therefore costs recurring LLM-agent token spend. The feature has two halves with different dependencies: a **verification pass** that reads live source (needs no intel, works in any language) and a **surface-injection** step that renders `api-map.json` into `API-SURFACE.md` for the planner (needs intel). ## Decision ### Part 1 — Defaults 1. Ship the **verification pass on by default**, behind a new, additive config key `plan_review.source_grounding` (boolean, default `true`, opt-out). Surface it as one question in `/gsd:new-project` (default Y) and as a toggle in `/gsd:settings`. It stays on permanently with an easy off switch. 2. The **`API-SURFACE.md` injection** stays gated on the existing `intel.enabled` (default unchanged). It ships in this release but only activates for projects that opted into intel and populated it. Its planner instruction is a **hint** ("prefer symbols in API-SURFACE.md; it may be incomplete"), never a hard rule. 3. **`intel.enabled` stays opt-in.** Flip its default to `true` only once all three hold: (a) deterministic population (tree-sitter/CJS parse, not an LLM agent); (b) auto-refresh on staleness; (c) multi-language coverage. None of these block the drift guard, because the default-on half does not depend on intel. 4. Only new, additive config keys are introduced. No pre-existing default is changed. ### Part 2 — Symbol-resolver seam The reviewer pass depends on a resolver seam, not a hardcoded tool: resolve(ref) -> VERIFIED{location, exported, signature?} | MISSING | AMBIGUOUS{candidates} | UNCHECKABLE{reason} Resolution is **three-valued, not boolean**. `UNCHECKABLE` (the adapter cannot analyze this language or symbol kind) never blocks and never falsely blesses; it is recorded as a coverage gap. Only `MISSING` from a *capable* adapter is actionable. Adapters form an authority ladder, selected by `plan_review.source_grounding_authority` (enum; default `grep`, auto-upgrades to `intel` when `intel.enabled`), with no prompt changes when climbing: | Rung | Backend | Asserts | This release? | |------|---------|---------|---------------| | 0 | ripgrep / Read | name present in source | yes (default) | | 1 | api-map.json (existing intel) | name in parsed export list | yes (when intel on) | | 2 | tree-sitter | real declaration + kind | deferred (new dep) | | 3 | LSP workspace/symbol | + resolved definition, signature | deferred (new dep) | | 4 | SCIP / GitNexus (#3802) | + exported?, signature, references | deferred (new dep) | Locked sub-decisions: - **Severity.** `MISSING` from rung 0–1 -> `needs-acknowledgement` (the plan proceeds if the author confirms the symbol is new/dynamic, logged), not a hard block — because rung 0–1 produce false positives on dynamic dispatch, re-exports, metaprogrammed decorators, and generated code. Hard block (HIGH) is reserved for rung >=3 adapters that can prove absence. `AMBIGUOUS` -> MEDIUM. `UNCHECKABLE` -> INFO. - **New vs. existing.** Plans declare created symbols in an "Artifacts this phase produces" section. The resolver only checks symbols not in that list, so greenfield work is never flagged MISSING. - **Extraction contract.** The reviewer enumerates a fixed set of symbol kinds (`@decorators`, `Class.method`, `module.function`, `--cli-flags`, file paths, dataclass/struct fields) and must quote the plan line for each, so coverage is auditable. - **Signature-drift** can only be asserted at rung >=3; rungs 0–1 return `UNCHECKABLE` for signatures (name-drift only this release). - **Cadence.** Resolve once per unique symbol per convergence cycle (cache within the cycle); run the pass every cycle so cycle-N fixes are re-verified in cycle N+1. - **Coverage reporting.** `REVIEWS.md` carries a "Verification coverage" INFO block listing every UNCHECKABLE/skipped symbol and why, so "the guard ran" can never silently mean "nothing was checked." ## Consequences ### Positive - Every project gets drift protection on day one, in any language, with zero setup and zero token cost (live Grep/Read against ground truth). - The high-authority, always-fresh check is the default; the low-authority, stale-able cache (intel) stays an explicit, paid opt-in. - One interface, many backends: raising verification authority (grep -> intel -> tree-sitter -> LSP -> SCIP) is a config change, never a workflow rewrite — the durable lever against worsening hallucination. - Three-valued resolution refuses the RAG trap where missing context is treated as permission to invent, and cannot be poisoned by stale/partial intel. - `needs-acknowledgement` keeps the default-on gate tolerable (no blocking valid plans), preserving adoption. - Honors the issue author's own sequencing ("treat the default flip as a separate proposal") and changes no established default. ### Negative - Two config surfaces (`plan_review.source_grounding` and `intel.enabled`) instead of one. - This release's rung 0–1 catch name-drift but not signature-drift; signature coverage waits on a rung >=3 adapter. - Requires planners to populate the "Artifacts this phase produces" section reliably; a missing list degrades precision (new symbols flagged for acknowledgement). - The default-on pass adds reviewer tool calls per convergence cycle (bounded by the cadence rule above). ## Alternatives considered - **Flip `intel.enabled` to default-on and gate the whole feature on it.** Rejected: enabling intel populates nothing, so default-on intel makes `API-SURFACE.md` inject a near-empty surface; combined with a "use only these symbols" instruction it tells the planner the codebase is empty — strictly worse than no surface. Also imposes silent emptiness on non-JS projects, widens the stale-data window, and pulls solo devs toward unrequested agent-token spend. - **Ship the feature opt-in (default-off).** Rejected: the protection is free in the default (live-source) configuration; opt-in would leave most projects unprotected for no benefit while hallucination worsens. - **Hardcode Grep/Read in the reviewer prompt.** Rejected: binds the workflow to one tool, over-claims ("found in text" = "verified"), and forces a prompt rewrite to adopt a better backend later. - **Boolean resolution (verified / not-verified).** Rejected: collapses "couldn't check" into "missing," producing false positives on every unsupported language and silently blessing nothing. - **Hard-block on any MISSING (as originally proposed).** Rejected for rung 0–1: false positives from dynamic/re-exported/generated symbols would block valid plans and get the default-on guard switched off. Retained only for rung >=3. ## References - Issue: open-gsd/gsd-core#22 (migrated from open-gsd/gsd-core#3813) - Relates to #3802 (GitNexus first-class code intelligence) — rung 4 backend - arXiv:2409.20550 — hallucination taxonomy + RAG mitigation (modest gains) - arXiv:2502.05111 — grammar-constrained decoding (soft vs hard constraints) - SCIP: https://github.com/sourcegraph/scip · LSP 3.17 spec · tree-sitter.github.io --- ### Adr/58 Runtime Install Policy Module # Runtime Install Policy Module owns the typed install-plan projection - **Status:** Accepted - **Date:** 2026-06-07 - **Issue:** #58 - **Subsumed by:** [ADR-1239](1239-gsd-embeddable-orchestration-engine.md) (GSD as an Embeddable Orchestration Engine) — read it first; see the amendment below - **Subsumed by:** [ADR-857](857-capability-system.md) (Capability system) — generalizes this module's install-plan projection; this seam remains live at `src/runtime-artifact-install-plan.cts:82` ## Amendment (2026-07-16): subsumed by ADR-1239 (EoS) [ADR-1239](1239-gsd-embeddable-orchestration-engine.md) — **GSD as an Embeddable Orchestration Engine** (EoS), Accepted — subsumes this ADR as an adapter: the typed `InstallPlan` projection this module owns becomes one of the surfaces the host negotiates for, rather than the outermost seam at which GSD meets a host. **This ADR is not superseded and its status is unchanged.** The `InstallPlan` seam is live and load-bearing. It is now a *component* of the EoS frame, not the top-level answer to "how does GSD meet a host?". **Read [ADR-1239](1239-gsd-embeddable-orchestration-engine.md) first.** Recorded because ADR-1239 declared this subsumption while this file recorded nothing. ## Amendment (2026-07-17): also subsumed by ADR-857 (Capability system) [ADR-857](857-capability-system.md) was ratified `Proposed → Accepted` on 2026-07-17 and generalizes this module's install-plan projection into the unified Capability model (install composes *active Features × the chosen Runtime* at this ADR's `InstallPlan` seam). **This ADR remains Accepted and live.** ADR-857's header originally read "Supersedes (generalizes)"; on ratification that was corrected to **Subsumes**, precisely because this seam is not dead — `InstallPlan` is live at `src/runtime-artifact-install-plan.cts:82`. This module is now a component of two broader frames: ADR-857 (what composes an install) and ADR-1239/EoS (how a host loads the engine at all). ## Context Runtime install logic is currently spread across one-off helper functions. `getGlobalDir(runtime, explicitDir)` in `bin/install.js` switch-dispatches to per-runtime helpers (`getOpencodeGlobalDir`, `getKiloGlobalDir`), and `getAgentsDir` lives separately in `src/core.cts`. These helpers resolve directories at ~11 call sites and are free to drift from the behavior that install and runtime-query paths actually expect, because nothing owns the *composition* of an install decision as a single value. Two adjacent seams already exist: - **ADR-3660 (Runtime Artifact Layout Module)** owns *where* per-runtime artifacts (commands, agents, skills) are placed. - **ADR-0009 (Shell Command Projection Module)** owns runtime-aware *command text* rendering (quoting, path style, wrapper prefixes). But no ADR owns composing those — placements + command text + per-runtime config intentions — into one unified, typed install-plan projection. That missing seam is why directory/config logic re-derives itself ad hoc at each call site. ## Decision Introduce a **Runtime Install Policy Module** as the seam that, given a runtime and an install context, **projects a pure, typed `InstallPlan` value** describing everything that should happen for that runtime. The projection: - composes artifact placements by delegating to the Runtime Artifact Layout Module (ADR-3660), - composes command text by delegating to the Shell Command Projection Module (ADR-0009), - declares config *intentions* (which config files need which keys/values for that runtime), - performs **no filesystem IO** and **no format-specific serialization** while resolving the plan. Concrete execution is owned by runtime-specific **adapters** (made explicit as a registry in #60). Adapters consume the `InstallPlan` and perform the effectful work: file mutations, directory creation, and rendering format-specific config (TOML, JSON, Markdown) for their runtime. This follows the repository's established pure-policy-projects / thin-adapters-execute pattern (ADR-0001, Dispatch Policy Module): the `InstallPlan` is the narrow waist, resolution stays free of IO, and callers become thin adapters over a stable interface rather than re-deriving directory logic. ## What stays OUTSIDE the policy module To keep the abstraction honest about the filesystem boundary, the following are explicitly **not** the policy module's responsibility and remain in the runtime adapters: - Concrete TOML / JSON / Markdown read-modify-write and serialization. - Merge semantics for pre-existing config files (preserving user keys, ordering, formatting). - Filesystem effects: directory creation, atomic write/rename, existence/permission checks. - Any path resolution that requires touching the disk. The policy module resolves *intent* as data; adapters turn that intent into bytes on disk. ## Consequences - Install logic becomes testable as pure data: assert the projected `InstallPlan` for a runtime without a filesystem. - The scattered directory helpers (`getGlobalDir`, `getOpencodeGlobalDir`, `getKiloGlobalDir`, `getAgentsDir`) gain a single projection to migrate onto, retiring or narrowing them (tracked in #56). - The plan/adapter contract becomes a stability surface that must be held narrow; drift there reintroduces the very divergence this seam removes. - Rollout is incremental, not big-bang: this ADR establishes the boundary (#58); the explicit Runtime Adapter Registry lands next (#60); legacy helper retirement follows (#56); downstream cleanup in #57. ## Implementation (2026-06-11) The `InstallPlan` is realized as the exported `resolveInstallPlan(runtime)` in `runtime-config-adapter-registry` (co-located with adapter-selection, not a standalone module). It collects the install-level descriptor axes — `installSurface`, `writesSharedSettings`, `finishPermissionWriter`, `hookEvents`, `extendedHookEvents`, and `hooksSurface` — into one typed `InstallPlan` value. `install()` and `finishInstall()` in `bin/install.js` consume it directly. The spatial axes (`configHome`, `artifactLayout`, `commandStyle`) remain behind their self-resolving adapter modules (`runtime-homes`, `runtime-artifact-layout`, `runtime-slash`) as the execution adapters — consistent with this ADR's adapters-execute boundary. ## References - ADR-0001 — Dispatch Policy Module (pure-policy-projects / thin-adapters-execute precedent). - ADR-3660 — Runtime Artifact Layout Module (per-runtime artifact placement; delegated to by this projection). - ADR-0009 — Shell Command Projection Module (runtime-aware command text; delegated to by this projection). - ADR-0008 — Installer Migration Module (adjacent installer seam). - `CONTEXT.md` § Glossary — Domain modules and seams (the architecture seam map / glossary this module is registered in). - Installer-refactor chain: #58 (this ADR) → #60 (explicit adapter registry) → #56 (retire legacy directory helpers) → #57. --- ### Adr/218 Release Version Validation # ADR-218: Harden release-workflow version validation — reject leading zeros and pre-check npm - **Status:** Accepted (2026-05-24) - **Date:** 2026-05-24 ## Context ### The leading-zero normalization incident The release workflow's `validate-version` job used the regex `^[0-9]+\.[0-9]+\.0$` to gate the `version` input. This regex accepts leading zeros in any segment (`1.01.0`, `01.0.0`, etc.) because `[0-9]+` matches one or more digits without anchoring against a leading zero. A maintainer triggered the workflow with `version=1.01.0`. The validator accepted it. Downstream steps called `npm version 1.01.0`, which silently normalized the string to `1.1.0` per the semver specification (leading zeros are stripped). From that point the pipeline had irreconcilably divergent state: | Artifact | Value | |---|---| | npm registry (`@opengsd/get-shit-done-redux`, `@opengsd/gsd-sdk`) | `1.1.0` — published, immutable | | git tag | `v1.01.0` | | GitHub release title | `v1.1.0` | | GitHub release tag | `v1.01.0` | | Release branch | `release/1.01.0` | The workflow run ultimately failed downstream. A subsequent attempt to re-run finalize for `version=1.1.0` failed at the `Dry-run publish validation` step because npm refuses to republish an already-published version. A second typo (`version=1.03.0`) produced a similar orphan tag and branch but never published to npm. ### The duplicate-version late-failure hole The validator only checked format. It did not check whether the requested version was already live on npm. A duplicate-version request (e.g. re-running `finalize` for an already-published version, or mistyping a version that normalizes to one already published) ran through the full checkout → install → build → test cycle — roughly 10 minutes — before failing at the `Dry-run publish validation` step. ## Decision ### 1. Tighten the format regex to forbid leading zeros Replace `^[0-9]+\.[0-9]+\.0$` with `^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.0$`. Each segment now matches either `0` exactly or a string starting with a non-zero digit followed by zero or more digits. `1.01.0` fails because the minor segment `01` matches neither alternative. The error message includes the offending value so maintainers can correct it immediately. The `IS_MAJOR` detection regex is updated in parallel: `^[0-9]+\.0\.0$` → `^(0|[1-9][0-9]*)\.0\.0$`. ### 2. Add a duplicate-version precheck against npm at validation time A new `Reject already-published versions` step runs immediately after format validation in the `validate-version` job. It calls `npm view "$pkg@$VERSION" version` for both `@opengsd/get-shit-done-redux` and `@opengsd/gsd-sdk`. If either resolves, the job fails in under 5 seconds with a clear error. No build or test cycle is wasted. ## Recovery sequence The following steps recover the production state left by the `1.01.0` and `1.03.0` incidents. Include them here so future maintainers do not need to re-derive them. ### 1. Cancel any failing in-flight runs ```sh gh run cancel --repo open-gsd/get-shit-done-redux ``` ### 2. Fix the v1.1.0 divergence (npm published, tag/release/branch on wrong name) ```sh # Fetch the release branch that holds the correct package.json at 1.1.0 git fetch origin release/1.1.0 # Create a proper v1.1.0 tag at the head of release/1.1.0 git tag v1.1.0 git push origin v1.1.0 # Retarget the existing GitHub release from the typo tag to the correct one gh release edit v1.01.0 --repo open-gsd/get-shit-done-redux --tag v1.1.0 gh release edit v1.1.0 --repo open-gsd/get-shit-done-redux --verify-tag # Delete the typo tag and branch git push origin :refs/tags/v1.01.0 git push origin --delete release/1.01.0 ``` ### 3. Clean the unpublished v1.03.0 orphan ```sh git push origin :refs/tags/v1.03.0 git push origin --delete release/1.03.0 ``` ### 4. Resume releases at 1.2.0 `1.1.0` is consumed on npm and the workflow only allows `.0` patch-component versions. The next valid release is `1.2.0`. ## Consequences - **Valid inputs are unaffected.** The new regex accepts every version that the old regex accepted minus leading-zero forms. All existing release runs used well-formed versions; no regression for normal use. - **Leading-zero inputs fail in under 5 seconds** at the `validate-version` job before any branch, install, or publish operation runs. - **Duplicate-version inputs fail in under 5 seconds** at `validate-version` rather than after a full install-and-test cycle. - **The late dry-run check in `finalize` is unchanged.** It remains a belt-and-suspenders guard; the new precheck does not remove it. ## See also - ADR 227 (`docs/adr/227-input-validation-shape-not-just-type.md`) generalises the principle this ADR documents in the narrower release-validation context: input validation at trust boundaries must check both type and semantic shape, with silent coercion on failure. --- ### Adr/227 Input Validation Shape Not Just Type # ADR 227: Input validation must check semantic shape, not just type - **Status:** Accepted (2026-05-24) - **Date:** 2026-05-24 ## Context Defensive normalization at trust boundaries typically starts with a type check: ```js if (typeof value !== 'string') return undefined; ``` This stops non-string values but accepts any string — including the empty string, garbage payloads, and values that are structurally correct (a string) but contractually invalid (not a UUID v4, not a semver, not a file path). The remaining attack surface is the gap between "is a string" and "satisfies the field's contract." ### The PR #225 trigger PR #225 (`refactor/178-trace-id-propagation`, P1.4 of ADR-0174 SDK retirement) introduced `parentTraceId` on `DispatchEvent`. The initial implementation normalized the field with a type-only guard: ```js parentTraceId: typeof raw.parentTraceId === 'string' ? raw.parentTraceId : undefined, ``` Codex adversarial-review (commit range `fb94ba8d`–`338d0951`) flagged that this propagated: - empty strings (`""`) — a correlation key that matches nothing - garbage strings (`"not-a-uuid"`, `";"`, `"