Files
msd-core/docs/adr/1244-capability-ecosystem.md
Tom Boucher 67a9243cf1 chore(#2356): make the ADR index a generated artifact and enforce ADR lifecycle invariants (#2367)
* chore: rebuild ADR index as a generated artifact and enforce lifecycle invariants

The ADR index in docs/adr/README.md was hand-maintained with nothing checking
it, and had drifted to 40 of 65 ADRs. The absent rows included the entire
capability family (857/894/959/1016/1143/1213/1244) and ADR-1239 (EoS) itself,
so the decisions a reader most needed were the ones they could not find.

Make the index a derived artifact, matching the repo's existing generated-file
idiom (lint:generated-sync), and enforce the corpus' lifecycle invariants:

- scripts/gen-adr-index.cjs generates the index between markers and validates
  the status vocabulary (Accepted/Proposed/Superseded/Legacy/Retired),
  successor links, id/filename agreement, and supersession symmetry.
- Wire --check into lint:generated-sync so drift fails CI.

Correct the lifecycle metadata the gate surfaced, without flipping any status:

- ADR-1239 (EoS) declared it subsumed ADR-1016/58/3660/894; none recorded it.
  Add reciprocal "Subsumed by" pointers + dated amendments. Subsumption keeps
  the target Accepted -- these are live adapters, not dead decisions.
- ADR-857/894 carry dated status caveats: they read Proposed while the
  capability system shipped and epic #857 is closed. Ratification is a
  maintainer act and is deliberately left open.
- Link ADR-0005/0007/0012/3524 -> ADR-0174 and ADR-0010 -> ADR-0009; record
  the reciprocal Supersedes on ADR-0009.
- ADR-218 declared itself "ADR-0175" -- an unfinished rename.
- The 0011 PRD moves from the non-canonical "Draft" to "Legacy".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test: capture stderr via spawnSync; record ADR-0010 draft supersession

Two fixes surfaced by the first gsd-test run and by regenerating the index:

- tests/adr-index-gate.test.cjs used execFileSync, which only surfaces stderr
  through the thrown error on non-zero exit. The `--write` path exits 0 while
  reporting outstanding violations on stderr, so the helper always saw ''.
  spawnSync captures both streams on both outcomes.
- The hand-maintained index recorded 0010-skill-surface-budget-module.md as
  "earlier draft superseded by ADR-0011" while the file itself still said
  Proposed. Deriving the index from the files would have dropped that
  assertion and resurrected a superseded draft as a live decision, so it is
  recorded at its source, with the reciprocal Supersedes on ADR-0011.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix: drop the dead sdk/ model-catalog candidate retired by ADR-0174

src/model-catalog.cts resolved model-catalog.json through three candidates, the
second being sdk/shared/model-catalog.json three levels up. That was the legacy
source-repo fallback kept by the #3288 fix ("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 (11918dcc3), so the candidate can no longer resolve in any layout: a source
repo has no sdk/, and an install layout points it at ~/.claude/sdk/shared/, which
the installer never writes -- the original #3288 bug. It was dead weight implying
a package boundary this repo no longer has.

No test depends on it: the #3288 regression tests in tests/install.test.cjs write
their own synthetic old-path fixture and assert it throws.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: ratify nine shipped ADRs; record why ten others stay Proposed

The corpus carried 19 Proposed ADRs, most describing architecture that had
already shipped. A Proposed label on live architecture tells contributors and
agents the decision is an unbuilt idea -- the capability system and EoS were
both being misread that way.

Audited all 19 against the shipped tree and GitHub. Each candidate flip then had
to survive two independent reviewers instructed to refute it.

Ratified Proposed -> Accepted, each with a dated Ratification section carrying
the verified evidence (file:line, symbols, tests, issue state):

  857  capability system      894  declaration format   1244 capability ecosystem
  1577 injection boundary     1610 size-budget ratchet  1990 existing-code onboarding
  15   cross-AI convergence   22   plan-drift guard     0011 default reviewers

Held ten, each now carrying a "Why this is still Proposed" section naming the
blocker and its unblock condition, so the audit is not repeated:

  2264 its own headline acceptance criterion is unmet in the tree
  230  live branch protection contradicts the decided spec (1 approval, not 2)
  660  the namesake release/<version> re-cut is manual, not automated
  959  issue #2346 is approved and plans its graduation as its own ADR
  1213 the shipped writer's return shape differs from the decided interface
  443  the orchestrator override path has no live caller
  1143 / 1606 each states its own bar for acceptance; neither is met
  612 / 1671 legitimately open

Shipped code proved necessary but not sufficient: eight ADRs had every named
module, symbol, and test present with their epics closed, and still failed the
bar. That lesson is written into README.md's ratification procedure.

Also corrected ADR-857's "Supersedes (generalizes)" to "Subsumes": taken
literally it would have marked two live seams dead -- ADR-0011 (surface.cts:348)
and ADR-58 (runtime-artifact-install-plan.cts:82). Both keep Accepted status and
gain Subsumed-by pointers.

Index: Active 39->48, Proposed 19->10, Superseded/Legacy 7. 65 total.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix: harden gen-adr-index against hostile titles and non-ADR filenames (#2356)

Three findings from the pre-PR orthogonal security review, all confirmed:

- An ADR title containing the literal ADR-INDEX:END marker was emitted verbatim
  into its table cell, relocating the splice boundary so the NEXT --write
  spliced against the wrong marker and truncated README.md. Titles now render
  through cellText(), which escapes pipes and angle brackets -- making an HTML
  comment (and any other HTML) unformable from ADR-authored text.
- A docs/adr/*.md without a numeric prefix crashed on match(...)[1] of null.
  Such a file is also invisible to the index -- the very failure this gate
  exists to prevent -- so it is now reported as a naming-convention violation
  naming the file and the fix.
- Tests leaked their mkdtemp dirs. They now use helpers.createTempDir/cleanup
  via t.after(); helpers.cleanup carries the Windows-EBUSY retry budget that a
  raw fs.rmSync lacks (caught by local/no-raw-rmsync-in-tests).

Adds five regression tests: marker hijack, HTML injection, pipe cell-break,
non-conforming filename, and splice stability across repeated writes.

Refs #2356

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix: close two gate false-passes; read ## Supersedes sections (#2356)

Second round of confirmed findings from the pre-PR orthogonal code review. Both
false-passes matter more than a false-fail: a gate that silently misses a
violation is worse than no gate, because it is trusted.

- A relation field mixing a link with a bare id silently dropped the bare claim:
  the check tested `rel.links.length` (does this field have ANY link?) instead
  of whether THAT id was linked. `Supersedes: [ADR-0001](...), ADR-0011` passed
  clean -- accepting exactly the ambiguous bare reference the rule forbids. Now
  each bare id is checked against the ids actually linked in the same field, so
  a repeat in trailing prose stays quiet while an unlinked claim is flagged.
- The ratification guard (`statusToken !== 'Accepted'`) skipped BOTH relation
  directions, which killed the IN check entirely: `supersedes.in` is only ever
  populated on an ADR whose status IS `Superseded`, so a dangling `Superseded by
  X` where X never claims it always passed. The guard now applies to OUT only --
  a prospective claim must not obligate its target, but an ADR's statement about
  ITSELF is always owed a reciprocal.
- Fixing that surfaced a parser gap: ADR-0174 declares its supersessions in a
  `## Supersedes` table SECTION, not a header field, and headerBlock() stops at
  the first `##`. The repo's best-documented supersession was invisible. Section
  form is now parsed for both relations.
- Replaced a vacuous test: the em-dash negation case passed whether or not
  NEGATED_RELATION_RE matched (a mutation to /$^/ survived). It now carries a
  link that would create a failing asymmetric relation if negation did not fire.

Also removes docs/adr/9401-test-target.md -- a synthetic fixture a reviewer
created in the worktree while reproducing a finding, swept in by `git add -A`.

Adds regression tests for each: mixed link+bare, linked-and-repeated-in-prose,
dangling superseded-by from a non-Accepted ADR, and the ADR-0174 section shape.

Refs #2356

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix: escape backslashes before pipes in the ADR index cell renderer (#2356)

CodeQL js/incomplete-sanitization (high) on scripts/gen-adr-index.cjs: cellText()
escaped `|` -> `\|` without first escaping the backslash. Markdown's escape
character is the backslash, so the input `\|` became `\\|`, which renders as a
literal backslash followed by an UNESCAPED pipe -- re-opening the cell break the
pipe escape exists to prevent. Order is load-bearing: escape the escape
character first, then everything that emits one.

Same class as the index-marker hijack fixed earlier: ADR-authored text breaking
out of the cell it is rendered into.

Adds a regression test asserting a `\|`-bearing title leaves exactly the row's
own 5 unescaped delimiters and cannot forge a Status cell. Uses split(/\r?\n/)
per local/no-crlf-fragile-split -- a literal "\n" split is CRLF-fragile on the
Windows CI leg.

Refs #2356

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 10:51:58 -04:00

19 KiB
Raw Blame History

ADR-1244 — Capability Ecosystem: third-party authoring, versioned manifests, and URL import/upgrade/remove

  • 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. 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/<id>/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.

Three structural facts make third-party capabilities impossible today:

  1. The registry is a build-time artifact. scripts/gen-capability-registry.cjs reads capabilities/*/capability.json at build time and emits gsd-core/bin/lib/capability-registry.cjs, which is committed and shipped read-only. Every runtime consumer (config-loader.cjs, surface.cjs, capability-state.cjs, command dispatch in gsd-tools.cjs) require()s that generated file. Nothing reads capability.json at runtime. A capability that is not in the shipped package literally cannot be seen by config federation, surface, state resolution, or dispatch. There is no build step on a user's machine.

  2. Capabilities are unversioned. capability.json carries no version. Version lives only at the registry schema level (SCHEMA_VERSION = '1') and on the package manifests (package.json, .claude-plugin/plugin.json, gemini-extension.json, stamped by scripts/sync-manifest-versions.cjs). "Is there a newer version of this capability?" and "does this capability work with my GSD version?" are both undefined.

  3. There is no per-capability install/upgrade/remove. The only uninstall surface is whole-product (bin/install.js --uninstall), which deletes everything matching the gsd-* prefix with no record of what an install wrote. Upgrade of one capability, and clean removal of one capability, are impossible.

The maintainer has decided the 1.6.0 scope: full ecosystem (the live loader ships in 1.6.0, not just docs) and full first-party parity for third-party capabilities (they may ship the same executable surfaces GSD ships — hooks, MCP servers, command modules), mediated by a trust/integrity/consent gate. This raises the security stakes and makes the trust model load-bearing.

The crux for every decision below: close the build-time/runtime gap with a runtime overlay, and make every executable surface pass through one consent + integrity seam.


Decisions

D1 — Versioned capability manifest

capability.json gains:

  • version — semver, required. The registry rejects a capability without one; a parity test fails the build if any native manifest lacks a version.
  • engines.gsd — a semver range expressing host compatibility (e.g. ">=1.6.0 <3.0.0"). Modelled on VS Code's engines.vscode. A hard gate at install and at load.
  • compatVersions (optional) — a capability-version → min-gsd-version table for graceful downgrade. Modelled on Obsidian's versions.json. Only meaningful for sources that enumerate versions (git tags, registry, npm).
  • integrity (optional) — sha512-<base64> of the capability bundle, populated by a registry or recorded at install.
  • provenance (optional) — { sourceRepo, commit }; SHOULD be emitted in CI for first-party and curated capabilities.

The build-time validator in gen-capability-registry.cjs is extended to enforce these fields. Native capabilities are stamped at release (D6). Rationale: versioning is the data substrate every other decision depends on — upgrade, compatibility, integrity, and the matrix all key off it.

D2 — Runtime Capability Registry overlay

Promote the registry from a frozen data file to a module with an interface:

loadRegistry({ includeInstalled }) → composed registry

It composes first-party (shipped, frozen) ∪ installed overlay — third-party manifests read at runtime from a per-scope install root (global: ~/.gsd/capabilities/<id>/; project: .gsd/capabilities/<id>/). The conformance validator (today build-time-only) is extracted to a runtime-callable validateCapability() / validateCrossCapability() and run at install time over the merged set, not just the new manifest.

Invariants enforced at install (over first-party ∪ ledger ∪ new):

  • First-party always wins. An overlay whose id collides with a first-party id, or that claims a skill/agent stem already owned, is rejected.
  • Cross-capability invariants from ADR-894 (owner-uniqueness, config-key exclusivity, artifact-production-uniqueness per point, requires acyclic + tier-monotone) re-checked over the merged set.

Load-time re-gate (default-resilient): the host can change under an installed overlay (a GSD upgrade renames a loop point, or engines.gsd no longer matches). At load, an invalid or incompatible overlay is skipped with a warning and flagged in the ledger as needing update — it never crashes the loop. This mirrors the existing defensive skip in capability-state.cjs.

Rationale: this is the load-bearing unlock. Without a runtime overlay seam, the loader, ledger, and dispatch have nowhere to land. Deletion test: remove the overlay and the third-party-install complexity reappears in every consumer.

D3 — Capability source resolver (the URL importer)

One seam, resolveCapabilitySource(spec), with one adapter per source kind:

Spec form Adapter
<name>@<registry> registry
https://…/repo.git#<tag> / #sha:<40-hex> git
npm:@org/pkg@<range> npm
https://…/cap-x.y.z.tgz tarball
./local/path local

Every adapter follows the same pipeline: fetch → verify integrity/SHA → check engines.gsd → return a staged, validated bundle. Git/npm sources shell out through the existing shell-command-projection seam with bounded timeouts; tarball/registry fetch uses Node's https + crypto. The trust gate (D5) lives at this single seam. Rationale: multiple real adapters = a real seam (not hypothetical); adding a source kind = adding an adapter, not editing the loader.

D4 — Capability ledger

A per-runtime install manifest, e.g. ~/.claude/.gsd-capabilities.json, recording per installed capability:

{
  "<id>": {
    "version": "1.2.0",
    "source": "https://github.com/org/cap.git#sha:…",
    "integrity": "sha512-…",
    "files": ["skills/…", "agents/…"],          // owned files written
    "sharedEdits": [{ "file": "settings.json", "marker": "<id>" }]
  }
}

The ledger is the commit point for installs/upgrades (atomic write, like surface.cjs writeSurface) and the basis for precise removal. It records not only owned files but fragments written into shared files (settings.json hooks, mcpServers) so removal can strip exactly those entries without deleting shared files. A reconciliation sweep on next run resolves crash orphans (files not in the ledger; ledger entries with missing files). Rationale: "remove by gsd-* prefix" has whole-product blast radius and no record of ownership; the ledger gives selective, reversible, crash-safe install.

D5 — Trust model: artifact parity is full, trust posture is tiered

Third-party capabilities may ship the same artifacts first-party ships (full parity, per the maintainer's scope), but trust is not symmetric:

  • First-party is implicitly trusted — it is the shipped package.
  • Third-party requires explicit, informed, revocable consent + SHA-pinned integrity.

Hard rules (MUST):

  1. Install never executes capability code. Staging is copy-only; no postinstall-equivalent. (npm --ignore-scripts lesson.)
  2. Executable surfaces are disclosed and consented at install. hooks, mcpServers, and command modules activate on the next tool call — there is no "first use" gate for a hook — so consent must be at install, naming every executable surface. The disclosure includes each MCP server's env and cwd (#1459), because an environment variable (e.g. NODE_OPTIONS=--require evil.js) can change what a command does without touching the command or argv; the disclosure signature folds env/cwd in as stable sorted JSON so any add/change forces re-consent. Declining aborts cleanly.
  3. Integrity is verified before extraction when an integrity/SHA is available; mismatch aborts. (npm registry-signature lesson.)
  4. Auto-update is OFF by default for third-party; enabling it still re-prompts when the executable set changes between versions. (VS Code stolen-PAT + silent-auto-update lesson.)
  5. Modules are require()'d only from the capability's own install root — parent-directory traversal in declared paths is rejected.
  6. gsd-* (and gsd-core-*, anthropic-*) ids/prefixes are reserved — third-party cannot impersonate first-party.
  7. strictKnownRegistries (managed/project config) can lock installs to an allowlist; [] means no external installs.
  8. The consent signal for a project-scope capability is a user-owned consent store, NOT the in-repo ledger (#1459). The store lives at ${GSD_HOME||homedir()}/.gsd/consent.json — outside any repository — keyed by (realpath(projectRoot), id) and bound to the bundle integrity + disclosure signature. Before activating a project-scope overlay (its declarative loop surfaces and its command dispatch) the loader requires a matching record on this machine; without it the capability is discovered-but-inactive. This retracts the prior limitation that a project-scope ledger living inside the repository was itself the consent — a forged/cloned project ledger could otherwise activate executable + declarative surfaces with no user decision. Global-scope installs (under the user's own home) need no per-project record. gsd capability trust list/revoke audit and revoke project consents.

Stated honestly: there is no sandbox. Node-level sandboxing is impractical and would defeat full parity. Consent + integrity + reversibility are the barrier. (Obsidian's honest acknowledgment.) Rationale: a one-time trust prompt does not make running arbitrary code safe; separating artifact parity from trust posture is what makes full parity defensible.

D6 — Upgrade and compatibility

  • Atomic stage-then-swap. Upgrade fully stages (fetch + verify + validate) before swapping; the ledger write is the commit point; a reconciliation sweep handles crash orphans. A mid-upgrade crash leaves either the old or the new version fully intact — never a half-state.
  • Two-layer compatibility. engines.gsd is a hard gate (block with a clear message at install and load); compatVersions provides graceful downgrade to the newest compatible version — but only for sources that enumerate versions (git tags, registry, npm). A bare tarball URL has one version and simply blocks.
  • Native version stamping. scripts/sync-manifest-versions.cjs (or a parallel capability sweep) stamps version into native capabilities/*/capability.json at release; the existing version-sync regression guard is extended to cover them.
  • "Update available?" is a per-source matrix (git: fetch tags/manifest; registry: catalog; npm: dist-tags; tarball: not auto-detectable → manual only) — documented, not silently partial.

D7 — Registry-driven dispatch (sequenced last, behind the gate)

Fulfil ADR-857 D7's deferred "registry over hardcoded switch": gsd-tools.cjs / command-routing-hub.cjs consult the (overlay-aware) registry's commands: [{ family, module, router }] and dispatch via dynamic require(module)[router](). This is where third-party code executes, so it is gated by the same consent (D5) and confined to the capability's install root. First-party in-tree modules (graphify, intel, audit) collapse onto the same seam (dogfooding). Sequenced last because it carries the highest risk.

D8 — Relationship to ADR-857 (amend, not reverse)

ADR-857 D7/D8 did not forbid third-party code — they deferred it pending (a) its own ADR and (b) a trust/validation gate. This ADR satisfies both. ADR-857 is updated to mark D7 and D8 "extended by ADR-1244." The only substantive change is moving third-party from "deferred" to "delivered, gated." The runtime overlay (D2) is consistent with 857's own direction ("each loop step authored as if it could become a Capability"; "registry over hardcoded switch").

D9 — The capability matrix

A generated-from-registry catalog at docs/reference/capability-matrix.md, mapping every capability to id, version, tier, extension points, hook kinds, and engines.gsd — kept honest by a drift guard (like docs/INVENTORY.md). It includes a documented section where third-party authors register their capability. Whether GSD operates/advertises a central community registry is left TBD/TBA (see the PRD); the documentation mechanic ships regardless of that decision.


Consequences

Positive

  • GSD becomes an actual platform: authors ship capabilities independently; users install/upgrade/remove them without forking or maintainer PRs.
  • Versioned manifests give native capabilities a real version surface and make compatibility explicit.
  • The overlay + ledger make install reversible and crash-safe; whole-product --uninstall is no longer the only removal path.
  • The trust gate is concentrated at one seam (D3/D5) — auditable, testable, and the single place the security posture is enforced.
  • D7 retires a long-standing hardcoded-switch debt and dogfoods first-party modules onto the same dispatch seam.

Negative / costs

  • New permanent attack surface. URL import + third-party code execution at full parity is the highest-maintenance, highest-risk part of GSD. The trust/integrity/consent model is a forever responsibility.
  • Runtime overlay couples the runtime validator to the ADR-894/1016 schema — a generative-parity assertion is required so build-time and runtime validators cannot drift.
  • Per-runtime ledger × 16 runtimes multiplies the install/remove test surface (cross-platform fault injection required).
  • Documentation breadth (COMMANDS / FEATURES / USER-GUIDE / CONFIGURATION / ARCHITECTURE / AGENTS + the generated matrix).

Risks & mitigations

  • Malicious capability via auto-update → auto-update OFF by default; re-consent on executable-set change; SHA pin.
  • Overlay drift / contract change under an installed capability → load-time re-gate, skip-with-warning, ledger flag.
  • Half-state install/upgrade → ledger-as-commit-point + reconciliation sweep.
  • Impersonation → reserved namespace; strictKnownRegistries allowlist.
  • Validator drift → shared validator module + generative-parity test.

Implementation phases (dependency-ordered)

  1. Versioned manifest (D1) + native stamping (D6) — the data substrate; the "documentation-release" piece.
  2. Runtime registry overlay (D2) — the structural unlock.
  3. Source resolver (D3) + ledger (D4) — additive, testable in isolation.
  4. Trust gate (D5) + upgrade/compat (D6).
  5. Registry-driven dispatch (D7) — last, behind the gate.
  6. Capability matrix (D9) + full diataxis documentation set.

Each phase ships as its own PR with a changeset and full gate compliance (per CONTRIBUTING: one concern per PR; docs-required for Added/Changed fragments).


Alternatives considered

  1. Stay build-time only (third parties fork or upstream-PR). Rejected — no ecosystem; every community capability becomes maintainer burden. This is the status quo 857 D7/D8 flagged.
  2. Declarative-only third-party (no hooks/MCP/code). Safer (matches 857 D7), but the maintainer chose full parity so authors ship the same power GSD ships — accepting the heavier trust model rather than a capped one.
  3. Centralized-registry-only (no URL import). Rejected for launch — gates every capability behind maintainer review (the Obsidian one-PR-per-version pitfall). URL/git import keeps distribution decentralized; a registry can layer on top later.
  4. Regenerate the committed registry on the user's machine at install. Rejected — requires the full build toolchain on every machine and mutates a shipped file; the overlay achieves the same without a build step.