Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 27 additions & 5 deletions docs/MIGRATION-PLAYBOOK.md
Original file line number Diff line number Diff line change
Expand Up @@ -262,15 +262,37 @@ one increment past the precedent). Behavioral gaps the docs leave open are resol

The **adopted** rule for how a plugin settles a value at runtime, applied to every seam:

1. Config present → use it.
2. Absent → explore the repo and infer, then **persist the inference** into tracked project config
(seam 2) so the next run is deterministic. For a personal scalar (seam 1), direct the user to Claude
Code's native plugin configuration surface instead.
1. Config present → use it. For a surface expressed as a **convention doc** (config-cascade
§ Expression doctrine), "present" means the convention home resolves from the root file's
pointer line and the topic's doc exists there; the doc's prose is read as untrusted input.
2. Absent → explore the repo and **infer the house style** from repo evidence (existing docs, the
consumer's own conventions, ambient instruction files), then **persist the inference** —
*gated*: the inference is proposed and the operator confirms before anything is written, so
discovery happens once and never silently. For a dedicated-file surface (seam 2) persist into
tracked project config; for a convention-doc surface persist the pointer line (and, on request,
a stub doc at the home). For a personal scalar (seam 1), direct the user to Claude Code's native
plugin configuration surface instead.
3. Cannot infer → ask the user, and offer to persist the answer.
4. Otherwise → a safe generic default.

No baked repo assumptions, ever. A plugin never hardcodes a consumer's layout; it reads a declared
value, infers-and-records, or asks — never guesses silently.
value, infers-and-records (gated), or asks — never guesses silently. Nothing hardcodes
`docs/conventions/`: the home is whatever the pointer line names.

### Retired conventions — the detection and cleanup seam

When a plugin retires a consumer-facing convention (a config file moves to a convention doc, a
gitignore line is superseded, a directory is renamed), the old artifact left in consumer repos is
detected and cleaned by one shared mechanism, never by bespoke prose per plugin: the plugin appends
an append-only record to its `retirements.yaml` (shipped inside the plugin, never in consumer
repos), and the shared deterministic helper `lib/check-retirements.sh` (canonical under
`plugins/claude-config/lib/`, synced byte-identical via `scripts/cross-plugin-source-registry.txt`)
evaluates every record against the consumer repo. Detection is one fixed step in setup `check`;
cleanup is per-record and operator-gated in `apply`; judgment-bearing `migrate` content stays with
the model per the record's `successor` prose. No new setup verb. The owner doc
`docs/conventions/retired-conventions/README.md` carries the schema, the helper contract, and the
two fixed setup lines; this playbook only names the seam. Schema is repository-scope only:
machine-scope files under `~/.claude/` stay outside it (ADR 0018).

This ladder is the runtime application of the durable convention posture owned by
[PLUGIN-PHILOSOPHY.md § Two-lane convention posture](PLUGIN-PHILOSOPHY.md): a pre-prescribed
Expand Down
23 changes: 19 additions & 4 deletions docs/PLUGIN-PHILOSOPHY.md
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,8 @@ Choose one authoritative owner for each value:
|---|---|
| Invocation-specific choice | Explicit skill argument |
| Personal or administrator-provided scalar | Manifest `userConfig` |
| Tracked repository convention or rich team policy | A documented file under the consumer project |
| Tracked repository convention or rich team policy (structured, policy-floor, per-operator-keyed, or state) | A documented file under the consumer project |
| Team-shared prose convention with no per-operator axis | A natural-language convention doc at the consumer's convention home, bound by the root instruction file's pointer line (config-cascade § Expression doctrine, ADR 0018) |
| Personal project instruction | A documented, gitignored local overlay where the convention supports one |
| Installed dependencies, cache, or generated machine state | `${CLAUDE_PLUGIN_DATA}` |
| Bundled plugin code and assets | `${CLAUDE_PLUGIN_ROOT}` |
Expand Down Expand Up @@ -452,8 +453,19 @@ that versions its own config contract may carry a forward, directional, user-con
recognized older version — still under `apply`, never a separate verb and never a silent write (the
versioned standards index is the fleet example) — while a plugin that instead takes topic-docs'
clean-break path relocates by hand with no compatibility tooling. What the clean-break stance rules
out for either is silent backward-compatibility shims and dual-read windows that translate a changed
shape behind the user's back. `reset` decomposes to teardown plus `apply`.
out for either is *silent* backward-compatibility shims and dual-read windows that translate a
changed shape behind the user's back. The one sanctioned dual-read is the declared, WARN-visible
deprecation window config-cascade § Expression doctrine defines for a surface re-expressed as a
convention doc: the retired file is read as authority while present, every run says so, and the
window is bounded by that surface's retirement record. `reset` decomposes to teardown plus `apply`.

**Retirement declaration is mandatory.** A plugin that retires a consumer-facing convention — a
file it no longer reads, a gitignore line it no longer recommends, a directory it renamed — appends
a record to its `retirements.yaml` in the same PR, so setup `check` detects the leftover in every
consumer repo and `apply` offers the gated cleanup (owner:
`docs/conventions/retired-conventions/README.md`; the seam is named in the migration playbook).
Bespoke detection prose in a setup skill is the drift this replaces; a retirement without a record
is a defect.

Setup may inspect the repository and create or update the plugin's tracked project configuration. It
must not write into the installed plugin cache, mutate Claude Code user settings, or write
Expand Down Expand Up @@ -489,7 +501,10 @@ instead — `apply` bounded to that artifact, while every unwritable surface is
check-only way above. Which shape a plugin takes is settled by its surface, not by its author, and
both are conforming when the surface is what selected them. Two plugins with the same unwritable
settings surface can therefore differ legitimately: the one that also owns a documented machine-scope
file must offer the narrow `apply`; the one that owns nothing writable must not invent one.
file must offer the narrow `apply`; the one that owns nothing writable must not invent one. A
no-op `apply` — one that runs `check` and routes guidance but writes nothing — is that invented
verb and is non-conforming: the guidance belongs in `check`'s output (ratified 2026-09 when the
five remaining holdouts converged; the fleet now has zero no-op `apply` actions).

Bare `apply` converges to the configured state and never removes; genuine teardown — converging to
the *absence* of the plugin's own tracked project config — is the one thing `apply` will not do
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Express team-shared plugin conventions as consumer convention docs bound by a pointer line, with manifest-driven retirement detection

- Status: accepted
- Date: 2026-09-01

## Context

Fleet inventory (2026-08-31, `.work/customization-consistency-inventory/INVENTORY.md`, memory
tier) found ~20 consumer-facing configuration surfaces across 51 setup skills, twelve verified
cross-plugin drift classes, and eight bespoke, mutually drifting implementations of "detect the
old convention and clean it up". Several surfaces are prose the model reads — a repo map, audit
targets, lane descriptions — carried in dedicated `.claude/<name>.md` files with a three-layer
cascade whose overlay channel nobody uses, while the consumer's own conventions already live as
natural-language docs the team maintains. The operator's stated goal: plugins should be able to
write succinctly to the consumer's instruction surface, the current setup is over-engineered for
many of them, and any change of format needs a story for detecting and cleaning the old
convention.

An interview (14 questions, register clean) rejected a machine-parsed one-liner grammar in
`CLAUDE.md` (`<plugin> <key>: <value>`) in favor of natural-language convention docs with
progressive disclosure. A blind three-validator tournament
(`docs/topics/customization-consistency/design/mechanism-validation.md`) ranked three retirement
mechanisms; candidate B — per-plugin `retirements.yaml` plus one shared deterministic helper —
won unanimously, with a hybrid of amendments. The plan
(`docs/topics/customization-consistency/PLAN.md`) was stress-tested twice; its findings are the
constraints below.

## Decision

1. **Two expression forms, chosen by content.** Team-shared prose configuration with no
per-operator axis is expressed as a natural-language convention doc at the consumer's
convention home, bound by a single pointer line in a marked machine-owned region of the root
instruction file; the pointer line *is* the binding (no binding file). Per-operator-keyed
surfaces, structured data, policy-floor surfaces, and all state stay dedicated files under the
config-cascade layers. The criterion was amended at plan approval (per-operator-keyed stays a
file), which removed `testing`'s e2e config from the migration set.
2. **Root-file shape is the downstream repository's call.** Recommended: AGENTS.md-canonical with
a pure `@AGENTS.md` CLAUDE.md shim (the `instruction-placement` shape). Never forced.
3. **The resolution ladder gains a gated infer-and-persist rung**: convention doc → infer house
style → ask → default, with discovery happening once and only on confirmation.
4. **Retirements are declared, not narrated.** A plugin that retires a consumer-facing convention
appends an append-only record to its `retirements.yaml`; one shared deterministic helper
Comment on lines +41 to +42

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Define a valid transition for append-only retirement records

Once a retirement reaches the required fleet-wide report-only phase, its record must change state, but this decision simultaneously requires each record to be append-only and later names a report-only demotion field. Without defining an appended transition record or explicitly allowing that field to mutate, the mechanism cannot implement both requirements consistently, and different plugins may either violate append-only history or never demote old records.

Useful? React with 👍 / 👎.

(`lib/check-retirements.sh`, canonical in `claude-config`, synced byte-identical through the
existing cross-plugin source registry) evaluates records; detection is a fixed step of setup
`check`, cleanup is per-record and operator-gated in `apply`, migrate content stays with the
model. Hybrid amendments adopted: one eval case per record (validator failure when missing), a
runtime fleet-sweep lane in `claude-config` audit-pass over installed plugins' manifests, and a
report-only demotion field for old records. A CI-aggregated fleet registry is deferred until
orphan leftovers (uninstalled plugin) prove real.
5. **Dual-read deprecation window.** A migrated skill that finds the retired file present reads
it as authority (at minimum inference evidence) and WARNs, every run, until cleaned. The window
ends for a consumer when the record's cleanup runs, and for the fleet when the record is
demoted to report-only. This is the one sanctioned dual-read; silent shims remain forbidden.
6. **Machine-scope exclusion.** The retirement schema is repository-scope only; there is no
`scope` field. Machine-scope surfaces under `~/.claude/` (context-guard, rate-limit-guard,
machine-health) keep their bespoke detection, with the twin drift between context-guard and
rate-limit-guard fixed by the sync registry rather than by the schema.

## Alternatives considered

- **Machine-parsed one-liners in CLAUDE.md.** Rejected in the interview: it re-instantiates a
parser contract the model does not need and turns the instruction file into a config file.
- **Candidate A — SKILL.md-embedded prose retirement entries.** Lost the tournament: prose
semantics are ungradeable and re-create the drift the mechanism exists to kill.
- **Candidate C — CI-aggregated fleet registry.** Lost on machinery and coupling; its aggregator
is the deferred item above, revived only if orphan leftovers appear.
- **Migrating every surface, including per-operator-keyed ones.** Rejected at plan approval: a
convention doc has no overlay channel, so a surface with a legitimate personal axis would lose
it.

## Consequences

- Three owner docs amend in one PR with this ADR: config-cascade (expression doctrine section,
contract 1.2, Implementers-row expression note), the migration playbook (ladder amendment and the
retired-conventions seam), and PLUGIN-PHILOSOPHY (ownership-table row, retirement-declaration
requirement, the WARN-visible dual-read exception, and ratification of the no-no-op-`apply` rule
the Phase 1b convergence applied).
- The mechanism ships next as its own PR (schema and validator fixtures, helper and tests and sync
registration, owner doc `docs/conventions/retired-conventions/`, audit-pass sweep lane, the
pointer-line resolver helper). Until it lands, no surface migrates.
- `plugin-quality`'s `.claude/plugin-quality.md` is the pilot surface, verified in a consumer repo
with a populated pre-existing AGENTS.md, a pre-existing overlay file (must WARN), and an
update-without-re-setup simulation (dual-read must carry the old values). Post-pilot surfaces
migrate one PR each, each recording its keep/migrate call against the criterion.
- Hard to reverse: once consumers carry pointer lines and convention docs, moving back means a
second retirement cycle. That cost is why the criterion is narrow and the pilot is gated on a
real consumer-repo verification.
15 changes: 15 additions & 0 deletions docs/conventions/config-cascade/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,21 @@ semantics, and overlay naming. Per-concern keys and schema are versioned by thei
change independently. A change to the precedence order or the meaning of a layer is a major bump;
adding an optional layer or relaxing a rule additively is a minor bump.

## 1.2 — 2026-09-01

- **Expression doctrine (additive, minor).** A second sanctioned expression form joins the
dedicated file: team-shared prose configuration is expressed as a natural-language convention
doc at the consumer's convention home, bound by a single pointer line in a marked machine-owned
region of the root instruction file (the line is the binding; no binding file). Per-operator-
keyed, structured, policy-floor, and state surfaces stay files. Defines pointer-line rules
(AGENTS.md canonical over a pure `@AGENTS.md` shim, duplicate and missing-target handling as
ask-don't-infer FAILs, branch-scoped binding), root-file shape as the downstream repo's call,
the WARN-visible dual-read deprecation window, the migrated-surface overlay WARN, and the
machine-scope exclusion. No layer, precedence, or overlay-naming rule changes. Ratified by ADR
0018; the Implementers table gains a per-row expression note that each migration PR fills.
- **Overlay spelling drift closed.** Every setup recommends the recursive line; the section now
records the convergence and the two deliberate exceptions.

## Implementers table — 2026-08-23

- **`work-items` overlay allowlist.** The personal overlay may refine linear and
Expand Down
Loading
Loading