ADR 0148: Strand detection covers every gate stage
Amendments.
- Vocabulary: current pointers use gate
capability/ customcheck→ known/customjob; the decision and reasoning are unchanged.- ADR 0262 — earlier stop: receipt-eligible runs now stop at the pre-group strand checkpoint, moving this check earlier for that class.
Status: accepted; extends ADR 0047, works with ADR 0094 and ADR 0116
Context
§ADR 0047 made done block when the fix stage strands changes on a committed-clean tracked file, and deliberately scoped the check to that one stage. The fix stage is the stage meant to mutate; CI's trailing git diff --exit-code stood as the guard for tracked mutations from non-fix stages.
Field use showed that residual gap has the exact failure mode ADR 0047 set out to remove. A project whose build stage (or a test, or a scope gate) regenerates a tracked artifact — a project generator rewriting a tracked manifest, a codegen step rewriting tracked schemas — dirties the tree outside the fix-stage snapshot window. The gate goes green, the receipt silently refuses (skipped_dirty), and the only signal is a hint riding on a green result — the "advisory on green" pattern ADR 0047 itself rejected as the thing agents ignore. An agent watching a long gate paid the full run, got an ambiguous green, blamed the strand on "the formatter", and paid a second full run after a diagnosis loop. CI's diff guard never enters that local loop.
discern's own repository carries the same shape: its build job runs the codegen task, which rewrites tracked schema, type, and reference files.
Decision
§done blocks when any stage group strands changes on a previously clean tracked file, and the diagnostic names the stage that did it.
- The snapshot window widens from one stage to all. The gate captures the tracked-dirty set once before any stage group runs and again after each green group (fix, build, check/test, scope gates). The stranded set keeps its spirit: paths dirty in the final snapshot but not at gate start — so a stage reworking the agent's own uncommitted edits (the inner loop) still never trips, and a path a later stage restores to its committed state does not count (the finished tree is what the receipt vouches for).
- Each strand is attributed to the first snapshot that shows it. The diagnostic says which stage produced each file — "the build stage", "a scope gate" — replacing the formatter-shaped guesswork the old green path invited. The failed-stage label is renamed
fix_drift→tree_driftto match the widened class; the diagnostic tool label istree-drift. - Everything else about ADR 0047 stands. Tracked changes only (an untracked file a stage emits is visible in
git statusand remains a legitimate pattern); a final gate check that runs only when the gate is otherwise green; block-don't-autocommit; no toggle; the cappedgit diffand commit-then-re-run rescue in the diagnostic; fail-open when a snapshot cannot be read.
Consequences
§- "Green but no receipt" from a gate-caused tracked mutation no longer exists. The failure moves from a quiet
skipped_dirtyhint after a full run to a red gate with the diff and the origin stage in hand — same worst-case two runs, but the first run explains itself. - The wire vocabulary changes:
failed_stage: "tree_drift"replaces"fix_drift". Schemas and types regenerate from the one vocabulary source; consumers switching on the closed set get a compile error until they handle the new label, which is the enrolment working as designed. - A gate job that legitimately regenerates tracked files now requires the regenerated output committed before
donepasses — true for this repository's own codegen build step. That is the discipline ADR 0094 already asks for at accept time, surfaced at the point the agent is looking. - Cost: one extra
git statusper green stage group (three to four per gate run), negligible beside the jobs themselves. - The remaining honest gap is unchanged from ADR 0047: a stage emitting a new untracked file still leaves the worktree dirty for the receipt without blocking the gate — visible in
git status, disclosed by the receipt hint (which now names the dirty paths), and outside CI's diff guard too.
Alternatives considered
§- Keep the fix-stage scope and improve the green-run hint. Rejected for the same reason ADR 0047 rejected advisory hints: agents key on
ok, and a hint on a green gate is what gets ignored — that was the observed failure. - Snapshot only at gate start and end (no attribution). Cheaper by two
git statuscalls, but the diagnostic could not name the origin stage, and a wrong guess at the origin ("the formatter did this") is precisely the diagnosis loop this change removes. - A separate failed-stage label per origin stage. Rejected: the class is one invariant (a green gate must leave the tracked tree as it found it); the origin belongs in the diagnostic, not the vocabulary.
- Auto-commit the stranded output. Rejected again per ADR 0047/0094: a gate is not a committer, and discern cannot know the commit boundary or message.