diff --git a/CONTEXT.md b/CONTEXT.md index 693fb2beb..5be4446cf 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -166,7 +166,7 @@ Module owning the `{"type":"commonjs"}` module-type marker GSD writes beside its Module owning validation for Installer Migration Module records and planned actions. It enforces migration metadata, explicit install scopes, ownership evidence for destructive/config actions, and runtime contract citations for runtime config rewrites before a migration can enter planning or apply. ### Installer Module -Primary installer for all runtimes. Single production file: `bin/install.js` (generated). Exports: `install(isGlobal, runtime[, configDir])` → typed result `{ runtime, configDir, settingsPath, settings, statuslineCommand, updateBannerCommand }`; `uninstall(isGlobal, runtime[, configDir])`; `installRuntimeArtifacts(runtime, configDir, scope, resolvedProfile)`; `uninstallRuntimeArtifacts(runtime, configDir, scope)`; `writeManifest(configDir, runtime)`. Runtime enum: `allRuntimes` (18 values: claude, antigravity, augment, cline, codebuddy, codex, copilot, cursor, hermes, kimi, kimi-code, kilo, opencode, pi, qwen, trae, windsurf, zcode). Directory helpers: `getDirName(runtime)` → local dir name; `getConfigDirFromHome(runtime, isGlobal)` → shell-quoted path fragment. Per-runtime global config-dir resolution is delegated to `gsd-core/bin/lib/runtime-homes.cjs:getGlobalConfigDir(runtime[, explicitDir])` — the canonical, env-var–aware projection (`explicitDir` override + opencode/kilo `*_CONFIG` file-path precedence); the legacy in-installer `getGlobalDir`/`getOpencodeGlobalDir`/`getKiloGlobalDir` were retired into it (#56). The same module exposes `detectAntigravityDirAmbiguity(opts)` — a side-effect-free probe reporting whether multiple `~/.gemini/antigravity{,-ide,-cli}` dirs coexist and which one GSD's `gsd-core/VERSION` marker (the `dot-home-nested` `probeExists`) resolves to, for installer / `/gsd-update` operator guidance when a pre-#217 install landed in the wrong sibling dir (#1441). Runtime-specific helpers: `resolveKiloConfigPath(configDir)`, `configureKiloPermissions(isGlobal[, explicitDir])`. Claude-specific permission helpers: `mergeClaudePermissions(settings)` — non-destructively appends GSD-owned allow/deny entries (see `GSD_CLAUDE_ALLOW_PERMISSIONS`, `GSD_CLAUDE_DENY_PERMISSIONS` constants) to a Claude Code settings object; called from `finishInstall` for `runtime === 'claude'` only; uninstall removes exactly these entries (#768). Layout-driven artifact copy/removal delegates to `gsd-core/bin/lib/runtime-artifact-layout.cjs:resolveRuntimeArtifactLayout` (throws `TypeError` for unknown runtimes). Five runtimes with non-recursive skill loaders (cline, qwen, hermes, augment, trae) use a nested router layout: 6 `gsd-ns-*` router bundles emitted as top-level skills, with concrete skills nested at `/skills//SKILL.md` (hermes prefix='': `skills/gsd/ns-*/…`). claude (reverted from nested per #924 — the Skill tool errors on unrouted names) and antigravity (one-level scan, but concrete skills must be top-level discoverable) plus the remaining skills-runtimes (cursor, codex, copilot, windsurf, codebuddy, opencode, kilo) use the flat `skills/gsd-/` layout. See Skill Surface Budget Module and Runtime Artifact Layout Module. +Primary installer for all runtimes. Single production file: `bin/install.js` (hand-authored JS — it is NOT generated from `src/*.cts`; ADR-1508 keeps it hand-authored deliberately, and no `npm run build` step emits it). Exports: `install(isGlobal, runtime[, configDir])` → typed result `{ runtime, configDir, settingsPath, settings, statuslineCommand, updateBannerCommand }`; `uninstall(isGlobal, runtime[, configDir])`; `installRuntimeArtifacts(runtime, configDir, scope, resolvedProfile)`; `uninstallRuntimeArtifacts(runtime, configDir, scope)`; `writeManifest(configDir, runtime)`. Runtime enum: `allRuntimes` (18 values: claude, antigravity, augment, cline, codebuddy, codex, copilot, cursor, hermes, kimi, kimi-code, kilo, opencode, pi, qwen, trae, windsurf, zcode). Directory helpers: `getDirName(runtime)` → local dir name; `getConfigDirFromHome(runtime, isGlobal)` → shell-quoted path fragment. Per-runtime global config-dir resolution is delegated to `gsd-core/bin/lib/runtime-homes.cjs:getGlobalConfigDir(runtime[, explicitDir])` — the canonical, env-var–aware projection (`explicitDir` override + opencode/kilo `*_CONFIG` file-path precedence); the legacy in-installer `getGlobalDir`/`getOpencodeGlobalDir`/`getKiloGlobalDir` were retired into it (#56). The same module exposes `detectAntigravityDirAmbiguity(opts)` — a side-effect-free probe reporting whether multiple `~/.gemini/antigravity{,-ide,-cli}` dirs coexist and which one GSD's `gsd-core/VERSION` marker (the `dot-home-nested` `probeExists`) resolves to, for installer / `/gsd-update` operator guidance when a pre-#217 install landed in the wrong sibling dir (#1441). Runtime-specific helpers: `resolveKiloConfigPath(configDir)`, `configureKiloPermissions(isGlobal[, explicitDir])`. Claude-specific permission helpers: `mergeClaudePermissions(settings)` — non-destructively appends GSD-owned allow/deny entries (see `GSD_CLAUDE_ALLOW_PERMISSIONS`, `GSD_CLAUDE_DENY_PERMISSIONS` constants) to a Claude Code settings object; called from `finishInstall` for `runtime === 'claude'` only; uninstall removes exactly these entries (#768). Layout-driven artifact copy/removal delegates to `gsd-core/bin/lib/runtime-artifact-layout.cjs:resolveRuntimeArtifactLayout` (throws `TypeError` for unknown runtimes). Five runtimes with non-recursive skill loaders (cline, qwen, hermes, augment, trae) use a nested router layout: 6 `gsd-ns-*` router bundles emitted as top-level skills, with concrete skills nested at `/skills//SKILL.md` (hermes prefix='': `skills/gsd/ns-*/…`). claude (reverted from nested per #924 — the Skill tool errors on unrouted names) and antigravity (one-level scan, but concrete skills must be top-level discoverable) plus the remaining skills-runtimes (cursor, codex, copilot, windsurf, codebuddy, opencode, kilo) use the flat `skills/gsd-/` layout. See Skill Surface Budget Module and Runtime Artifact Layout Module. ### I/O Module Module owning the tool's CLI I/O primitives: `output()` result emission (with large-payload temp-file spillover via `GSD_TEMP_DIR`/`ensureGsdTempDir`/`reapStaleTempFiles`), `error()` stderr emission with exit-code mapping, and the JSON-error-mode toggle (`setJsonErrorMode`/`getJsonErrorMode`, `ERROR_REASON`). Extracted from the Core module per ADR-857 rollout phase 1 (#859) so feature modules (`graphify`, `intel`, `audit`, `profile-pipeline`) depend on a small I/O seam instead of the core god-module; the `core.cjs` re-export spine was retired in epic #1267, so callers import this leaf directly. Source of truth: `gsd-core/bin/lib/io.cjs` (generated from `src/io.cts`). diff --git a/docs/adr/1016-runtime-capability-descriptor.md b/docs/adr/1016-runtime-capability-descriptor.md index fb6ea8c8e..15916f8a2 100644 --- a/docs/adr/1016-runtime-capability-descriptor.md +++ b/docs/adr/1016-runtime-capability-descriptor.md @@ -8,6 +8,7 @@ - **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 +- **Amended by:** [ADR-2866](2866-install-surface-resolution.md) (Install-surface resolution) — **one axis is added to this ADR's closed descriptor vocabulary: host trigger precedence.** ADR-2866 is the review this ADR's closed-vocabulary friction exists to force. The axis is *required-with-default*, so descriptors authored against today's schema keep working and [ADR-894](894-capability-declaration-format.md)'s additive-only contract holds; the registry generator and validator move with it. **Timing:** the decision is recorded and `Accepted`; the descriptor schema itself changes at epic [#2866](https://github.com/open-gsd/gsd-core/issues/2866) Phase 2 ([#2871](https://github.com/open-gsd/gsd-core/issues/2871)), not before. Nothing else in this ADR's vocabulary opens — precedence is a fact about the *host*, which is exactly why it belongs on the descriptor rather than in a per-runtime branch. - **Amended by:** [ADR-2782](2782-reviewer-lane-capability-surface.md) (Reviewer Lane capability surface) — a `role: "runtime"` capability may now carry a `reviewer` body **alongside** its runtime body. The runtime body itself remains closed and unchanged, and no feature-only field becomes permissible on it. ADR-2782 D6 **upholds** this ADR's closed-vocabulary principle: the lane's `handler` is a closed enum of first-party names (the `ConverterName` construction of Decision 3), never an open escape hatch, so §Alternatives #2 stands unreversed. ## Amendment (2026-07-16): subsumed by ADR-1239 (EoS) — this ADR is the *declarative adapter*, not the whole architecture diff --git a/docs/adr/2866-install-surface-resolution.md b/docs/adr/2866-install-surface-resolution.md new file mode 100644 index 000000000..710a221ed --- /dev/null +++ b/docs/adr/2866-install-surface-resolution.md @@ -0,0 +1,164 @@ +# ADR-2866: Install-surface resolution — the install pipeline resolves `(runtime × scope × trigger)` as a value + +- **Status:** Accepted +- **Date:** 2026-08-09 +- **Issue:** [#2866](https://github.com/open-gsd/gsd-core/issues/2866) (epic); Phase 0 tracked by [#2869](https://github.com/open-gsd/gsd-core/issues/2869) +- **Amends:** [ADR-3660](3660-runtime-artifact-layout-module.md) (widens the Runtime Artifact Layout Module from placement-only to placement **+** trigger resolution) and [ADR-1016](1016-runtime-capability-descriptor.md) (adds one axis — host trigger precedence — to its closed descriptor vocabulary). Neither is superseded; both remain `Accepted` and live, and both carry the reciprocal `Amended by` field. The decision is recorded now; the modules change at Phase 2 — see [Reciprocal amendment notes](#reciprocal-amendment-notes). +- **Relationship to prior work:** *completes* [ADR-58](58-runtime-install-policy-module.md) rather than revising it (its rollout's cleanup step never landed); preserves [ADR-1508](1508-runtime-artifact-conversion-module.md)'s dependency direction (installer/layout → conversion, never upward); Phase 3's manifest schema bump is [ADR-0008](0008-installer-migration-module.md) territory; Phase 2's schema change is additive-with-default per [ADR-894](894-capability-declaration-format.md). Like the adapters it touches, this ADR sits **beneath** [ADR-1239](1239-gsd-embeddable-orchestration-engine.md) (EoS) — it widens one negotiated surface of the Host-Integration Interface; it does not re-answer how GSD meets a host. + +## Context + +[#2218](https://github.com/open-gsd/gsd-core/issues/2218) is the presenting defect: a user who installs the Claude runtime at **both** scopes — `--claude --global` and `--claude --local` — silently loses 100% of the project-local `/gsd-*` surface. It is "every time — 100% reproducible", the reporter's impact assessment is "major — core feature is broken, no workaround", and its triage stalled at `ready-for-human` because every proposed remediation read as a product decision bolted onto the installer. + +It stalled for a deeper reason. **The codebase cannot state the problem.** + +### The trigger namespace is modeled nowhere + +[ADR-3660](3660-runtime-artifact-layout-module.md) answers exactly one question — *given this artifact kind and this runtime, where does the file go?* — and stops. That narrowness was deliberate and, at the time, correct: [ADR-1508](1508-runtime-artifact-conversion-module.md) draws the sibling line in one sentence, "Layout owns placement; this module owns content." + +The seam has since gained a `scope` parameter — `resolveRuntimeArtifactLayout(runtime, configDir, scope: 'local' | 'global' = 'global', capabilityRegistry?)` — but placement-only is a property of the **returned value**, not of the parameter list. `Layout` is `{ runtime, configDir, scope?, kinds: ArtifactKind[] }` and `ArtifactKind` is `{ kind, destSubpath, prefix, stage, home?, converter? }`. Neither carries a trigger. + +That absence is the defect. Both the `skills` and `commands` kinds derive their stems from the same `commands/gsd/*.md` sources, so a Claude user with both installs gets two artifacts resolving to one `/gsd-` trigger. The host resolves **skill over command** *and* **personal over project**, so the project-local tree becomes unreachable. No module and no test can name the collision, because it is an absence rather than a value. + +All 19 runtime descriptors declaring `artifactLayout`, partitioned exhaustively: + +| Count | Descriptors | Shape | Shadowing behavior | +|---|---|---|---| +| 12 | `antigravity`, `augment`, `codebuddy`, `codex`, `copilot`, `cursor`, `hermes`, `kilo`, `opencode`, `qwen`, `trae`, `zcode` | `skills` at **both** scopes | Same mechanic, identical kind — the personal copy wins and points at a tree that exists. A **loud override**, not silent loss. | +| 1 | `claude` | `global=[skills]`, `local=[commands, agents]` — the only runtime whose scopes emit **disjoint** trigger-bearing kinds | The whole local surface vanishes rather than merely being overridden. **This is #2218.** | +| 1 | `windsurf` | `global=[agents]`, `local=[commands, agents]` — also scope-asymmetric | Its global scope emits **no command trigger**, so nothing collides. Does **not** exhibit #2218's failure. | +| 3 | `cline`, `kimi`, `kimi-code` | `global=[…]`, `local=[]` | No local emission at all, so no cross-scope collision is reachable. | +| 2 | `pi`, `vscode` | `global=[]`, `local=[]` | No GSD artifact surface. | +| **0** | — | modules that model the trigger namespace or cross-scope precedence | **This is the absence the epic exists to fix.** | + +**The mechanism is general; the failure is specific to `claude`** — and the partition above is the reason: only `claude` combines a trigger-bearing global kind with a *disjoint* trigger-bearing local kind. + +### Scope is a bare string re-derived at every layer + +`bin/install.js` contains 12 literal `isGlobal ? 'global' : 'local'` sites; the boolean is then reconstructed downstream in `runtime-artifact-layout.cts`, `runtime-artifact-install-plan.cts` and `surface.cts`. There is no shared resolver — `runtime-homes.cts`'s `resolveConfigHomeFromDescriptor` takes no `scope` parameter at all, and the one partial projection that exists (`hostBehaviors.settingsFileByScope`) has a single consumer inside `bin/install.js` with a hardcoded fallback, so no other module can reach it. + +The concept also has three incompatible spellings in `src/` alone: + +| Site | Representation | +|---|---| +| `src/runtime-artifact-layout.cts` | `scope?: 'local' \| 'global'` | +| `src/capability-lifecycle.cts` | `scope?: 'global' \| 'project'` | +| `src/capability-consent.cts` | `ConsentRecord.scope: 'project'` — a single literal; global is encoded as *absence* | + +Nothing can answer "what is installed, where": `gsd-file-manifest.json` records `{version, timestamp, mode, files}` with **no `scope` and no `runtime` field**, and global and local installs write two manifests to two directories that are never merged and never cross-read. + +### The consequence + +Every remediation proposed for #2218 reads as a special case, because no module owns the concept that would carry the fix — and, as recorded below, two of the three proposed remediations turn out not to work at all. + +## Decision + +**The install pipeline resolves *surface identity* — `(runtime × scope × trigger)` — as a value, instead of implying it from destination paths.** + +Concretely: the Runtime Artifact Layout Module returns resolved **triggers**, not only placements, with host precedence declared as a descriptor axis; scope becomes one resolved value produced by one module rather than a string re-interpreted in seven places; and the install manifest records `scope` and `runtime` so an Installed Surface Resolver can answer *"which surfaces are installed, at which scopes, for which runtimes — and is one shadowing another?"* #2218's shadowing then becomes a field on a returned value rather than an invisible outcome of host resolution. + +The four sections below record what that costs the ADRs it touches. Everything else — the phase map, the consequences, the rejected alternatives — follows from them. + +### 1. Amendment to ADR-3660: placement-only stopped paying + +[ADR-3660](3660-runtime-artifact-layout-module.md) is **amended, not superseded.** Its placement decision is correct and load-bearing: 19 descriptors and three consumers depend on it, and its `Consequences` — a new runtime is one table row, and out-of-seam placement knowledge is drift — remain in force. + +**What changes:** the module widens from *placement* to *placement + trigger resolution*. It gains a projection returning, per emitted artifact, the `/gsd-` trigger it will occupy, the kind and scope that produced it, its destination, and whether another install shadows it. `resolveRuntimeArtifactLayout` stays for callers that only need placement, so no existing consumer is forced to move. + +**Why the narrowness stopped paying — stated so a future reader does not re-narrow it.** ADR-3660's scope was right for the question it was built for (#3659: `applySurface` failed to prune skill directories — a pure *placement* omission, and the layout table fixed it). It stops paying at the first question whose answer is not a path. #2218 is that question. The colliding `/gsd-` trigger is not a value anywhere in the tree, so no module can express the collision, no test can assert it, and no error message can name it. A seam that cannot state a defect in its own domain has drawn its boundary one concept too small. Placement is *where the file goes*; the trigger is *what the user types* — and it is the second one the host arbitrates. + +**This does not absorb historical kinds.** Legacy-layout migrations stay in the Installer Migration Module ([ADR-0008](0008-installer-migration-module.md)) and continue to run before layout-driven copy, exactly as ADR-3660 decided. The layout module still describes only the current canonical target. + +### 2. One axis added to ADR-1016's closed descriptor vocabulary: host trigger precedence + +[ADR-1016](1016-runtime-capability-descriptor.md) designs the runtime descriptor's vocabulary as **closed on purpose** — a new axis is intentional friction requiring review, so that "add a runtime" stays "author one `capability.json`" instead of "teach N modules what this runtime means". **This ADR is that review, and it approves exactly one axis: host trigger precedence.** + +**Why it must be a descriptor axis rather than code.** Precedence is a fact about the *host*, not about GSD: Claude resolves skill-over-command and personal-over-project; another host may do neither. Encoding it in a module means a per-runtime `if` — the precise drift ADR-3660's table exists to prevent and ADR-1016's descriptor exists to prevent. It belongs where every other host fact already lives. + +**Why one and not two.** Scope's own precedence rank is a property of a *scope*, not of a *placement*, and it is produced by the Install Scope Module (Phase 1). Only the host's kind-level trigger arbitration is genuinely descriptor-shaped. Widening the vocabulary by two axes when one is enough would spend ADR-1016's friction budget on the wrong thing. + +**Compatibility.** The axis is **required-with-default**: third-party runtime capabilities authored against today's schema keep working unchanged, which keeps the change additive-only per [ADR-894](894-capability-declaration-format.md)'s stability contract. The registry generator and the validator must be updated in the same change as the schema — a descriptor field the validator does not know is a field third-party authors cannot rely on. + +### 3. The `@`-include constraint — and why #2218 triage option 1 is refuted + +**The constraint, recorded here as a first-class design fact:** markdown `@`-includes expand `~` but do **not** expand environment variables, and no conditional-include syntax exists. It is currently written down only as a comment on a SessionStart hook (`hooks/gsd-ensure-canonical-path.js`), framed as a marketplace-plugin problem — so nothing tells a contributor it also constrains *spec-root emission*. Whatever literal string is baked into an `@…` is what the host statically resolves; there is no cwd-conditional spec resolution anywhere in the emitted surface today. + +This is the hard constraint on any #2218 fix, and it decides two of the three remediations that #2218's triage proposed: + +- **Triage option 1 — "make `--local` also emit a skill" — is REFUTED, not deprioritized.** The host rule is *personal overrides project* for identically-named skills. Emitting a project-scope skill changes **which artifact wins**; it does not change **what the winning artifact points at**. The local spec tree stays exactly as unreachable as before. Recorded here so it is not re-proposed — it is the intuitive fix, and its failure is not visible from the symptom. +- **Triage option 3 — "document scope mutual-exclusivity" — is rejected as a fix.** The reporter's configuration (per-project customizations plus a global install for projects without one) is legitimate; documenting it away removes a working configuration rather than supporting it. It is retained **only** as the fallback if the Phase 4b mechanism is rejected, in which case the limitation must be documented explicitly rather than left implicit. +- **Triage option 2 — detect and warn — is necessary but not sufficient.** It removes the *silence*, which is #2218's worst property, but leaves the user unable to actually use both installs. + +**Therefore the only lever is what the winning artifact points at.** That is why Phase 4 splits into an unconditional detection floor and a separately-signed-off behavioral fix — see [What this ADR does not decide](#what-this-adr-does-not-decide). + +### 4. Non-conflicts: completes ADR-58, preserves ADR-1508's direction + +**[ADR-58](58-runtime-install-policy-module.md) is completed, not revised.** Its decision — install logic testable as pure data, resolution free of IO, thin adapters executing — is unchanged and still right. Its rollout sequenced *registry → adapter → helpers → cleanup*, and the cleanup step never landed: `installRuntimeArtifacts()` still returns `void`, so install's correctness is observable only by re-reading disk. The counter-example is already in the tree — `createRuntimeArtifactInstallPlan()` returns a pure `{ok, plan}` its test asserts in one comparison with injected stage dependencies and no real filesystem. Phase 5 finishes the sequence ADR-58 wrote. Nothing in ADR-58 is reopened. + +**[ADR-1508](1508-runtime-artifact-conversion-module.md)'s dependency direction is preserved: installer/layout → conversion, never upward.** No phase makes `src/` depend on `bin/install.js`. Phase 6's Layout Materializer is extracted *from* the installer *into* `src/`, with `bin/install.js` as a caller — the same direction ADR-1508 established. `bin/install.js` remains hand-authored JS, as ADR-1508 explicitly decided ("Only the moved functions become TypeScript; `install.js` itself is not converted"), which is also why this PR corrects `CONTEXT.md`'s stale "(generated)" annotation for that file. + +## What this ADR does not decide + +Recorded explicitly, because an ADR that quietly ratifies these would be laundering decisions no one made. + +- **Phase 4b's mechanism is NOT decided here.** Making the project-local spec tree reachable requires changing what the winning artifact points at. The epic's recommendation — for the Claude runtime only, emit the **spec-root reference only** as a two-step imperative resolution (prefer `/.claude/gsd-core/…`, fall back to `~/.claude/gsd-core/…`) instead of a single static `@`-include — is recorded as **recommended, pending explicit maintainer sign-off**, with its tradeoff stated plainly: an `@`-include is pre-expanded by the host and is *guaranteed inclusion*; a resolved reference costs the agent one read and is *instruction-following*. That is a genuinely weaker promise, and scoping it to the spec-root reference alone (every other `@`-include stays static) is what makes it acceptable rather than what makes it equivalent. If the tradeoff is rejected, Phase 4a still ships and #2218 downgrades from "broken silently" to "unsupported loudly" — which must then be documented per option 3 above. +- **The trigger-resolution interface is not specified here.** `{trigger, kind, scope, destPath, shadowedBy}` is a sketch. Phase 2 ([#2871](https://github.com/open-gsd/gsd-core/issues/2871)) settles the shape. This ADR decides *that* the layout module resolves triggers, not its signature. +- **Phase 6's Layout Materializer is a new module and therefore owes its own ADR.** It is not decided here; folding a second module decision into this file would violate CONTRIBUTING's "one issue = one ADR-or-PRD = one PR". +- **The `local`/`project` spelling is not chosen here.** Phase 1 reconciles the three-way split and records the chosen spelling in `CONTEXT.md`'s glossary, which is where domain vocabulary is owned. +- **`hostIntegration.embeddingMode: "imperative"` is unrelated to any of this.** Per `docs/reference/host-integration-capability-matrix.md` it classifies the host's plugin API, not spec-path emission. It shares a word with Phase 4b's "imperative reference" and nothing else. + +### Reciprocal amendment notes + +[ADR-3660](3660-runtime-artifact-layout-module.md) and [ADR-1016](1016-runtime-capability-descriptor.md) each carry an `Amended by: ADR-2866` back-reference, added in this same PR — the corpus's established practice for an amendment relation ([ADR-1016](1016-runtime-capability-descriptor.md) already carries the equivalent field for [ADR-2782](2782-reviewer-lane-capability-surface.md)). A one-way pointer is the failure mode this corpus has actually suffered: a reader landing on the amended file learns nothing about the decision that moved it. + +Each back-reference states **when the widening takes effect** — the decision is recorded now (this ADR is `Accepted`); the shipped modules still resolve placement only until Phase 2 ([#2871](https://github.com/open-gsd/gsd-core/issues/2871)) lands. Recording the relation without that timing note would tell a reader the layout module already resolves triggers, which would be false for four phases. + +*Mechanical note for future readers:* `scripts/gen-adr-index.cjs` tracks only `Supersedes`/`Subsumes` and their inverses. **`Amends` is not machine-checked in either direction** — the back-links above are a convention this ADR honors deliberately, not something the gate would have caught had they been omitted. + +## Phase map + +Each phase is its own issue and its own PR. `0 → 1 → 2 → 3 → 4` is a hard dependency chain; 5 and 6 are independent of 1–4 once 0 lands; 7 follows 5 and 6. + +| Phase | Issue | Deliverable | ADR touched | +|---|---|---|---| +| 0 | [#2869](https://github.com/open-gsd/gsd-core/issues/2869) | This ADR + the `CONTEXT.md` correction | — | +| 1 | [#2870](https://github.com/open-gsd/gsd-core/issues/2870) | Install Scope Module — one resolved scope value | — | +| 2 | [#2871](https://github.com/open-gsd/gsd-core/issues/2871) | Trigger resolution + host-precedence axis | [ADR-3660](3660-runtime-artifact-layout-module.md), [ADR-1016](1016-runtime-capability-descriptor.md) amended here | +| 3 | [#2872](https://github.com/open-gsd/gsd-core/issues/2872) | Manifest `scope`+`runtime`; Installed Surface Resolver | [ADR-0008](0008-installer-migration-module.md) (migration) | +| 4 | [#2873](https://github.com/open-gsd/gsd-core/issues/2873) | **Resolves [#2218](https://github.com/open-gsd/gsd-core/issues/2218)** — detection floor + behavioral fix | — | +| 5 | [#2874](https://github.com/open-gsd/gsd-core/issues/2874) | Executed-plan return value | [ADR-58](58-runtime-install-policy-module.md) completed | +| 6 | [#2875](https://github.com/open-gsd/gsd-core/issues/2875) | Layout Materializer + #1874-F19 durable staging | needs its own ADR | +| 7 | [#2876](https://github.com/open-gsd/gsd-core/issues/2876) | Retire dead + pass-through installer exports | [ADR-1508](1508-runtime-artifact-conversion-module.md) / [ADR-857](857-capability-system.md) re-export mandate revisited | + +## Consequences + +- **#2218 is fixed and can no longer regress.** The coexistence case gets its first test in the suite's history: no test today installs a runtime at both scopes and asserts on the combined result. +- **Shadowing is decided in one module.** Today it is decided in the host, invisibly, and modeled nowhere. After Phase 2 it is a field on a returned value that an error message, `/gsd-health`, and a test can each read. +- **One resolved projection serves install, uninstall, `/gsd:surface`, the migration planner, and the docs matrix.** Adding a runtime stays "author one `capability.json`" — ADR-1016's stated goal — instead of also teaching N modules what its scopes mean. +- **Install becomes assertable as a value** (Phase 5), so the 10,539-line `install.test.cjs` can collapse toward the shape `runtime-artifact-install-plan.test.cjs` already demonstrates. +- **Deletions, not just additions:** 12 scope conversions in `bin/install.js`, 12 dead exports, and 2 of the 3 duplicate layout walkers. [#1874](https://github.com/open-gsd/gsd-core/issues/1874)-F19's durable-staging fix lands inside Phase 6's extraction rather than as a second conflicting pass over the same choreography. +- **The costs, stated:** ADR-1016's closed vocabulary is one axis wider and stays wider forever; the Runtime Artifact Layout Module now owns two concepts instead of one, which is a boundary future reviews must hold rather than keep widening; and Phase 3's manifest schema bump obliges a v1-tolerant read path for the life of that format — **users must not need to reinstall.** Phase 4a adds install-time output and must not change exit codes: a shadowed install is a warning, not a failure. +- **Extraction, not rewrite.** `bin/install.js` keeps working at every phase boundary. This is the pattern that has actually worked in this repo — [ADR-857](857-capability-system.md), [ADR-1508](1508-runtime-artifact-conversion-module.md) and [ADR-3660](3660-runtime-artifact-layout-module.md) have each moved one slice — and the reason the whole-installer rewrite is rejected below. + +## Alternatives considered + +1. **Fix #2218 directly today — bespoke detection in `bin/install.js`.** Rejected as the whole answer. The manifest records neither scope nor runtime, so the check would be a hand-rolled pair of existence probes, untestable through any interface, leaving the next cross-scope question equally unanswerable. Phase 4a delivers the same user-facing outcome built on something that can be asserted. +2. **Triage option 1 — make `--local` also emit a skill.** **Refuted** — see Decision §3. Personal overrides project, so the local tree stays unreachable. +3. **Triage option 3 — document scope mutual-exclusivity.** Rejected as a fix; retained only as the documented fallback if Phase 4b's mechanism is not approved. +4. **A SessionStart hook that repoints `~/.claude/gsd-core` per project.** Rejected. There is precedent for the mechanism (`hooks/gsd-ensure-canonical-path.js` symlinks a plugin tree into the global dir), but making a machine-global path depend on the cwd of whichever session ran last is unsafe with concurrent sessions in different projects. +5. **One big installer rewrite.** Rejected. `bin/install.js` is ~13.5k hand-authored lines with 42 test files bound to its exports. ADR-1016 predicted it would "shrink substantially"; three extraction ADRs have each moved a slice, and that is the approach with a track record here. A complex system that works evolved from a simpler one that worked — a from-scratch installer would have to rediscover every edge case the current one already encodes. +6. **Skip Phase 1 and put precedence directly in the layout module.** Rejected. A scope's precedence rank is a property of a *scope*, not of a *placement*, and the 12 conversion sites are exactly why nobody can currently thread it through. Putting it in the layout module would make the module's second concept a third. +7. **Sequence #1874-F19 separately from Phase 6.** Rejected on maintainer direction: Phase 6 rewrites that exact choreography, so landing the durable staging inside the extraction is one pass instead of two conflicting ones. +8. **Write this as a new ADR superseding ADR-3660.** Rejected. `Superseded` in this corpus means "do not follow this" and obliges naming a replacement; ADR-3660's placement decision is live and correct. Amendment is the accurate relation, and the corpus already models amendments as dated sections on the amended file. + +## References + +- Presenting defect: [#2218](https://github.com/open-gsd/gsd-core/issues/2218) — `--local` Claude install silently shadowed by a coexisting `--global` skills install +- Epic: [#2866](https://github.com/open-gsd/gsd-core/issues/2866); phases [#2869](https://github.com/open-gsd/gsd-core/issues/2869)–[#2876](https://github.com/open-gsd/gsd-core/issues/2876) +- Incorporated: [#1874](https://github.com/open-gsd/gsd-core/issues/1874)-F19 (durable user-artifact staging) lands with Phase 6; the rest of that epic is untouched +- Placement seam this amends: [ADR-3660](3660-runtime-artifact-layout-module.md); content sibling: [ADR-1508](1508-runtime-artifact-conversion-module.md) +- Descriptor vocabulary this widens: [ADR-1016](1016-runtime-capability-descriptor.md); its stability contract: [ADR-894](894-capability-declaration-format.md) +- Install-plan projection this completes: [ADR-58](58-runtime-install-policy-module.md); its generalization: [ADR-857](857-capability-system.md) +- Migration policy for Phase 3's schema bump: [ADR-0008](0008-installer-migration-module.md) +- The frame all of the above sit beneath: [ADR-1239](1239-gsd-embeddable-orchestration-engine.md) (EoS) +- The `@`-include constraint's original site: `hooks/gsd-ensure-canonical-path.js` diff --git a/docs/adr/3660-runtime-artifact-layout-module.md b/docs/adr/3660-runtime-artifact-layout-module.md index 4ad626d15..bbdf8093a 100644 --- a/docs/adr/3660-runtime-artifact-layout-module.md +++ b/docs/adr/3660-runtime-artifact-layout-module.md @@ -5,6 +5,7 @@ - **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 +- **Amended by:** [ADR-2866](2866-install-surface-resolution.md) (Install-surface resolution) — **this module widens from *placement* to *placement + trigger resolution*.** The `Layout` returned here models where a file goes but not the `/gsd-` trigger it occupies, so nothing in the tree can express the cross-scope collision in [#2218](https://github.com/open-gsd/gsd-core/issues/2218). ADR-2866 adds a projection returning the resolved trigger, its kind and scope, its destination, and whether another install shadows it. **This ADR's placement decision is unchanged and still in force** — `resolveRuntimeArtifactLayout` stays for callers that only need placement, the per-runtime table remains the single owner of placement knowledge, and legacy-layout migrations stay in [ADR-0008](0008-installer-migration-module.md). **Timing:** the decision is recorded and `Accepted`; the module changes at epic [#2866](https://github.com/open-gsd/gsd-core/issues/2866) Phase 2 ([#2871](https://github.com/open-gsd/gsd-core/issues/2871)), not before — until then this module resolves placement only. ## Amendment (2026-07-16): subsumed by ADR-1239 (EoS) diff --git a/docs/adr/README.md b/docs/adr/README.md index 110ea6a77..6a0579e9f 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -181,6 +181,7 @@ These govern the system as it stands. Cite these. | [ADR-2629](2629-phase-effort-estimation-calibration.md) | Phase effort is estimated against a calibrated smart-zone budget, not a static heuristic | Accepted | — | | [ADR-2719](2719-emitted-artifact-attribution.md) | Emitted-artifact attribution — replace the committed parity fixtures with a computed conservation law | Accepted | — | | [ADR-2782](2782-reviewer-lane-capability-surface.md) | Reviewer Lane — the cross-AI reviewer handoff becomes a declared capability surface | Accepted | — | +| [ADR-2866](2866-install-surface-resolution.md) | Install-surface resolution — the install pipeline resolves `(runtime × scope × trigger)` as a value | Accepted | — | | [ADR-2966](2966-loop-qa-walk.md) | Test the five-step loop as a continuous walk, not isolated points | Accepted | — | | [ADR-3180](3180-planning-semantic-model-single-owner.md) | Planning Semantic Model — Single Owner per Derivation | Accepted | — | | [ADR-3212](3212-lexical-seam-consolidation.md) | The Lexical Seam — Safe Pattern Construction, Line-Terminator Normalization, and Tokenizer-First Stateful Grammars | Accepted | — |