diff --git a/docs/adr/0005-sdk-architecture-seam-map.md b/docs/adr/0005-sdk-architecture-seam-map.md index 50537f2d3..cdb6159f5 100644 --- a/docs/adr/0005-sdk-architecture-seam-map.md +++ b/docs/adr/0005-sdk-architecture-seam-map.md @@ -1,6 +1,6 @@ # SDK Architecture seam map for query/runtime surfaces -- **Status:** Superseded by ADR-0174 (2026-05-23); originally Accepted (2026-05-09) +- **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. diff --git a/docs/adr/0007-sdk-package-seam-module.md b/docs/adr/0007-sdk-package-seam-module.md index 42f76e0fb..14b79f1ad 100644 --- a/docs/adr/0007-sdk-package-seam-module.md +++ b/docs/adr/0007-sdk-package-seam-module.md @@ -1,6 +1,6 @@ # SDK Package Seam Module owns SDK-to-get-shit-done-redux compatibility -- **Status:** Superseded by ADR-0174 (2026-05-23); originally Accepted (2026-05-07) +- **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. diff --git a/docs/adr/0009-shell-command-projection-module.md b/docs/adr/0009-shell-command-projection-module.md index e1ecf9b89..a7385ddab 100644 --- a/docs/adr/0009-shell-command-projection-module.md +++ b/docs/adr/0009-shell-command-projection-module.md @@ -1,6 +1,7 @@ # 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. diff --git a/docs/adr/0010-file-operation-engine-module.md b/docs/adr/0010-file-operation-engine-module.md index 89cf17f39..79ed64b80 100644 --- a/docs/adr/0010-file-operation-engine-module.md +++ b/docs/adr/0010-file-operation-engine-module.md @@ -1,6 +1,6 @@ # File Operation Engine Module owns safe runtime/config file mutations -- **Status:** Superseded by ADR-0009 (Shell Command Projection Module expansion, Phases 3–4, `#3467`–`#3468`) +- **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 diff --git a/docs/adr/0010-skill-surface-budget-module.md b/docs/adr/0010-skill-surface-budget-module.md index b530c4ef9..f486c0d9d 100644 --- a/docs/adr/0010-skill-surface-budget-module.md +++ b/docs/adr/0010-skill-surface-budget-module.md @@ -1,8 +1,10 @@ # Skill Surface Budget Module owns install-time skill listing curation -- **Status:** Proposed +- **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 diff --git a/docs/adr/0011-review-default-reviewers-prd.md b/docs/adr/0011-review-default-reviewers-prd.md index 79f09f297..f64961733 100644 --- a/docs/adr/0011-review-default-reviewers-prd.md +++ b/docs/adr/0011-review-default-reviewers-prd.md @@ -1,11 +1,11 @@ # PRD — `review.default_reviewers` config key for `/gsd-review` reviewer selection -- **Status:** Draft +- **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` +- **Related ADR:** [`0011-review-default-reviewers.md`](0011-review-default-reviewers.md) -> This PRD is filed alongside its ADR under `docs/adr/` for co-location. The repo does not yet have a `docs/prd/` directory; if maintainers prefer one, this file can move there with the `0011-` prefix preserved. +> **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 diff --git a/docs/adr/0011-review-default-reviewers.md b/docs/adr/0011-review-default-reviewers.md index d7832af72..640701e45 100644 --- a/docs/adr/0011-review-default-reviewers.md +++ b/docs/adr/0011-review-default-reviewers.md @@ -1,10 +1,28 @@ # `review.default_reviewers` config key scopes the no-flag `/gsd-review` fan-out -- **Status:** Proposed +- **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). diff --git a/docs/adr/0011-skill-surface-budget-module.md b/docs/adr/0011-skill-surface-budget-module.md index 434f0307e..3fb7eaa5b 100644 --- a/docs/adr/0011-skill-surface-budget-module.md +++ b/docs/adr/0011-skill-surface-budget-module.md @@ -3,6 +3,8 @@ - **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). diff --git a/docs/adr/0012-command-routing-hub.md b/docs/adr/0012-command-routing-hub.md index 842fe4333..18a03c318 100644 --- a/docs/adr/0012-command-routing-hub.md +++ b/docs/adr/0012-command-routing-hub.md @@ -1,6 +1,6 @@ # CommandRoutingHub as single dispatch seam for CJS command families -- **Status:** Superseded by ADR-0174 (2026-05-23); originally Accepted (2026-05-20) +- **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 diff --git a/docs/adr/1016-runtime-capability-descriptor.md b/docs/adr/1016-runtime-capability-descriptor.md index 5c11fdcdb..0cfb373ef 100644 --- a/docs/adr/1016-runtime-capability-descriptor.md +++ b/docs/adr/1016-runtime-capability-descriptor.md @@ -7,6 +7,20 @@ - **Realizes:** [ADR-857](857-capability-system.md) Branch 8 (host-CLI support as `role: runtime` Capabilities) - **Materializes:** [ADR-58](58-runtime-install-policy-module.md) (the typed `InstallPlan` projection) - **Builds on:** [ADR-3660](3660-runtime-artifact-layout-module.md) (artifact layout), [ADR-894](894-capability-declaration-format.md) (the `role: runtime` body, already validated) +- **Subsumed by:** [ADR-1239](1239-gsd-embeddable-orchestration-engine.md) (GSD as an Embeddable Orchestration Engine) — read it first; see the amendment below + +## Amendment (2026-07-16): subsumed by ADR-1239 (EoS) — this ADR is the *declarative adapter*, not the whole architecture + +[ADR-1239](1239-gsd-embeddable-orchestration-engine.md) — **GSD as an Embeddable Orchestration Engine** (EoS), Accepted — subsumes this ADR **as the declarative adapter** in a larger frame, and inverts its direction of travel: + +- This ADR answers *"how does GSD project its files onto a host CLI we already know?"* — GSD reaches into the host. +- ADR-1239 inverts that: **GSD is the engine; the host loads it through a negotiated Host-Integration Interface**, and a third party writes the thin host-plugin. ADR-1239 calls this "flips *projection* to *embedding*, and **unifies** them." + +**This ADR is not superseded and its status is unchanged.** The runtime descriptor is real, live, and load-bearing: it remains the *declarative* adapter within EoS. But it is a **component of** the current architecture, not the statement of it. A reader who takes this ADR as the top-level answer to "how does GSD meet a host?" will reach the wrong conclusion for any new host. + +**Read [ADR-1239](1239-gsd-embeddable-orchestration-engine.md) first.** + +Recorded because ADR-1239 declared this subsumption while this file recorded nothing, leaving the pointer one-way and EoS undiscoverable from here. ## Context diff --git a/docs/adr/1143-claude-orchestration-capability.md b/docs/adr/1143-claude-orchestration-capability.md index 4541bce43..145ebdd2c 100644 --- a/docs/adr/1143-claude-orchestration-capability.md +++ b/docs/adr/1143-claude-orchestration-capability.md @@ -7,6 +7,14 @@ - **Blocked by:** [#857](https://github.com/open-gsd/gsd-core/issues/857) being **released** (Proposed → Accepted + capability infrastructure shipped). Not actionable until then. - **Relates to:** [#853](https://github.com/open-gsd/gsd-core/issues/853) (Claude Code backgrounded agents cannot nest subagents), existing BETA skill `gsd-ultraplan-phase` +## Why this is still `Proposed` (audited 2026-07-17) + +Confirmed shipped, on-tree: the capability is real and registered, not vaporware. `capabilities/claude-orchestration/capability.json` exists with detection + emission (`detectWorkflowBackend` / `emitWorkflowScript`) in `src/claude-orchestration.cts` (compiled to `gsd-core/bin/lib/claude-orchestration.cjs`), federated config (`claude_orchestration.enabled` / `execution_backend` / `min_agent_sdk_version`), and 1,552 lines of tests across `tests/claude-orchestration.test.cjs`, `tests/claude-orchestration-command-router.test.cjs`, and `tests/fix-2285-claude-orchestration-wiring.test.cjs`. The previously-fatal wiring bug, #2285 ("claude-orchestration capability (#1143) registered as active but never wired into execute-phase orchestrator prompt"), is closed COMPLETED (2026-07-15) — one day before this audit — and the owning feature issue #1143 is also closed COMPLETED. + +**The blocker.** The ADR sets its own bar for ratification in its own Amendment (above): "flipping to Accepted follows maintainer sign-off on the E2E behaviour once exercised on Claude Code with the Workflow tool present." No such exercise is recorded anywhere in issues, PRs, or tests. Every test in the three files above operates at the contract or CLI-subprocess layer — asserting the *shape* of an emitted script or the return value of `resolve-wave-dispatch` — none constructs or executes an actual Workflow-tool run (`grep -rn "Workflow(" tests/claude-orchestration*.test.cjs tests/fix-2285-*.test.cjs` returns no hits). Two further gaps sit inside the ADR's own Decision section: (1) Decision §1's claimed net effect — "wave parallelism, the plan-checker, and the verifier are restored" — is narrower than what shipped: `capability.json`'s own description says "the plan-checker and verifier remain inline until separately wired — this capability delivers the parallel-execution backend, not those gates"; (2) Decision §3's fold-in of the `gsd-ultraplan-phase` skill into the capability's `skills[]` has not happened — `capability.json` still shows `"skills": []`, and no follow-up issue for the migration the Amendment promises exists (searched via `gh issue list --search`, no result). + +**Unblock condition.** Ratify once: (a) a real Claude Code session with the Workflow tool present and `claude_orchestration.enabled=true` drives an `execute-phase` wave through the Workflow backend, and the result is recorded (issue comment, PR, or a test that actually builds/executes a `Workflow` script rather than asserting emitted-script shape) — that is the maintainer sign-off the ADR itself asks for; and (b) the Decision section's "plan-checker and verifier restored" language is reconciled with the shipped scope (either corrected to match, or backed by a tracked issue for the deferred wiring `capability.json` already discloses). The `skills[]` migration (item 3) is lower priority since it is openly disclosed as deferred rather than silently dropped, but should carry a tracked issue number before ratification so it doesn't quietly vanish. + ## Context Claude Code ships two orchestration primitives GSD does not yet treat as first-class: diff --git a/docs/adr/1213-capability-state-writer.md b/docs/adr/1213-capability-state-writer.md index 3dafca6b6..2de0eae8f 100644 --- a/docs/adr/1213-capability-state-writer.md +++ b/docs/adr/1213-capability-state-writer.md @@ -6,6 +6,16 @@ - **Completes:** Capability system (ADR-857) — the write half of the phase-4 "Wire" step - **Builds on:** Capability declaration format (ADR-894), Capability command contribution (ADR-959), Skill Surface Budget Module (ADR-0011) +## Why this is still `Proposed` (audited 2026-07-17) + +**What shipped.** The module this ADR decided is real and in production use: `setCapabilityState` / `cmdCapabilitySet` are implemented at `src/capability-writer.cts:140` and `:446`, wired into the CLI at `gsd-core/bin/gsd-tools.cjs:1941` and `:2314`, and `gsd-core/workflows/settings.md:448` routes gate writes through `capability set --gate`. `CONTEXT.md:249` carries the glossary entry, and the landing commit (`bf634b95c`, "feat(#1213): Capability State Writer — write-side inverse of the resolver (#1225)") is a confirmed ancestor of `origin/next`. The write-side invariant this ADR set out to build — off means off, enforced at write time — is in force. + +**The blocker.** This ADR's own Decision section (lines 27–29, as written above) declares the writer's return shape as `{ capabilities: CapabilityStateEntry[]; warnings: string[] }`, and decision item 4 says post-write divergence "is returned as warnings, not silently swallowed" — a warnings-only channel, no separate hard-failure signal. The shipped code does not match that: `src/capability-writer.cts:113-117` defines `SetCapabilityStateResult` as `{ capabilities, warnings, errors }`, and `errors[]` is populated both by pre-write validation rejections (e.g. `"unknown capability"`, `"cannot enable ... not in the install profile"`, which abort with zero writes) and by post-write assert failures (e.g. `"failed to disable ... still surfaced after write"`), which the CLI (`cmdCapabilitySet`, lines 483–493) turns into a non-zero exit — a real hard-failure channel this ADR's Decision section does not describe. This is not drift or a bug: it is a later, deliberate redesign. ADR-1411 (Accepted; 2026-06-18 Amendment) states explicitly that `capability-writer`'s "`errors[]` (operation-not-applied) is load-bearing and cannot fold into `warnings[]` (advisory)" and records the mutation-verb shape as `{ capabilities, warnings, errors }` (ADR-1411 lines 81, 86) — superseding the two-field interface this ADR decided. ADR-1213's own text has never been updated to note the amendment or to revise the signature, so as written it misdescribes the interface actually shipped. + +**Dropped claim.** A second refutation argument held that the parent ADR-857 carried an explicit governance caveat reserving any Proposed→Accepted flip in this ADR family for a maintainer, and that flipping ADR-1213 on shipped-code evidence alone would repeat a move ADR-857 itself refused to make unilaterally. That premise no longer holds: `docs/adr/857-capability-system.md` now reads "Status: Accepted — ratified 2026-07-17" with a "## Ratification (2026-07-17)" section, and the caveat text this argument quoted is no longer present anywhere in that file (confirmed by direct search). ADR-857 was ratified in the same 2026-07-17 audit pass that reviewed this ADR, so this argument is dropped rather than carried forward as a live blocker. + +**Unblock condition.** Revise this ADR's Decision section — the return-shape signature and item 4's assert-and-report description — to match what shipped: `{ capabilities: CapabilityStateEntry[]; warnings: string[]; errors: string[] }`, with `errors` describing operation-not-applied hard failures (pre-write validation rejects, post-write assert failures) distinct from advisory `warnings`. Either fold in a one-line "Amended by ADR-1411" pointer or edit the signature directly. Once the Decision section states the interface actually in the tree, this ADR is ready to ratify — the underlying mechanism is already proven in production. + ## Context ADR-857 promised: *"one resolved capability state replaces three contradicting toggle systems; 'off' means off."* The **read** side delivers it. The **Capability State Resolver** (`src/capability-state.cts`) collapses three substrates into one resolved state: diff --git a/docs/adr/1244-capability-ecosystem.md b/docs/adr/1244-capability-ecosystem.md index 441ce1be6..01cd57aeb 100644 --- a/docs/adr/1244-capability-ecosystem.md +++ b/docs/adr/1244-capability-ecosystem.md @@ -1,12 +1,33 @@ # ADR-1244 — Capability Ecosystem: third-party authoring, versioned manifests, and URL import/upgrade/remove -- **Status:** Proposed +- **Status:** Accepted — ratified 2026-07-17 (originally Proposed 2026-06-14); see "Ratification" below - **Date:** 2026-06-14 > **Relationship to other ADRs.** This ADR **amends and extends ADR-857 Decisions 7 and 8** — it does not reverse them. ADR-857 D7 deferred third-party code-loading "to its own ADR"; D8 deferred third-party CLI support "to an external loader + trust/validation gate, no rework because runtimes are already descriptors." This *is* that ADR, and it *delivers* that gate. It builds on **ADR-894** (capability declaration format), **ADR-1016** (runtime capability descriptor), and **ADR-58** (InstallPlan seam). Tracked by [#1244](https://github.com/open-gsd/gsd-core/issues/1244). Target release: **1.6.0**. --- +## Ratification (2026-07-17): Proposed → Accepted + +Ratified by explicit maintainer directive; the Status field sat at Proposed for 33 days after the owning issue and all six phase sub-issues had already closed as shipped. + +**Evidence the decision shipped:** + +- Issue #1244 and all six phase sub-issues (#1430–#1435, Phase 1 through Phase 6) are CLOSED / `stateReason: COMPLETED`. +- **D1** (versioned manifest): `capabilities/*/capability.json` carry `version` + `engines` (confirmed in `ai-integration`, `antigravity`, `claude-orchestration`). +- **D2** (runtime overlay): `src/capability-loader.cts:486` exports `loadRegistry({ includeInstalled })`. +- **D3** (source resolver): `src/capability-source.cts:1080` exports `resolveCapabilitySource`, backed by the four adapters `resolveLocal` (861), `resolveGit` (892), `resolveNpm` (944), `resolveTarball` (1017). +- **D4** (ledger): `src/capability-ledger.cts` (42.4K) exists with `tests/capability-ledger.test.cjs` (111.0K) covering it. +- **D5** (trust model): `src/capability-trust.cts` and `src/capability-consent.cts` exist; `strictKnownRegistries` is threaded through `src/capability-lifecycle.cts` at lines 171, 881, 956, and 1079, each backed by `tests/capability-trust.test.cjs` and `tests/capability-consent.test.cjs`. +- **D6** (upgrade/compat): `src/capability-lifecycle.cts:1078` implements `upgradeCapability` under the documented atomic stage-then-swap (comment header at line 1056); `compatVersions` downgrade handling is present at lines 126, 920, and 1111. +- **D9** (capability matrix): `docs/reference/capability-matrix.md` (9.4K) exists and is generated from the registry. + +Governance: owning issue #1244, `stateReason: COMPLETED`, closed 2026-07-07. + +**Known gaps at ratification:** the D8 cross-reference promised back into ADR-857 ("D7 and D8... extended by ADR-1244") was never written — `docs/adr/857-capability-system.md` has no mention of ADR-1244. And epic #1900 (ADR-1244 edge hardening: MCP arg/cwd confinement, tarball/registry SSRF denylist, duplicated injection patterns) remains OPEN with all three of its filed children (#1901, #1902, #1903) closed `NOT_PLANNED` — the epic's own text scopes this as post-ship hardening on an already fail-closed pipeline, not a reversal of any D1–D9 decision, but the hardening itself is not yet scheduled. + +--- + ## Context ADR-857 turned the five-step loop into a **host** with **12 Loop Extension Points** and made every feature a **Capability** — a folder `capabilities//capability.json` declaring owned skills/agents, lifecycle hooks, a federated config slice, and loop-extension registrations (`step` / `contribution` / `gate`). 32 capabilities ship today (20 `role:feature`, 12 `role:runtime`). The architecture is in place; the **ecosystem is not**. diff --git a/docs/adr/15-autonomous-cross-ai-convergence.md b/docs/adr/15-autonomous-cross-ai-convergence.md index 67443c7ff..f25ecfbef 100644 --- a/docs/adr/15-autonomous-cross-ai-convergence.md +++ b/docs/adr/15-autonomous-cross-ai-convergence.md @@ -1,11 +1,27 @@ # Cross-AI Plan Convergence via Existing Orchestration Commands -- **Status:** Proposed +- **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. diff --git a/docs/adr/1577-untrusted-input-boundary-and-injection-blocking.md b/docs/adr/1577-untrusted-input-boundary-and-injection-blocking.md index 7c7312e11..f2fd0a547 100644 --- a/docs/adr/1577-untrusted-input-boundary-and-injection-blocking.md +++ b/docs/adr/1577-untrusted-input-boundary-and-injection-blocking.md @@ -1,9 +1,26 @@ # ADR-1577: Untrusted-input boundary + opt-in injection blocking -- **Status:** Proposed +- **Status:** Accepted — ratified 2026-07-17 (originally Proposed 2026-06-25); see "Ratification" below - **Issue:** [#1577](https://github.com/open-gsd/gsd-core/issues/1577) - **Part of:** [#1573](https://github.com/open-gsd/gsd-core/issues/1573) (harden the agent layer against documented LLM failure modes) +## Ratification (2026-07-17): Proposed → Accepted + +Ratified by explicit maintainer directive; the Proposed status had gone unconfirmed for 22 days since the ADR landed on 2026-06-25. + +**Evidence the decision shipped:** + +- Issue #1577 is closed (`state=CLOSED`, `stateReason=COMPLETED`, closed 2026-06-24T21:07:24Z) as split A of the umbrella #1573, scoped exactly to this ADR's decision. +- `hooks/gsd-read-injection-scanner.js:118` extends the scanner to `SCANNED_TOOLS = new Set(['Read', 'WebFetch', 'WebSearch'])`, wired via `hooks/hooks.json:34`'s `"Read|WebFetch|WebSearch"` matcher — closing the WebFetch/WebSearch gap named in Context. +- `hooks/gsd-read-injection-scanner.js:212` gates blocking on `cfg.security?.injection_blocking === true`, read directly via `fs.readFileSync`/`JSON.parse` (independent of `src/configuration.cts`'s key whitelist, so no drop risk). +- `security.injection_blocking` is a registered config key end-to-end: `gsd-core/bin/shared/config-schema.manifest.json:109` lists it and `gsd-core/bin/shared/config-defaults.manifest.json:103` defaults it `false`; `src/configuration.cts:47` builds `VALID_CONFIG_KEYS` from that manifest and `src/config-schema.cts:61` (`isValidConfigKey`) consults it. +- `gsd-core/references/untrusted-input-boundary.md` exists and is `@`-included by exactly the 10 ingest agents named in the Decision: `gsd-advisor-researcher`, `gsd-ai-researcher`, `gsd-assumptions-analyzer`, `gsd-doc-classifier`, `gsd-doc-synthesizer`, `gsd-domain-researcher`, `gsd-phase-researcher`, `gsd-project-researcher`, `gsd-research-synthesizer`, `gsd-ui-researcher`. +- `docs/explanation/security-model.md:150-165` documents the PostToolUse pre-filter framing and names all 10 agents; `docs/CONFIGURATION.md:910` documents `security.injection_blocking` with a direct link to ADR-1577. +- `tests/read-injection-scanner.security.test.cjs` runs `SCAN-WF-01`, `SCAN-WF-02`, `SCAN-WF-03`, and `SCAN-WS-01` against the real hook subprocess for WebFetch/WebSearch payloads and asserts real detections. +- `tests/injection-blocking-config.test.cjs` asserts `isValidConfigKey('security.injection_blocking')` is true, `isValidConfigKey('security')` is false, and `CONFIG_DEFAULTS.security.injection_blocking === false`. + +**Governance state:** owning issue #1577 — CLOSED, stateReason COMPLETED, closed 2026-06-24T21:07:24Z. + ## Context The research/doc-ingest agents concatenate text returned by WebFetch / WebSearch / Read into their context with no data/instruction separation, and the `gsd-read-injection-scanner` hook only scanned the `Read` tool — leaving WebFetch/WebSearch (the largest untrusted channel) unscanned. Prompt injection via fetched content is a documented LLM failure mode (arXiv [2506.05739](https://arxiv.org/abs/2506.05739), [2507.15219](https://arxiv.org/abs/2507.15219), [2504.20472](https://arxiv.org/abs/2504.20472)). diff --git a/docs/adr/1606-prohibition-enforcement-verify-seam.md b/docs/adr/1606-prohibition-enforcement-verify-seam.md index 4330ea5ca..dca4a2eee 100644 --- a/docs/adr/1606-prohibition-enforcement-verify-seam.md +++ b/docs/adr/1606-prohibition-enforcement-verify-seam.md @@ -37,6 +37,41 @@ addenda. The boundary: this ADR, leaving 550 to own the contract and this ADR to own the mechanism. Until that is agreed, 550's addenda remain authoritative and this ADR is non-binding. +## Why this is still `Proposed` (audited 2026-07-17) + +**What shipped.** The audit confirmed the enforcement mechanism this ADR describes is real +and in place, not aspirational. All seven Decision points are present in +`src/prohibition-enforcement.cts` and `src/probe-core.cts`: `runProhibitionEnforcement` +(`src/prohibition-enforcement.cts:654-732`), `dispositionForProhibition` +(`src/probe-core.cts:470-516`), the vacuity guards `isNonVacuousNodeTestRed` +(`src/prohibition-enforcement.cts:351-355`) and `isNonVacuousNodeTestPass`, and +`defaultProveFailFirst`'s node-test branch (`src/prohibition-enforcement.cts:591-627`) — +which does implement the #1906 mandatory-`cleanFixture` causation control exactly as +Decision 4 / the 2026-07-03 addendum describe, not merely as a documented intent. Test +coverage is substantial (`tests/prohibition-enforcement.test.cjs`, 1336 lines), and all six +contributing issues (#644, #1259, #1278, #1279, #1346, #1906) are closed as COMPLETED on +GitHub. + +**The blocker.** This ADR names its own precondition for becoming binding, in its own words: +"on accepting this ADR, replace ADR-550's 2026-06-12 / #1259 / #1279 / #1346 / #1278 +enforcement addenda with a one-line pointer to this ADR ... Until that is agreed, 550's +addenda remain authoritative and this ADR is non-binding." That dedup has not happened. +Direct read of `docs/adr/550-spec-phase-probe-contract.md` confirms all four named addenda — +"Addendum (2026-06-12; updated 2026-06-15)", "Addendum (2026-06-15, #1279)", "Addendum +(2026-06-21, #1346)", and "Addendum (2026-06-15): optional `check` descriptor ... (#1278)" — +remain in ADR-550 in full, verbatim; none has been collapsed to a pointer. A later, separate +addendum in ADR-550 (2026-06-22, from #1607) does cross-reference ADR-1606 for the +*recall/representation-side* "Alternatives considered," but that is additive scaffolding, not +the enforcement-addenda dedup this ADR names as its own condition — the four target addenda +are untouched by it. No commit, PR, or tracked issue was found executing the dedup. + +**Unblock condition.** Edit `docs/adr/550-spec-phase-probe-contract.md` to collapse the four +named addenda (2026-06-12/2026-06-15 update, #1279, #1346, #1278) into the one-line pointer +this ADR calls for, then flip both ADR-550's cross-reference and this ADR's Status in the +same PR. To check in minutes: grep `docs/adr/550-spec-phase-probe-contract.md` for +`## Addendum (2026-06-12`, `#1279`, `#1346`, and `#1278` — if those headings still carry the +full addendum text rather than a one-line pointer, the precondition remains unmet. + ## Context ADR-550 D4 originally specified the `test` tier as a "hard gate in both interactive and diff --git a/docs/adr/1610-workflow-agent-size-budget-ratchet.md b/docs/adr/1610-workflow-agent-size-budget-ratchet.md index 0f4035038..c1fa7c0b6 100644 --- a/docs/adr/1610-workflow-agent-size-budget-ratchet.md +++ b/docs/adr/1610-workflow-agent-size-budget-ratchet.md @@ -1,6 +1,6 @@ -# ADR 1610: workflow & agent size-budget ratchet (per-file byte baseline + tier hard caps) [Proposed] +# ADR 1610: workflow & agent size-budget ratchet (per-file byte baseline + tier hard caps) [Accepted] -- **Status:** Proposed +- **Status:** Accepted — ratified 2026-07-17 (originally Proposed 2026-06-22); see "Ratification" below - **Date:** 2026-06-22 > **Provenance.** Drafted 2026-06-22 to give an already-shipped architectural governance @@ -13,6 +13,22 @@ > `scripts/lib/allowlist-ratchet.cjs` on `next`. The rationale here is lifted from those > tests' own doc comments (the decision was documented in-code but never as an ADR). +## Ratification (2026-07-17): Proposed → Accepted + +Ratified by explicit maintainer directive after independent re-verification of the evidence below; the ADR had sat in `Proposed` for 25 days after the decision it documents had already shipped. + +**Evidence the decision shipped:** + +- Owning issue #1074 ("replace tier-max workflow size-budget ratchet with a per-file baseline + loose hard caps") is CLOSED, `stateReason: COMPLETED`, closed 2026-06-12T14:00:56Z. +- All three landing PRs are MERGED: #1089 (`test(#1074): add additive per-file workflow size baseline guard`, 2026-06-12T03:59:57Z), #1096 (`test(#1074): swap workflow size enforcement to baseline + loose hard caps`, 2026-06-12T13:30:44Z), #1097 (`test(#1074): agent-size-budget per-file baseline + line→byte rebase`, 2026-06-12T13:58:44Z). +- `scripts/workflow-size.cjs:32-35` — `lfByteCount()` implements the CRLF→LF-normalized byte count described in Decision point 2 (#683). +- `scripts/workflow-size.cjs:64-72,80-82` — `measureMdFiles`/`measureWorkflows` is the single shared measurement path cited in Decision point 5, re-exported for both the guard and `scripts/update-size-baseline.cjs`. +- `scripts/lib/allowlist-ratchet.cjs:180` exports `assertFileBaseline` — the per-file baseline assertion named in Decision point 3 and Cross-references. +- `tests/workflow-size-budget.test.cjs:95-97,102` defines `XL_CAP = 98304` (96 KiB), `LARGE_CAP = 61440` (60 KiB), `DEFAULT_CAP = 40960` (40 KiB), `NEW_FILE_CAP = 32768` (32 KiB) — the exact numbers quoted in Decision point 3. + +**Governance:** owning issue #1074, `stateReason: COMPLETED`, closed 2026-06-12T14:00:56Z. + + ## Context `gsd-core/workflows/*.md` and `agents/*.md` are loaded **verbatim into agent context** every diff --git a/docs/adr/1990-existing-code-onboarding.md b/docs/adr/1990-existing-code-onboarding.md index aa9b47c3f..ed6bc9bab 100644 --- a/docs/adr/1990-existing-code-onboarding.md +++ b/docs/adr/1990-existing-code-onboarding.md @@ -1,10 +1,25 @@ # Existing Code Onboarding Module owns deterministic repo-state detection and onboarding route selection -- **Status:** Proposed +- **Status:** Accepted — ratified 2026-07-17 (originally Proposed 2026-07-06); see "Ratification" below - **Date:** 2026-07-06 - **Issue:** #1990 - **Implementation:** PR #1994 +## Ratification (2026-07-17): Proposed → Accepted + +Ratified by explicit maintainer directive after independent re-verification of the evidence below; the Status field sat at Proposed for 11 days after the decision shipped. + +**Evidence the decision shipped:** + +- Issue #1990 ("Add /gsd:onboard for existing-codebase setup") is CLOSED, stateReason COMPLETED, closed 2026-07-07T04:23:56Z; PR #1994 ("feat(#1990): add brownfield onboarding workflow") is MERGED into `next` at 2026-07-07T04:23:55Z with body `Closes #1990`. +- `src/onboard-projection.cts` (15,248 bytes) and its compiled `gsd-core/bin/lib/onboard-projection.cjs` are both present on disk, implementing the projection this ADR describes. +- `src/init.cts:56` imports the projection as `onboardProjection`, and `src/init-command-router.cts:75` wires the `onboard:` route that consumes it — confirming the Init Command Module integration (the ADR's literal handler name "initOnboard" is not itself a grep-matched symbol; the consumption is the router entry plus the destructured import). +- `gsd-core/workflows/onboard.md`, `commands/gsd/onboard.md`, and `skills/gsd-onboard/SKILL.md` all exist on disk, matching the "What stays OUTSIDE this Module" boundary. +- `tests/onboard-command.test.cjs` (25,129 bytes) contains named tests covering the load-bearing gate order — `routes planning artifacts without PROJECT.md to partial planning`, `fast mode routes incomplete planning to partial-planning before the complete-map gate (regression #1990: fast map gate misroute)` — vendor exclusion (`ignores generated and vendor directories when detecting existing code`), and package-manifest brownfield detection (`treats package manifests as brownfield even without source files`). +- Six commits tagged `#1990` landed the ADR, the projection module, and doc/index updates: `3c7d722ed`, `e8fb05e96`, `d0b8eacd3`, `1171499f3`, `bc751a64e`, `192764f0c`. + +**Governance state:** Owning issue #1990 — CLOSED, stateReason COMPLETED, closed 2026-07-07T04:23:56Z. + ## Context GSD already ships strong individual primitives for adopting an existing codebase: `/gsd:map-codebase` (parallel codebase analysis), `/gsd:ingest-docs` (classify and consolidate existing ADR/PRD/SPEC/RFC docs), and `/gsd:new-project` (planning initialization). What it lacked was a single guided entry point that inspects a brownfield repository and tells the user *which primitive runs first*. diff --git a/docs/adr/218-release-version-validation.md b/docs/adr/218-release-version-validation.md index 28cf322d2..138745f1b 100644 --- a/docs/adr/218-release-version-validation.md +++ b/docs/adr/218-release-version-validation.md @@ -1,4 +1,4 @@ -# ADR-0175: Harden release-workflow version validation — reject leading zeros and pre-check npm +# ADR-218: Harden release-workflow version validation — reject leading zeros and pre-check npm - **Status:** Accepted (2026-05-24) - **Date:** 2026-05-24 diff --git a/docs/adr/22-plan-drift-guard.md b/docs/adr/22-plan-drift-guard.md index 10c139dad..5ef997bbb 100644 --- a/docs/adr/22-plan-drift-guard.md +++ b/docs/adr/22-plan-drift-guard.md @@ -1,9 +1,23 @@ # Plan-vs-codebase drift guard: defaults and symbol-resolver seam -- **Status:** Proposed +- **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. diff --git a/docs/adr/2264-golden-parity-redesign.md b/docs/adr/2264-golden-parity-redesign.md index c4dcfa6ff..f054aef17 100644 --- a/docs/adr/2264-golden-parity-redesign.md +++ b/docs/adr/2264-golden-parity-redesign.md @@ -6,6 +6,14 @@ - **Supersedes:** nothing; amends the ADR-1239 Phase-B safety-net harness - **Relationship to prior work:** Evolves `tests/golden-install-parity.test.cjs` (ADR-1239 Phase B). Related: #2086 (claude-local realpath normalization), #2095/#2100/#2117 (exclusion-set drift incidents), #1691 (scoped-CI drift guard). +## Why this is still `Proposed` (audited 2026-07-17) + +Audited 2026-07-17 against the live tree and GitHub. Phase 1 shipped cleanly and is genuinely done: `buildParityManifest`, `buildInstallTree`, and the four exclusion constants (`VOLATILE_FILES`, `HOOK_CONFIG_FILES`, `HOOK_CONFIG_RELATIVE_PATHS`, `EXCLUDED_PREFIXES`) live as the single source of truth in `tests/helpers/install-shared.cjs` (lines 104-234); `scripts/gen-golden-install-parity-zcode.cjs` now imports them instead of re-declaring them; the anti-divergence guard (`tests/golden-parity-single-source.test.cjs`) enforces no second copy; `scripts/ci-test-scope.cjs` (lines 121, 145, 175-176) selects the golden suite on the wider path set the Amendment describes; `npm run gen:golden` (`package.json:93`) exists; and all five issues (#2264-#2268) are closed as completed. + +**The blocker:** Acceptance criteria 3 and 4 name mechanisms that were never built. AC3 requires "A simulated converter bug ... caught by the converted-artifact golden" and AC4 requires "A simulated verbatim-copy corruption ... caught by the copy-parity property test" — neither `tests/fixtures/converted-artifacts/` nor any `*copy-parity*` test file exists anywhere in the tree (confirmed absent by direct filesystem check). The ADR's own same-day Amendment explains why (§3/§4 were found "unsound" in the Phase 2 spike) and states the old monolithic content-hash golden "is retained as-is" — but the Acceptance Criteria section itself was never edited to drop or reword AC1/AC3/AC4 to match the revised design. That leaves AC1 — the document's headline must-have, "editing the content of a verbatim/path-injected copied shipped file (e.g. a `workflows/*.md`) requires zero manual fixture regeneration" — literally unmet: `tests/golden-install-parity.test.cjs` still content-hashes every emitted file via the retained `buildParityManifest` (confirmed at `install-shared.cjs:218`, `crypto.createHash('sha256')`), and `EXCLUDED_PREFIXES` (`install-shared.cjs:154`) excludes only `gsd-core/bin/lib/` — `workflows/*.md` is still fully inside the hashed manifest, so editing one still requires `npm run gen:golden`. The functional invariants AC3/AC4 care about are still covered, but only by the legacy hash golden this ADR set out to partly replace, not by the mechanisms the criteria name. + +**Unblock condition:** Edit the Acceptance Criteria section (items 1, 3, 4) to match the shipped, amended design — replace "zero manual fixture regeneration" and the named converted-artifact/copy-parity mechanisms with the criteria the Amendment actually delivers (file-set snapshot catches structural drift; the retained content-hash golden catches converter and copy corruption; `npm run gen:golden` is the one-command fix for legitimate content changes) — then flip Status. No further code work is required; this is a documentation edit against already-shipped, already-closed work. + ## Context `tests/golden-install-parity.test.cjs` snapshots the installer output for 18 runtime layouts. For each runtime it runs a real `runMinimalInstall`, walks every emitted file, normalizes volatile bits (temp root → ``, package version → ``, macOS realpath `/private` → ``), SHA-256s each file (16-char slice), and compares the entire path→hash map against a committed fixture under `tests/fixtures/golden-install-parity/*.json` (18 files, ~520 KB, ~7,500 hash lines). diff --git a/docs/adr/230-introduce-next-integration-branch.md b/docs/adr/230-introduce-next-integration-branch.md index 81c3d6949..f473a3b00 100644 --- a/docs/adr/230-introduce-next-integration-branch.md +++ b/docs/adr/230-introduce-next-integration-branch.md @@ -8,6 +8,72 @@ > open a `chore:` issue, replace `XXXX` with the assigned issue number, and > rename the file accordingly. +## Why this is still `Proposed` (audited 2026-07-17) + +The architectural shift is real and operating: the live default branch is +`next` (`gh api repos/open-gsd/gsd-core --jq .default_branch`), `.github/workflows/auto-backmerge.yml` +runs unconditionally (`if: true`, not the Phase-1 `if: false` stub) and has +produced real, merged `main → next` back-merge PRs across multiple releases +(#671, #1337, #1673, and others), `release.yml` cherry-picks from +`origin/next` with an `origin/main` fallback per the Phase-3 patch, +`pr-target-validator.yml` enforces (`WARN_ONLY: 'false'`), and +`auto-branch.yml` branches from `next` with a `main` fallback. + +**The blocker.** The Decision section requires differentiated branch +protection: `main` — "strict: 2 reviewer approvals, all CI green, ... +restrict push to maintainers via PR only"; `next` — "loose: ... 'require +branches up to date' OFF." Live settings invert this. `main`'s classic +branch protection (verified via `gh api repos/open-gsd/gsd-core/branches/main/protection` +and its `required_pull_request_reviews` / `required_status_checks` +sub-resources) shows `required_approving_review_count: 1` (spec: 2), +`required_status_checks` returns 404 "not enabled" (spec: all CI green +required — there is no CI gate on `main` at all), and +`allow_force_pushes.enabled: true` (spec: restrict push to maintainers via +PR only). `next`'s protection, by contrast, has `required_status_checks.strict: true` +across 7 contexts and `allow_force_pushes.enabled: false` — stricter than +`main`, not looser. The two GitHub Rulesets that might have compensated +(`main-protection` id 16752567, `release-branches` id 16752568) are both +`enforcement: "evaluate"` (dry-run, non-blocking) and were never promoted +to active; `main-protection`'s condition further targets `~DEFAULT_BRANCH`, +a dynamic alias that now resolves to `next` (the current default branch), +so even if activated it would apply to the wrong branch. Migration Phase 2 +step 3 ("Apply branch protection: `bash scripts/setup-branch-protection.sh`") +was evidently run for `next` but never durably applied to `main`. + +Issue #230's own closure (`state_reason: completed`) certifies only Phase 1 +(additive infrastructure) — its body scopes itself explicitly to Phase 1 +and defers branch-protection application, the default-branch flip, and +workflow-enforcement flags to a "Phase 2 follow-up (separate PR)"; that +follow-up evidently landed for `next` but not for `main`'s protection. +Separately, `next`'s "require branches up to date OFF (this is the whole +point)" was reversed five days later by ADR-415 (Accepted, 2026-05-28), +which set `required_status_checks.strict = true` on `next` after a real +stale-base regression (#406/#411/#412) — so the specific rebase-treadmill +relief this ADR promises for `next` no longer holds exactly as written, +though the broader architectural decision (integration branch, isolated +`main`, automated back-merge) is unaffected. Migration Phase 4 cleanup +(drop `develop` from `branch-naming.yml`'s `alwaysValid`; drop the `|| main` +fallbacks in `release.yml`/`auto-branch.yml`) is also still open, gated on +"2-3 successful releases" per the ADR's own text — cosmetic, not blocking. + +**Unblock condition.** Ratify once `main`'s live branch protection matches +this ADR's Decision section — `required_approving_review_count: 2`, +`required_status_checks` enabled and required, `allow_force_pushes: false` +— applied via `scripts/setup-branch-protection.sh` (or an equivalent `gh api` +call), and the two `evaluate`-mode Rulesets are either activated with +corrected `ref_name` conditions or removed as redundant with classic +protection. Verify with: + +``` +gh api repos/open-gsd/gsd-core/branches/main/protection/required_pull_request_reviews --jq .required_approving_review_count # expect 2 +gh api repos/open-gsd/gsd-core/branches/main/protection/required_status_checks # expect 200, not 404 +gh api repos/open-gsd/gsd-core/branches/main/protection --jq .allow_force_pushes.enabled # expect false +``` + +Until then, either bring `main`'s protection into line with the Decision +section, or amend this ADR (as ADR-415 did for one `next` parameter) to +record the protection posture actually in force. + ## Context Today every contributor branch — `feat/`, `fix/`, `chore/`, `docs/`, diff --git a/docs/adr/3524-cjs-sdk-hard-seam.md b/docs/adr/3524-cjs-sdk-hard-seam.md index 2ad3b5989..b44c206cd 100644 --- a/docs/adr/3524-cjs-sdk-hard-seam.md +++ b/docs/adr/3524-cjs-sdk-hard-seam.md @@ -1,6 +1,6 @@ # CJS↔SDK hard seam — one source of truth per Shared Module -- **Status:** Superseded by ADR-0174 (2026-05-23); originally Proposed (2026-05-14) +- **Status:** Superseded by [ADR-0174](0174-retire-gsd-sdk-package-boundary.md) (2026-05-23); originally Proposed (2026-05-14) - **Date:** 2026-05-14 - **Tracking issue:** [#3524](https://github.com/open-gsd/get-shit-done-redux/issues/3524) - **Related PRD:** [`docs/prd/3524-cjs-sdk-hard-seam.md`](../prd/3524-cjs-sdk-hard-seam.md) diff --git a/docs/adr/3660-runtime-artifact-layout-module.md b/docs/adr/3660-runtime-artifact-layout-module.md index bb315cb5c..4ad626d15 100644 --- a/docs/adr/3660-runtime-artifact-layout-module.md +++ b/docs/adr/3660-runtime-artifact-layout-module.md @@ -4,6 +4,19 @@ - **Date:** 2026-05-17 - **Issue:** #3660 - **Implementation:** #3663 (Phase 1), feat/3663-runtime-artifact-layout-module-phase-1-m +- **Subsumed by:** [ADR-1239](1239-gsd-embeddable-orchestration-engine.md) (GSD as an Embeddable Orchestration Engine) — read it first; see the amendment below + +## 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: per-runtime artifact placement becomes one negotiated surface of the Host-Integration Interface rather than the outermost seam at which GSD meets a host. + +**This ADR is not superseded and its status is unchanged.** The artifact-layout seam is live and load-bearing; it is now a *component* of the EoS frame. + +**Read [ADR-1239](1239-gsd-embeddable-orchestration-engine.md) first.** + +Recorded because ADR-1239 declared this subsumption while this file recorded nothing. + +## Context The **Runtime Surface Module** (`gsd-core/bin/lib/surface.cjs`, introduced by ADR-0011 Phase 2) re-materializes a resolved Skill Surface profile to disk via `applySurface`. It currently hardcodes two artifact kinds (`commands`, `agents`) and re-derives their source directories via `_findInstallSource` / `_findAgentsSource` walk-up heuristics. The install and uninstall pipelines in `bin/install.js` each encode the same per-runtime artifact layout independently across ~14 install sites and ~6 uninstall sites. Bug #3659 surfaced the resulting drift: `applySurface` omits the `skills` kind for runtimes whose canonical layout is `skills/gsd-/SKILL.md`, so `gsd-surface profile ` leaves ~67 skill directories on disk under the install-time profile's footprint when the resolved profile should have pruned them — roughly 2.7k tokens per session on a measured workstation. diff --git a/docs/adr/443-opus48-unified-effort-and-fast-mode-routing.md b/docs/adr/443-opus48-unified-effort-and-fast-mode-routing.md index f28704ebc..a1b9ea1eb 100644 --- a/docs/adr/443-opus48-unified-effort-and-fast-mode-routing.md +++ b/docs/adr/443-opus48-unified-effort-and-fast-mode-routing.md @@ -4,6 +4,14 @@ - **Date:** 2026-05-28 - **Tracking issue:** [#443](https://github.com/open-gsd/get-shit-done-redux/issues/443) +## Why this is still `Proposed` (audited 2026-07-17) + +The audit confirmed the cross-provider resolver/renderer/CLI machinery genuinely shipped: `resolveEffortInternal`, `resolveEffortForTier`, `renderEffortForRuntime`, `RUNTIMES_WITH_FAST_MODE`, and `cmdResolveExecution` (`src/model-resolver.cts:534,654`; `src/commands.cts`) implement the cascade and clamping exactly as Decision items 1–3, 5, and 6 describe, and static install-time propagation is real and end-to-end tested — `tests/install-runtime-artifacts.test.cjs`'s `describe('#443 Claude install: effort: injected into frontmatter')` runs the actual `install()` function and reads the resulting agent `.md` files off disk, confirming `gsd-planner` gets `effort: xhigh`, `gsd-codebase-mapper` gets `effort: low`, and `gsd-executor` gets `effort: high`. That test predates the QA audit below (landed 2026-05-29 in the original `#443` PR, commit `5ca646f01`), so the "resolver-only, nothing reaches the runtime" framing of the original flavor-text problem this ADR set out to fix is fixed for the static path. + +**The blocker.** Decision item 1's cascade names an "(1) orchestrator invocation override" as the *highest*-precedence layer, and Decision item 6 adds a dynamic escalation path ("effort steps up the ladder on a failed attempt"). Both exist only as CLI-callable resolver code — `resolveEffortInternal`'s invocation-override step (`src/model-resolver.cts:535`) and `resolveEffortForTier`'s attempt-based escalation (`src/model-resolver.cts:654`) — exercised solely by unit/CLI tests. Nothing in the shipped orchestration actually calls them: a search across every file in `gsd-core/workflows/*.md` and `agents/*.md` for `resolve-execution` or `CLAUDE_CODE_EFFORT_LEVEL` returns zero hits; the only workflow-level mentions of "effort" are documentation of the config keys in `settings-advanced.md`'s confirmation table. The only propagation channel actually wired into a real GSD flow is the static one (config → `install()` → frontmatter, baked once at install time) — the ADR's own decided design promises more than that, and the more-than-static-baking part has no consumer. Separately, the repo's own dated QA test-architecture audit (`docs/issueevidence/1192-adr-test-audit-2026-06-13.md`, produced under issue #1192, closed COMPLETED) rated ADR-443 "partial ... **end-to-end effort propagation untested**" and named it in its action plan ("Strengthen ... ADR-443 end-to-end effort propagation," line 220); that action item was never converted into a tracked follow-up issue, and no commit since 2026-06-13 addresses it. That audit's blanket "untested" framing overstates the gap — the static path is tested — but the underlying signal (a decided mechanism with no live caller) is real and independently confirmed here. + +**Unblock condition.** Either (a) wire the orchestrator-invocation-override and attempt-based-escalation paths into an actual GSD workflow or agent dispatch (so `resolveEffortForTier`'s escalation and `resolveEffortInternal`'s invocation-override step have a real caller outside `src/commands.cts`'s CLI surface and tests), and add a test exercising that live path the way `tests/install-runtime-artifacts.test.cjs` exercises the static one; or (b) if the ADR's intended scope is in fact limited to static install-time propagation, amend Decision items 1 and 6 to say so explicitly and close out audit issue #1192's action-plan item 18 with a note pointing at the shipped install-wiring tests. Either is a maintainer call this file records but does not make. + ## Context ### Effort control and fast mode in Claude Opus 4.8 diff --git a/docs/adr/58-runtime-install-policy-module.md b/docs/adr/58-runtime-install-policy-module.md index a0556988f..feb8eddf4 100644 --- a/docs/adr/58-runtime-install-policy-module.md +++ b/docs/adr/58-runtime-install-policy-module.md @@ -3,6 +3,24 @@ - **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 diff --git a/docs/adr/660-release-from-next-head.md b/docs/adr/660-release-from-next-head.md index 94e49ce8b..f1fc5e13f 100644 --- a/docs/adr/660-release-from-next-head.md +++ b/docs/adr/660-release-from-next-head.md @@ -3,6 +3,38 @@ - **Status:** Proposed - **Date:** 2026-06-03 +## Why this is still `Proposed` (audited 2026-07-17) + +Confirmed shipped: immutable per-release tags (`finalize` mints `v` exactly once at +line 629; `rc` auto-increments `v-rc.N` at line 353 — no force-push or re-tag anywhere +in the file), the `@next`/`@latest` dist-tag split (`npm publish --provenance --access public +--tag next` in the `rc` job at line 437 vs. the default/`latest` publish in `finalize` at line +636), and the Amendment (2026-06-12, #1104) "`next` rests at last published" behavior, wired +through `scripts/sync-next-version.cjs` in both the `rc` job (`release.yml:479`) and the +`main`→`next` back-merge (`auto-backmerge.yml:176-178`). + +**The blocker.** Decision §1 — the mechanism this ADR is named for — is not implemented: "recreate +(or hard-reset) an **ephemeral** `release/` branch from `origin/next` HEAD at the *start* +of each `rc`/`finalize` run." In the live `.github/workflows/release.yml`, the `create` job still +creates `release/` once and hard-errors if it already exists ("Branch $BRANCH already +exists. Delete it first or use rc/finalize.", lines 126–133); the `rc` job's checkout (line 330) +and the `finalize` job's checkout (line 522) both simply check out that same pre-existing ref — +neither job fetches, resets, or recreates it from `origin/next`. This is exactly the "persistent +branch you never backport into" antipattern the ADR's own Context section set out to kill, and +precisely the alternative its own Alternatives section rejected ("Keep the persistent branch but +cherry-pick RC fixes into it ... Rejected as primary"). `docs/adr/README.md:98` already names this +ADR in the corpus audit as one whose "namesake mechanism is performed by hand." Issue #660 is +closed `COMPLETED`, but its scope was landing the ADR/design decision, not the `release.yml` +re-cut step — no commit since has added it; today, cutting an rc "on the head of `next`" still +requires a manual `git push --force origin :refs/heads/release/` before +dispatching the workflow. + +**Unblock condition.** Add a step to both the `rc` and `finalize` jobs in +`.github/workflows/release.yml` that hard-resets (or recreates) `release/` from +`origin/next` HEAD before the version bump, so the re-cut happens automatically on every +dispatch instead of via a manual force-push. Once that step exists in the file and one real +`rc`/`finalize` run has exercised it end to end, this ADR is ready for another ratification pass. + ## Context The release pipeline (`.github/workflows/release.yml`) is a three-mode `workflow_dispatch` diff --git a/docs/adr/857-capability-system.md b/docs/adr/857-capability-system.md index 9aeba61ed..3ad4938f6 100644 --- a/docs/adr/857-capability-system.md +++ b/docs/adr/857-capability-system.md @@ -1,12 +1,42 @@ # ADR-857: Capability system — five-step loop as core, features as plug-ins behind Loop Extension Points [Proposed] -- **Status:** Proposed +- **Status:** Accepted — ratified 2026-07-17 (originally Proposed 2026-06-08); see "Ratification" below - **Date:** 2026-06-08 - **Issue:** #857 -- **Supersedes (generalizes):** Skill Surface Budget Module (ADR-0011), Runtime Install Policy Module (ADR-0058) +- **Subsumes (generalizes):** Skill Surface Budget Module ([ADR-0011](0011-skill-surface-budget-module.md)), Runtime Install Policy Module ([ADR-58](58-runtime-install-policy-module.md)) — both remain **Accepted and live**; this ADR generalizes them, it does not replace them. See "Relation to ADR-0011 and ADR-58" below. - **Builds on:** CommandRoutingHub (ADR-0012), Runtime Artifact Layout Module (ADR-3660), generated-cjs single source (ADR-457) - **Amended:** 2026-06-12 — phase-6 boundary settled before the Migrate phase freezes it: the **verifier↔predicate contract** is classified as core verification substrate (not an off-by-default Feature Capability). See *Verification substrate vs. plug-in tier (the predicate boundary)* below. Prompted by @davesienkowski's boundary analysis on #857; coordinates with ADR-550 (spec-phase probe contract). +## Ratification (2026-07-17): Proposed → Accepted + +Ratified by maintainer directive. This ADR read `Proposed` for over a month while the capability system it decides **was the shipped architecture of 1.6.0/1.7.0** — a label that invited contributors and agents to treat the live plug-in architecture as an unbuilt idea. + +**Evidence the decision shipped** (each item verified against the tree, then independently re-verified by two reviewers instructed to refute this ratification; neither could): + +- **The lifecycle exists.** `src/capability-lifecycle.cts:880-1053` implements `installCapability`, plus `upgradeCapability` / `removeCapability` / `bindProjectConsent`. `src/capability-state.cts` resolves capability state; `gsd-core/bin/lib/capability-validator.cjs` validates descriptors; `scripts/gen-capability-registry.cjs` generates the registry into `gsd-core/bin/lib/capability-registry.cjs`. +- **The plug-in split is real, not notional.** `capabilities/` holds 30+ descriptors spanning `role: feature` (research, ui, security, code-review, graphify, intel, audit, profile-pipeline, tdd, schema-gate, drift, gap-analysis, nyquist, pattern-mapper, ai-integration, mempalace, assumption-delta, external-job) and `role: runtime` (claude, codex, antigravity, cline, cursor, windsurf, kilo, qwen, hermes, pi, trae, augment, copilot, codebuddy, opencode, vscode). +- **The god-module is gone.** `src/core.cts` — the 2271-line module named in this ADR's Context — no longer exists; it decomposed into `io.cts`, `config-loader.cts`, `phase-locator.cts`, `model-resolver.cts`, `roadmap-parser.cts`. Epic #1267 ("retire the core.cjs re-export spine") is closed. Corroborated independently at [`612-bracket-phase-id-convention.md`](612-bracket-phase-id-convention.md):165 ("core.cts no longer exists"). +- **The Loop Extension Points are wired.** All 12 named points (`discuss:pre/post`, `plan:pre/post`, `execute:pre`, `execute:wave:pre/post`, `execute:post`, `verify:pre/post`, `ship:pre/post`) have live render-hook call sites in the host loop. +- **The workflow bodies shrank, as this ADR's Consequences promised.** `plan-phase.md` is 93,959 bytes against a frozen pre-phase-6 ceiling of 94,519; `execute-phase.md` is 93,363 against 93,600. +- **Tests exercise it.** 16 dedicated `tests/capability-*.test.cjs` files (largest: `capability-registry.test.cjs` at 297K, `capability-lifecycle.test.cjs` at 184K), plus `tests/phase6-capstone-conformance.test.cjs`, whose three assertions are marked "RED BY DESIGN until phase 6 is actually complete" — added after #1139 was caught closing green on a false completion — and all three pass. +- **Governance closed.** Epic [#857](https://github.com/open-gsd/gsd-core/issues/857) is CLOSED with `stateReason=COMPLETED` (2026-06-14). All six rollout-phase sub-issue clusters are CLOSED/COMPLETED: #870/#885 (phases 1–2), #894/#896/#903/#910/#918 (phase 3), #942/#945/#959/#961/#1136/#1138/#1213 (phase 4), #1016/#1035/#1056/#1077 (phase 5), #1120/#1135/#1137/#1139/#1820 (phase 6). +- **The corpus already treats it as live.** 20+ later ADRs (894, 959, 1016, 1056, 1077, 1143, 1213, 1239, 1244, 1372, 1593, 1606, 1671, 1769, 1817, 1820, 550, 58, 612) build on this ADR's capability model as current architecture; none claims to replace it. [`1244-capability-ecosystem.md`](1244-capability-ecosystem.md):12, authored independently, states: "32 capabilities ship today (20 `role:feature`, 12 `role:runtime`). The architecture is in place." + +### Relation to ADR-0011 and ADR-58 — generalized, not replaced + +This ADR's header field originally read "**Supersedes** (generalizes)". On ratification that wording was corrected to **Subsumes**, because taking "supersedes" literally would have stamped two live decisions as dead: + +- [ADR-0011](0011-skill-surface-budget-module.md)'s Skill Surface Budget Module is live — `applySurface` at `src/surface.cts:348`. +- [ADR-58](58-runtime-install-policy-module.md)'s typed `InstallPlan` seam is live — `src/runtime-artifact-install-plan.cts:82`. + +Both keep `Accepted` status and now carry a `Subsumed by` pointer here. The parenthetical "(generalizes)" was always the accurate word; only the field name was wrong. + +### What this ratification does not settle + +[ADR-959](959-capability-command-contribution.md) (command contribution) stays `Proposed`: issue [#2346](https://github.com/open-gsd/gsd-core/issues/2346) — "ADR: Command Dispatch Completion" — is OPEN and maintainer-approved, and explicitly plans 959's graduation as its own capstone ADR. Ratifying it here would preempt that. + +See also [ADR-1239](1239-gsd-embeddable-orchestration-engine.md) (EoS, Accepted), which realizes and **inverts** this ADR's Decision 8 — flipping *projection* to *embedding*. For how GSD meets a host, EoS is the current frame. + ## Context GSD has no real line between **the loop** and **a feature**. The five-step loop — Discuss → Plan → Execute → Verify → Ship — is the product, but its workflow bodies have absorbed every optional feature as inline `if config.X` branches: diff --git a/docs/adr/894-capability-declaration-format.md b/docs/adr/894-capability-declaration-format.md index 6e480ab24..67f5e9849 100644 --- a/docs/adr/894-capability-declaration-format.md +++ b/docs/adr/894-capability-declaration-format.md @@ -1,10 +1,35 @@ -# ADR-894: Capability declaration format + registry generation [Proposed] +# ADR-894: Capability declaration format + registry generation [Accepted] -- **Status:** Proposed +- **Status:** Accepted — ratified 2026-07-17 (originally Proposed 2026-06-08); see "Ratification" below - **Date:** 2026-06-08 (amended same day across two design grillings — see "Grilling amendments") - **Issue:** #894 -- **Parent:** ADR-857 (Capability system) — resolves its Open question #1 +- **Parent:** [ADR-857](857-capability-system.md) (Capability system) — resolves its Open question #1 - **Phase:** ADR-857 rollout phase 3a (design-only) +- **Subsumed by:** [ADR-1239](1239-gsd-embeddable-orchestration-engine.md) (GSD as an Embeddable Orchestration Engine) — read it first; see the amendment below + +## Amendment (2026-07-16): subsumed by ADR-1239 (EoS); status is stale + +[ADR-1239](1239-gsd-embeddable-orchestration-engine.md) — **GSD as an Embeddable Orchestration Engine** (EoS), Accepted — subsumes this ADR as an adapter. The capability declaration format remains the vocabulary a descriptor is written in; EoS is the frame that decides how a host loads the engine at all. + +**Read [ADR-1239](1239-gsd-embeddable-orchestration-engine.md) first.** + +Recorded because ADR-1239 declared this subsumption while this file recorded nothing. + +## Ratification (2026-07-17): Proposed → Accepted + +Ratified by explicit maintainer directive after independent re-verification of the evidence below; the `Proposed` label had been stale for roughly 39 days (2026-06-08 → 2026-07-17) after the format it specifies had already shipped. + +**Evidence the decision shipped:** + +- Owning issue [#894](https://github.com/open-gsd/gsd-core/issues/894) and parent epic [#857](https://github.com/open-gsd/gsd-core/issues/857) are both CLOSED / COMPLETED. +- `scripts/gen-capability-registry.cjs` (886 lines) implements the §4 generator: reads every `capabilities//capability.json`, validates each via `capability-validator.cjs`, and enforces the one-owner / acyclic / tier-monotone / config-exclusivity / unique-producer invariants. +- `gsd-core/bin/lib/capability-validator.cjs` (2,346 lines) validates the §2 schema, including the gate `check` discriminator — exactly one of `query` / `predicate` / `agentVerdict` (around lines 1566–1573). +- 37 real `capabilities//capability.json` files exist on disk; `capabilities/ui/capability.json` matches the ADR's worked UI example (`tier`/`requires`/`skills`/`agents`/`steps`/`gates`) near-verbatim, plus additive fields (`version`, `engines`, `runtimeCompat`) not in the original text. +- `capabilities/codex/capability.json` has concrete `role: "runtime"` enums filled in — `commandStyle: "shell-var"`, `hooksSurface: "codex-hooks-json"`, `sandboxTier: "codex-agent-sandbox"` — resolving the ADR's own "deferred to phase 5" open question. +- `scripts/gen-loop-host-contract.cjs` (18,992 bytes) parses the `` comment markers out of the five step workflows (confirmed at `gsd-core/workflows/plan-phase.md:1`) into the generated host contract. +- `gsd-core/bin/lib/capability-registry.cjs` (235,826 bytes, generated) contains `byLoopPoint` (line 3033), `configKeys` (line 3578), and `requiresClosure()` (line 5779) — the role-partitioned §5 shape. + +**Governance state:** owning issue #894 CLOSED/COMPLETED (closed 2026-06-08); parent epic #857 CLOSED/COMPLETED (closed 2026-06-14). ## Context diff --git a/docs/adr/README.md b/docs/adr/README.md index f3fbb11e2..b7ffbf440 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -4,6 +4,15 @@ This directory contains Architecture Decision Records (ADRs) for GSD. Each ADR documents one architectural decision: what was decided, why, and what consequences follow. ADRs are append-only. Amendments extend existing ADRs with a dated section rather than replacing them. +## Reading this corpus + +**Start with the [index](#index) below, and respect the status.** The index is grouped so that the first table — *Active decisions* — is the set that governs the system as it stands. An ADR in *Superseded, Retired, and Legacy* is historical: it records what was once decided and names what replaced it. Do not cite it as current architecture. + +Two things the index makes explicit, because getting them wrong has actually misled readers here: + +- **"Read first"** on an active ADR points at a *broader* ADR that now frames it. A decision can be entirely correct and still not be the whole picture. The runtime capability descriptor ([ADR-1016](1016-runtime-capability-descriptor.md)) is live and load-bearing, but [ADR-1239](1239-gsd-embeddable-orchestration-engine.md) (**EoS** — GSD as an Embeddable Orchestration Engine) subsumes it as the *declarative adapter* and inverts its direction: GSD is the engine a host embeds, not an installer that projects onto a host. For **how GSD meets a host, EoS is the current frame.** +- **`Proposed` means not ratified — and it is kept honest.** On 2026-07-17 the corpus was audited against the shipped tree and nine ADRs whose decisions had demonstrably shipped were ratified to `Accepted`, each carrying a dated **Ratification** section with the evidence (see [ADR-857](857-capability-system.md) for the fullest example). The ADRs that remain `Proposed` are `Proposed` **for a reason recorded in the file** — an unmet acceptance criterion, an outstanding phase, or a successor ADR already planned — not through neglect. Trust the label; if you think it is wrong, prove it in a dated section and see [Ratifying a stale `Proposed`](#ratifying-a-stale-proposed). + ## Naming Convention New ADRs use **issue#-prefix slug** naming: @@ -12,90 +21,206 @@ New ADRs use **issue#-prefix slug** naming: docs/adr/-.md ``` -Examples: `3485-adr-prd-naming-convention.md`, `3464-review-default-reviewers.md`. +Examples: `2264-golden-parity-redesign.md`, `1239-gsd-embeddable-orchestration-engine.md`. ### Why Two developers computing "next ADR number" locally against `main` will independently pick the same integer and both ship. The collision is already on disk — `0010-*` exists twice and `0011-*` exists three times. GitHub issue numbers are server-assigned and atomic: the moment you open an issue, that number is reserved globally. Two PRs that both edit the `### Fixed` block of `CHANGELOG.md` always conflict on merge — two PRs that each use a distinct issue# as their ADR prefix never collide. Same shape, same solution. -### Legacy ADRs +### Legacy *naming* is not `Legacy` *status* -Files `0001-*` through `0011-*` are preserved as immutable historical record. The duplicate `0010-*` and the three-way `0011-*` are documented residue of the old local-compute convention — not patterns to imitate. Do not renumber them. +Files `0001-*` through `0012-*` (and `0174-*`) are preserved as immutable historical record of the old local-compute numbering. The duplicate `0010-*` and the three-way `0011-*` are documented residue of that convention — not patterns to imitate. **Do not renumber them.** + +This is a statement about **filenames only**. Many of those ADRs are `Accepted` and load-bearing today ([ADR-0002](0002-command-contract-validation-module.md), [ADR-0004](0004-worktree-workstream-seam-module.md), [ADR-0008](0008-installer-migration-module.md), [ADR-0009](0009-shell-command-projection-module.md)). An old filename says nothing about whether a decision still holds. The `Legacy` **status** in the table below is a separate claim — see the vocabulary. + +Because `0010-*` and `0011-*` each resolve to more than one file, a bare cross-reference like "ADR-0011" is genuinely ambiguous. Link the file (see [Lifecycle rules](#lifecycle-rules)). ### Full process See **[CONTRIBUTING.md — "Proposing an ADR or PRD"](../../CONTRIBUTING.md#proposing-an-adr-or-prd)** for the end-to-end workflow: opening the issue, waiting for approval, naming the file, and submitting the PR. +PRDs live in [`docs/prd/`](../prd/), not here. ([`0011-review-default-reviewers-prd.md`](0011-review-default-reviewers-prd.md) predates that directory and is kept in place as frozen historical record.) + +## Lifecycle rules + +These are enforced by `scripts/gen-adr-index.cjs`, which runs in CI via `npm run lint:generated-sync`. A violation fails the build with the exact file and fix. + +### 1. Every ADR declares one status from the canonical vocabulary + +The first word of the `Status` field must be one of: + +| Status | Means | Obligation | +|--------|-------|------------| +| `Accepted` | Decided and in force. Cite it. | — | +| `Proposed` | Decided in principle, not ratified. Do not cite as settled. | If the work has demonstrably shipped, ratify it (below) — do not leave the label lying. | +| `Superseded` | A specific newer ADR replaced this decision. | **Must name the successor as a file link.** | +| `Retired` | What this ADR decided no longer exists at all, and no single ADR replaced it. | Say what was removed and when. | +| `Legacy` | Frozen historical record, kept for provenance; not a pattern to follow. | Say why it is frozen. | + +Prose may follow the token (`Superseded by [ADR-0174](0174-retire-gsd-sdk-package-boundary.md) (2026-05-23); originally Accepted (2026-05-09)`). Both the bullet form (`- **Status:** Accepted`) and the table form (`| **Status** | Accepted |`) are accepted. + +### 2. Cross-references to other ADRs are file links, never bare ids + +Write `[ADR-0011](0011-skill-surface-budget-module.md)`, not `ADR-0011`. Bare ids are ambiguous for `0010`/`0011`, and unlinked references cannot be checked. + +If you mean an **issue**, write `#857` — not `ADR-857`. (An ADR and its owning issue often share a number; that is intentional and not a conflict.) + +### 3. Supersession and subsumption are symmetric + +These are different relations. Do not conflate them: + +- **`Supersedes` / `Superseded by`** — the target is *replaced*. Its status becomes `Superseded`. +- **`Subsumes` / `Subsumed by`** — the target *still holds*, but a broader ADR now frames it. Its status is **unchanged**; it becomes a component of the larger decision. + +If A declares either relation toward B, **B must record the reciprocal.** A one-way pointer is the failure this corpus actually suffered: [ADR-1239](1239-gsd-embeddable-orchestration-engine.md) declared it subsumed four ADRs, none of which said so, and none of which pointed back — so a reader landing on any of them concluded the superseded frame was the way forward. + +Only an `Accepted` ADR is owed the back-link. A `Proposed` ADR's claim is **prospective**: it has not taken effect, so its target is not marked. On ratification, the check begins demanding the back-links. + +### 4. The declared id matches the filename + +An H1 of `# ADR-0175: …` in a file named `218-*.md` is a rename that never finished. The id in the title must match the filename's prefix. + +### Ratifying a stale `Proposed` + +A stale `Proposed` is not cosmetic: it tells contributors and agents that live architecture is an unbuilt idea. Fix it — but on evidence, not vibes. + +**The bar.** All four must hold before flipping to `Accepted`: + +1. The decided mechanism demonstrably **exists** in the tree — name the files, symbols, and tests. +2. The owning issue is closed **as completed**. A closed issue is not proof: `stateReason` of *not planned* / duplicate means the decision was **dropped** (that is `Legacy` or `Retired`, not `Accepted`). +3. **No material part is unshipped.** If the ADR defines phases and one is outstanding, or states its own bar for acceptance and that bar is unmet, it stays `Proposed`. +4. No later ADR supersedes it, and no approved issue already plans its graduation as separate work. + +**The procedure.** Set the status to `Accepted — ratified (originally Proposed )`, add a dated `## Ratification` section holding the evidence, then run `node scripts/gen-adr-index.cjs --write`. If the ADR claims to supersede or subsume others, the gate will now demand their back-links — that is the point. Ratify deliberately. + +**Two traps worth knowing**, both hit during the 2026-07-17 audit: + +- **Shipped code is necessary, not sufficient.** Eight ADRs had every named module, symbol, and test present and their epics closed — and still failed the bar: [ADR-2264](2264-golden-parity-redesign.md)'s own headline acceptance criterion is unmet in the tree, [ADR-230](230-introduce-next-integration-branch.md)'s decided branch protection does not match the live API, [ADR-660](660-release-from-next-head.md)'s namesake mechanism is performed by hand, and [ADR-959](959-capability-command-contribution.md) has an approved issue planning its graduation as its own ADR. Verify the *decision*, not just the code. +- **"Supersedes" is often "subsumes".** Read what the ADR means before the gate makes you act on what it says. [ADR-857](857-capability-system.md) said "Supersedes (generalizes)"; taken literally, ratifying it would have stamped two live seams ([ADR-0011](0011-skill-surface-budget-module.md), [ADR-58](58-runtime-install-policy-module.md)) as dead. The parenthetical was the truth; the field name was wrong. + +## Maintaining the index + +**The index is generated. Do not hand-edit it.** Everything between the `ADR-INDEX:START` / `ADR-INDEX:END` markers is derived from the ADR files themselves: + +```bash +node scripts/gen-adr-index.cjs # print the index +node scripts/gen-adr-index.cjs --write # regenerate it into this file +node scripts/gen-adr-index.cjs --check # CI: fail if stale or invalid +``` + +After adding an ADR, or changing any ADR's status or relations, run `--write` and commit the result. `npm run lint:generated-sync` runs `--check` in CI, so a missing or stale row fails the build rather than rotting silently. + +This replaces a hand-maintained table that had drifted to **40 of 65 ADRs** — the entire capability family and EoS itself were missing from it, which is precisely why the ADRs a reader most needed were the ones they could not find. + ## Index -| ADR | Title | Status | -|-----|-------|--------| -| [0001-dispatch-policy-module.md](0001-dispatch-policy-module.md) | Dispatch policy module as single seam for query execution outcomes | Accepted | -| [0002-command-contract-validation-module.md](0002-command-contract-validation-module.md) | Command Contract Validation Module | Accepted | -| [0003-model-catalog-module.md](0003-model-catalog-module.md) | Model Catalog Module as single source of truth for agent profiles and runtime tier defaults | Accepted | -| [0004-worktree-workstream-seam-module.md](0004-worktree-workstream-seam-module.md) | Planning Workspace Module as single seam for worktree and workstream state | Accepted | -| [0005-sdk-architecture-seam-map.md](0005-sdk-architecture-seam-map.md) | SDK Architecture seam map for query/runtime surfaces | Superseded by ADR-0174 | -| [0006-planning-path-projection-module.md](0006-planning-path-projection-module.md) | Planning Path Projection Module for SDK query handlers | Accepted | -| [0007-sdk-package-seam-module.md](0007-sdk-package-seam-module.md) | SDK Package Seam Module owns SDK-to-get-shit-done-redux compatibility | Superseded by ADR-0174 | -| [0008-installer-migration-module.md](0008-installer-migration-module.md) | Installer Migration Module owns install-time upgrade safety | Accepted | -| [0009-shell-command-projection-module.md](0009-shell-command-projection-module.md) | Shell Command Projection Module owns runtime-aware OS command rendering | Accepted | -| [0010-file-operation-engine-module.md](0010-file-operation-engine-module.md) | File Operation Engine Module owns safe runtime/config file mutations | Proposed | -| [0010-skill-surface-budget-module.md](0010-skill-surface-budget-module.md) | Skill Surface Budget Module — earlier draft superseded by ADR-0011 | Superseded by 0011 | -| [0011-skill-surface-budget-module.md](0011-skill-surface-budget-module.md) | Skill Surface Budget Module owns install-time profile staging and runtime surface control | Accepted | -| [0011-review-default-reviewers.md](0011-review-default-reviewers.md) | Review default-reviewers selection policy for /gsd:review | Accepted | -| [0011-review-default-reviewers-prd.md](0011-review-default-reviewers-prd.md) | PRD for review.default_reviewers feature (#3464) | Reference | -| [0012-command-routing-hub.md](0012-command-routing-hub.md) | CommandRoutingHub as single dispatch seam for CJS command families | Superseded by ADR-0174 | -| [15-autonomous-cross-ai-convergence.md](15-autonomous-cross-ai-convergence.md) | Cross-AI plan convergence via existing orchestration commands | Proposed | -| [22-plan-drift-guard.md](22-plan-drift-guard.md) | Plan-vs-codebase drift guard: defaults and symbol-resolver seam | Proposed | -| [3524-cjs-sdk-hard-seam.md](3524-cjs-sdk-hard-seam.md) | CJS↔SDK hard seam — single canonical owner per responsibility (#3524) | Superseded by ADR-0174 | -| [3660-runtime-artifact-layout-module.md](3660-runtime-artifact-layout-module.md) | Runtime Artifact Layout Module owns per-runtime artifact placement | Accepted | -| [0174-retire-gsd-sdk-package-boundary.md](0174-retire-gsd-sdk-package-boundary.md) | Retire @opengsd/gsd-sdk package boundary — single-runtime collapse | Accepted | -| [452-eslint-lint-harness.md](452-eslint-lint-harness.md) | Adopt standard ESLint flat-config lint harness; retire homegrown regex scanners | Accepted | -| [456-test-rigor-architecture.md](456-test-rigor-architecture.md) | Test-rigor architecture — deterministic scheduling, antagonistic tier, typed-surface mandate, delete-bad-tests policy | Accepted | -| [457-generated-cjs-single-source.md](457-generated-cjs-single-source.md) | Collapse hand-written CJS to generated single-source | Proposed | -| [660-release-from-next-head.md](660-release-from-next-head.md) | Release from the head of next; immutable release tags; @next dist-tag as the RC surface | Proposed | -| [58-runtime-install-policy-module.md](58-runtime-install-policy-module.md) | Runtime Install Policy Module owns the typed install-plan projection | Accepted | -| [766-claude-code-plugin-manifest-module.md](766-claude-code-plugin-manifest-module.md) | Claude Code Plugin Manifest Module owns the projection of gsd-core surfaces onto the Claude Code plugin contract | Accepted | -| [1016-runtime-capability-descriptor.md](1016-runtime-capability-descriptor.md) | Runtime Capability Descriptor | Accepted | -| [1235-descriptor-driven-agent-conversion-migration.md](1235-descriptor-driven-agent-conversion-migration.md) | Migrate agent conversion to the descriptor-driven install path (parity + per-runtime cutover) | Proposed | -| [1411-resolution-provenance.md](1411-resolution-provenance.md) | Resolution must report provenance, not fall open silently | Accepted | -| [1508-runtime-artifact-conversion-module.md](1508-runtime-artifact-conversion-module.md) | Runtime Artifact Conversion Module owns per-runtime content rewriting | Accepted | -| [1593-skill-mapping-converter-methodology.md](1593-skill-mapping-converter-methodology.md) | Skill mapping & converter methodology across runtimes | Accepted | -| [1769-state-md-transition-module.md](1769-state-md-transition-module.md) | STATE.md Transition Module — intent-based transitions over scattered RMW callbacks | Proposed | -| [1817-state-md-rebuild-derivability-contract.md](1817-state-md-rebuild-derivability-contract.md) | STATE.md rebuild — derivability contract (capstone 11th transition) | Accepted | -| [2008-command-exit-zero-gate.md](2008-command-exit-zero-gate.md) | Generic gate-predicate evaluator with a `command-exit-zero` kind (#2008) | Accepted | -| [1990-existing-code-onboarding.md](1990-existing-code-onboarding.md) | Existing Code Onboarding Module owns deterministic repo-state detection and onboarding route selection | Proposed | -| [2121-phase-identifier-parsing-consolidation.md](2121-phase-identifier-parsing-consolidation.md) | Phase-identifier parsing consolidation — single canonical owner (phase-id.cts) + anti-divergence guard | Accepted | -| [2143-markdown-table-and-mutation-consolidation.md](2143-markdown-table-and-mutation-consolidation.md) | Markdown table model, bounded mutation, and fail-loud consolidation (#1372 part 2) | Accepted | -| [2164-statusline-scope-boundary.md](2164-statusline-scope-boundary.md) | Statusline draws its data boundary at local, read-only sources (no external/credentialed data) | Accepted | -| [612-bracket-phase-id-convention.md](612-bracket-phase-id-convention.md) | Bracket phase-ID convention — lift the milestone into a `[PROJECT.MM]` prefix; terminal deprecation of M-NN | Proposed | -| [2264-golden-parity-redesign.md](2264-golden-parity-redesign.md) | Redesign golden-install-parity: single-source manifest builder + split invariant | Proposed | -| [959-capability-command-contribution.md](959-capability-command-contribution.md) | Capability Command Contribution — `commands` field; family routers discovered via registry in `runCommand` default case | Accepted (graduated by ADR-2346) | -| [2346-command-dispatch-completion.md](2346-command-dispatch-completion.md) | Command Dispatch Completion — dissolve the 73-case `runCommand` switch into a two-layer (registry + leaf-table) dispatch | Accepted | + + +### Active decisions (50) + +These govern the system as it stands. Cite these. + +| ADR | Title | Status | Read first | +|-----|-------|--------|------------| +| [ADR-0001](0001-dispatch-policy-module.md) | Dispatch policy module as single seam for query execution outcomes | Accepted | — | +| [ADR-0002](0002-command-contract-validation-module.md) | Command Contract Validation Module | Accepted | — | +| [ADR-0003](0003-model-catalog-module.md) | Model Catalog Module as single source of truth for agent profiles and runtime tier defaults | Accepted | — | +| [ADR-0004](0004-worktree-workstream-seam-module.md) | Planning Workspace Module as single seam for worktree and workstream state | Accepted | — | +| [ADR-0006](0006-planning-path-projection-module.md) | Planning Path Projection Module for SDK query handlers | Accepted | — | +| [ADR-0008](0008-installer-migration-module.md) | Installer Migration Module owns install-time upgrade safety | Accepted | — | +| [ADR-0009](0009-shell-command-projection-module.md) | Shell Command Projection Module owns runtime-aware OS command rendering | Accepted | — | +| [ADR-0011](0011-review-default-reviewers.md) | `review.default_reviewers` config key scopes the no-flag `/gsd-review` fan-out | Accepted | — | +| [ADR-0011](0011-skill-surface-budget-module.md) | Skill Surface Budget Module owns install-time profile staging and runtime surface control | Accepted | [ADR-857](857-capability-system.md) | +| [ADR-15](15-autonomous-cross-ai-convergence.md) | Cross-AI Plan Convergence via Existing Orchestration Commands | Accepted | — | +| [ADR-22](22-plan-drift-guard.md) | Plan-vs-codebase drift guard: defaults and symbol-resolver seam | Accepted | — | +| [ADR-58](58-runtime-install-policy-module.md) | Runtime Install Policy Module owns the typed install-plan projection | Accepted | [ADR-1239](1239-gsd-embeddable-orchestration-engine.md), [ADR-857](857-capability-system.md) | +| [ADR-0174](0174-retire-gsd-sdk-package-boundary.md) | Retire @opengsd/gsd-sdk package boundary — single-runtime collapse | Accepted | — | +| [ADR-218](218-release-version-validation.md) | Harden release-workflow version validation — reject leading zeros and pre-check npm | Accepted | — | +| [ADR-227](227-input-validation-shape-not-just-type.md) | Input validation must check semantic shape, not just type | Accepted | — | +| [ADR-415](415-prevent-stale-base-token-reintroduction.md) | Prevent stale-base reintroduction of retired runtime tokens | Accepted | — | +| [ADR-452](452-eslint-lint-harness.md) | Adopt standard ESLint flat-config lint harness | Accepted | — | +| [ADR-456](456-test-rigor-architecture.md) | Test-rigor architecture — deterministic scheduling, antagonistic tier, typed-surface mandate, and delete-bad-tests policy | Accepted | — | +| [ADR-457](457-generated-cjs-single-source.md) | Generation model for `bin/lib/*.cjs` type safety | Accepted | — | +| [ADR-550](550-spec-phase-probe-contract.md) | spec-phase probe pattern and prohibition contract | Accepted | — | +| [ADR-0656](0656-research-module-seam.md) | Research Module — L2-hybrid seam for cached, curated-first research | Accepted | — | +| [ADR-766](766-claude-code-plugin-manifest-module.md) | Claude Code Plugin Manifest Module owns the projection of gsd-core surfaces onto the Claude Code plugin contract | Accepted | — | +| [ADR-857](857-capability-system.md) | Capability system — five-step loop as core, features as plug-ins behind Loop Extension Points | Accepted | — | +| [ADR-894](894-capability-declaration-format.md) | Capability declaration format + registry generation | Accepted | [ADR-1239](1239-gsd-embeddable-orchestration-engine.md) | +| [ADR-959](959-capability-command-contribution.md) | Capability Command Contribution | Accepted | — | +| [ADR-1016](1016-runtime-capability-descriptor.md) | Runtime Capability Descriptor | Accepted | [ADR-1239](1239-gsd-embeddable-orchestration-engine.md) | +| [ADR-1235](1235-descriptor-driven-agent-conversion-migration.md) | Migrate agent conversion to the descriptor-driven install path | Accepted | — | +| [ADR-1239](1239-gsd-embeddable-orchestration-engine.md) | GSD as an Embeddable Orchestration Engine | Accepted | — | +| [ADR-1244](1244-capability-ecosystem.md) | Capability Ecosystem: third-party authoring, versioned manifests, and URL import/upgrade/remove | Accepted | — | +| [ADR-1372](1372-markdown-sectionizer-seam.md) | Canonical markdown-structure parsing — the `markdown-sectionizer` seam | Accepted | — | +| [ADR-1411](1411-resolution-provenance.md) | Resolution must report provenance, not fall open silently | Accepted | — | +| [ADR-1508](1508-runtime-artifact-conversion-module.md) | Runtime Artifact Conversion Module owns per-runtime content rewriting | Accepted | — | +| [ADR-1517](1517-reviewer-instances-config-surface.md) | Reviewer instances — bounded config surface for same-adapter multi-model review | Accepted | — | +| [ADR-1577](1577-untrusted-input-boundary-and-injection-blocking.md) | Untrusted-input boundary + opt-in injection blocking | Accepted | — | +| [ADR-1593](1593-skill-mapping-converter-methodology.md) | Skill mapping & converter methodology across runtimes | Accepted | — | +| [ADR-1610](1610-workflow-agent-size-budget-ratchet.md) | workflow & agent size-budget ratchet (per-file byte baseline + tier hard caps) | Accepted | — | +| [ADR-1703](1703-portability-enforcement-architecture.md) | Cross-platform portability enforcement as AST ESLint rules | Accepted | — | +| [ADR-1769](1769-state-md-transition-module.md) | STATE.md Transition Module — intent-based transitions over scattered RMW callbacks | Accepted | — | +| [ADR-1787](1787-gsd-next-smart-entry.md) | `/gsd:next` smart-entry front door delegates advancement to `/gsd:progress --next` | Accepted | — | +| [ADR-1817](1817-state-md-rebuild-derivability-contract.md) | STATE.md rebuild — derivability contract (capstone transition) | Accepted | — | +| [ADR-1820](1820-spec-optional-predicate-rail.md) | Spec-Optional Predicate Rail — the Spec-Section Detection Module, the fallback toggle, and the SPEC↔probe precedence contract | Accepted | — | +| [ADR-1866](1866-agent-skills-dual-injection-contract.md) | agent_skills dual injection — orchestrator-side + agent-side self-load | Accepted | — | +| [ADR-1990](1990-existing-code-onboarding.md) | Existing Code Onboarding Module owns deterministic repo-state detection and onboarding route selection | Accepted | — | +| [ADR-2008](2008-command-exit-zero-gate.md) | Generic gate-predicate evaluator (`command-exit-zero`) | Accepted | — | +| [ADR-2121](2121-phase-identifier-parsing-consolidation.md) | Phase-Identifier Parsing Consolidation | Accepted | — | +| [ADR-2143](2143-markdown-table-and-mutation-consolidation.md) | Markdown Table Model, Bounded Mutation, and Fail-Loud Consolidation (#1372 part 2) | Accepted | — | +| [ADR-2164](2164-statusline-scope-boundary.md) | Statusline draws its data boundary at local, read-only sources | Accepted | — | +| [ADR-2207](2207-status-field-lifecycle-ownership.md) | STATE.md `Status` lifecycle — phase-completion writes an intermediate state; milestone-close owns termination | Accepted | — | +| [ADR-2346](2346-command-dispatch-completion.md) | Command Dispatch Completion | Accepted | — | +| [ADR-3660](3660-runtime-artifact-layout-module.md) | Runtime Artifact Layout Module owns per-runtime artifact placement | Accepted | [ADR-1239](1239-gsd-embeddable-orchestration-engine.md) | + +### Proposed (9) + +Decided in principle, not yet ratified. Do not cite as settled architecture. + +| ADR | Title | Status | Read first | +|-----|-------|--------|------------| +| [ADR-230](230-introduce-next-integration-branch.md) | Introduce `next` as a long-lived integration branch | Proposed | — | +| [ADR-443](443-opus48-unified-effort-and-fast-mode-routing.md) | Unified cross-provider effort controls and fast-mode-aware routing | Proposed | — | +| [ADR-612](612-bracket-phase-id-convention.md) | Bracket Phase-ID Convention | Proposed | — | +| [ADR-660](660-release-from-next-head.md) | Release from the head of `next`; immutable release tags; `@next` dist-tag as the RC surface | Proposed | — | +| [ADR-1143](1143-claude-orchestration-capability.md) | Claude orchestration capability — Workflow tool (ultracode) as a runtime-gated loop execution backend | Proposed | — | +| [ADR-1213](1213-capability-state-writer.md) | Capability write side — the Capability State Writer | Proposed | — | +| [ADR-1606](1606-prohibition-enforcement-verify-seam.md) | prohibition-enforcement verify-time seam | Proposed | — | +| [ADR-1671](1671-dynamic-context-management-platform.md) | Dynamic context management platform | Proposed | — | +| [ADR-2264](2264-golden-parity-redesign.md) | Redesign golden-install-parity — single-source manifest builder + split invariant | Proposed | — | + +### Superseded, Retired, and Legacy (7) + +Historical record. **Do not follow these** — each names what replaced it, or why it was retired. + +| ADR | Title | Status | Replaced by | +|-----|-------|--------|-------------| +| [ADR-0005](0005-sdk-architecture-seam-map.md) | SDK Architecture seam map for query/runtime surfaces | Superseded | [ADR-0174](0174-retire-gsd-sdk-package-boundary.md) | +| [ADR-0007](0007-sdk-package-seam-module.md) | SDK Package Seam Module owns SDK-to-get-shit-done-redux compatibility | Superseded | [ADR-0174](0174-retire-gsd-sdk-package-boundary.md) | +| [ADR-0010](0010-file-operation-engine-module.md) | File Operation Engine Module owns safe runtime/config file mutations | Superseded | [ADR-0009](0009-shell-command-projection-module.md) | +| [ADR-0010](0010-skill-surface-budget-module.md) | Skill Surface Budget Module owns install-time skill listing curation | Superseded | [ADR-0011](0011-skill-surface-budget-module.md) | +| [ADR-0011](0011-review-default-reviewers-prd.md) | PRD — `review.default_reviewers` config key for `/gsd-review` reviewer selection | Legacy | — | +| [ADR-0012](0012-command-routing-hub.md) | CommandRoutingHub as single dispatch seam for CJS command families | Superseded | [ADR-0174](0174-retire-gsd-sdk-package-boundary.md) | +| [ADR-3524](3524-cjs-sdk-hard-seam.md) | CJS↔SDK hard seam — one source of truth per Shared Module | Superseded | [ADR-0174](0174-retire-gsd-sdk-package-boundary.md) | + +_66 ADRs. Generated by `scripts/gen-adr-index.cjs` — run `--write` after adding or restatusing an ADR._ + + ## Seam map -ADR 0005 is the top-level SDK seam index. It references per-seam ADRs and states the narrow-waist principle each seam follows. Use it as the entry point for understanding SDK module ownership. +Orientation for the module-ownership ADRs. This section is prose and hand-maintained; the index above is the authority on status. -ADR 0006 documents how SDK query handlers project planning paths (`cwd → effectiveRoot → .planning//...`). Cross-reference with the Planning Workspace Module (ADR 0004) for workstream pointer policy. +**How GSD meets a host — start at [ADR-1239](1239-gsd-embeddable-orchestration-engine.md) (EoS).** It is the current frame and subsumes the descriptor/projection ADRs ([ADR-1016](1016-runtime-capability-descriptor.md), [ADR-58](58-runtime-install-policy-module.md), [ADR-3660](3660-runtime-artifact-layout-module.md), [ADR-894](894-capability-declaration-format.md)) as adapters beneath it. -ADR 0008 documents the Installer Migration Module for safe install-time moves, removals, config rewrites, and user-data preservation. +**The SDK seam map is gone.** [ADR-0005](0005-sdk-architecture-seam-map.md) was once the entry point for SDK module ownership; it is **superseded by [ADR-0174](0174-retire-gsd-sdk-package-boundary.md)**, which retired the `@opengsd/gsd-sdk` package boundary entirely. There is no `sdk/` tree. Read ADR-0174 for the single-runtime collapse; the seam-Module vocabulary survives under one `src/`. -ADR 0009 documents the Shell Command Projection Module seam for runtime-aware -projection of installer-owned command text and projection IR. +[ADR-0006](0006-planning-path-projection-module.md) documents how query handlers project planning paths (`cwd → effectiveRoot → .planning//...`). Cross-reference the Planning Workspace Module ([ADR-0004](0004-worktree-workstream-seam-module.md)) for workstream pointer policy. -ADR 0010 documents the File Operation Engine Module seam for converging -installer/migration/planning file mutation safety policy, and its relationship -to ADR 0009 hook-command ownership policy. +[ADR-0008](0008-installer-migration-module.md) documents the Installer Migration Module for safe install-time moves, removals, config rewrites, and user-data preservation. -ADR 0011 documents the Skill Surface Budget Module for install-time skill/agent -profile staging (`--profile=`, `.gsd-profile` marker, `requires:` closure) -and the Phase 2 runtime `/gsd:surface` command for cluster-level enable/disable -without reinstall. +[ADR-0009](0009-shell-command-projection-module.md) documents the Shell Command Projection Module seam for runtime-aware projection of installer-owned command text and projection IR. Its Phases 3–4 absorbed the File Operation Engine Module ([ADR-0010](0010-file-operation-engine-module.md)). -ADR 1411 establishes the Resolution Provenance principle: context resolution -(config loading, project-root anchoring, workstream resolution) must report its -provenance rather than fall open silently to defaults. It is the resolution-side -analog of ADR 227 (input-validation shape), binds the Config Loader Module, -Project-Root Resolution Module, and I/O Module, and is the decision record for -epic #1411 (phases P1–P4). +[ADR-0011](0011-skill-surface-budget-module.md) documents the Skill Surface Budget Module for install-time skill/agent profile staging (`--profile=`, `.gsd-profile` marker, `requires:` closure) and the Phase 2 runtime `/gsd:surface` command. + +[ADR-1411](1411-resolution-provenance.md) establishes the Resolution Provenance principle: context resolution (config loading, project-root anchoring, workstream resolution) must report its provenance rather than fall open silently to defaults. It is the resolution-side analog of [ADR-227](227-input-validation-shape-not-just-type.md) (input-validation shape). diff --git a/package.json b/package.json index 95acd9e1c..9eeb27d43 100644 --- a/package.json +++ b/package.json @@ -109,7 +109,7 @@ "lint:test-file-count": "node scripts/lint-test-file-count.cjs", "lint:pr-checks": "node scripts/lint-pr-check-project-dir.cjs", "lint:changeset": "node scripts/changeset/lint.cjs", - "lint:generated-sync": "node scripts/gen-capability-registry.cjs --check && node scripts/gen-loop-host-contract.cjs --check && node scripts/gen-capability-matrix.cjs --check && node scripts/sync-manifest-versions.cjs --check && node scripts/gen-inventory-manifest.cjs --check && node scripts/generate-package-identity.cjs --check && node scripts/gen-plugin-skills.cjs --check && node scripts/gen-registry.cjs --check", + "lint:generated-sync": "node scripts/gen-capability-registry.cjs --check && node scripts/gen-loop-host-contract.cjs --check && node scripts/gen-capability-matrix.cjs --check && node scripts/sync-manifest-versions.cjs --check && node scripts/gen-inventory-manifest.cjs --check && node scripts/generate-package-identity.cjs --check && node scripts/gen-plugin-skills.cjs --check && node scripts/gen-registry.cjs --check && node scripts/gen-adr-index.cjs --check", "lint:docs": "node scripts/lint-docs-required.cjs", "lint:legacy-name": "node scripts/lint-legacy-dir-name.cjs", "ci:test-scope": "node scripts/ci-test-scope.cjs", diff --git a/scripts/gen-adr-index.cjs b/scripts/gen-adr-index.cjs new file mode 100644 index 000000000..074f3de07 --- /dev/null +++ b/scripts/gen-adr-index.cjs @@ -0,0 +1,526 @@ +#!/usr/bin/env node +'use strict'; + +/** + * Generates the ADR index table in docs/adr/README.md from the ADR files + * themselves, and validates the corpus' lifecycle invariants. + * + * The index is a DERIVED artifact: it is regenerated from every + * `docs/adr/-.md` on disk, so it cannot silently drift out of date + * the way a hand-maintained table does. CI re-runs this with `--check` and + * fails on any diff or invariant violation. + * + * Invariants enforced (see docs/adr/README.md "Lifecycle rules"): + * 1. Every ADR declares `- **Status:** ` with Token in STATUSES. + * 2. A Superseded/Retired ADR names its successor as a markdown link to the + * target file — never a bare "ADR-N", which is ambiguous (ADR-0010 and + * ADR-0011 each resolve to more than one file). + * 3. Supersession is symmetric: if A supersedes B, B records superseded-by A. + * 4. An ADR whose H1 declares an id must match its filename's id. + * 5. The committed index equals the generated index. + * + * Usage: + * node scripts/gen-adr-index.cjs # print the index to stdout + * node scripts/gen-adr-index.cjs --write # rewrite the index in README.md + * node scripts/gen-adr-index.cjs --check # exit 1 if stale or invalid + */ + +const fs = require('node:fs'); +const path = require('node:path'); + +const { ExitError, runMain } = require('./lib/cli-exit.cjs'); + +const ROOT = path.resolve(__dirname, '..'); +const ADR_DIR = path.join(ROOT, 'docs', 'adr'); +const README_PATH = path.join(ADR_DIR, 'README.md'); + +const START_MARKER = ''; +const END_MARKER = ''; + +/** + * The canonical status vocabulary. + * + * `Legacy` and `Retired` are deliberately distinct from `Superseded`: + * - Superseded — a specific newer ADR replaced this decision. Names it. + * - Retired — the thing this ADR decided no longer exists at all, and no + * single ADR replaced it (e.g. a deleted package boundary). + * - Legacy — frozen historical record, kept for provenance, not a + * pattern to imitate. + * + * NOTE: `Legacy` describes a DECISION's standing, not a filename. The + * `0001-`..`0012-` sequential *naming* era is legacy, but many of those ADRs + * (e.g. 0002, 0004, 0008, 0009) are Accepted and load-bearing today. Do not + * conflate the two: grep the naming rule in README.md, not this enum. + */ +const STATUSES = ['Accepted', 'Proposed', 'Superseded', 'Legacy', 'Retired']; + +/** + * Header fields that assert a lifecycle relation. + * + * Two DISTINCT relations, deliberately not conflated: + * + * supersedes — the target decision is REPLACED. The target's status becomes + * Superseded and it must name this ADR. (ADR-0174 → ADR-0005.) + * + * subsumes — the target decision still HOLDS, but a broader ADR now frames + * it; the target keeps its Accepted status and becomes a component of the + * larger decision. (ADR-1239/EoS subsumes ADR-1016 "as the declarative + * adapter" — the descriptor is still real and still correct.) + * + * Both directions are symmetry-checked, but only `supersedes` implies a status + * change on the target. Collapsing subsumption into supersession would mark + * four live, load-bearing ADRs as dead — the opposite of the truth. + */ +const RELATION_FIELDS = new Map([ + ['supersedes', { kind: 'supersedes', dir: 'out' }], + ['superseded by', { kind: 'supersedes', dir: 'in' }], + ['subsumes', { kind: 'subsumes', dir: 'out' }], + ['subsumed by', { kind: 'subsumes', dir: 'in' }], +]); + +/** Relation kinds and the header field a reader should add to fix each gap. */ +const RELATION_SPEC = { + supersedes: { out: 'Supersedes', in: 'Superseded by' }, + subsumes: { out: 'Subsumes', in: 'Subsumed by' }, +}; + +/** + * A relation field whose value opens with "nothing"/"none"/"n/a" asserts the + * absence of the relation, whatever prose follows it. + */ +const NEGATED_RELATION_RE = /^\s*(?:nothing|none|n\/a|[—–-])\s*(?:$|[;,.]|\s)/i; + +/** + * Header fields appear in two shapes across the corpus, both legitimate: + * bullet — `- **Status:** Accepted` + * table — `| **Status** | Accepted |` + * Yield [field, value] for either. + */ +function* headerFields(header) { + const bullet = /^\s*[-*]\s*\*\*([^*:]+?)(?::)?\*\*\s*(.*)$/gm; + let m; + while ((m = bullet.exec(header)) !== null) yield [m[1].trim(), m[2].trim()]; + + const row = /^\s*\|\s*\*\*([^*|]+?)(?::)?\*\*\s*\|\s*(.*?)\s*\|\s*$/gm; + while ((m = row.exec(header)) !== null) yield [m[1].trim(), m[2].trim()]; +} + +/** Numeric identity of an ADR: "0011" and "11" are the same id. */ +function canonicalId(raw) { + return String(raw).replace(/^0+(?=\d)/, ''); +} + +/** The documented filename shape: `-.md`. */ +const ADR_FILENAME_RE = /^[0-9]+-[a-z0-9-]+\.md$/; + +function adrFiles() { + return fs + .readdirSync(ADR_DIR) + .filter((f) => f.endsWith('.md') && f !== 'README.md') + .filter((f) => fs.statSync(path.join(ADR_DIR, f)).isFile()) + .sort(); +} + +/** + * Split the directory into files this tool can parse and files it cannot. + * + * A file without a numeric prefix is not merely unparseable — it is invisible + * to the index, which is the failure this gate exists to prevent. Report it as + * a violation naming the convention, rather than crashing on `match(...)[1]` + * or silently skipping it. + */ +function partitionAdrFiles() { + const conforming = []; + const nonConforming = []; + for (const f of adrFiles()) (ADR_FILENAME_RE.test(f) ? conforming : nonConforming).push(f); + return { conforming, nonConforming }; +} + +/** Extract the leading bullet-field header block (everything before the first `##`). */ +function headerBlock(text) { + const body = text.split(/\r?\n/); + const stop = body.findIndex((l) => /^##\s/.test(l)); + return (stop === -1 ? body : body.slice(0, stop)).join('\n'); +} + +/** + * A relation may also be declared as a whole SECTION rather than a header field. + * ADR-0174 is the exemplar: a `## Supersedes` heading over a table whose first + * column links each superseded ADR and whose remaining columns explain why. + * That is the richest form in the corpus and must count — reading only the + * header block would report the repo's best-documented supersession as missing. + * + * Returns { supersedes: [file…], subsumes: [file…] } from matching sections. + */ +const RELATION_SECTION_RE = /^##\s+(Supersedes|Subsumes)\b[^\n]*$/i; + +function relationSections(text) { + const lines = text.split(/\r?\n/); + const out = { supersedes: [], subsumes: [] }; + for (let i = 0; i < lines.length; i++) { + const m = lines[i].match(RELATION_SECTION_RE); + if (!m) continue; + const kind = m[1].toLowerCase() === 'supersedes' ? 'supersedes' : 'subsumes'; + // Collect until the next heading of any level. + let j = i + 1; + const body = []; + for (; j < lines.length && !/^#{1,6}\s/.test(lines[j]); j++) body.push(lines[j]); + const chunk = body.join('\n'); + if (NEGATED_RELATION_RE.test(chunk.trim())) continue; + out[kind].push(...linkedAdrFiles(chunk)); + i = j - 1; + } + return out; +} + +/** All markdown links to sibling ADR files inside a chunk of text. */ +function linkedAdrFiles(text) { + const out = []; + const re = /\]\(\s*(?:\.\/)?([0-9]+-[a-z0-9-]+\.md)\s*\)/gi; + let m; + while ((m = re.exec(text)) !== null) out.push(m[1]); + return out; +} + +/** Bare `ADR-123` / `ADR 123` mentions that are NOT part of a markdown link. */ +function bareAdrRefs(text) { + const withoutLinks = text.replace(/\[[^\]]*\]\([^)]*\)/g, ''); + const out = []; + const re = /\bADR[-\s]0*(\d+)\b/gi; + let m; + while ((m = re.exec(withoutLinks)) !== null) out.push(canonicalId(m[1])); + return out; +} + +function parseAdr(file) { + const full = path.join(ADR_DIR, file); + const text = fs.readFileSync(full, 'utf8'); + const lines = text.split(/\r?\n/); + + // `fileId` is the numeric identity used for comparison ("0011" === "11"); + // `displayId` preserves the filename's prefix exactly as written, because the + // corpus and its cross-references say "ADR-0001" and "ADR-58", not "ADR-1". + const rawId = file.match(/^([0-9]+)-/)[1]; + const fileId = canonicalId(rawId); + const displayId = rawId; + + const h1 = (lines.find((l) => /^#\s/.test(l)) || '').replace(/^#\s+/, '').trim(); + // Title as displayed: drop a leading "ADR-123 — " / "ADR-123: " prefix and a + // trailing "[Proposed]"-style status bracket, both of which the index renders + // from structured fields instead. + const title = h1 + .replace(/^ADR[-\s]?0*\d+\s*(?:[—:-]\s*)?/i, '') + .replace(/\s*\[(?:Proposed|Accepted|Superseded|Legacy|Retired)\]\s*$/i, '') + .trim(); + + const declaredIdMatch = h1.match(/^ADR[-\s]?0*(\d+)\b/i); + const declaredId = declaredIdMatch ? canonicalId(declaredIdMatch[1]) : null; + + const header = headerBlock(text); + + let statusRaw = null; + // relations[kind][dir] = [{field, value, links, bare}] + const relations = { supersedes: { out: [], in: [] }, subsumes: { out: [], in: [] } }; + + for (const [field, value] of headerFields(header)) { + if (field.toLowerCase() === 'status') { + if (statusRaw === null) statusRaw = value; + continue; + } + // "Supersedes (generalizes)" / "Subsumes as adapters" → "supersedes" / "subsumes" + const key = field.toLowerCase().replace(/\s*\([^)]*\)\s*/g, ' ').replace(/\s+as\s+.*$/, '').trim(); + const spec = RELATION_FIELDS.get(key); + if (!spec) continue; + // "Supersedes: nothing; amends the ADR-1239 harness" asserts NO relation. Such a + // field routinely name-drops other ADRs in its prose ("related", "amends", "builds + // on"); reading those as supersession claims invents links that were never made. + if (NEGATED_RELATION_RE.test(value)) continue; + relations[spec.kind][spec.dir].push({ field, value, links: linkedAdrFiles(value), bare: bareAdrRefs(value) }); + } + + const statusToken = statusRaw ? (statusRaw.match(/^([A-Za-z]+)/) || [])[1] : null; + + // A "Superseded by X" written into the Status line itself is the relation. + if (statusRaw && /^Superseded\b/i.test(statusRaw)) { + relations.supersedes.in.push({ field: 'Status', value: statusRaw, links: linkedAdrFiles(statusRaw), bare: bareAdrRefs(statusRaw) }); + } + + // `## Supersedes` / `## Subsumes` sections count as OUT claims (ADR-0174's table). + const sections = relationSections(text); + for (const kind of ['supersedes', 'subsumes']) { + if (sections[kind].length === 0) continue; + relations[kind].out.push({ field: `## ${kind === 'supersedes' ? 'Supersedes' : 'Subsumes'} section`, value: '', links: sections[kind], bare: [] }); + } + + return { file, fileId, displayId, title, declaredId, statusRaw, statusToken, relations, text }; +} + +function buildCorpus() { + const { conforming, nonConforming } = partitionAdrFiles(); + const adrs = conforming.map(parseAdr); + const byFile = new Map(adrs.map((a) => [a.file, a])); + const byId = new Map(); + for (const a of adrs) { + if (!byId.has(a.fileId)) byId.set(a.fileId, []); + byId.get(a.fileId).push(a); + } + return { adrs, byFile, byId, nonConforming }; +} + +function validate({ adrs, byFile, byId, nonConforming }) { + const errors = []; + const add = (file, msg) => errors.push(`${file}: ${msg}`); + + for (const f of nonConforming) { + add( + f, + 'filename does not match the `-.md` convention, so it cannot appear in the index. ' + + 'Rename it (see docs/adr/README.md "Naming Convention"), or move it out of docs/adr/ if it is not an ADR.', + ); + } + + for (const a of adrs) { + if (!a.statusToken) { + add(a.file, 'no `- **Status:** ` field found in the header block.'); + continue; + } + if (!STATUSES.includes(a.statusToken)) { + add(a.file, `status "${a.statusToken}" is not one of ${STATUSES.join(' | ')} (full line: "${a.statusRaw}").`); + } + if (a.declaredId && a.declaredId !== a.fileId) { + add(a.file, `H1 declares ADR-${a.declaredId} but the filename says ${a.fileId}. The id must match the filename.`); + } + + // A Superseded ADR must point at its successor by FILE LINK. + if (a.statusToken === 'Superseded') { + const links = a.relations.supersedes.in.flatMap((r) => r.links); + if (links.length === 0) { + const bare = a.relations.supersedes.in.flatMap((r) => r.bare); + add( + a.file, + bare.length + ? `status is Superseded and mentions ADR-${bare.join('/')} but not as a markdown link to the file. ` + + 'Bare ids are ambiguous (ADR-0010 and ADR-0011 each resolve to multiple files) — link the target file.' + : 'status is Superseded but names no successor. Write `Superseded by [ADR-N](N-slug.md)`.', + ); + } + } + + // Every relation link must resolve; every bare id must be linked (and exist). + for (const kind of Object.keys(RELATION_SPEC)) { + for (const dir of ['out', 'in']) { + for (const rel of a.relations[kind][dir]) { + for (const l of rel.links) { + if (!byFile.has(l)) add(a.file, `"${rel.field}" links "${l}", which does not exist in docs/adr/.`); + } + // The synthetic relation lifted out of the Status line is already covered by + // the dedicated Superseded check above; reporting it again just duplicates. + if (rel.field === 'Status') continue; + // Ids already linked ANYWHERE in this field. A field legitimately + // reads "…([ADR-58](58-x.md)) — see 'Relation to ADR-58' below": the + // trailing prose repeats an id that is linked earlier, and flagging + // that would be noise. But an id that appears ONLY bare is an + // unchecked claim — and testing `rel.links.length` instead of the + // specific id silently dropped every bare claim in a field that + // happened to carry one link. + const linkedIds = new Set(rel.links.map((l) => (byFile.get(l) || {}).fileId).filter(Boolean)); + for (const b of rel.bare) { + if (linkedIds.has(b)) continue; + const candidates = byId.get(b) || []; + if (candidates.length === 0) { + add(a.file, `"${rel.field}" names ADR-${b}, which does not exist in docs/adr/. If it is an ISSUE number, write "#${b}" — not "ADR-${b}".`); + } else { + add( + a.file, + `"${rel.field}" names ADR-${b} without a file link` + + (candidates.length > 1 ? ` (ambiguous — resolves to ${candidates.length} files: ${candidates.map((c) => c.file).join(', ')})` : '') + + '. Link the target file so the relation is checkable.', + ); + } + } + } + } + } + } + + // Symmetry, per relation kind: A -out-> B <=> B -in-> A. + // + // Only a RATIFIED (Accepted) claimant is owed the back-link. A Proposed ADR's + // supersession claim is prospective — it has not taken effect, so stamping its + // target as superseded would assert something untrue (ADR-857 is Proposed and + // claims to generalize ADR-0011/ADR-58, both of which are Accepted and live). + // When such an ADR is ratified to Accepted, this check starts demanding the + // back-links at exactly the right moment. + const OPPOSITE = { out: 'in', in: 'out' }; + for (const a of adrs) { + for (const kind of Object.keys(RELATION_SPEC)) { + for (const dir of ['out', 'in']) { + // The ratification guard applies to the OUT direction only: an unratified + // ADR's claim over someone else is prospective and must not obligate the + // target. The IN direction is this ADR's statement about ITSELF ("I am + // superseded by X") and is always owed a reciprocal — guarding it too + // would skip every Superseded ADR (statusToken !== 'Accepted') and leave + // dangling one-way claims unchecked, which is the bug this gate exists + // to catch. + if (dir === 'out' && a.statusToken !== 'Accepted') continue; + for (const target of new Set(a.relations[kind][dir].flatMap((r) => r.links))) { + const b = byFile.get(target); + if (!b) continue; + const back = new Set(b.relations[kind][OPPOSITE[dir]].flatMap((r) => r.links)); + if (back.has(a.file)) continue; + const needed = RELATION_SPEC[kind][OPPOSITE[dir]]; + const claim = dir === 'out' ? `it ${kind} this ADR` : `it is ${kind === 'supersedes' ? 'superseded' : 'subsumed'} by this ADR`; + add( + target, + `${a.file} declares ${claim}, but this ADR does not record it. ` + + `Add \`- **${needed}:** [ADR-${a.displayId}](${a.file})\` so a reader of THIS file learns the decision moved on.`, + ); + } + } + } + } + + return errors; +} + +const GROUPS = [ + { + heading: 'Active decisions', + blurb: 'These govern the system as it stands. Cite these.', + match: (a) => a.statusToken === 'Accepted', + }, + { + heading: 'Proposed', + blurb: 'Decided in principle, not yet ratified. Do not cite as settled architecture.', + match: (a) => a.statusToken === 'Proposed', + }, + { + heading: 'Superseded, Retired, and Legacy', + blurb: 'Historical record. **Do not follow these** — each names what replaced it, or why it was retired.', + match: (a) => ['Superseded', 'Retired', 'Legacy'].includes(a.statusToken), + }, +]; + +/** + * Render ADR-authored text (a title) into a markdown table cell. + * + * Three hazards, all from text this script does not control: + * - `|` would split the cell and corrupt the row. + * - An HTML comment would be emitted verbatim into README.md. A title + * containing the END marker relocates it, so the NEXT `--write` splices + * against the wrong boundary and silently eats the rest of the file. + * Escaping `<`/`>` makes a comment sequence unformable, which also blocks + * any other HTML injected through a title. + * - A backslash is markdown's own escape character, so it MUST be escaped + * first. Escaping `|` → `\|` without it turns the input `\|` into `\\|`, + * which markdown reads as a literal backslash followed by an UNESCAPED + * pipe — re-opening the cell break the pipe escape exists to prevent. + * Order is load-bearing: backslash first, then everything that emits one. + */ +function cellText(text) { + return String(text) + .replace(/\\/g, '\\\\') + .replace(/\|/g, '\\|') + .replace(//g, '>') + .replace(/\r?\n/g, ' ') + .trim(); +} + +function linkCell(files, byFile) { + if (files.length === 0) return '—'; + return files.map((l) => `[ADR-${(byFile.get(l) || {}).displayId || '?'}](${l})`).join(', '); +} + +function renderIndex(corpus) { + const { byFile } = corpus; + const out = [START_MARKER, '']; + + for (const g of GROUPS) { + const rows = corpus.adrs.filter(g.match).sort((x, y) => Number(x.fileId) - Number(y.fileId)); + if (rows.length === 0) continue; + + out.push(`### ${g.heading} (${rows.length})`, '', g.blurb, ''); + const isHistorical = g.heading.startsWith('Superseded'); + // "Read first" points at the broader ADR that now frames this one. It is how a + // reader of a still-Accepted component decision (e.g. the runtime descriptor) + // discovers the wider decision that reframed it (e.g. EoS) instead of assuming + // the component IS the architecture. + out.push( + isHistorical ? '| ADR | Title | Status | Replaced by |' : '| ADR | Title | Status | Read first |', + isHistorical ? '|-----|-------|--------|-------------|' : '|-----|-------|--------|------------|', + ); + for (const a of rows) { + const cells = [`[ADR-${a.displayId}](${a.file})`, cellText(a.title), a.statusToken]; + cells.push( + isHistorical + ? linkCell([...new Set(a.relations.supersedes.in.flatMap((r) => r.links))], byFile) + : linkCell([...new Set(a.relations.subsumes.in.flatMap((r) => r.links))], byFile), + ); + out.push(`| ${cells.join(' | ')} |`); + } + out.push(''); + } + + out.push( + `_${corpus.adrs.length} ADRs. Generated by \`scripts/gen-adr-index.cjs\` — run \`--write\` after adding or restatusing an ADR._`, + '', + END_MARKER, + ); + return out.join('\n'); +} + +function spliceIntoReadme(readme, index) { + const start = readme.indexOf(START_MARKER); + const end = readme.indexOf(END_MARKER); + if (start === -1 || end === -1) { + throw new ExitError( + 1, + `docs/adr/README.md is missing the index markers.\nExpected:\n ${START_MARKER}\n ${END_MARKER}\n`, + ); + } + return readme.slice(0, start) + index + readme.slice(end + END_MARKER.length); +} + +function main() { + const [, , flag] = process.argv; + + const corpus = buildCorpus(); + const errors = validate(corpus); + + if (errors.length > 0 && flag !== '--write') { + process.stderr.write( + `docs/adr/ has ${errors.length} lifecycle violation(s).\n` + + 'See docs/adr/README.md "Lifecycle rules" for the contract.\n\n', + ); + for (const e of errors) process.stderr.write(` ✗ ${e}\n`); + process.stderr.write('\n'); + throw new ExitError(1); + } + + const index = renderIndex(corpus); + + if (flag === '--check') { + const readme = fs.readFileSync(README_PATH, 'utf8'); + const expected = spliceIntoReadme(readme, index); + if (expected !== readme) { + process.stderr.write( + 'docs/adr/README.md index is stale. Run:\n node scripts/gen-adr-index.cjs --write\n\n', + ); + throw new ExitError(1); + } + process.stdout.write(`docs/adr/README.md index is up to date (${corpus.adrs.length} ADRs).\n`); + } else if (flag === '--write') { + const readme = fs.readFileSync(README_PATH, 'utf8'); + fs.writeFileSync(README_PATH, spliceIntoReadme(readme, index)); + process.stdout.write(`Wrote ADR index into ${README_PATH} (${corpus.adrs.length} ADRs).\n`); + if (errors.length > 0) { + process.stderr.write(`\n${errors.length} lifecycle violation(s) remain — --check will fail:\n\n`); + for (const e of errors) process.stderr.write(` ✗ ${e}\n`); + } + } else { + process.stdout.write(index + '\n'); + } +} + +runMain(main); diff --git a/src/model-catalog.cts b/src/model-catalog.cts index 8afdfdf30..a577d663a 100644 --- a/src/model-catalog.cts +++ b/src/model-catalog.cts @@ -16,11 +16,19 @@ const _require: NodeRequire = require; // works in every layout: // // 1. Co-located install path — gsd-core/bin/shared/model-catalog.json -// 2. Source-repo dev path — sdk/shared/model-catalog.json -// 3. GSD_MODEL_CATALOG env override +// 2. GSD_MODEL_CATALOG env override +// +// A third candidate — `sdk/shared/model-catalog.json`, three levels up — used to +// sit between them. It was the legacy source-repo path kept as a fallback by the +// #3288 fix, whose contract was "check the co-located path FIRST, before the +// legacy source-repo path". ADR-0174 then retired the `@opengsd/gsd-sdk` package +// boundary and deleted the `sdk/` tree outright, so that candidate can no longer +// resolve in any layout: in a source repo there is no `sdk/`, and in an install +// layout it points at `~/.claude/sdk/shared/`, which the installer never writes +// (the original #3288 bug). It is removed rather than left as dead weight that +// implies a package boundary this repo no longer has. const _catalogCandidates: string[] = [ path.resolve(__dirname, '..', 'shared', 'model-catalog.json'), - path.resolve(__dirname, '..', '..', '..', 'sdk', 'shared', 'model-catalog.json'), ...(process.env['GSD_MODEL_CATALOG'] ? [path.resolve(process.env['GSD_MODEL_CATALOG'])] : []), ]; diff --git a/tests/adr-index-gate.test.cjs b/tests/adr-index-gate.test.cjs new file mode 100644 index 000000000..f93d572de --- /dev/null +++ b/tests/adr-index-gate.test.cjs @@ -0,0 +1,438 @@ +'use strict'; + +/** + * Behavioral tests for scripts/gen-adr-index.cjs — the ADR index generator and + * lifecycle gate (#2340). + * + * These drive the real CLI as a subprocess against synthetic ADR corpora in a + * temp dir, asserting on exit code and emitted text. No source-grepping: the + * runtime behavior is the contract. + */ + +const { test } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const { spawnSync } = require('node:child_process'); + +const { createTempDir, cleanup } = require('./helpers.cjs'); + +const REPO_ROOT = path.resolve(__dirname, '..'); +const SCRIPT_REL = path.join('scripts', 'gen-adr-index.cjs'); + +const START = ''; +const END = ''; + +/** + * Build a throwaway repo whose docs/adr/ contains exactly `files`, and whose + * scripts/ holds a copy of the generator + its cli-exit dependency. A unique + * mkdtemp per call keeps parallel tests from colliding, and the dir is removed + * via `t.after()` so a failing assertion cannot leak it. + */ +function makeRepo(t, files) { + // helpers.cleanup (not raw fs.rmSync) carries the Windows-EBUSY retry budget. + const root = createTempDir('gsd-adr-index-'); + t.after(() => cleanup(root)); + fs.mkdirSync(path.join(root, 'docs', 'adr'), { recursive: true }); + fs.mkdirSync(path.join(root, 'scripts', 'lib'), { recursive: true }); + + fs.copyFileSync(path.join(REPO_ROOT, SCRIPT_REL), path.join(root, SCRIPT_REL)); + fs.copyFileSync( + path.join(REPO_ROOT, 'scripts', 'lib', 'cli-exit.cjs'), + path.join(root, 'scripts', 'lib', 'cli-exit.cjs'), + ); + + for (const [name, body] of Object.entries(files)) { + fs.writeFileSync(path.join(root, 'docs', 'adr', name), body); + } + fs.writeFileSync( + path.join(root, 'docs', 'adr', 'README.md'), + `# ADRs\n\n## Index\n\n${START}\n${END}\n`, + ); + return root; +} + +/** + * Run the generator in `root`; never throws — returns {status, stdout, stderr}. + * + * spawnSync (not execFileSync) because BOTH streams matter on BOTH outcomes: + * `--write` exits 0 while reporting outstanding violations on stderr, and + * execFileSync only surfaces stderr via the thrown error on non-zero exit. + */ +function run(root, args = []) { + const res = spawnSync(process.execPath, [path.join(root, SCRIPT_REL), ...args], { + cwd: root, + encoding: 'utf8', + timeout: 30_000, + }); + if (res.error) throw res.error; + return { status: res.status, stdout: res.stdout || '', stderr: res.stderr || '' }; +} + +const adr = (title, fields) => `# ${title}\n\n${fields.map((f) => `- ${f}`).join('\n')}\n\n## Context\n\nBody.\n`; + +test('a clean corpus generates an index and --check passes', (t) => { + const root = makeRepo(t, { + '0001-alpha.md': adr('Alpha module', ['**Status:** Accepted', '**Date:** 2026-01-01']), + '900-beta.md': adr('ADR-900: Beta module', ['**Status:** Proposed', '**Date:** 2026-01-02']), + }); + + const write = run(root, ['--write']); + assert.equal(write.status, 0, `--write failed: ${write.stderr}`); + + const check = run(root, ['--check']); + assert.equal(check.status, 0, `--check failed: ${check.stderr}`); + + const readme = fs.readFileSync(path.join(root, 'docs', 'adr', 'README.md'), 'utf8'); + assert.match(readme, /\[ADR-0001\]\(0001-alpha\.md\)/, 'zero-padded id must render as written, not ADR-1'); + assert.match(readme, /\[ADR-900\]\(900-beta\.md\)/); + assert.match(readme, /Active decisions \(1\)/); + assert.match(readme, /Proposed \(1\)/); +}); + +test('--check fails when an ADR is added but the index is not regenerated', (t) => { + const root = makeRepo(t, { '0001-alpha.md': adr('Alpha', ['**Status:** Accepted']) }); + assert.equal(run(root, ['--write']).status, 0); + + // A new ADR lands without re-running --write. This is the exact drift that let + // the hand-maintained index reach 40/65. + fs.writeFileSync(path.join(root, 'docs', 'adr', '901-gamma.md'), adr('Gamma', ['**Status:** Accepted'])); + + const check = run(root, ['--check']); + assert.equal(check.status, 1, 'a missing index row must fail CI'); + assert.match(check.stderr, /stale/i); +}); + +test('a status outside the vocabulary is rejected and names the offender', (t) => { + const root = makeRepo(t, { '0001-alpha.md': adr('Alpha', ['**Status:** Draft']) }); + const res = run(root, ['--check']); + assert.equal(res.status, 1); + assert.match(res.stderr, /0001-alpha\.md/); + assert.match(res.stderr, /"Draft" is not one of/); +}); + +test('an ADR with no status field at all is rejected', (t) => { + const root = makeRepo(t, { '0001-alpha.md': '# Alpha\n\nNo header fields.\n\n## Context\n\nBody.\n' }); + const res = run(root, ['--check']); + assert.equal(res.status, 1); + assert.match(res.stderr, /no `- \*\*Status:\*\* ` field/); +}); + +test('the table header form is parsed as legitimately as the bullet form', (t) => { + // ADR-2008 uses a markdown table for its header. Treating that as "missing a + // status" would flag a correct ADR. + const root = makeRepo(t, { + '0001-alpha.md': '# Alpha\n\n| | |\n|---|---|\n| **Status** | Accepted |\n| **Date** | 2026-01-01 |\n\n## Context\n\nBody.\n', + }); + const res = run(root, ['--write']); + assert.equal(res.status, 0, `table-form header must parse: ${res.stderr}`); + assert.match(fs.readFileSync(path.join(root, 'docs', 'adr', 'README.md'), 'utf8'), /Active decisions \(1\)/); +}); + +test('Superseded must name its successor as a file link, not a bare id', (t) => { + const root = makeRepo(t, { + '0001-alpha.md': adr('Alpha', ['**Status:** Superseded by ADR-900 (2026-02-01)']), + '900-beta.md': adr('Beta', ['**Status:** Accepted', '**Supersedes:** [ADR-0001](0001-alpha.md)']), + }); + const res = run(root, ['--check']); + assert.equal(res.status, 1); + assert.match(res.stderr, /not as a markdown link/); +}); + +test('Superseded with no successor at all is rejected', (t) => { + const root = makeRepo(t, { '0001-alpha.md': adr('Alpha', ['**Status:** Superseded']) }); + const res = run(root, ['--check']); + assert.equal(res.status, 1); + assert.match(res.stderr, /names no successor/); +}); + +test('a one-way supersession is rejected and the message names the fix', (t) => { + const root = makeRepo(t, { + // Beta claims Alpha; Alpha says nothing back. + '900-beta.md': adr('Beta', ['**Status:** Accepted', '**Supersedes:** [ADR-0001](0001-alpha.md)']), + '0001-alpha.md': adr('Alpha', ['**Status:** Accepted']), + }); + const res = run(root, ['--check']); + assert.equal(res.status, 1); + assert.match(res.stderr, /0001-alpha\.md/); + assert.match(res.stderr, /does not record it/); + assert.match(res.stderr, /\*\*Superseded by:\*\* \[ADR-900\]\(900-beta\.md\)/); +}); + +test('a symmetric supersession pair passes', (t) => { + const root = makeRepo(t, { + '900-beta.md': adr('Beta', ['**Status:** Accepted', '**Supersedes:** [ADR-0001](0001-alpha.md)']), + '0001-alpha.md': adr('Alpha', ['**Status:** Superseded by [ADR-900](900-beta.md) (2026-02-01)']), + }); + assert.equal(run(root, ['--check']).status, 1, 'index not yet written'); + assert.equal(run(root, ['--write']).status, 0); + assert.equal(run(root, ['--check']).status, 0, 'a symmetric pair must pass'); +}); + +test('subsumption is symmetry-checked but does NOT mark the target superseded', (t) => { + // The EoS case: ADR-1239 subsumes ADR-1016 as an adapter. ADR-1016 stays + // Accepted — collapsing this into supersession would kill a live decision. + const root = makeRepo(t, { + '900-eos.md': adr('EoS', ['**Status:** Accepted', '**Subsumes as adapters:** [ADR-0001](0001-alpha.md)']), + '0001-alpha.md': adr('Alpha', ['**Status:** Accepted', '**Subsumed by:** [ADR-900](900-eos.md)']), + }); + assert.equal(run(root, ['--write']).status, 0); + const readme = fs.readFileSync(path.join(root, 'docs', 'adr', 'README.md'), 'utf8'); + assert.match(readme, /Active decisions \(2\)/, 'a subsumed ADR stays Active'); + // The subsumer is surfaced in the "Read first" column so EoS is discoverable + // from the component ADR. + assert.match(readme, /\| \[ADR-0001\]\(0001-alpha\.md\) \|[^|]*\| Accepted \| \[ADR-900\]\(900-eos\.md\) \|/); +}); + +test('a missing subsumption back-link is rejected', (t) => { + const root = makeRepo(t, { + '900-eos.md': adr('EoS', ['**Status:** Accepted', '**Subsumes:** [ADR-0001](0001-alpha.md)']), + '0001-alpha.md': adr('Alpha', ['**Status:** Accepted']), + }); + const res = run(root, ['--check']); + assert.equal(res.status, 1); + assert.match(res.stderr, /\*\*Subsumed by:\*\* \[ADR-900\]\(900-eos\.md\)/); +}); + +test("a Proposed ADR's supersession claim is prospective — no back-link demanded", (t) => { + // ADR-857 is Proposed and claims to generalize live ADRs. Demanding the + // back-link would stamp an Accepted decision as superseded by an unratified one. + const root = makeRepo(t, { + '900-beta.md': adr('Beta', ['**Status:** Proposed', '**Supersedes:** [ADR-0001](0001-alpha.md)']), + '0001-alpha.md': adr('Alpha', ['**Status:** Accepted']), + }); + assert.equal(run(root, ['--write']).status, 0); + const check = run(root, ['--check']); + assert.equal(check.status, 0, `a Proposed claimant must not force a back-link: ${check.stderr}`); +}); + +test('ratifying that Proposed ADR to Accepted then demands the back-link', (t) => { + const root = makeRepo(t, { + '900-beta.md': adr('Beta', ['**Status:** Accepted', '**Supersedes:** [ADR-0001](0001-alpha.md)']), + '0001-alpha.md': adr('Alpha', ['**Status:** Accepted']), + }); + const res = run(root, ['--check']); + assert.equal(res.status, 1, 'on ratification the back-link becomes required'); + assert.match(res.stderr, /does not record it/); +}); + +test('"Supersedes: nothing" asserts no relation even when it name-drops an ADR', (t) => { + // ADR-2264 says "Supersedes: nothing; amends the ADR-1239 harness". Reading that + // as a supersession claim invents a link the author never made. + const root = makeRepo(t, { + '900-beta.md': adr('Beta', ['**Status:** Accepted', '**Supersedes:** nothing; amends the [ADR-0001](0001-alpha.md) harness']), + '0001-alpha.md': adr('Alpha', ['**Status:** Accepted']), + }); + assert.equal(run(root, ['--write']).status, 0); + const check = run(root, ['--check']); + assert.equal(check.status, 0, `a negated relation field must assert nothing: ${check.stderr}`); +}); + +test('an em-dash relation value asserts no relation', (t) => { + // Vacuous unless the negated field also carries a LINK: with a bare em-dash + // there is nothing to mis-parse, so the test passes whether or not negation + // works. Linking an ADR after the em-dash makes it discriminating — if the + // field were read as a real claim, symmetry would demand 0001 record it. + const root = makeRepo(t, { + '900-beta.md': '# Beta\n\n| | |\n|---|---|\n| **Status** | Accepted |\n| **Supersedes** | — see [ADR-0001](0001-alpha.md) for context |\n\n## Context\n\nBody.\n', + '0001-alpha.md': adr('Alpha', ['**Status:** Accepted']), + }); + assert.equal(run(root, ['--write']).status, 0); + const check = run(root, ['--check']); + assert.equal(check.status, 0, `an em-dash field must assert nothing: ${check.stderr}`); + assert.doesNotMatch(check.stderr, /does not record it/); +}); + +test('a mixed field with one link and one bare id still flags the bare id', (t) => { + // Regression: testing `rel.links.length` instead of the specific id meant a + // field carrying ANY link silently dropped every bare claim beside it. + const root = makeRepo(t, { + '900-beta.md': adr('Beta', ['**Status:** Accepted', '**Supersedes:** [ADR-0001](0001-alpha.md), ADR-0002']), + '0001-alpha.md': adr('Alpha', ['**Status:** Superseded by [ADR-900](900-beta.md)']), + '0002-gamma.md': adr('Gamma', ['**Status:** Accepted']), + }); + const res = run(root, ['--check']); + assert.equal(res.status, 1); + assert.match(res.stderr, /names ADR-2 without a file link/); +}); + +test('a bare id repeated in prose beside its own link is not flagged', (t) => { + // The corpus legitimately writes "…([ADR-0001](0001-alpha.md)) — see ADR-0001 + // below". That repeat must not be noise. + const root = makeRepo(t, { + '900-beta.md': adr('Beta', ['**Status:** Accepted', '**Supersedes:** Alpha ([ADR-0001](0001-alpha.md)) — see the ADR-0001 note below']), + '0001-alpha.md': adr('Alpha', ['**Status:** Superseded by [ADR-900](900-beta.md)']), + }); + assert.equal(run(root, ['--write']).status, 0); + const check = run(root, ['--check']); + assert.equal(check.status, 0, `a linked-and-repeated id must not be flagged: ${check.stderr}`); +}); + +test('a dangling "Superseded by" is caught even though the ADR is not Accepted', (t) => { + // Regression: the ratification guard skipped every non-Accepted ADR, which + // killed the IN direction entirely — a Superseded ADR pointing at a successor + // that never claims it went unchecked. + const root = makeRepo(t, { + '0001-alpha.md': adr('Alpha', ['**Status:** Superseded by [ADR-900](900-beta.md)']), + '900-beta.md': adr('Beta', ['**Status:** Accepted']), + }); + const res = run(root, ['--check']); + assert.equal(res.status, 1, 'a one-way superseded-by must fail'); + assert.match(res.stderr, /does not claim it|does not record it/); +}); + +test('a `## Supersedes` table section counts as the claim (ADR-0174 shape)', (t) => { + // The richest form in the corpus declares supersession as a section+table, not + // a header field. Reading only the header block reported the repo's + // best-documented supersession as missing. + const root = makeRepo(t, { + '900-beta.md': '# Beta\n\n- **Status:** Accepted\n\n## Supersedes\n\n| ADR | What it said | Why superseded |\n|---|---|---|\n| [ADR-0001](0001-alpha.md) | a thing | a reason |\n\n## Context\n\nBody.\n', + '0001-alpha.md': adr('Alpha', ['**Status:** Superseded by [ADR-900](900-beta.md)']), + }); + assert.equal(run(root, ['--write']).status, 0); + const check = run(root, ['--check']); + assert.equal(check.status, 0, `a ## Supersedes table must satisfy symmetry: ${check.stderr}`); +}); + +test('a title id that disagrees with the filename is rejected', (t) => { + // The real ADR-218 case: renamed to the issue# convention, title left behind. + const root = makeRepo(t, { '218-release.md': adr('ADR-0175: Harden release validation', ['**Status:** Accepted']) }); + const res = run(root, ['--check']); + assert.equal(res.status, 1); + assert.match(res.stderr, /H1 declares ADR-175 but the filename says 218/); +}); + +test('a relation link to a nonexistent ADR is rejected', (t) => { + const root = makeRepo(t, { + '0001-alpha.md': adr('Alpha', ['**Status:** Accepted', '**Supersedes:** [ADR-404](404-ghost.md)']), + }); + const res = run(root, ['--check']); + assert.equal(res.status, 1); + assert.match(res.stderr, /does not exist in docs\/adr\//); +}); + +test('a bare id naming a nonexistent ADR is steered toward issue syntax', (t) => { + // ADR-1610 says "superseding the #597 tier-max ratchet" — #597 is an ISSUE. + // Written as "ADR-597" it would be an unresolvable reference. + const root = makeRepo(t, { + '0001-alpha.md': adr('Alpha', ['**Status:** Accepted', '**Supersedes:** ADR-597 tier-max ratchet']), + }); + const res = run(root, ['--check']); + assert.equal(res.status, 1); + assert.match(res.stderr, /If it is an ISSUE number, write "#597"/); +}); + +test('an ambiguous bare id reports every file it could mean', (t) => { + const root = makeRepo(t, { + '0011-one.md': adr('One', ['**Status:** Accepted']), + '0011-two.md': adr('Two', ['**Status:** Accepted']), + '900-beta.md': adr('Beta', ['**Status:** Accepted', '**Supersedes:** ADR-0011']), + }); + const res = run(root, ['--check']); + assert.equal(res.status, 1); + assert.match(res.stderr, /ambiguous — resolves to 2 files/); + assert.match(res.stderr, /0011-one\.md/); + assert.match(res.stderr, /0011-two\.md/); +}); + +test('--check fails loudly when the README markers are missing', (t) => { + const root = makeRepo(t, { '0001-alpha.md': adr('Alpha', ['**Status:** Accepted']) }); + fs.writeFileSync(path.join(root, 'docs', 'adr', 'README.md'), '# ADRs\n\nNo markers here.\n'); + const res = run(root, ['--check']); + assert.equal(res.status, 1); + assert.match(res.stderr, /missing the index markers/); +}); + +test('--write still emits the index while reporting outstanding violations', (t) => { + // --write must remain usable as a repair tool on a corpus that is not yet clean, + // but must not pretend the corpus is healthy. + const root = makeRepo(t, { + '900-beta.md': adr('Beta', ['**Status:** Accepted', '**Supersedes:** [ADR-0001](0001-alpha.md)']), + '0001-alpha.md': adr('Alpha', ['**Status:** Accepted']), + }); + const res = run(root, ['--write']); + assert.equal(res.status, 0, '--write proceeds'); + assert.match(res.stderr, /lifecycle violation\(s\) remain/); + assert.match(fs.readFileSync(path.join(root, 'docs', 'adr', 'README.md'), 'utf8'), /Active decisions \(2\)/); +}); + +test('an ADR title cannot hijack the README splice with an index marker', (t) => { + // Review finding: a title carrying the literal END marker was emitted verbatim + // into the table cell, relocating the boundary so the NEXT --write spliced + // against the wrong marker and ate the rest of README.md. + const root = makeRepo(t, { + '0001-alpha.md': adr(`Evil ${END} title`, ['**Status:** Accepted']), + }); + assert.equal(run(root, ['--write']).status, 0); + + const readme = fs.readFileSync(path.join(root, 'docs', 'adr', 'README.md'), 'utf8'); + const endCount = readme.split(END).length - 1; + assert.equal(endCount, 1, 'exactly one END marker must survive — the title must not forge another'); + + // The splice must remain stable across repeated writes. + assert.equal(run(root, ['--write']).status, 0); + const again = fs.readFileSync(path.join(root, 'docs', 'adr', 'README.md'), 'utf8'); + assert.equal(again.split(END).length - 1, 1); + assert.equal(run(root, ['--check']).status, 0, 'a hostile title must not leave the index permanently stale'); +}); + +test('a title cannot inject raw HTML into the generated index', (t) => { + const root = makeRepo(t, { + '0001-alpha.md': adr('Alpha module', ['**Status:** Accepted']), + }); + assert.equal(run(root, ['--write']).status, 0); + const readme = fs.readFileSync(path.join(root, 'docs', 'adr', 'README.md'), 'utf8'); + assert.ok(!readme.includes('