ADR-0023.
manifest --sync — runward scaffolds the form, never the content
Date: 2026-07-16 Status: accepted Deciders: Thibault Souris (maintainer) Method: decision-loop — factual audit (the hand-maintained manifest as the adoption brake and rubber-stamp feeder), challenge against the operator-owns-the-decision invariant, durable position
Context
The conformance manifest is maintained entirely by hand: the operator (or their agent) writes every row of every gated deliverable's table. The audit named the two costs. Tedium feeds theater: the more painful the table plumbing, the more a tired operator stamps it — the exact failure mode ADR-0001's trigger (b) watches. Rule-set evolution lands on the operator: a renamed rule (ADR-0006) must be hand-edited in every manifest, and a rule newly mapped to a phase surfaces only as a red row at check time, one run at a time.
The plumbing is deterministic; the decisions are not. Today runward makes the operator do both.
Decision
A runward manifest command. Read-only by default: for each gated deliverable, the overview — rows accounted for, expected rules missing, renamed slugs with their migration, duplicates, unknown slugs. --sync writes the form, never the content:
- creates a missing
## Rule conformancesection (with its documentation blockquote) in a gated deliverable that lacks one; - appends one row per missing expected rule with empty status and empty evidence — and the gate refuses an empty status with a message that names the choice (
applied | deviated | n/a), so a scaffolded row can never pass as accounted for; - rewrites a renamed slug in place per the migration record (ADR-0006), preserving the row's status and evidence;
- reports and never deletes: duplicates, removed rules, unknown slugs stay the operator's edit.
The invariant, stated once: --sync never sets a status, never writes evidence, never deletes a row. The decision content stays the operator's and their agent's; runward moves the deterministic plumbing out of their way so the human effort lands on the pointer, not the table syntax. Respects --dry-run.
Alternatives discarded
- Pre-filling scaffolded rows with a default status (
n/a, orappliedwith a TODO). Builds the rubber stamp into the tool — a table full of plausible defaults is exactly what gets waved through. An empty status that the gate loudly refuses is the honest scaffold. - A virtual manifest (derive the table at check time, no file). The manifest is an operator-owned document with provenance and history (ADR-0001); hiding it in the tool makes the trace invisible and the diff unreviewable.
- Report-only linting (no scaffold). Keeps the exact tedium that feeds the stamping; the audit's point was to kill the plumbing, not to describe it.
Consequences
- Positive. A new mission or an updated rule set is one
manifest --syncaway from a complete, honest table skeleton; migrations stop being a hand-edit per manifest; the adoption brake and the theater feeder shrink together. - Negative, accepted. A write path into operator-owned documents — bounded by the form-only invariant and
--dry-run; the section-insertion logic must stay conservative (append, never reorder). - On other boundaries.
src/lib/manifest-sync.ts+src/commands/manifest.ts+ a CLI entry; the gate's empty-status message names the expected gesture; smoke and unit tests cover scaffold, migration, idempotence, and that a scaffolded row still fails--strict.
Reevaluation trigger (mandatory, dated)
Reopen if operators start treating a freshly synced (empty-status) table as progress in itself — the plumbing became the theater — or if --sync ever needs to touch a row's content to stay useful: that would cross the invariant and must be refused or re-decided here.
Trigger set on: 2026-07-16 · Watched via: how synced manifests look when missions cross the gate (empty rows filled, or lingering).