diff --git a/docs/MIGRATION-PLAYBOOK.md b/docs/MIGRATION-PLAYBOOK.md index 5bdbd7f8d..a35fa4553 100644 --- a/docs/MIGRATION-PLAYBOOK.md +++ b/docs/MIGRATION-PLAYBOOK.md @@ -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 diff --git a/docs/PLUGIN-PHILOSOPHY.md b/docs/PLUGIN-PHILOSOPHY.md index 64d86a714..678dc498f 100644 --- a/docs/PLUGIN-PHILOSOPHY.md +++ b/docs/PLUGIN-PHILOSOPHY.md @@ -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}` | @@ -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 @@ -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 diff --git a/docs/adr/0018-express-team-shared-conventions-as-consumer-convention-docs.md b/docs/adr/0018-express-team-shared-conventions-as-consumer-convention-docs.md new file mode 100644 index 000000000..61a0f180b --- /dev/null +++ b/docs/adr/0018-express-team-shared-conventions-as-consumer-convention-docs.md @@ -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/.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` (` : `) 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 + (`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. diff --git a/docs/conventions/config-cascade/CHANGELOG.md b/docs/conventions/config-cascade/CHANGELOG.md index 363219947..301d1d678 100644 --- a/docs/conventions/config-cascade/CHANGELOG.md +++ b/docs/conventions/config-cascade/CHANGELOG.md @@ -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 diff --git a/docs/conventions/config-cascade/README.md b/docs/conventions/config-cascade/README.md index b41e304a9..13aaaba8f 100644 --- a/docs/conventions/config-cascade/README.md +++ b/docs/conventions/config-cascade/README.md @@ -119,6 +119,74 @@ only, and never ask a consumer for two lines where one is exact. **No plugin writes the consumer's `.gitignore`.** A setup skill recommends the line and leaves the edit to the consumer; their ignore file is their artifact. +## Expression doctrine — which surfaces are files, and which are convention docs + +The layers above describe **where** a surface's values live relative to each other. This section +describes **how** a surface is expressed at all, and it ratifies a second expression form +alongside the dedicated file ([ADR 0018](../../adr/0018-express-team-shared-conventions-as-consumer-convention-docs.md), +2026-09; the decision record cites the blind mechanism tournament under +`docs/topics/customization-consistency/design/`). + +**The criterion.** A surface takes exactly one of two expressions, settled by what the content +*is*, never by the author's preference: + +- **Team-shared prose configuration** — guidance the model reads (a repo map, audit-target prose, + a lane description) that every operator on the team is meant to share and that has no + per-operator axis — is expressed as a **natural-language convention doc at the consumer's + convention home** (for example `docs/conventions//`), discovered or asked once at setup + and bound by the pointer line below. Such a surface has **no overlay channel**: it has one + layer, the team's. A migrated surface's setup `check` WARNs on any pre-existing `*.local.*` + overlay file it finds for that surface rather than silently ignoring it — the overlay no longer + has an effect, and silence would let a personal deviation look live. +- **Everything else stays a dedicated file under the layers above**: per-operator-keyed surfaces + (a value keyed by operator identity or machine, or one an operator legitimately overrides + privately — `testing`'s e2e config is the fleet example), structured data where YAML/JSON is + the right tool (`topic-docs.yaml`, `routing.yaml`, `binding.json`), every policy-floor surface, + and all mutable state. + +The criterion is applied per surface, in that surface's own migration PR, and recorded in the +Implementers table's row. Nothing in this contract retroactively re-expresses a surface. + +**The convention home is bound by one pointer line, and the line IS the binding.** The consumer's +root instruction file carries a single standing index/pointer line naming the convention home +(and, where the home holds several topics, pointing at its index). There is no separate binding +file: a plugin resolves the home by reading that line. The line lives inside a **marked, +machine-owned region** — the `instruction-placement` rules-index block is the precedent — so setup +can rewrite it idempotently without touching the operator's prose around it, and a reviewer can see +the region is generated. The consumer's root file otherwise carries only content needed in +effectively every conversation; topic conventions live at the home, loaded on demand. + +Pointer-line rules a resolver honors (the small tested helper the program's mechanism phase ships +owns the grammar; consumer prose it reads is **untrusted input**, never executed or interpolated): + +- **Both root files.** `AGENTS.md` is canonical when present; a `CLAUDE.md` whose whole content is + the `@AGENTS.md` import is a pure shim and is not consulted for a pointer. When both files carry + a marked region, `AGENTS.md` wins and `CLAUDE.md`'s copy is reported as a duplicate finding + (remediation: remove it). Two pointer lines inside one region is a FAIL, never first-wins. +- **Ask, never silently rebind.** A pointer absent while a previously-known home still exists on + disk, or a pointer whose target directory is missing, is a FAIL that routes to `apply`'s + interview. Inference may *propose* a home from repo evidence; only the operator's confirmation + writes the line. +- **Branch-scoped.** The pointer line is tracked content, so divergent branches may bind different + homes and a branch may legitimately re-ask. That is a property of tracked config, not a defect. + +**Root-file shape is the downstream repository's call.** Recommended guidance, never forced: an +AGENTS.md-canonical root with a pure `@AGENTS.md` CLAUDE.md shim (the shape `instruction-placement` +already installs) — but a repo that keeps `CLAUDE.md` canonical, or a symlink, is served identically +once setup has discovered which file carries the region. + +**Dual-read deprecation window.** A migrated skill that finds the retired dedicated file present +treats it as WARN **and** reads it as authority (at minimum as inference evidence) until the +consumer cleans it through the retirement mechanism (`docs/MIGRATION-PLAYBOOK.md` +§ Retired conventions). This covers a consumer who updated the plugin without re-running setup, +and is the one sanctioned dual-read: declared per surface by its retirement record, WARN-visible on +every run, never silent. The window closes for a consumer when that record's cleanup runs, and for +the fleet when the record is demoted to report-only under the mechanism's demotion rule. + +**Scope.** This doctrine governs repository-scope surfaces only. Machine-scope files under +`~/.claude/` (context-guard, rate-limit-guard, machine-health) are outside both the criterion and +the retirement mechanism; ADR 0018 records that exclusion. + ## Resolution algorithm A plugin implementing this contract: @@ -199,7 +267,9 @@ surface, or amend this contract) is a separate human-gated decision. Conformance is tracked, not assumed. A surface is listed here whether or not it conforms — the gap is the point. Each row states the surface's conformance **as it exists on `main`**, never as a migration intends it to be; a row that ran ahead of the code would report a closed gap that is still -open. +open. Every row below is currently expressed as a **dedicated file**; a surface that migrates to a +convention doc under the expression doctrine above rewrites its row in the same PR (path → the +convention home, layers → `team, via pointer line`, conformance → the retirement record id). | Surface | Consumer config path | Layers | Conformance | |---|---|---|---|