Files
msd-core/docs/adr/2866-install-surface-resolution.md
Tom Boucher cf6de5e1c0 feat(#2871): resolve triggers and host precedence, not just placement (#3291)
* test(#2871): failing-first suite for trigger-surface resolution

23 tests over the 50-test-matrix rows. RED by construction:
resolveTriggerSurface and DEFAULT_TRIGGER_PRECEDENCE do not exist yet,
and the validator silently ignores triggerPrecedence today.

Written in the per-runtime describe idiom the other four
runtime-artifact-layout suites use, not a table.

The rows that carry the weight: windsurf must NOT report a shadow it
does not have, since its global scope emits only agents and agents are
not trigger-bearing; agents and kimi-agents must be absent from the
output for every runtime; and reordering a runtime's triggerPrecedence
must flip the winner, which is the only assertion that proves the axis
is read rather than decorative.

Stems are injected, never scanned, so the surface is assertable with no
filesystem.

* feat(#2871): resolve triggers and host precedence, not just placement

resolveTriggerSurface(runtime, scopes) returns every /gsd-<name> trigger
a runtime emits, with the scope and kind that produced it, whether the
host registers it directly or only through a router, and which artifact
shadows it. resolveRuntimeArtifactLayout is untouched -- its 7 callers
need placement only and the issue requires them unchanged.

AGENTS ARE NOT TRIGGER-BEARING, and ADR-2866 said they were. The
host-integration matrix models command and dispatch as separate interface
points: an agent is invoked through the Agent tool's subagent_type, not
by typing a slash trigger, and _copyStaged never applies the kind prefix
to an agents entry. So agents and kimi-agents are excluded from the
surface entirely, and this commit amends ADR-2866 with a dated
correction. #2218's conclusion is unchanged -- the collision is strictly
commands-vs-skills, and claude's local /gsd-* trigger surface is still
fully shadowed -- but the ADR implied the local agents surface was lost
too, and it is not.

That correction is what makes windsurf come out right. Its global scope
emits only agents, so it has no global trigger and its local commands
are unshadowed. Model agents as trigger-bearing and windsurf falsely
reports a full shadow.

The triggerPrecedence axis lands on all 19 descriptors as an ordered
kind list, one value with one owner, rather than a numeric rank spread
across N kind entries with nothing keeping them consistent. Validation
uses a required-with-default shape that has no precedent in this
validator -- every existing axis is hard-required -- so a third-party
capability.json omitting the field still validates, which is what
ADR-894's additive-only contract promises.

Winner resolution reads Phase 1's scope rank first, then the kind
ordering. A test reorders the axis and asserts the winner flips, since
an axis that is added, validated and never consulted would pass every
other assertion.

shadowedBy ships unread. Phase 4 (#2873) is its first consumer, per this
issue's out-of-scope note.

Verified via the remote runner.

* fix(#2871): single-source namespacedByDir and close two test gaps

Four findings from the isolated adversarial review.

The namespacedByDir rule had reached three copies -- install-engine,
surface, and the new trigger resolver -- one of which carried a
hand-written keep-in-sync comment and no assertion. That is this repo's
generative-fix-divergence class. Extracted to one exported predicate all
three now call. Verified by diverging one copy deliberately: the existing
#816 parity test failed, and passes again on revert.

The omission test was vacuous. Row 16 asserted that a descriptor without
triggerPrecedence still validates, but built its fixture from claude's
shipped descriptor -- which this PR had just added the axis to. It now
clones and deletes the key, following the shippedDescriptorWithout
pattern, and asserts both that validation passes and that the resolver
still picks the right winner from the default. The second half is what
makes it prove anything.

resolveTriggerSurface silently dropped an unrecognized scope while every
sibling in this epic throws. Two phases of one epic should not disagree
about whether an invalid scope is an error, so it now rejects through the
same shared validator; an empty scope list still returns empty rather
than throwing.

The ADR amendment had been spliced into the middle of the References
list, orphaning its last bullet. Moved to the top, after the header
block, which is where ADR-3660 and ADR-1016 both put dated amendments.
No lint checks markdown structure, so this was green while malformed.

* fix(#2871): single-source the command filename composition too

The earlier fix shared the namespacedByDir boolean but left the
filename composition around it written twice -- once in _copyStaged as
what actually gets written, once in resolveTriggerSurface as what gets
predicted. The predictor could go stale silently.

One exported helper now composes it for both. The entry.name asymmetry
that looked like it would block extraction does not: entry.name is
filtered to end in .md and stem is entry.name minus those three
characters, so the two branches are the same string by construction.

Divergence proven to fail: injecting a marker into the helper broke the
trigger-surface suite; reverting restored 25/25. The four sibling layout
suites hold at 227 unchanged.

* docs(#2871): correct the ADR timing notes that this phase makes stale

The Amended by back-links on ADR-3660 and ADR-1016 were written in
Phase 0, when the widenings they describe had not shipped. Each carried
a forward-looking clause -- "the module changes at Phase 2, not before,
until then this module resolves placement only" -- which becomes false
the moment this PR merges. ADR-2866's own Amends header and its
reciprocal-notes section carried the same tense.

All four now describe what shipped. This is a tense and status
correction on Accepted ADRs, not a change to any decision.

Worth stating because it is the failure mode this epic keeps meeting:
gen-adr-index.cjs tracks only Supersedes and Subsumes, so nothing in CI
would have caught either the missing back-link in Phase 0 or these stale
clauses now. They stay correct only because someone checks.

* chore(#2871): backfill changeset PR number

---------

Co-authored-by: sim <sim@local>
2026-08-09 22:25:42 -04:00

28 KiB
Raw Blame History

ADR-2866: Install-surface resolution — the install pipeline resolves (runtime × scope × trigger) as a value

  • Status: Accepted
  • Date: 2026-08-09
  • Issue: #2866 (epic); Phase 0 tracked by #2869
  • Amends: ADR-3660 (widens the Runtime Artifact Layout Module from placement-only to placement + trigger resolution) and ADR-1016 (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. Both widenings shipped in Phase 2 (#2871) — see Reciprocal amendment notes.
  • Relationship to prior work: completes ADR-58 rather than revising it (its rollout's cleanup step never landed); preserves ADR-1508's dependency direction (installer/layout → conversion, never upward); Phase 3's manifest schema bump is ADR-0008 territory; Phase 2's schema change is additive-with-default per ADR-894. Like the adapters it touches, this ADR sits beneath ADR-1239 (EoS) — it widens one negotiated surface of the Host-Integration Interface; it does not re-answer how GSD meets a host.

Amendment (2026-08-10): agents is not a trigger-bearing kind — claude's disjointness is commands vs skills, not commands, agents vs skills

Phase 2 (#2871) found, while implementing resolveTriggerSurface, that the Context table above (row claude) and this ADR's own prose both mis-describe claude's local scope. local=[commands, agents] is correct as a placement fact — both kinds are emitted locally — but the row's label, "the only runtime whose scopes emit disjoint trigger-bearing kinds", overstates it: agents is not trigger-bearing at all.

  • docs/reference/host-integration-capability-matrix.md already models command and dispatch as two separate interface points — command is "slash-command routing and invocation", dispatch is "subagent/multi-agent dispatch". Claude's own row cites dispatch.namedDispatch: true with the evidence agents: { … subagent_type: block.inp }: an agent is invoked through the Agent/Task tool's subagent_type, not by a user typing /gsd-<name>.
  • _copyStaged (install-engine.cts:404-493) never applies kind.prefix to an agents kind — the filename passes through verbatim (L474-483). An agent stem carries gsd- because the source file is named gsd-planner.md, a filesystem convention, not a trigger registration.

Consequence for this ADR: the claude row's shape is global=[skills], local=[commands, agents] unchanged (placement), but the trigger-bearing collision it describes is strictly commands vs skills — agents plays no part in it. resolveTriggerSurface (runtime-artifact-layout.cts, Phase 2) returns commands and skills only; agents and kimi-agents are absent from its output entirely.

#2218 itself is unaffected by this correction. Claude global emits skills; claude local emits commands; both derive from the same commands/gsd/*.md stems, so the entire local /gsd-* trigger surface is still fully shadowed exactly as this ADR's Context section describes — "the whole local surface vanishes rather than merely being overridden" remains true as written, because it is scoped to the /gsd-* trigger surface, and that surface never included agents in the first place. What changes is precision, not outcome: the local agents surface (subagent dispatch) is a different interface point, is not shadowed by the global skills install, and this ADR should not have implied it was.

This correction is also why windsurf's row above reads correctly without amendment: its global=[agents] already correctly describes "no command trigger" (agents were never counted as one), which is exactly the case Phase 2's test suite locks in as "windsurf must not report a shadow it does not have."

No decision in this ADR changes as a result — Phase 2's resolveTriggerSurface signature, the triggerPrecedence axis, and the phase map above were all designed against the corrected model. See .gsd/phase/feat-2871-trigger-resolution/40-design.md for the full analysis.

Context

#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 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 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-<name> 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 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-<name> 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-<name> 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) 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 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'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.

4. Non-conflicts: completes ADR-58, preserves ADR-1508's direction

ADR-58 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'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 <cwd>/.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) 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 and ADR-1016 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 already carries the equivalent field for ADR-2782). 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 was recorded when this ADR became Accepted, while the shipped modules still resolved placement only until Phase 2 (#2871) landed; both back-references now describe a widening that has actually shipped. Recording the relation without that timing note would have told a reader the layout module already resolved triggers before it did, which would have been 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 This ADR + the CONTEXT.md correction —
1 #2870 Install Scope Module — one resolved scope value —
2 #2871 Trigger resolution + host-precedence axis ADR-3660, ADR-1016 amended here
3 #2872 Manifest scope+runtime; Installed Surface Resolver ADR-0008 (migration)
4 #2873 Resolves #2218 — detection floor + behavioral fix —
5 #2874 Executed-plan return value ADR-58 completed
6 #2875 Layout Materializer + #1874-F19 durable staging needs its own ADR
7 #2876 Retire dead + pass-through installer exports ADR-1508 / ADR-857 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-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, ADR-1508 and ADR-3660 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 — --local Claude install silently shadowed by a coexisting --global skills install
  • Epic: #2866; phases #2869–#2876
  • Incorporated: #1874-F19 (durable user-artifact staging) lands with Phase 6; the rest of that epic is untouched
  • Placement seam this amends: ADR-3660; content sibling: ADR-1508
  • Descriptor vocabulary this widens: ADR-1016; its stability contract: ADR-894
  • Install-plan projection this completes: ADR-58; its generalization: ADR-857
  • Migration policy for Phase 3's schema bump: ADR-0008
  • The frame all of the above sit beneath: ADR-1239 (EoS)
  • The @-include constraint's original site: hooks/gsd-ensure-canonical-path.js