ADR 0043: The provider registry is the enforced single source for every agent surface
Amendments.
- Vocabulary: current pointers use
done(formerlyfinish); the decision and reasoning are unchanged.- ADR 0042 — consolidated here: per-agent skills materialization folds into this record — a
Provider.skillsDirfield directs the effective skill set into every configured agent's directory (claude_code → .claude/skills, andcodex/gemini →the shared.agents/skills, deduped onto one target), gated onfeatures.skills— and this record makes the provider registry the enforced single source for it, guarded by the parity test below.- ADR 0166 — identity extension: a broader identity catalogue now derives
AGENT_NAMESand owns advisory logbook markers;PROVIDERSremains the enforced single source for every native integration surface, and this decision's parity guarantees are unchanged.
Status: accepted; completes ADR 0031, extends the currency check of ADR 0034 to skills, and builds on and consolidates ADR 0042.
Context
§ADR 0031 made the typed provider registry (PROVIDERS in src/lib/providers.ts) the single source of truth for everything agent-specific, and a total Record<AgentName, Provider> so adding a name to AGENT_NAMES forces a complete entry — a compile error otherwise.
That coupling protected the runtime engine consumers (the guidance compiler, skills materialization, MCP wiring, hook-stripping). But a tier of satellite consumers re-encoded a hardcoded subset of the same agent facts, with no compile-time tie to the registry, and silently fell out of parity as the registry grew:
- The seed
.gitignorefragment listed the agent files/dirs as static literals. DEFAULTS.scopesNeutralhardcoded.claude/and omitted.agents/— added by ADR 0042 but never reflected here.- The audit's
anyAgentFile()hardcoded the three filenames;plan_viewgrouped.claude/only;skills.tskept aCLAUDE_SKILLS_RELduplicate of the registry value; the default agent set was encoded three times. - Skills had no currency check. Guidance files were guarded by
checkGuidanceCurrent(ADR 0034:statusadvisory,doneblocks on stale), but materialized skills dirs had no equivalent — a hand-edited, removed, or upgrade-stale skill was invisible to the gate. GEMINI.mdduplicated the full body though Gemini CLI supports the same@pathimport as Claude Code.
The pattern was clear: the registry held the data, the satellites ignored it, and each new agent widened the drift — exactly the trajectory the project's single-source-of-truth policy forbids.
Before deciding, the three vendors' actual conventions were verified against their official docs (not assumed), which sorts the divergences into expected (a real mechanism difference) and unexpected (an unforced omission):
| Surface | Claude Code | Codex | Gemini |
|---|---|---|---|
Instruction-file @import |
✅ @path |
❌ none (concat) | ✅ @path (.md-only) |
Reads .agents/skills/ |
❌ (#31005 open) | ✅ native | ✅ alias (precedence) |
| MCP config | .mcp.json |
~/.codex/config.toml |
.gemini/settings.json |
| Worktree create/remove hook | yes | ❌ none | ❌ none |
Decision
§Every cross-cutting consumer derives agent paths from the registry, and a registry-driven parity test fails the build when a new agent isn't yet handled everywhere.
- Registry-derived aggregators (
allGuidanceFilePaths,allSkillsDirs,neutralAgentScopePaths,agentArtifactPaths) are the one place satellites read agent paths from.scopesNeutral,anyAgentFile, and theplan_viewgrouping now call them. The duplicates collapse:DEFAULT_AGENTS+resolveConfiguredAgentsmove to the shared schema module (one definition for the compiler, dispatcher, skills check, init seed, and migration fallback);CLAUDE_SKILLS_RELis dropped.
- A parity test (
tests/agent_parity_test.ts) is the forcing function for the surfaces that can't be compile-coupled — the static seed.gitignorefragment, the neutral-scope seed, and each hooks provider's seed settings template. For everyAGENT_NAMESentry it asserts the fragment ignores the guidance file and covers the skills dir, and the neutral scopes neutralize the generated dir; and for every provider that declares a hooks surface it asserts the seed settings template (templates/.claude/settings.json.tmpl) carries the registry'sworktreeEventKeys+sessionHookNeedle. A new agent — or a renamed hook event — red-lights the gate until each surface learns it. The gitignore "covered" semantics are a SINGLE exported definition (ignoreCovers) that both this test and the upgrade-time reconciler use, so the guard and the convergence can never disagree.
.gitignoreconvergence is registry-derived. ADR 0093 replaces the original additive helper with a canonical discern-owned block reconciler run on everydiscern upgrade: a future agent's artifacts get ignored on the next upgrade with no bespoke per-agent migration, and historical one-off# discern:sections are absorbed into one block. The frozen schema-9/10 gitignore migrations stay as historical records.
- Skills join guidance under one "generated artifacts are current" discipline.
checkSkillsCurrentis the stateless skills analog ofcheckGuidanceCurrent: re-resolve the effective set and diff against disk (bundled = byte-equal to source, authored = live symlink to[skills].dir), no stored hash. Same dispositions as ADR 0034 —statusadvisory;doneblocks onstaleonly;missing(a not-yet-materialized dir on a fresh checkout) andforeign(an unmanaged drop-in, never clobbered) do not block.
GEMINI.mdbecomes an@AGENTS.mdpointer, sharing oneatImportPointerwith Claude Code (the@pathsyntax is byte-identical and vendor-supported for both).AGENTS.mdstays the canonical full-body file precisely because Codex has no import directive — the one expected asymmetry, now the only one.
doctorsurfaces per-agent integration coverage, so the expected divergences (MCP/hooks are Claude-only because Codex/Gemini use different mechanisms) are reported explicitly rather than read as a silent gap.
The expected divergences are kept and made visible: MCP wiring and worktree hooks remain Claude-only (Codex/Gemini expose no equivalent discern can target), and Claude keeps its own .claude/skills/ (it doesn't read .agents/skills/, #31005 open). What changes is that nothing agent-specific is encoded outside the registry without a test tying it back.
Consequences
§- Divergence is a failing test, not a silent gap. Adding a fourth agent is a
PROVIDERScompile error (runtime) and a parity-test failure (static surfaces) until every satellite is taught — the architectural guarantee the SSOT policy wants. - Skills are guarded like guidance. A drifted materialized skill is caught at
doneand surfaced atstatus, closing the asymmetry ADR 0034 left open. - One agent file holds the body. With Gemini pointing at
AGENTS.md, the compiled guidance lives in exactly one file; the others import it and cannot drift. - Existing installs self-heal forward. The convergence step means a future agent needs no migration code to get its artifacts ignored.
Alternatives considered
§- Generate the
.gitignorefragment from the registry. Rejected: the fragment carries load-bearing prose (the.claude/*-plus-exceptions structure, the deliberately-tracked.mcp.jsonnote). A parity test over the authored fragment is lighter and equally regression-proof — adding an agent fails it just the same. - Store an expected hash for skills. Rejected for the same reason ADR 0034 rejected it for guidance: a second source of truth to keep in sync. Recompile (here, re-resolve) and compare is stateless and always correct.
- Block
doneon missing/foreign skills. Rejected: a gitignored artifact is legitimately absent on a fresh checkout (would red-light first-run CI), and a foreign drop-in must never be presented as discern's to fix. Onlystaleblocks. - Leave
GEMINI.mda full copy. Rejected once Gemini's@pathimport was verified — the duplication was unforced, and the pointer makes all three agents uniform but for Codex's genuine lack of an import directive.