ADR-0010: Consumer distribution needs a version gate before the first real consumer
- Status: Proposed (2026-06-19, TASK-462) — posture recorded, no implementation yet
- Deciders: Kourosh Ebrahimzadeh
- Context tags: modularity, adapters, distribution, blast-radius, strategic-audit-2026-06
Context
cos init / cos update wire a consumer project's .claude/hooks/* as live
symlinks that point straight into the installed core
(src/core/scripts/install-adapter.sh → ln -sf; src/cli/update.py:319
→ link.symlink_to). The Modularity Map in AGENTS.md
states the consequence plainly: src/core/hooks/*.sh propagates to ALL
consumer projects with Rebuild: none.
That means there is no version boundary between core and its consumers:
- A single bad edit to any of the 21 PreToolUse BLOCK hooks — or a syntax error
in
cos-env.sh, which all 89 hookssource(Rule 3) — reaches every consumer instantly, with no pin, no canary, no staged rollout. - The only version mechanism that exists today,
src/cli/core_version.py, is a passive WARN oncos update/cos doctor(update.py:455-461reports drift and re-stamps regardless). Its own module docstring admits the gap: "Consumers pin to core via live symlinks with no version signal (D6); a breaking hook/MCP change otherwise breaks them silently oncos update."
The strategic audit (2026-06-19) flagged this as the single sharpest coupling / failure-scenario risk for the moment consumers exist. It is not a problem today: there are zero external consumers and the operating reality is "breaking changes are free." Building a rollout system now would be premature (Rule 22) — but the decision should exist before it is needed, so it is not made under pressure during a live incident.
Decision
Record the seam now; do not build it yet. When the first real external consumer is onboarded, the distribution posture changes from editable symlinks to versioned, copied artifacts behind a blocking compatibility gate:
- Ship copies, not symlinks. Consumers receive core hooks/rules as copied
files (a wheel / pinned snapshot), so a core edit does not mutate a running
consumer mid-session. The live-symlink mode stays the default only for the
meta-repo dogfood and for local development, where instant propagation is the
point. This is a posture flip in
install-adapter.sh(symlink vs copy), not a new subsystem. - Promote
core_versionfrom WARN to BLOCK.cos doctor/cos updatefails closed when a consumer's stamped core version is incompatible with the installed core, with a remediation path — rather than re-stamping and warning. The stamp + comparison already exist; only the exit behavior changes. - Stage propagation. Core releases are tagged; consumers opt into an update
explicitly (
cos update) rather than inheriting everymaincommit through a live link.
The trigger to implement is binary: the first non-dogfood consumer that the maintainer does not personally control. Until then this ADR is the gate.
Consequences
- Positive: the highest-blast-radius failure mode (one bad hook → global
consumer outage) gets a deliberate boundary; the decision is made calmly, in
advance, with the cheap-fix path (
symlink → copy) already named. - Positive: keeps the meta-repo's fast inner loop (live symlinks) intact — the gate applies only to external distribution.
- Negative / cost: when implemented, copied artifacts mean consumers no
longer get core fixes instantly — they must run
cos update. That is the intended trade (safety over instant propagation), but it adds an update step and a compatibility matrix to maintain. - Deferred: no code lands from this ADR. A future task implements (1)–(3)
gated on the first-consumer trigger. Linked from the audit memory
strategic-audit-2026-06.
See also
- ADR-0006: External task-tracker seam — same "name the contract before building it" pattern.
src/cli/core_version.py— the existing (WARN-only) version stamp this ADR promotes to a gate.- AGENTS.md Modularity Map — the
Rebuild: nonepropagation row this ADR bounds.