Use when implementation in this repository reaches a pause and review begins, when running subagent or Codex review rounds, when fixing findings and moving to the next round, and when judging whether the loop has converged. Required reading for 「レビューして」「レビュー回して」「指摘を直して」「codex review」「この PR merge していい?」「まだ見るべきところある?」 and immediately after finishing the implementation of an audit finding or an issue. The subject is **review of changes you made**; do not use it for reading existing spec, docs, or implementation to judge whether they are sound (review without a change).
Recursos
2Instal·la
npx skillscat add seiya/atmofab/atmofab-review-loop Instal·la des del registre de SkillsCat.
The review convergence loop
What this skill holds is how to run a review: round structure, what to tell reviewers,
convergence criteria, mutation checking. If the change touches enforcement machinery (a gate,
validator, hook, or capability), start atmofab-enforcement-change as well — it owns the
domain-specific traps (dual-read pairs, failure attribution, verification commands) and they
are not duplicated here.
Track record: PR #51 converged after 17 subagent rounds plus 3 Codex passes, and 15 of the
defects had been introduced by the fixes themselves. What follows was derived backwards from
that breakdown.
This file carries the rules; references/ carries the episode each rule came from. A rule
here that does not obviously apply to your case is answered in its reference file, not by
guessing. L128 / L174 name entries of TODO.md by the line they sat on when the work
happened, and TODO:269 / TODO:414 name two more the same way (as does L118 in the sibling
skill). Those entries are no longer in TODO.md: issue #181 moved every finished entry to a
comment on the issue or pull request it belongs to, so the label is a name for an episode and
nothing resolves it in the file. Resolve a label at the merge base of the pull request that used it — git show <pr merge>^:TODO.md | sed -n '<n>p',
and find that merge with git log --oneline --merges --ancestry-path <the commit that did the work>..origin/main | tail -2
when the label carries no pull request number, which is the case for L118, L128 and L174. That is how these were checked: L118 is the duplicate `_split_top_level_commas`,L128 the `&`-continuation blindness,L174 the tree-sitter front-end swap,TODO:269 the `STALE_DEPENDENCY_IR_MARKER` exit-code work
and TODO:414 the development-documentation branch.L200, which PR #57's title names, is the `agent_role` work.
references/mutation-testing.md— round 0's episodes, and the "spinning in neutral" familyreferences/measurement-records.md— why a measured number rots once per roundreferences/round-conduct.md— commit granularity, PR staging, orphan waits, over-refusalreferences/codex-episodes.md— the launch mechanics and the stall / filter case logreferences/sonnet-delegation.md— the delegation experiment log and how to keep measuringreferences/class-descent-log.md— per-PR stopping-condition histories and the proxies' evidencereferences/signs-episodes.md— the case history behind each mid-loop sign
Review target and commit granularity
The target is git diff origin/main...HEAD (everything the branch has stacked on main).
Show only the most recent commit and you miss the defects the previous round's fix introduced —
in practice most findings sat inside the previous round's fix.
Confirm the working tree matches the commits before handing it over (git status --porcelain empty).
One commit per finding is the default: the mutation check works as-is with --range HEAD~1..HEAD, which fix answers which finding stays traceable — put the gist of the finding in the commit
message — and the next round can be told to look hardest at the files you just fixed. Folding
is allowed when splitting is unnatural, for an experimental check, or for something you expect to
revert immediately — say in one line why, and read git show --stat before writing the message, which must
describe everything in the commit (PR #67 folded eight fixes and described two).
This is not only a record rule, and it is not only about folded commits. A reviewer reads the
message to decide where to look, so a file the message omits is a file the round does not attack.
On issue #142 the first commit's message described the code and the prompt template and said
nothing about the four documents it also edited — and one of those documents carried a COPY of the
rule the template stated, in the unbounded form. Round 1 fixed the template; round 2 had to find
the same defect again in the document, as a leaf shortcut on the other transport. One omitted
line in a message cost a whole round. Read the stat, and name every file, especially the ones you
edited as an afterthought — those are exactly the copies.
The trigger that actually fires is ONE FILE EDITED FOR TWO FINDINGS, and the two rules above
read as if it is not. They are written for a FOLDED commit — one you chose to make wide — so
neither comes to mind while you are splitting properly. Issue #169 broke it twice in one loop
while following both: the fixes for two findings landed in one file, git add <that file> swept
the second finding's work into the first finding's commit, and the message described work that
was not there while the next commit claimed work already committed. Nothing detects it, because
both commits are individually coherent and the suite is green either way. The rule is
mechanical and belongs before git add, not before the message: when a round touched a file for
more than one finding, stage that file with git add -p or commit the findings in the order the
file was edited. And read the stat BEFORE writing the message rather than after — writing it
first and staging second is how a correct-looking message gets attached to the wrong diff.
When it has already happened and the commit is pushed or built on, do not rewrite history to
tidy it — say in the later commit which finding's work is in which commit, and why. A reader
looking for a witness needs the pointer more than the history needs to be neat; and give them a
SHA that resolves, since an amended commit leaves a dangling one that looks fine locally and
exists in no clone.
git commit --amend for a message-only fix requires an empty index (or --only); it
silently absorbs whatever is staged (PR #58: an amend swallowed a file replacement, leaving a
commit that claims work git log -S cannot find). If it is unpushed you can fix it by
squashing — and say in the message that you did.
If the plan staged the PRs, close each stage before starting the next — implement A →
review → fix → merge → then B. The review target is the whole stack, so splitting the history
afterwards misaligns the review unit from the PR unit, and every fix lands on B while A's tip
stays "A before review". The moment stage A is not correct on its own, the point of staging
is gone. Recovery: fold into one, or move A's fixes onto A and rebase, or merge in order —
all more expensive than keeping the order (L174).
Episodes for all of the above: references/round-conduct.md.
Before you hand it over (round 0)
- Pass the mutation check. Spending a round on a reviewer telling you the tests are weak is
waste.
# right after a fix commit (the default)
python3 .claude/skills/atmofab-review-loop/scripts/mutation_check.py \
--range HEAD~1..HEAD --paths <sources you touched> \
--test-cmd "python3 -m pytest tools/tests/<relevant file> -q -p no:randomly -x"
# to look at the whole branch at once (three-dot, like the review target: it excludes the
# commits main gained after you branched)
# --range origin/main...HEAD
# if you changed scripts/mutation_check.py itself, its tests live beside it;
# `pytest tools/tests` does not collect them:
# python3 -m pytest .claude/skills/atmofab-review-loop/scripts/tests -q -p no:randomlyPass -x — but only with a --test-cmd whose baseline is green (see the handwritten-sweep
bullet below, which is where that bites). The exit code decides the verdict — the output is read
only to tell a real failure from a suite that never ran. Hunks run in separate worktrees, min(cores - 2, 4) at a
time by default and never more than the hunk count (--jobs); 4 hunks × 805 tests measured 5m52s → 43s. Do not put a TMPDIR= prefix in--test-cmd: each job already gets its own temp root, a prefix puts them all back on one, and
failures then belong to no hunk and are recorded as killed — a false pin (at more than one job
the script refuses the combination, exit 2). Never give --test-cmd both -x and -n (pytest-xdist): -x stops the workers mid-test, a finally that restores a file is skipped, and the file stays changed in the checkout — measured 2026-09-25 on the backend-boundary baseline. -n inside a job also multiplies with --jobs, and it is only available where xdist is installed; a kill that a parallel run alone reports is re-run serially before it is banked (the /tmp/repo bullet below). Run the un-mutated baseline first; a red
baseline exits 2 (--skip-baseline removes the check — write down why).
Exit codes: 0 = clean, or the sole survivors are prose-only, or nothing was left to check —
so read the hunk count. 1 = an unannotated survivor, an inconclusive or skipped hunk, or a
change with no revertible hunk. 2 = the run itself cannot be trusted (red baseline, baseline--timeout, unresolvable range, --repo that is not one, TMPDIR= with several jobs).
The rules, one line each; references/mutation-testing.md has the episode and the reasoning for
when a rule does not obviously apply:
- A red baseline is the HARNESS's fault before it is the suite's — the per-job temp root and
the--workdirlocation are inputs to the suite. The harness's scratch paths must not be
paths the suite asserts about. The BASELINE RED message names both levers; try them before
touching a test - A documentation-only diff reports every hunk SURVIVED, and that is a MEASUREMENT — split
the hunks: mechanically checkable claims get a check, the rest is declared prose in the commit
message with the count. Do not write a check that parses prose to decide what a claim IS - Pass
--continue-on-collection-errorswhen a hunk comes back INCONCLUSIVE, and drop-x
when you do — a mutant can kill pytest during collection and aFAILED-line scorer reads
that as green (PR #68: 3 mutants hid 41-47 real failures). Put both facts in the reviewer's
instructions too - Run the baseline for handwritten sweeps too, and read it before you trust
-x— the script
refuses a red baseline (exit 2); a sweep you write yourself will not. Two causes, same false
green: a stale worktree (PR #67), a sweep KILLED mid-run — a timeout or a Ctrl-C skips thefinallythat restores the file, so the NEXT run measures a mutated baseline (PR #104; the
script caught it and exited 2, which is the behaviour to keep) — and a suite that ALREADY has a
failure unrelated to the change,
where-xstops every mutant at that same failure so every mutant reads askilled— a false
green over the whole run, not a per-hunk slip. A fourth cause is the SHELL, and it is the one
that leaves no trace in a summary line: this environment's shell is zsh, which does not
word-split an unquoted$var, so a sweep that builds its test command in a variable
(run(){ pytest $1 }) hands pytest one argument that is several paths glued together and
collects zero tests — every mutant then reportsno tests ran, which a scorer reading only
the last line scores as not-failed. Hit for real on issue #142's round 0. Read the baseline's
COUNT, not its colour:197 passedandno tests ran in 0.00sare both non-red. atmofab's standing instance USED to be the
path-depth-coupledForbidBackendCredentialReadTestscases — one PR #98 reviewer reported 12 of
12 mutants killed that way before re-running — and issue #84 closed it on 2026-09-02: those
cases now build their own$HOMEand checkout, so they pass at any depth and in a worktree.
The class is not closed with them: check the baseline is green for the--test-cmdyou pass,
and if it is not, deselect the failures or drop-x - If the change is a move, hunk mutation answers almost nothing —
--rangecannot reach code
that moved without changing. For move / rename / extract PRs build mechanism-level mutants
over every mechanism in the files you touched (PR #68: my 39 mutants, 0 unexpected survivors;
a reviewer's independent sweep found 11) - Past 50 lines, a hunk hides the decisions inside it — re-target each judgment individually
- Survivors mean no pin, or a neighbouring check killing it. Fix them or write down why not.
Test-file hunks are excluded by default (--include-testskeeps them); nothing else is
excluded on a guess about#: it opens a heading in Markdown, a preprocessor directive in the
c/cpp families, a shebang, a lint pragma, and inside a Python string or a YAML block scalar it is
the prompt-template text this repository pins. Prose-only hunks are annotated, not excluded
([prose-only (comment/docstring) — expected]), by AST comparison over Python files only — so
a#inside a string literal is not prose, a prose hunk in another file type is unlabelled,
and a hunk that also carries a code move is never annotated: a missing annotation is not
evidence that a hunk is code. One half of a code move is expected to survive — read the pair
together, and for a move between Python modules pass--continue-on-collection-errorsFIRST:
the halves whose import breaks at collection report INCONCLUSIVE otherwise, and you have spent
a run to learn it - Get
--range's base wrong and "no hunks in range" looks green (exit 0). Causes: a base
that resolves but is wrong (origin/main..HEADis empty after a merge; after a rebase it is
your own commits replayed), a--pathsmatching nothing, a round that is all test files.
A change with no revertible hunk — pure rename, binary, mode change, empty new file — is
listed by name and exits 1 whatever else the run found, because nothing was tested for it - The worktree is checked out at the range's END COMMIT, so a pin that is still UNCOMMITTED is
invisible and its hunk reports SURVIVED. Not a false positive — the script is answering about
the commit — but it reads exactly like "this behaviour has no test", and the natural response
is to go and write the test you already wrote. Commit the tests before the sweep, and when
a survivor surprises you, revert that one hunk by hand against your working tree before
believing it (references/mutation-testing.md) - Hand-revert a KILL that surprises you, not only a survivor. The rule above doubts survivors;
the reverse happened on issue #153. A re-run reported EVERY hunk killed, including one whose whole
content was a widened type annotation and a deleted unused local — no behaviour to pin — and a
hand revert of that same hunk was green. Cause unidentified; the runs differed in--paths
and--test-cmd, baseline green in both. The verdict you keep is the one you can reproduce by
hand; report an unreproducible kill as such rather than banking it, because "every hunk is
pinned" is what a reader will quote back. The tell is a kill on a hunk you cannot name a
behaviour for — and the sharpest instance of that tell is a hunk that is half of a code
MOTION, which the prose-annotation bullet above already calls an EXPECTED survivor. When the
script reports one of thosekilled, two rules of this file contradict each other, and the
hand revert is how you find out which. Issue #153 hit it a second time: the extraction of_structure_readingout of_fortran_procedure_envelopeswas reported killed, and reverting
exactly that hunk by hand — restoring the inline block while the extracted function stayed —
left the file green, as a behaviour-preserving motion must. Cause unidentified both times, so
do not spend the round on it; spend the two minutes on the revert, and report the kill as
unreproducible rather than counting it toward "every hunk is pinned".
PR #282 found one cause, and it is the script's default parallelism. Two prose-only
hunks came backkilledat the default--jobs, and both SURVIVED on a--jobs 1
re-run of the same range.tools/tests/test_workflow_conductor.pybuilds its conductors on
the LITERAL/tmp/repo(46 uses in code), which the per-jobTMPDIRdoes not isolate, so
concurrent jobs overwrite each other's files there. Four concurrent copies of the suite
failedLeafTransientRetryTest::test_transient_retry_uses_a_fresh_launch_request_and_min_mtime_per_attempt
in 5 of 8 runs: the probe's mtime was another process's. The same collision happens between
your suite and a reviewer's suite running at the same time. PR #283 moved that file to a
per-process root (four concurrent copies: 0 failures in 8 runs), and the CLASS stays, because
other fixtures still name literal/tmppaths. So: when a kill surprises you, re-run with--jobs 1before anything else.
Issue #405 found a second cause: a flaky test the BRANCH ITSELF added, red only under load —
and the script's own parallel jobs are that load. A docstring-only hunk came backkilled;
the hand revert was green twice and red once. The red was a new row asserting a SIGKILLed
descendant was gone, checked before the asynchronous kill had landed: six concurrent copies of
the suite (seq 6 | xargs -P6 -I{} python3 -m pytest …) failed it 1 in 6, and serial runs
never did. So a hand revert that is not reproducibly red is a flake finding, not a verdict
on the hunk — run the suite concurrently to name the test, fix the test (a bounded settle,
never a longer fixed wait), and confirm the mutant it exists for is still red
A kill that has a cause and still pins nothing is a DEPENDENCY kill. Reverting a hunk that
defines a name another hunk of the range calls raisesNameError, and the script scores
thatkilled. Neither the #153 kills nor #282's dead-clause kill has been re-run serially,
so "cause unidentified" still stands for them - If the change's mechanism lives inside a test file, hunk mutation does not apply — "nothing
to check" with a correct base is not applicable, not a pass, and--include-testsdoes not
rescue it (reverting an ADDED test hunk deletes an assertion, so it always survives; a hunk
that CHANGED an assertion is different — reverting it makes the old assertion contradict the
fixed code, so it reportskilledwhile saying nothing about the code under review). Build
mutants that kill each decision of the new machinery one at a time - Do not handwrite a mutation harness (PR #53's
str.replacerewrote all occurrences at
once, hiding 2 reachable fail-opens). If you must: hit occurrences one at a time, assert the
patch applied AND that it changed behaviour, and re-point a mutant list reused across
rounds — a stale target reports as a survivor (PR #86, visible only because the harness
printedPATCH DID NOT APPLYinstead of counting them green), and a not-applied patch is a
failed run, not a finding — report it as its own category and count it out of the denominator.
"Applied" is not enough on its own; a semantically identical rewrite patches cleanly and proves
nothing. And do not PATCH a mutant list between rounds — write it out, and read the RUN's
own output, not only the artifact it produces (PR #104: three silent misses, two of them
reporting plausible totals for a sweep that measured nothing new). The script is
not universal either: one hunk can bundle a pinned and an unpinned change, so follow up at line
granularity when one rule lives in N places. A handwritten sweep over a SINGLE file must run
withPYTHONDONTWRITEBYTECODE=1and-p no:cacheprovider— consecutive same-byte-length
rewrites within one second reuse a stale.pyc, and the mutant that reportskilledis the
PREVIOUS one. Two reviewers hit it independently on PR #100, one of them reporting three live
mutants as killed; the skill's own script is unaffected because each hunk gets its own worktree.
The trigger is any script that rewrites a Python file and runs a test reading it within the
same second, not only a mutation sweep: a drift re-pin replaces a 64-hex digest with another
of the same length, and issue #316 PR-4's re-pin helper then saw the suite compare against the
digest it had just replaced, twice, until the.pycwas deleted - A fixture that writes OUTSIDE its own
TemporaryDirectorymakes its own self-test
satisfiable by a leftover. Issue #168: a traversal-rejection row used"../outside/ir", which
resolves to a SIBLING of the tempdir — a fixed path in the shared temp root. The litter is the
small half; the sharp half is that the row's own "the file is where discovery would look" check
passed on a residue from a previous run rather than on this run's write, and on a shared machine
another user's copy turns the row into aPermissionErrorinstead of a rejection. It also
collides with the parallel worktreesscripts/mutation_check.pyruns underTMPDIR. Keep the
escaping path INSIDE the tempdir (traverse back into it) and assert the landing path resolves
under it. - A reviewer's hand mutation must mutate, run and restore in ONE Bash call, with a
cmp
against the backup after the restore, and the launch prompt says so. "cp a backup first and
restore from it" was in the prompt of the round-2 reviewer on issue #178 that split the three
steps across Bash calls, lost track of which tree its backup path pointed at, and wrote a
MUTATEDtools/orchestration_runtime.pyinto the primary checkout — then reverted it there withgit checkout -- <file>, the command the same prompt forbade. Nothing was lost because every
edit was committed, and the end-of-roundgit status --porcelainis what caught it; the rule
is here because the prompt wording "cp a backup" permits the split that caused it
(references/round-conduct.md) - Never revert a mutation with
git checkout -- <file>. TWO reasons, and the second is worse:
it deletes uncommitted work, and on an UNTRACKED file it is a SILENT NO-OP — PR #104 shipped a
mutant for two commits that way. Use a worktree (the script's default;--keepleaves it
REGISTERED, andgit worktree prunewill NOT unregister one whose directory still exists —git worktree remove <path>does) or acpbackup, and nevercpout of a worktree you have
been mutating without checking it is clean first - Mutation checking cannot detect a test spinning in neutral. A live-but-unobserved hunk
looks like a pass (L128: deleting the scope mechanism outright kept everything green and all 5
tests meant to pin it were inert). Countermeasures — each with its episode in the reference:- run one mutation that deletes a whole mechanism (pass-through
return, constant condition) - a mutation list you build carries your own blind spots; hence the standing instruction to
reviewers to build their own, and do not hand over your list - doubt once why a test passed, especially when it felt obvious: until every other path in
the fixture that could produce the expected violation is removed, it does not pin its name - a negative assertion is green when the detector breaks — self-test the detector
- when a mutant dies, read why: a kill from a setup error is worth exactly as much as green
- a fake driver that REPEATS its last scripted result reports a lost budget only as a
HANG — and a hang killed by the harnesstimeoutis neither a kill nor a survivor. Issue
#170's producer fake clamped the spawn index to the lastProcResult; with the wait'susage_waits += 1dropped, the loop re-launched the same quota death forever,pytest
produced no result line, and two rounds recorded the mutant as "hangs" while the verify
twin's identical regression was a red row. Give every scripted driver a launch bound past
its script that RAISES (MAX_SPAWNS_PAST_SCRIPT), so an unbounded loop names itself.
Trigger: anyprocs[min(i, len(procs) - 1)]in a fixture that drives a retry loop
(references/mutation-testing.md) - a witness for an ABSENCE assertion must mutate the WRITER, not the function you are
calling. Making the unit under test create file X reddens ANYassertFalse(X.exists()),
including one naming a file no code writes — so it distinguishes spellings, not coverage.
Issue #180 banked exactly that as the witness for a vacuity fix, and the assertion was
still unfalsifiable: the method it drove authors no verdict, and the real writer was one
frame up. Ask whether the mutation would also have reddened an assertion about
something the unit under test cannot produce; if the answer is yes it measured the
assertion's syntax. The parent rule above did not fire because the kill did not LOOK
like a setup error — it looked like the mutation working - when you rewrite a test, diff what the old version observed
- a test that reproduces the wiring does not observe the wiring — and that shape lies in its
docstring easily; grep the body for the function names the docstring claims to drive - a CLASS docstring goes stale by ADDITION — re-read it when you append a row
- a test can pass because of the SUITE'S OWN ENVIRONMENT (a
conftest.pyredirect). The
tell: the test reasons about a RELATIONSHIP whose two halves are not both built by the
fixture. The check is one command — run the new classes under pytest and underenv -u <VAR> python3 -m unittest <dotted.path>and diff the verdicts. The fix is to build
the relationship in the fixture, not to unset the variable.
When the relationship is with the HOST rather than with a variable, the diff has to be over
a FAMILY of environments, and the family needs every property the rows spell — not just the
one that is red. Issue #84's rows reasoned about$HOMEand the checkout: a sweep over the
checkout's DEPTH found a third coupled row the red one had hidden, and a round-1 reviewer then
found two more by varying the SHAPE of$HOME(/home/<name>, a path containing the letter
a glob operand spells), which the depth sweep could not see. Enumerate the properties the
assertions name, then vary each; a sweep that varies one property answers for one property - a mechanism that guards the HARNESS cannot be witnessed from inside the suite — put the
witness in a subprocess outside the runner - a rule whose answer on THIS tree is "nothing" cannot be observed through the assertion that
consumes it.assertEqual(rule(corpus), set())is green whether the rule computes the right
empty set or returns one unconditionally, and mutating it mutates an assertion, which survives
by construction. Move the rule into a module and drive it on synthetic input where the answer
is not nothing — both directions, because for a rule that REFUSES something the over-refusing
direction is the one you will get wrong (PR #104, three mechanisms, same fix) - when a comment JUSTIFIES a rule, mutate the property the justification names — the rule
has a witness, the property holding it up usually does not - kill enumerations one element at a time (regex alternatives, keyword tables); checked
together, a missing element goes unnoticed - generating the probes FROM a constant gives set identity, not a family that can distinguish
anything — the sign "you measured a family and reported the conclusion" below carries the
two checks; reach for it whenever a table-driven test is the evidence you are about to cite - one test per occurrence of a rule, not per rule — and the sharpest trigger is a TWIN:
when a change touches one of a matched pair, list the pair, list your witnesses, compare the
two lists before handing over - when the guard tests a TYPE, the family must STRADDLE that type test, and assert that it
does — the type case of the family sign below, and its own line because it recurred three
times in one branch after two diagnoses (issue #153). A family of malformed values assembled by
imagination comes out malformed the SAME WAY: ten members, every one iterable, cannot kill anisinstance(x, list)guard. More members do not close it; a self-test in the test body does
— loop the members throughiter()and require both aTypeErrorand a success, because that
is what a later member added on one side only turns red. Trigger: anyisinstance/hasattrin a guard whose purpose is to not raise — name the operation that would raise
(for … in,sort,len,[k]) and ask which member reaches it - a test that asserts a property ON A COMPLETE FIXTURE cannot see the property — the
general case, of which the type and threshold rows are two instances. Issue #168: a row
claiming to hold "no declared context key may be empty" asserted non-emptiness on a fixture
where every file was present, so it could not tell a read that DEGRADES from one that RAISES
— which was the whole property — and its real coverage was the three filenames the previous
commit happened to delete. Measured: the fail-open it was written to pin was reinstated on
the other five keys with the suite green. The recipe that closed it: one row per
contributing input, each REMOVING that input and requiring the failure to name it, plus a
set-identity row against the contract table with the derived inputs listed as NAMED
exemptions — so an input added later is either given a row or an exemption, and never
silently neither. Trigger: any assertion of the form "all of X are well-formed" whose
fixture supplies all of X - when the code applies a THRESHOLD, the fixture must straddle it too, and the straddle is
usually a LENGTH — the same shape, and the one that hides best because the fixture looks
realistic. Issue #168 again: a class whose subject is how a failure reason is composed
carried an 80-character flake constant while the code clipped the evidence at 110, so every
row in it passed while observing nothing about the composition — and a later change to that
clip shipped a regression through all of them. Trigger: any cap, clip, budget,[:n],max_/MAX_or size comparison on the path under test. Name the threshold, then assert IN
THE TEST BODY that the probe is on the far side of it (assertGreater(len(probe), 110)), so
a later shortening of the fixture is red rather than silent - a hand-built fixture can test a shape that does not exist — check the construct against
the real corpus before writing the witness - for stateful code, match the fixture to the lifetime of the state, and always include a
version with one level of syntactic nesting
- run one mutation that deletes a whole mechanism (pass-through
Do not type measured values by hand; generate them from the artifact you measured — what
worked was a script that substitutes the numbers in TODO / docs from the measurement
artifacts, run and diffed. Leave no path where a human transcribes. A list
of "every place you wrote a number, re-measured at the end" is not enough, and "re-measure
at the end" fails outright once a review loop starts, because the end keeps moving — a fix
commit adds a test or a hunk, so answering a finding invalidates the ledger describing the
branch. The form is the bug, not the numbers:- Write every measurement as a HISTORICAL RECORD naming the commit it was taken at ("at
7c6d187: 17 hunks, 10 killed …"). A record cannot go stale; a claim about the present can,
and will, once per round - A REBASE invalidates every figure in the rebased commits' messages, and the rule above does
not save you — it says to name the commit, and a rebase makes the commit a different one
with the same message. It fires on a stacked branch every time the lower PR gains a commit:
issue #176 rebased the upper branch five times, and three bodies kept a suite count measured
before the lower branch grew a revert that restores 8 tests, so the branch reads as jumping
+8 at a commit whose diff adds no test. Re-measure after the FINAL rebase, or write the
figure as measured at a named pre-rebase SHA. The same rebase orphans any SHA a body
CITES — three there, two of them different SHAs for one commit — and those resolve from
loose objects on the author's machine while resolving nowhere else.references/measurement-records.md - State the PROPERTY the count stands for, next to the count ("every behavioural hunk is
pinned") — that is what the reader needs and what survives when the number is obsolete - A count with no unit is not reproducible — "14 hunks" came back as 19 and 16 because the
figure depends on the diff CONTEXT WIDTH. Name the width, the command, and the exclusions - Name WHERE a suite figure was measured. A number can be right and still be a defect if it
does not say which checkout produced it: atmofab's suite USED to be one lower in a/tmp
worktree than in the primary checkout, because a declared skip fired there — on PR #104 a
reviewer correctly reported a commit message as wrong for that reason, and the message was
right, it simply had not said where. That particular skip is gone (issue #84, 2026-09-02) and
the rule is not: a figure still has to name its checkout, because the next environment-shaped
difference is not announced in advance - A count that no method can reproduce is worse than no count. PR #104 quoted five figures
for "the names this tree reads"; the two that named their reader re-derived exactly, and three
named no spelling, no file set and no reader — so the sentence claiming each answered a
different question could not be checked. Either write the command beside the number, or
write the property instead and check it - Recording that a number "was right when written" does not stop the next rot. Only
changing the form stopped it - A BATCH EDIT THAT RAISES BEFORE IT WRITES LOSES THE EDITS THAT SUCCEEDED, and the commit
message then describes work that did not happen. On PR #116 one script made three
corrections to a canonical document and asserted its way to a fourth; the fourth assertion
failed, the process exited beforewrite_text, and I read the traceback as being about the
fourth alone — so three corrections the commit message claimed were never applied, and the
document shipped two rounds later still stating what the code had explicitly retracted. Two
rules, both cheap: one write per edit, verified after the write (re-read the file and
assert the new text is in it, not that the old text was in it), and never let an assertion
for edit N+1 sit between edit N and its write. A review round found it; nothing else could
have, because every symptom was in a document no test read.
The trigger is MORE THAN ONE EDIT IN ONE SCRIPT, not "a script that substitutes numbers" —
and that distinction is why this rule failed a second time, on issue #161, after I had read it
the same session. There the two edits were PROSE, inTODO.md, and neither raised: the secondwrite_textwas built from the pre-edit-1 text and silently overwrote the first, so the commit
shipped claiming a correction the file did not carry. Nothing about numbers was involved, which
is exactly why the rule did not come to mind while writing the script. Verify the write, not the
intent, for every edit — including the ones that are only words - And then verify the substitution RAN. The remedy has its own failure mode: PR #98 escalated
to scripted substitution after four hand-typed numbers came out wrong, and the very next commit
shipped with eight unsubstituted{PLACEHOLDER}tokens in its message, directly under the
sentence saying the numbers were no longer typed (an f-string ate the doubled braces). Assert
that no placeholder survives before you commit — onere.search— because a message cannot be
fixed after it is pushed, and this failure looks exactly like success - When the measurement is a FAILURE read out of a sectioned log, map each line to its own
section before you quote it. A CI log, apytestFAILURES block and a mutation sweep's
output all printheaderthendetail, repeatedly, and the eye slides from one section's
header to the next section's error. On issue #161 I attributed a test's failure to an error
belonging to the section BELOW it, wrote that into a commit message and then intoTODO.md,
and a review round found it two commits later — after which my own correction got the same
detail wrong for one of the three runs. The mechanical form is one loop: walk the log, keep
the last header seen, and emit(header, error)pairs; quote from the pairs, never from the
scroll. Reading it yourself is not the safeguard — this is the same class as writing
someone else's measurement as your own, with the same cure, and the fact that you ran the
command does not make what you read off it a measurement - Before you DERIVE a threshold from a measurement, enumerate the comparable runs you already
have. Generating a number from the artifact stops transcription errors and does nothing
about this one: on PR #100 a requirement inferred from one closure was falsified by the NEXT
DAY's closure — same node, same endpoint, same models, running the very configuration the
document recommended, and sitting in the sameworkspace/the measurement came from. Three
rounds of reviewers re-derived the number correctly because they, too, were handed the one
run. A derived threshold needs its POPULATION stated ("largest completed draw across the
N runs that have one"), and the sweep that finds the population is one loop over the corpus,
not a judgment call. Where the population is one, say so and call the number a bound rather
than a requirement - When you PUBLISH the instrument beside the number, republish it whenever either changes, or
stamp it with the commit it was run at. Generating a figure from the artifact is not enough
once the record carries the script that generated it: on issue #181 a pull request published a
verification script in a COMMENT beside a body quoting its output, changed the script two
commits later, and republished neither. A reviewer ran what was published against the tree it
certifies and it failed — on the newer commit's own deliberate edits, which from outside are
indistinguishable from work the change lost. A published instrument and a published number
are one record. Note where the surface is: a record split across a body and its comments
drifts without either looking edited. The cheap form is the stamp — "run at<sha>" makes a
listing historical rather than wrong, which is the same cure this list already prescribes for
a figure - A SHA you type into a PR body or a commit message is a measurement too, and check it
resolves before you publish. PR #282's body named a round-3 range starting at a SHA
that exists nowhere; nothing had produced it. One loop over the text catches it:git cat-file -eon every hex token of commit length
Episodes:
references/measurement-records.md.- Write every measurement as a HISTORICAL RECORD naming the commit it was taken at ("at
Run the verification set and record the measurements. The commands are in
.claude/skills/atmofab-enforcement-change/references/verification.md(suite baseline, ruff
diff against origin/main, doc size ceilings; itsmcp_callend-to-end section is for
enforcement machinery). Run it again after every later commit of the loop, a comment-only
one included: a file that a derivation transformation tuple hashes
(tools/tests/test_derivation_transformation_drift.py—tools/verdict_evaluator.py,tools/primary_evidence.py, the compile-inlined documents, the conductor methods it names)
moves its pin on ANY byte, and PR #257 shipped a three-line comment commit whose message said
the sweep was closed while the suite was red on that pin until the next round found it.
The drift pins are not the only reader of prose, so "the files this edit touches" is not
the suite to run. The backend-boundary token ratchet counts technology tokens in
neutral-core docstrings and comments: issue #316 PR-1 wrote a language's name into a gate's
docstring in a prose-only fix commit, ran the drift file and the per-file suites, and four
commits went by with the full suite red onTokenRatchetTestsuntil the next full run.
Run the WHOLE suite after every commit — it is half a minute at-n 16. And read its SKIP
count, not only its failures: every skip intools/tests/is a missing host tool, and a
skipped row is code the branch was never run against (issue #424 PR-2:16 skippedhid a red
CUDA build-through row for four commits, until round 2).-rsnames them; put the tools on PATH or
say in the record which rows did not run (verification.md§The suite).Leave the list of surfaces you touched in the commit message or the pull request. That is where
reviewers attack from.
A billed run while the loop is open
A billed acceptance run launched before review keeps reading the checkout: the conductor starts
the validator, the renderers and every leaf's template from disk at each substep, so a fix
committed mid-run reaches the rest of that run and not the part already done. Rule: before
committing a fix, ask whether it changes something the live run has still to read for a node it
has not finished — a gate, a renderer, a template or fragment a leaf is handed, an instruction
that run's leaves would follow. If it does, stop the run (TaskStop, then check ps for its
leaf and launcher children), commit, and relaunch: --with-deps reuses every node already
certified, because an in-place re-pin moves no derivation key. If it does not (a test, a
comment, a doc no leaf is handed), leave the run going. Either way record, per certified node,
the orchestration id and the branch HEAD it was launched at, and before publishing re-run the
final HEAD's deterministic gates over the certified sources without billing, so the record says
the certification still holds at the head. Issue #316 PR-4 stopped its run twice (round 1 moved
the harness-use rule a leaf reads; round 2 found the leaf-facing definition of the partition
metadata wrong for the very components about to be generated); references/round-conduct.md
§"A billed run during the loop".
Running a round
One round = two subagents in parallel, on separate axes (security-bypass /
correctness+regression+doc-truth).
One round of the loop puts BOTH agents on the disclosure axis instead — doc-truth bundled
with correctness loses to it, measured. Plan it before you decide to stop; the two briefs are
under "Stopping conditions". A disclosure round runs no security agent, so it neither advances
nor resets the two-consecutive-clean-security-rounds condition.
Do not edit until both results are in — touch a file while one is running and its findings
go stale. Do not rely on the prohibition; absorb it in the launch prompt:"HEAD may advance while you run. Re-verify each finding against the current HEAD before
reporting, and state which revision you measured on."If you do start editing, write the fact and the time into the next round's prompt.
Always in the prompt: the target is
git diff origin/main...HEAD, and "do not modify the
checkout; run mutation tests against agit archivesnapshot or a separate worktree"A scratchpad per agent: "create your own subdirectory, named uniquely to you (add your
PID); do not reuse or overwrite existing paths, and delete only the subdirectory you created,
never the round's shared root". Put your own working files in a subdirectory the same way.
Two agents given the same round directory is enough: on this skill's own review round, one
reviewer's cleanup deleted another's scratch out from under its running pytestSay where a worktree may be created.
git worktree addwith a relative path under-C <repo>puts the worktree INSIDE the checkout, where it is untracked clutter another reviewer
then prunes. Ask for an absolute path under the agent's own scratch directory, and forgit worktree removewhen finishedHand over this environment's trap: in an agent session
grepmay be shadowed — a shell
function execingugrep --ignore-files, so it respects.gitignore(check withtype grep). Have corpus measurements enumerate withfind/os.walk(1 of 365 files was visible
once, and I nearly designed on "interfaceoccurs 0 times"). Handing it over is not enough,
and this one has now been measured from both ends: on issue #168 a reviewer that had this
warning in its own prompt still returned "cited by noSKILL" for a document twoSKILLs cite.
The reader-side rule is under "Verify a reviewer's negative claims" below — the writer-side
instruction does not close itWrite "report only what you ran. Give reproduction steps and file:line. State explicitly if
you found nothing" — this prevents fillerWhat you may hand over to reduce duplication is the axis, not the list. "Hunk mutation over
the diff range is done and green" moves budget to sibling rules outside the diff and to
mechanism level; handing over your mutation list breaks independenceInclude "build your own mutants that delete one mechanism at a time, run them, and report
survivors"Say "mutate the SUBJECT, not the test", in the launch prompt. Round 0's rules already state
that reverting an added test hunk deletes an assertion and survives by construction, but that is
written for the script and reviewers do not read it: on issue #149 a security axis reported a
whole sweep as vacuous on the strength of replacing the test's ownfor line in CONSTANT:withfor line in []:— which of course stayed green. A second axis mutated the CONSTANT in the
production module the same round and it was red. A vacuity claim is only worth the mutation
behind it, so ask for the mutation to be named with the claim, and re-run any that mutated the
test rather than the thing the test is aboutSpell out the process and shared-resource rules: no unscoped
pkill; create only waits
whose exit condition can be satisfied, and no background polling;/tmpis a shared tmpfs,
delete the trees you create. Four accidents happened for real, each spilling into other
sessions. Handing over the rules is not enough — all four were in the prompt and broken
anyway, and I have broken them myself for 5.7 hours at a stretch. The DEV hook now
refuses a sleep-based wait outright (tools/hooks/dev_session_hygiene.py, added 2026-08-28),
which is the only
countermeasure in this section that does not depend on the reviewer reading it; the wording
below is what to hand over anyway, because a refusal that arrives after the agent has already
chosen to poll still costs the round. It matches the token anywhere in the command line — a
heredoc, a Python string, a test fixture's shell body — and refuses the WHOLE command, so
nothing in it runs, an edit included. Issue #316 lost an edit that way: a fixture needed a
wrapper that hangs, spelled with a sleep inside apython3 - <<'PY'edit. For a fixture
program that must block, use one that is not a sleep (exec /usr/bin/tail -f /dev/null), and
put a long edit in a file with Write rather than in the command:- Forbid the MECHANISM, not the behaviour, and forbid it by name. "No background polling"
was in the prompt of the agent that produced 144 orphaned shells in 36 minutes and returned
no report. What worked, measured over the two rounds after it — zero orphans — was four
concrete sentences: do not use the Bash tool'srun_in_background; do not writesleep,until,while … donepolling,pgreploops, or&; run every command in the FOREGROUND
with a boundedtimeoutand a narrow scope; do not run the whole suite in one command —
per-file, one at a time. The last one is what makes the third affordable, and leaving it out
is what makes an agent reach for a background job in the first place. - Say what to do when the work does not fit, or the rules above just get broken quietly:
"if a sweep would take too long, SHRINK IT and say what you cut — a smaller sweep you ran
beats a large one you waited on." Two reviewers cut their sweeps and said so; both still
found real defects, and one of them found the largest gap of its round inside the part it
kept. - A wait must not carry the name of what it waits for in its own command line. Safe forms:
(a) for work the harness tracks, do not poll — the completion notification arrives,
(b) wait on the PID, (c) split the matching string - Evaluate the exit condition ONCE, by hand, before you leave a wait running. "Can be
satisfied" is not visible by reading; run it and look at the exit status - The end-of-round check is
ListAgentsandps, both — pick orphans up withps -eo pid,ppid,etimes,args | grep -E "sleep|until|pgrep"andkillby PID (pkill -f
re-enacts the first accident), after confirming no process doing real work is alive at the
same time. The same holds for YOUR OWN cleanup, not only a reviewer's: on issue #405 I
cleared a mutant's leftovertail -f /dev/nullwithpkill -f "tail -f /dev/null" -u $USER -n— the pattern is in the command line of the shell running it, so-fmatches that shell
and-n(newest) selects it. Select on the process NAME, not its arguments
(ps -eo pid,comm,args | awk '$2=="tail"'), andkillthe PIDs it prints. It is a reconciliation: keep the PID of every wait you start and match the
list — and remember a backgrounded wait returns immediately, so a turn that launches one
has not waited - The symptom disguises itself as "the subagent is running and never returns" — when
ListAgentsshows running, suspect that agent's child processes - Check the checkout itself at the end of a round, not only processes:
git status --porcelainandgit worktree list. A reviewer's stray worktree inside the repository
contaminated another reviewer's suite run on this skill's own review
- Forbid the MECHANISM, not the behaviour, and forbid it by name. "No background polling"
If the reported HEAD is a hash you do not recognize, find out what commit it is first —
the user or another session can commit to the same branch. That is concurrency, not staleness:
read it and judge whether it collides with your scopeWhen a round's reviewers cannot LAUNCH, the round is not clean and it is not spent. A
subagent that dies to a transport error before its first tool call has reviewed nothing; check
the transcript for tool calls rather than reading the failure as a result. Retry once, and if
the up-model stays unavailable there are two moves and they are not equal: convert the axis
into an enumerated checklist and delegate it (that is verifiable work, so the sonnet line
applies), or run it yourself and lose the independence. Say which you did, in the commit and
to the user — an axis run by the author advances neither stopping condition. On issue #149 SEVEN
consecutive up-model launches died to HTTP 500 / 529 across two rounds, and the round that
recovered did so as a checklist of seventeen named mutations plus five over-refusal probes; what
that CANNOT tell you is whether an open-ended attacker would have found an eighteenth, because
the list was the author's.
A launch the model's safety classifier stops is the same case, and its retry is a REWORDED
brief, not the same one. The report arrives as a normal hand-back ("stopped partway through;
no findings"), and the transcript shows reads but no command. On issue #293 PR-2 an attack-framed
security brief ("security-bypass / fail-open", "attack the status channel") and its rewording
("evidence integrity", "construct counterexamples") were both stopped; the SAME questions framed
as "record correctness and failure classification", with the executed program described as "an
input to check", ran in rounds 4 and 5 and found each round's blockers. Frame the axis by the
property the record must have, not by the adversaryHand over the premises in one paragraph (
AGENTS.md§"Development premises" is canonical;
hand over this short form): "a single-operator research workflow platform. What is defended
against is aleaf shortcut— anLLMleaf is not malicious and takes shortcuts, so
anything getting it closer to reporting its task DONE without earning it (a loosened assertion,
a hardcoded expected value, a check recorded as run, a gate edited rather than satisfied, a past
artifact read as input) is in scope at full severity — and the defects my own changes introduce.
A hole getting the leaf no closer to done is out of scope: the operator's credentials, a
read outside the checkout, another orchestration, anything outliving the run. So is hardening a
path only the operator can reach, and a construct occurring zero times in the real corpus."
Both halves are load-bearing: without the first, the review skips the machinery that makes a
verdict mean anything; without the second, it fills with escapes that gain a leaf nothingMake each finding carry its own gain sentence — "a leaf taking this gets ⟨what⟩ toward
reporting its task done". A finding that cannot carry one is not a finding, it is a mechanism
description, and the sentence is what you triage against afterwards. It also states the
over-refusal probe's other half: for a check, the same sentence is what a legitimate input saysRequire a WORKFLOW ROUTE on every finding, in the launch prompt. The gain sentence says what
a leaf wins; the route says where the damage comes out. Together they are the whole filter, and
without the second a round fills with code that is wrong in the abstract and reaches nothing.
Hand this over verbatim:"Report a finding only when you can name the route by which it shows up in a real
workflow
run: whichphase/step/substepreaches the code, what input reaches it (a shape
that exists in this tree'sspecand generated artifacts, not one you invented), and what
comes out wrong — a wrongverdict, a broken artifact contract, a false record, a lost--resume, or aleaf shortcutthat lets a leaf report its task done without earning it.
State the route with the finding, in one sentence. If you cannot establish the route, do
not report it as a finding and do not drop it either: list it under a separate headingroute not established, one line each."Three things are INSIDE the route, and leaving them out is how this instruction goes wrong:
a test that would not notice the defect — its route is the next workflow that runs on
unpinned code, so "the implementation is right but the test is weak" stays real; text a leaf
or an operator ACTS ON — its route is the action it causes (see "Only prose remains" under
Stopping conditions); and a record about the branch that is false — its route is the audit
trail, which is a workflow output. What the requirement actually cuts is the other family: a
mechanism description with no run behind it, a construct occurring zero times in the real
corpus, a hardening of a path only the operator can reach, and style.Read the
route not establishedsection yourself every round — it is what keeps the
requirement from becoming an exclusion list inside the reviewer's head, and it is exactly where
a real defect whose route nobody has measured sits (PR #98's widest gap spent three rounds
inside a list of things already believed covered). Items there are input to your own
reproduction, not a residue list, and they are not carried into the next round's promptDo not let the route requirement narrow the SEARCH. It governs what is REPORTED, not where a
reviewer looks: the mutation sweep, the witness census and the over-refusal probe are run in
full, and a survivor is reported with the route of the workflow that would run on unpinned code.
If a round comes back small, check that it did not shrink because the reviewer pre-filtered its
own sweep — ask what it ran, not only what it foundBut do not name individual findings as excluded — that is an exclusion list, subject to the
three-round rule belowFor a change that adds checking machinery, include "construct legitimate work that this check
wrongly refuses". Ask only for misses and the over-refusals stay. The criterion is whether the
pin is on the rule or on the result the rule produced; pinning results makes ordinary
work fail and teaches the habit of regenerating without reading the ruleOver-refusal is not a one-off trap; it is my default error direction. Put a probe in every
round. Five instances across PR #66 / #67, three of them recurring each time I rewrote the
same rule. Miss-direction bugs come one per round; over-refusal comes in a new shape with
every rewrite. Four countermeasures that DETECT it: (a) for each check you write, construct one
piece of correct work that violates it, (b) keep the probe in the reviewer instructions
through the final round, (c) if over-refusals persist after two rewrites, conclude the rule
is not an invariant and change its shape, (d) build probes from the project's own "what we
do next", not from imagination — implementing the next TODO / plan item is the cheapest
over-refusal probe there isThe fifth countermeasure, and the one that actually worked: when you add a FLOOR, default to
FILL rather than REFUSE. An input that omits a value has said nothing that contradicts you;
if the value is something the layer already knows, supply it. Refuse only a DISAGREEMENT, where
two sources make incompatible claims and you cannot tell which is wrong. Before writing a
refusal into a floor, name the legitimate input it rejects — if you cannot, you have not
looked; if you can, that input is the test
Episodes for the last three bullets, and the four accidents in full: references/round-conduct.md.
Mid-round mutation is where the round-0 rules actually bite, and they read as if they do not.
Everything under "Before you hand it over" is written for the round-0 sweep, but you will mutate
by hand in every round after it — to reproduce a finding, to check a fix, to see whether a
reviewer's mutant is real. The two that cost the most are the two easiest to read as round-0-only:
never revert with git checkout -- <file> (it deletes uncommitted work, and is a silent
no-op on an untracked file), and run the baseline before trusting a green result. PR #107 hit
the first in round 2, on the fix commit's own tests — four uncommitted test edits destroyed, redone
from context, an hour lost — with the rule sitting in this file in those words. cp the file to a
scratch path first and restore from that, or mutate in a worktree; the discipline is the same at
every point in the loop, not just at round 0. git stash is the same move wearing a different
name, and it has a second failure the checkout one does not: on a CLEAN tree it stashes nothing
and exits 0, so the stash pop you wrote to undo it pops whatever stash the repository already
held — another branch's work, dropped into your tree as conflict markers. PR #257 did exactly
that while comparing a ruff count against origin/main (every edit was committed, so onegit restore recovered it and the foreign stash survived). Compare a file against another
revision with git show <rev>:<path> | <tool> --stdin-filename <path> - or in a worktree;
never by stashing the tree you are working in (references/mutation-testing.md).
A THIRD one, and it is the mid-loop mutation's own shape rather than a round-0 rule read late:
the hand mutation you take to CONFIRM A FIX is the one nobody checks. A round-0 mutant is
scored by a script that reports survivors; a mid-loop mutant is scored by you, on a fix you
already believe in, and a red result is what you were hoping for. Issue #180 banked one — the
absence-assertion witness above — and it took the next round to find that the red came from the
mutation reaching the assertion rather than the mechanism. Before writing "witnessed" into a
commit message, name what the mutation would ALSO have reddened. If that list includes an
assertion you would call vacuous, the witness is not one.
Reproduce a finding yourself before classifying it. Real / false positive / residual /
real but out of scope (below) are decided only with a record of a reproduction you ran. The
route the reviewer stated is a claim, not the reproduction — a route you cannot drive is not a
route, and a finding whose route was reconstructed by reading is the shape "verify a reviewer's
POSITIVE claims by asking what it EXECUTED" below is about. Treat
"the implementation is right but the test is weak" as real. (atmofab-enforcement-change judgment
rules 1 and 1-b own the residual / unreachable half and state what a "record" has to be there;
the false-positive and out-of-scope classes are this skill's own, below. This line is the round's
trigger, not a second statement of the rule.)
Verify a reviewer's negative claims the same way. In PR #66 a sweep reported "only the token
ratchet kills this hunk"; a test in another file caught it correctly and was simply outside the
sweep's gate. When told "only X caught it", check whether the files holding the other guards
were inside that reviewer's test command. If not, it is a report about the measurement scope.
An ABSENCE is a negative claim, and it is the one that arrives looking like a fact. "X occurs
nowhere", "no SKILL cites it", "no caller exists" reads as a measurement rather than as a
conclusion, so it goes unchecked — and on this machine the default enumerator is wrong for it
(grep shadowed by ugrep --ignore-files, which honours .gitignore). Criterion: an absence
claim is worth nothing until the reviewer names the ENUMERATOR. find / os.walk counts; a
bare grep does not, and neither does reading. Issue #168: one round-2 reviewer reporteddocs/WORKSPACE_LAYOUT.md as cited by no SKILL — skills/workflow-generate-generate/SKILL.md
cites it, and a round-1 reviewer had already said so, so the two reports contradicted each other
and only re-running it settled which was right. Two reviewers disagreeing is the cheap case;
the expensive one is a single absence claim nobody contradicts. Ask for the command, or take
the count yourself — it is one loop.
And verify a reviewer's POSITIVE claims by asking what it EXECUTED. A "verified true" is
worth exactly the run behind it, and a reviewer that traces a code path instead of driving it
will report MATCHES on a claim that is false. PR #107, twice in one round: the mechanical axis
read run_gate top to bottom and confirmed "every refusal invalidates the copy" and "callee
raises are covered a fortiori" — both false, because two guards sat above the invalidation and
an ownership guard returned silently, and both were found by the reviewers who RAN the refusals.
The tell is a verdict justified by structure ("X runs before Y, so Y is covered") rather
than by an observation ("I called it with a blank token and the file survived"). Ask for the
command; a claim that cannot name one is a reading. This is not a reason to distrust the
axis — the same reviewer refused two false premises I had planted in its own checklist in the
same run — it is a reason to re-run the positive verdicts your change actually rests on.
A reviewer can report TWICE, and the second report does not retract the first. One agent
returning two completion notifications is not a resend: each is a separate stop, and the later
one is written against a later HEAD without reconciling itself against the earlier. On issue
#177's round 2 the first report's highest finding — a binary_not_bound over-refusal after a
second Generate attempt — was simply ABSENT from the final one, which said in its own words
that it had found no over-refusal that wedges a real run. Neither report is wrong; they answer
at different commits, and nothing in the pair says which findings the later one dropped
deliberately. Rule: when an agent notifies more than once, diff the reports yourself and
treat every finding the later one does not mention as OPEN. Then apply this section to it —
reproduce it or establish it cannot happen — rather than reading the silence as a retraction.
There it did not reproduce (conduct sets idx = target_idx and walks forward, so Build
re-runs), and the commit says so; the cost of having assumed either way was one reproduction.
The twin of that: a completion notification is not a stop. Since 2026-09-18 a reviewer's
report can arrive as a task notification while ListAgents still shows the agent RUNNING —
its own background work (a suite run in its worktree) re-invokes it, and it re-delivers the
same report at each stop; issue #250 PR-3 saw every agent of three rounds report two to four
times, with its worktrees still REGISTERED and a full-suite pytest live in one of them each
time. Rule: act on a report only once it is stable across deliveries, then TaskStop the
agent, git worktree remove --force what it left registered and delete its scratch yourself
— the "removed" in its report was written before the harness re-invoked it. The
end-of-round hygiene check (ListAgents + ps + git worktree list) is what shows this;
the notification alone reads exactly like a finished agent. Editing the primary checkout
while such an agent runs is safe for its worktree (detached at a fixed commit) and unsafe for
its findings only in the way the section above already covers. references/round-conduct.md
§"An agent that notifies twice" carries both episodes.
Delegate verifiable work to sonnet
Operational conclusion (14 data points; the confound resolved in PR #72 by giving both models
the same checklist, the axis run as delegated in PR #88): sonnet ⊂ opus, with real misses. Move the mechanical-recomputation axis
permanently to sonnet and keep judgment on the up-model. Costs came out roughly equal, so "it is
cheap, so run more" does not hold — use it only to free up a slot. Run one via Agent withmodel: "sonnet", in parallel with the up-model reviewers; it adds an axis rather than
replacing one.
Work you may delegate (verifiable = running it settles the truth): re-measuring every number
in the diff and reporting mismatches; back-checking "recorded in X" / "pinned by Y" /
"covered by Z" (grep for existence, then delete what the test is ABOUT and see the test fail
— deleting the test itself USUALLY proves nothing, since removing a passing unittest method
usually cannot turn another one red, and a reviewer who reads that instruction literally will
report the whole axis as vacuous. Not always, though, and the exceptions are worth knowing: two
rows here DO notice a deleted sibling — AgentRoleFailClosedTests::test_this_class_census_is_accurate
compares an in-class census against dir(type(self)), and SkillReachabilityTests._run_row
constructs a test by name); a correspondence
table of whether each new failure class has a test; counting the call sites that make the same
decision; contradictions between prose and implementation.
Work you must not delegate: open-ended "find bugs" (plausible noise grows and I pay the
triage), and layers needing a hypothesis → mutate → run cycle (gate semantics, parsers, offset
arithmetic).
Make the prompt a checklist, not free-form, and hand over the same ground rules as above.
This conclusion holds only inside the axis delegated — "sonnet matched opus at recomputing
numbers" does not give "it matches on parser semantics". Measure per axis.
Tell it to report claims it cannot locate rather than accounting for them — refusing a false
premise I had put in its prompt is the most valuable thing this axis has done — and the most
reproducible, having now happened in seven of the fourteen data points (5, twice on PR #107, on
issue #142, on PR #116 — where it reported that the ten-item mutant list a commit message referenced
was recorded nowhere it could find, true, and the thing I would least have checked myself — on
issue #149, where it refused a checklist item of mine asserting a sweep was vacuous, by mutating
the subject rather than the test and getting RED, and on issue #153, where a checklist item of mine
attributed a "pure and NEVER raises" docstring to the wrong function: it reported the premise as not
locatable, went and checked the functions that DO make that claim, and found one that could raise).
That last one is the shape to design the checklist for. The item was wrong about WHERE the claim
lived, and a reviewer that answered the question as asked would have returned "not applicable"; what
made it pay is the instruction to report an unlocatable claim, which turned a defective checklist
item into the round's live defect.
This stays an experiment: collect real/total findings, elapsed time and the overlap count,
add a data point each time, and delete this section if it stops paying. How to read overlap, how
not to confound the comparison, and why the reverse (opus reviewing sonnet's implementation) is
not an experiment at all: references/sonnet-delegation.md.
Exclusion lists (the most important section)
As rounds accumulate, it gets tempting to hand over a list of "already reported, do not repeat"
and "accepted residual". It saves tokens and propagates your own errors with it. PR #51's only
P1 survived five rounds inside that list (two reviewers had found it, and an unverified premise
had dropped it into residual).
- Your own RESIDUE list rots the same way, and nothing above covers it. A published "what this
does not catch" list is read as a completeness claim —docs/RUNBOOK.mdpointed an operator at
one — so re-verify every entry each round, in BOTH directions: an entry that is no longer residue,
and a route the list calls covered that is not. On PR #98 the list omitted its largest member for
two rounds and asserted the interpreter route was covered for three, and round 5 established that
route was the widest gap on the branch - Hand over an exclusion list for at most three rounds
- From round four, run at least one reviewer with no exclusions: "you get no history — attack
from scratch" - Above all, never put anything you classified as residual or unreachable into the exclusion
list. That is exactly the dangerous part
When to bring in Codex (budget: once per branch by default)
Codex's token budget is scarce. One launch per branch as a rule, two at the most. Do not launch
in round 1 — round 1 findings are the coarse layer subagents also produce, and spending the launch
there leaves nothing for the moment independence pays most: after your own fixes have piled up.
Launch once in round 2 (do not save it for the end). Round 3 is not an equal alternative under
the budget above: the launch has to leave a round behind it for its own findings' fixes to be
reviewed in, and at the default of round 0 plus three, only a round-2 launch does.
Cases where not launching is better (spend a blank-slate subagent review instead): a change
that adds checking machinery (Codex structurally almost always finds "one more construct", so
clean never comes back and it is not a convergence signal — use it once as a source of test
cases, or not at all); a prose / doc-centred diff; a HEAD whose
previous findings are still unfixed.
/codex:review and /codex:adversarial-review are disable-model-invocation: true — I cannot
launch them; the command bodies are one line of the companion script, so call that directly.
The mechanics, the flags and the failure modes are in references/codex-episodes.md. The rules
that decide what you do:
- Prefer native
review;adversarial-reviewstalls more often (measured), even though only
adversarial takes focus text. Run native once first even when you want to narrow the focus - Never wrap it in
timeout;--backgrounddoes not detach. Let the harness's background
execution do the waiting; add no polling of your own - Check the base before launching — on a merged branch
origin/mainis your own merge commit - Before suspecting a stall, check whether you killed it (commands cut off at one instant =
an external kill; a true stall showsphasenot advancing and no commands accumulating).
Treat the same phase for more than 15 minutes as a stall and cancel result <job-id>returnsNo job foundbefore completion — take in-flight information from
status'sProgress:and the launch command's stdout, where a partial verdict sometimes
appears. Read the output up to the stall- Do not count a stall as clean (same for a filter drop) — but stalls are intermittent, so
do not conclude "Codex cannot be used on this branch". The two-launch cap is a budget, not
evidence of quality; decide a third by how large the remaining doubt is. If you stop, write in
the PR / TODO that this branch could not use Codex as a convergence signal - It can be dropped by the content filter — engineering-flavoured phrasing gets through, limit
the target files explicitly, ask for counterexample construction rather than attack. One
rewrite-and-retry at most
Use it as one independent pass on another axis, not as a convergence signal: not "run until
clean" but "pass once over the surfaces subagents structurally do not look at" — dependencies,
preflight, execution policy, consistency with repository conventions. It fails
differently from a subagent because it does not share the premises you handed over —
subagents get the diff and the threat model, Codex gets only AGENTS.md. So do not over-brief
Codex on history either. Interaction with
elements that come from anywhere other than "the input I built" is the subagent blind spot it
covers (PR #72). Do not expect a clean axis split — L174 split cleanly, PR #66 did not
reproduce it and one of its two runs fully duplicated a subagent's finding.
Fix or out of scope (cut by this repository's purpose)
Reviewers always report down to the details. Fixing everything is not correct. Beyond real /
false positive / residual, keep an explicit fourth class: real but out of scope. When you drop
something there, write one line of justification and do not carry the list into the next round
(same reason as exclusion lists — and if reachability is unverified, do not drop it, run the
reproduction).
AGENTS.md §"Development premises" is canonical for the model — what this repository is, the
two defended classes, and the decision criterion. It moved there because Codex gets onlyAGENTS.md (see "When to bring in Codex"), so the independent pass was running with no premise at
all. Do not restate it here; what this section holds is what a REVIEW does with it.
The leaf is not malicious and does not hold back. Those are one premise, not a middle. The
reviewer's question is never "could a leaf do this" — a capable model under a goal can do nearly
anything — but "does doing it get the leaf closer to reporting its task DONE?" That is the
cut, and it runs both ways:
- On the shortest route to done — a
leaf shortcut(docs/GLOSSARY.md§12): an assertion
loosened until it passes, an expected value hardcoded, a check recorded as run, agate
satisfied by editing it, an unmeasured value written down, a past artifact read as input. In
scope, and the leaf will spell it however it has to: "a leaf would not phrase it that way" is
no defense here, because reaching done is the motive that makes it phrase it that way - Buying the leaf nothing toward done — the operator's credentials, a read outside the
checkout, anotherorchestration, anything outliving the run. Out of scope however real the
mechanism, and saying so costs no reproduction: name what the leaf would gain, show it is
nothing, stop. That permission is narrow and is not rule 1's —atmofab-enforcement-change
rule 1-e states the two apart
Both halves have a recorded failure, in opposite directions, so apply both (episodes:references/class-descent-log.md §"The gain cut").
Fix (directly serves the purpose):
- a wrong verdict — a violation passed, or a correct artifact failed.
gate/validator/judgefail-opens are here - a broken artifact contract — a schema or invariant of the IR,
verdict.json, or a sidecar
not upheld - false evidence — recording something as run that was not, breaking the audit trail, a commit
message / PR / doc disagreeing with the artifact - loss of reproducibility —
--resumebreaks, non-determinism enters a verdict - a
leaf shortcut— anything letting a leaf report done without earning it. It subsumes the
four above whenever a leaf is what reaches them - a hole an
LLMleaf hits with ordinary spelling — a shape that exists in the real corpus
(this tree's spec and generated artifacts). For a shortcut, ordinary spelling also covers
whatever spelling reaches done
Do not fix (declare out of scope and stop):
- anything a leaf gains nothing from — the operator's credentials, a read outside the checkout,
anotherorchestration's state, persistence past the run. No route through one of them ends at a
green verdict. Check the gain before the mechanism. Mind the near miss: reading a PAST
ARTIFACT is on the other side of this line, forbidden bydocs/workflow/WORKFLOW_CORE.md
precisely because it shortens the route to done, so "a read the manifest does not grant" is by
itself neither verdict - defenses against paths only the operator can reach — hand-crafting invalid argv, editing
files to bypass a gate. The operator owns the machine - handling constructs that occur zero times in the real corpus — future forms an adversarial
reviewer wrote to break the rule. Declare the scope: "this is regression prevention for ordinary
spelling, not enforcement against someone trying to circumvent it". This is a cheap proxy for
the gain test, not a second rule: onegrepdecides it, so reach for it first, and when the
two disagree the gain test governs — a shape absent from today's corpus is still in scope when
taking it shortens a leaf's route to done - generalization for future extension — unnecessary while there is no second caller
- optimization that measurement shows is not the bottleneck — the bottleneck is thinking, not
Python - style and wording preferences — but errors in descriptions a reader relies on get fixed,
on the "false evidence" side - rough edges in existing behaviour this PR does not touch — file them in TODO.md and hand
them over. Pulling them in compounds "fixes calling for fixes" - the SIBLING file a reviewer noticed has the same gap — when the gap is structural but your
EVIDENCE does not reach it, the gap is the honest thing to leave. PR #100 took a reviewer's
"this other sample omits the same field" and wrote the rule into a second provider's sample on
the strength of the omission being identical; the guidance was not, and the next round found a
wrong field name, the rule's own censoring principle inverted, and a warning about a code path
that cannot execute. The whole edit was reverted. The test is not "is the gap the same" but
"does my measurement cover the sibling" — if it does not, say in the PR that the gap is
deliberate and belongs to whoever measures it
When the change ADDS A REFUSAL, read the remedy texts of the neighbouring guards. A sibling's
remedy can steer the refused party straight onto the route you did not close, and then the hole is
not in your rule but in what the surrounding system tells someone to do instead. On PR #98 the new
refusal's own residue was "a writer inside a script FILE handed to an interpreter" — andforbid_python_inline_write's block text says "use a real script file (python3 script.py)", over
an allow-list entry that permits exactly that and a leaf-read document that teaches it. Three
sources agreeing on a route none of them guards is not something any single-file review sees.
The route the reviewer handed over is what this section triages against — check it, do not
rebuild it, and when a finding arrives with no route, treat it as the reviewer's ownroute not established entry: reproduce it or declare the scope, never fix it on the strength of
the mechanism alone.
Two questions when in doubt, both of which must answer yes: "if this stays unfixed and oneworkflow runs, does a wrong certification or a false record come out?" and "does a leaf
get closer to reporting its task done by taking it?" If either is no, out of scope is fine — and
when the second is the no, say what the leaf would have gained. Write the count and
the breakdown of what you dropped into the PR body / your report to the user — never drop silently.
Stopping conditions
The budget comes first. Every condition below is a property of the FINDINGS, and none of them
bounds the cost — which is why no recorded loop was ended by one of them alone.
Round 0 plus THREE rounds is the default; FIVE rounds is the cap. At the cap you stop, whatever
the class did.
The default is three because of where Codex lands. The launch goes in round 2 (see "When to
bring in Codex": not round 1, which is the coarse layer, and not the end), it is the pass that
structurally sees what subagents do not, and what it finds then has to be fixed — so round 3 is
the round that reviews those fixes. Ending at round 2 would ship the answers to Codex's findings
unreviewed, against this loop's strongest recorded regularity: most findings sit inside the
previous round's fix. Round 3 is not slack in the budget; it is the round Codex's launch creates.
A change that fixes existing machinery, or one where the Codex launch is deliberately not spent,
often closes at round 0 plus one or two — spend fewer than the default when the reason is that
kind, never to reach a deadline.
What counts: every round counts, the disclosure round and the census round included — the
disclosure round is one of the budgeted rounds, never an addition to them. A Codex pass rides
inside a round and is not a round of its own.
What the cap ends is the SEARCH, not the REPAIR. An in-scope finding already on the table at
the cap — a leaf shortcut, a wrong verdict, a broken contract, a false record — is fixed before
the branch goes anywhere. The budget never ships a known one, and a fix commit answering a
finding you already hold is not a new round. What stops is looking for the next finding.
Rounds past the cap do find real defects. The budget is a decision to pay that cost, not a claim
that nothing is left — writing it down the second way would be false, and recorded false: issue
#71's round 15 found five defects in a committed measurement script, two of them functional, and
PR #72's third Codex pass found what four subagent rounds had missed. What the budget weighs
against them is the work not being done meanwhile (references/class-descent-log.md §"The budget").
Stopping at the cap is not convergence. Say which condition you did not meet, in the PR body
and to the user, and never write "converged" for it.
The remainder goes into the PR body as a disclosure, not into a new issue. Filing it as an
issue reads as closure while the backlog is what actually grew; measured, every open issue on this
repository is infrastructure spun out of a review loop. File an issue only for work someone has
decided to do, not for a remainder nobody has. This is about a ROUND's remainder and leaves the
residue convention alone: a rough edge in existing behaviour the PR does not touch still goes toTODO.md under "Fix or out of scope" above.
Reaching the cap with something in scope still open is a signal about the CHANGE. Do not
answer it with a sixth round — split the branch, narrow the rule, or hand it over with what would
have to be built to make a strong claim. The sign below ("five rounds without the class descending
→ the shape of the rule is wrong") is the same reading arrived at from the findings' side.
Past the cap, adding a round is the USER's decision, put to them with the count, what the last
round found, and what you would spend the round on.
The main condition is "class descent plus a demonstration that the remainder is bounded". Stop
once the severity class of the findings has dropped and you can show the remainder is bounded.
Read class descent as "is it a hole in the original design / a hole in my fix / a hole in the
witnesses", not as a count — in issue #63 the count barely fell between the last two rounds.
A third shape, from a loop that ended on prose alone: once every remaining finding is "this
prose asserts something unmeasured about code the PR does not touch", the fix is to DELETE the
claim, not to write a test proving it — a PR is not obliged to characterise behaviour it left
alone (issue #40 / PR #41; references/class-descent-log.md).
A second stopping shape, equal in standing: if every finding in one round falls into "real but
out of scope", stop at that round. More rounds produce the same layer, and fixing them moves the
diff away from the purpose. State the out-of-scope breakdown and why you declared the scope.
The superior condition (stop there if you reach it): the security axis produces nothing new
for two consecutive rounds (a disclosure round is skipped in that count rather than breaking
it). It has never been achieved in any recorded loop. Run assuming you will not reach it — do
not add rounds waiting for it.
Do not make "Codex is clean" a stopping condition — as a condition it becomes a motive to
relaunch a Codex you have no budget for. Clean has come back twice, and BOTH times the same round's
subagent work was not: on PR #67 two subagents produced unwitnessed mechanisms, over-refusals and an
abandoned mirror, and on issue #153 the blank-slate subagent sharing that round returned four
findings, one of them a guard whose test family could not fail. On TODO:269 Codex was clean in round
2 and round 3 found a mechanism with no behavioural witness at all.
Practical proxies for "the remainder is bounded", both of which must hold:
- a reviewer with no exclusions returns zero functional defects — only prose and consistency
findings remain - an independent mutation sweep is fully killed (one the reviewer built, not one you ran)
But the two are not equal in standing. For changes that fix existing machinery both apply.
For changes that add checking machinery the sweep is not grounds for stopping: it answers "is
what I built pinned", not "is what I built enough", and can only mutate mechanisms that exist,
so a missing mechanism is structurally invisible. There the proxy becomes a blank-slate review
with zero functional defects for two consecutive rounds; still run the sweep, but read it as a
list of "unpinned but correct behaviour".
"Only prose remains" is a claim about SEVERITY, and it is wrong whenever the prose is executed
by someone. Classify each remaining finding by audience and consequence, not by whether it is
code:
- Text a leaf or an operator ACTS ON is behaviour delivered as prose — refusal messages,
remedies, the leaf-read contract, the runbook step for a failure mode. Treat a defect there at
the severity of the action it causes - Text a maintainer reads to decide — a residue entry, a justification comment, a measured
number — is descriptive, and belongs in the bounded remainder - Text only a maintainer reads gets no ROUND of its own, which is a statement about the budget
and not about whether it is fixed.TODO.md,references/, a skill file, a commit-message body:
correct a defect there in the commit that notices it — "false evidence" keeps it in the fix list
— but it never advances or resets a stopping condition and never justifies spending a budgeted
round. These are the files where a round can always find one more thing, and measured over one
monthTODO.mdalone took more than an order of magnitude more commits thanspec/did - The tell that you are in the first category: the sentence contains an imperative, or names a
condition under which something is refused
Also name any script, harness or fixture generator the branch committed as review surface —
"zero functional defects" otherwise means "zero in the files anyone looked at" (issue #71's round
15 found five defects in a committed measurement script, two of them functional).
The move that finds them: spend one round on the disclosure axis alone, with no functional
brief, before stopping rather than as an extra round after deciding to stop. Two briefs:
"verify every claim in the commit messages at HEAD" and "read it as the next maintainer: what
would mislead you, can the deletion's measurement be re-taken from what is written, what CHECK
went with what was deleted, what does a LEAF see, what does an OPERATOR see, would you merge". If
it returns first-category items, the record has not converged even though the enforcement code has.
The "what CHECK went with what was deleted" clause is not a flourish. On PR #125 it is what
surfaced the branch's only blocker —
a check origin/main had that a round-2 fix narrowed away — and no other instrument in this loop
could have: the sweep mutates what exists, the census enumerates what exists, and a blank-slate
reviewer reads HEAD. Everything else compares HEAD against itself. The disclosure round is the
one place a reviewer is pointed at the previous revision, so it is the only place a deleted
guarantee is visible.
Read "deleted" widely: a guarantee is lost by RELOCATION as often as by deletion, and that
form has no diff to point at. On issue #175 a fact moved out of the structure an existing
comparison reads — the adopted profile left the dependency sidecar's all_nodes for a key of
its own, while the R6-lite freshness signature reads all_nodes — so the check still ran, still
passed, and stopped seeing a whole class of change. Nothing was removed; a key was ADDED. Asking
the axis for "a check that was narrowed away" would have missed it, so ask for a check that
still runs and now covers less, and give the reviewer the old revision to run the old defect
against: red-then-GREEN is the finding whether the cause was a narrowing, a deletion, or a move.
A third form: the change RECLASSIFIES the dirt a fixture relied on. PR #242 made an IR-subject
io_contract finding the terminal rc 4 class; two exit-code test classes had used "an IR with no
io_contract" as their source of ORDINARY co-findings, so the fixtures were reworked — and the two
rows that pinned the §5.1 guard's TYPE kept answering rc 4 from the fixture's remaining dirt,
whatever the guard emitted (str( at its emit site: red on origin/main, green in all 18
subprocess rows at round 2's HEAD). Nothing was narrowed, nothing moved; the row's exit code
simply acquired a second source. Ask the disclosure axis, of every row a fixture rework
touched, what makes its assertion true NOW and whether that is still the thing the row is named
for (references/class-descent-log.md §PR #242).
And "what does a LEAF see" means RENDER THE PROMPT, once per leaf that receives it — WHEN the
branch changed text a leaf is handed. That condition is the whole scope: a branch that touches
no leaf-read document owes this nothing, and asking for it there buys a standing-up of an
orchestration and a six-figure byte count per render for no reason. When it does apply, ask for
the production entry point (build_launch_request -> prepare_launch_request_payload ->render_launch_prompt_text) and for the result to be read end to end, as that leaf, against what
the host will actually refuse. Reading the template is not the same thing: a host-inlined document
arrives whole, and the sentence that matters is usually one nobody wrote for this leaf. It is how
issue #168 found the defects of its rounds 3, 4 and 5, and nothing else on that branch found any
of them. (Its worst defect overall was round 1's, and that one came from a security axis measuring
gates against real corpus artifacts — this instrument is not a substitute for that one.)
For a change that adds checking machinery, run a witness census once. Instruct a dedicated
reviewer:
Enumerate every decision in the checking machinery (predicates, constants, each table entry,
each regex branch, globs, exclusions, file-format assumptions, every assertion added), and
classify each by execution as witnessed (a constructed input witnesses it) /
corpus-dependent (green only because today's tree happens to contain no violation) /
vacuous (already observing nothing). Classification by reading is not allowed. For the
unwitnessed ones, construct a violating input yourself and report whether the suite notices.
Practical notes on reading a census: keep "killed only by the token ratchet" as a fourth
class; a vacuous finding may be closed by marking rather than deleting; aim "does it wrongly refuse
legitimate work" at the instrument too; claim vacuity only by construction — a corpus
measurement does not prove it; a census conclusion rots in one round, so re-run it the round
after you consume it, recording the conclusions that survive re-measurement rather than the
numeric breakdown; when you replace an enumeration with a computation, witness the
computation on a synthetic tree (cf. tools/tests/test_backend_boundary.py::ScannedSetTests).
Before concluding "the shape of the rule is wrong" from recurrence, measure inherited decisions
separately from decisions the last fix added. Inherited got worse → a problem of shape (stop and
hand over); concentrated in what the last fix added → a problem of habit (write the witnesses and
it closes).
Another proxy: does the finding exist in the real corpus? If every finding in a round is a
construct that occurs zero times in the real corpus, what remains is not implementation but a
written scope declaration. Measure the count per finding class with one grep each round.
Look at ListAgents before judging a round finished — as part of the stopping decision, not
only as orphan hygiene. I once issued a "merge recommended" without checking which reviewers were
still running, and the report that arrived afterwards carried a real defect.
In every case, finish with one convergence judgment. The question is not "any new findings" but
"is any finding left that would change code, tests, or a description a reader relies on?" If
only preferences remain, have it recommend stopping explicitly.
When you stop short, state the condition you did not meet and hand it to the user. Do not say
"converged". Put the class transitions and the defects your own fixes introduced into the PR body
as a disclosure.
The per-PR histories, and the episode behind each census note and most of the proxies:references/class-descent-log.md.
Signs to catch mid-loop
Each sign is one line plus its criterion; references/signs-episodes.md carries the case history
that tells you how it closed.
- A finding sits inside the previous round's fix → not a coincidence. Name the fixed files
as the focus in the next round's prompt. The more the change is a move or rename whose body is
known correct, the more the review is really about your own fixes — put that instruction in
from round 1 - The fix was to a RECORD, so you verified it by reading → a record fix carries the same
defect rate as a code fix, and this is the sub-case of the row above that gets skipped, because
prose does not look like something you run. Criterion: the corrected sentence must pass the
check the wrong one failed. If the defect was "this key does not exist", resolve the new key
against the artifact; if it was "this count is wrong", compute the new count; if it was "this
function is not named", drive the function. PR #146 shipped three in a row — a half-followable
operator remedy replaced by an unfollowable one (afailure_analysis.jsonkey nothing writes on
that route), a commit message that described the author's uncommitted working tree as the
previous commit's state, and a "measured" claim repeated from a reviewer's report without
re-running it. And the check written to hold a corrected record must observe the thing the
record describes: the first of those was pinned by asserting the key's name occurred in a
module — it did, in a CONSUMER the same sentence says never runs — so the check ratified the
error (references/signs-episodes.md).
The sharpest form is a correction you make ON A REVIEWER'S MEASUREMENT, where the rule you
already know does not fire:atmofab-enforcement-changerule 3 says not to write someone else's
measurement as your own, and issue #153 broke it there — replacing a correct figure with a wrong
one, one item after condemning that substitution. A reviewer's finding arrives already carrying
evidence, so the correction feels verified before you write it, and a corrections entry is the
one place a reader will not re-check. Re-measure at the commit you are about to NAME, not at
HEAD; for a per-commit figure take the whole series in one worktree-per-revision loop, so the
attribution is visible rather than inferred. Do not delete a wrong correction — record that it
was wrong, or nobody can tell an audited corrections bullet from an unaudited one
(references/measurement-records.md) - You corrected prose a LEAF reads, and you corrected it for ONE reader → every other leaf
handed the same text got your correction too, and a sentence that is true for one can be aleaf shortcutin another. This is the RECORD row's twin on the delivery side: there the
correction was unverified, here it is verified for the reader you had in mind and shipped to
readers you did not. Criterion, and it is not a reading: list every leaf the document
reaches, RENDER each one's prompt through the production entry point, and read the new sentence
in place as that leaf — then ask the gain question of it, "a leaf following this sentence is
closer to ___". A host-inlined contract makes this the default rather than the exception: one
document, several personas, opposite obligations. Issue #168 hit it three rounds running, twice
in the SAME section: a per-path paragraph written for the producer told the reviewer that
authoring the IR and returning a null failure reason — the reviewer's own PASS invariant — was
"the ordinary thing", and a bounding sentence in the reviewer's template ("that list is the
whole of your scope") made a four-way disagreement in the inlined document load-bearing, whose
narrowest reading dropped one of the two invariant families with no deterministic backstop. Both were
written while fixing this same class. Two consequences worth carrying: a summary of a
substep's scope stated anywhere but where the scope is defined is a second definition, so delete
it rather than correct it; and nothing in this loop catches these except the rendered prompt
— the sweep mutates code, the census enumerates code, a blank-slate reviewer reads code, and the
suite stays green with the sentence inverted (measured, all three rounds) - You DELETED a summary that kept coming out wrong and stated the mechanism instead → right
move, and it has a price: stating a mechanism invites POINTERS, and a pointer onto a guarded
surface walks past the guard, because every guard here reads for a token and afile:line
contains none.atmofab-enforcement-changesurface 13 owns the rule and the repair; the sign is
here because the trigger is a review-loop move — the same rewrite this file's "rewritten the
same string three times" row tells you to make. Criterion: for each pointer the rewrite adds,
name what its TARGET holds that this surface forbids. Issue #168 added fourtools/*.py:<line>
citations plus "read them" to a document both verifySKILLs name in a list that forbids readingtools/, and the severity allowlist stayed green because no severity VALUE was added. Nothing
in this loop compares the branch to anything but itself, so nothing sees it: what found it, in
round 3, was a corpus count of the citation FORM on the guarded surfaces at both revisions —
0 before, 4 after - You have rewritten the same string three times → the problem is not the rule but the prose
citing it. Switch to the grep sweep
(.claude/skills/atmofab-enforcement-change/references/verification.md). Rewriting one
statement repeatedly is a SWEEP problem, which this row owns; several sites that each state the
rule is a COUPLING problem, whichatmofab-enforcement-changerule 3-a owns and states the
threshold for. Do not restate its number here — that is the drift this pair is about.
A sweep does not help when the sentence SUMMARISES a measurement — "at the top of the band",
"above every rate observed" — because there is one site and it is wrong on its own terms. PR #100
got that sentence wrong in three consecutive rounds, each version written to fix the previous
one. What closed it was DELETING the summary: state the figures, state what they are being
compared against, and leave the comparison to the reader. A summary of a spread is a claim with
no witness; the spread itself has one.
And when you do reach for the sweep, sweep the FACT, not the spelling. PR #125 corrected one
figure three times, each correction sweeping the wording it had just written, and each missing a
different phrasing of the same measurement — the fourth spelling sat eleven lines from the third
and survived four rounds. Sweep for the NUMBER and for what it is a number OF, then read every
hit; agrepfor last round's sentence finds last round's sentence.
A corrected QUANTIFIER is still a quantifier. "every X" found false and rewritten as "every
X that also Y" is the summary-of-a-measurement case wearing a narrower hat: it still asserts a
set you have not enumerated, and the disclosure round will find the member outside it. Issue
#178 did this to one comment twice in two rounds; what closed it was the same move as the
spread — delete the quantifier and state the MECHANISM (which files a short-circuit drops and
why), which has a witness the set never had (references/signs-episodes.md) - A comment or docstring RESTATES a measurement the assertion beside it already carries →
delete the restatement; do not correct it. Criterion: could this sentence and the line under it
disagree? Then they will, and the sentence is the one that will be wrong, because nothing runs
it. PR #125's four-round figure lived three lines aboveassertEqual(..., [])saying something
the assertion contradicted, and each round corrected the prose rather than asking why a
measurement was stated twice. This is not the "prose that enumerates entities" row: that one
says turn prose into a check, this one says the check is already there - A sentence of yours explains WHY A TOOL BEHAVES as it does → drive it or delete it; do not
correct it. Its subject is a third party's behaviour — pytest's collection, a CLI's flag, git's
path resolution — so there is no assertion beside the sentence and nothing to turn it into a
check; you have to go and make the check. Criterion: name the run whose output the sentence is
a reading of, and say which reading it is. One command is the common case; a DIFFERENTIAL (the
same command with the property and without it) and a cited upstream default plus one run both
qualify. What does not is a mechanism inferred from an outcome — you measured that the count did
not change and wrote down WHY it did not. The fix is not to delete the "because": a
justification is what stops the next person removing the line, and this repository's ownpytest.iniandtests.ymlcomments are correct examples. State the observation, name its run,
and mark a generalization as one. Nearest neighbour: "The fix was to a RECORD, so you verified
it by reading" above — same criterion, different subject, and it is the row to read first.
These are second-category text by the taxonomy under "Stopping conditions", so correct one in
the commit that notices it and do not spend a round on it: put "run every command and every
mechanism claim the prose asserts, and report the ones whose output is not what the sentence
says" in the round-1 launch prompt instead. Issue #183 / PR #202 paid five rounds for not having
done that;references/signs-episodes.mdhas what it cost and what did not close it - A term you coined for a tool has appeared in a document as if the system used it → check it
against the vocabulary the repository already defines. On PR #100 a reporting script labelled any
body it could not parse "body is not an event stream", and that phrase was then written intodocs/ORCHESTRATION.mdas the run's own classification — whereresponse_not_an_event_streamis a DIFFERENT thing that fails closed on the first attempt, so
the document had one leaf both spending a retry budget and belonging to a class that cannot. A
new term needs onegrepagainst the canonical documents before it ships, and a tool whose
output will be transcribed should say what it OBSERVED ("no frames parsed") rather than what it
concluded - Prose that enumerates entities in the code (test names, call-site counts, numbers of
readers) → re-measuring loses. Turn it into a check. Criterion: should this prose break if
one test is renamed? Then make it a check (the general form, and when a check is the wrong
answer, is rule 3-a — this row is the enumeration case of it) - Your fix NARROWS a check to stop it over-refusing → the OLD check is a mutant, and you owe
it a run against BOTH revisions. Narrowing is the standard fix for over-refusal and it removes
coverage by construction; what you have to establish is that what it stops catching is only the
legitimate input. Procedure, two commands: take the defect the old check caught, apply it atorigin/mainand atHEAD, and require red-then-red. Red-then-GREEN is the finding. PR #125
narrowed a whole-document version-range identity to one table column and un-pinned the operator's
own install line with it — driftingpipx install 'fortitude-lint>=0.8,<0.10'to<0.11left
HEAD green andorigin/mainred, and no round noticed until the disclosure axis read the branch
as the next maintainer. Nothing else in this loop looks backwards: every other instrument
compares HEAD against itself, so a check the branch deleted is invisible to all of them - Your fix WIDENS what a predicate accepts, to stop a refusal → every earlier witness whose
fixture relied on the old refusal can go vacuous, and it stays green. This is the NARROWS row
from the test side: there a check stops catching a defect, here a TEST stops observing its
mechanism. PR #282's round 2 made per-turn detection accept "one model row equalsusage".
Round 1's cold-fallback witness used a helper-model envelope, and that envelope now took the
new path. The mutant it had been written to kill survived from then on, and its docstring
became false; the disclosure round found it. Criterion: after a widening fix, re-run the
witness mutations named in the earlier rounds' commit messages — they are listed there, and
the run takes minutes - Your change REMOVES something from the default run, and you then write a witness for it →
the witness is how it comes back. A row that drives the real thing over the real corpus
re-couples the default run to exactly what the change decoupled, and it reads as extra coverage
while doing it. This is the previous row's twin from the other side: there a check the branch
deleted stayed invisible, here a check the branch deleted comes back and nothing says so.
Criterion, one command: inject the input the change was supposed to stop reacting to, and run
the default suite — green is the property the change claims, red is the finding. None of
this loop's instruments can see it: the mechanism lives in a test file, so the sweep's mutants
of the production code all still die, and the census counts the row as a witness rather than as
a coupling. Ask it of every witness written for something the change moved OUT of the default
run, and put the injection in the next round's prompt rather than asking a reviewer to notice.
The fix is injection, not deletion — give the production function the root or path the
witness needs and drive a synthetic one; the coverage is real, it just must not be taken from
the corpus the change stopped reading (PR #187) - Your change SELECTS a subset — of text, of behaviour, of an allowlist — and you are choosing
what goes IN → the DEFAULT for an element nobody considered is a design decision made once, and
it decides whether a MISS is loud or silent. Criterion, one question: if I never think about
element X, what happens to it, and would I notice? The answer decides the polarity; the sign
does not. Where dropping X produces a REFUSAL — a gate allowlist, a write root, an import
roster — drop-by-default is the loud direction and is correct, which isatmofab-enforcement-change
surface 8 and not negotiable. Where dropping X produces silence, keeping unless explicitly
dropped costs size and nothing else. A third answer is often the best: make an unconsidered X
REFUSE until someone decides, which is what_UNSCANNED_ROOT_FILESdoes after two files landed
outside a scan unnoticed. On issue #181 three consecutive rounds found the same shapes — a
dropped fix direction, a pointer whose antecedent went, a claim outliving its qualifier — each
time in the items the previous round had not opened, and each time they were fixed one at a time;
inverting the default lowered the rate and the loop still ended at its cap. Reach for the
question at the SECOND round of one class, not the third: the recurrence is not evidence that
you are careless, it is evidence that the misses are invisible by construction. This is the
selection case of "the same family appeared two rounds running" below — that row asks whether the
rule is placed where a later addition is protected automatically, and the default IS that
placement. Inverting a default is a shape change, so the "split everything after that into
another PR" row below applies to it - A fix changed the shape of the rule (denylist → allowlist and the like) → split everything
after that into another PR. If you continue without splitting, give the user the options and
ask - Five rounds or more without the class descending → the shape of the rule is wrong. Change
the design instead of adding a round - You rebuilt the instrument itself and the second one behaved the same → do not build a
third. Declare the scope and hand it over, naming what would have to be built to make a strong
claim. But do not confuse "a third instrument" with "a defect in the second": the test is
whether what broke it was the shape or an implementation bug / a missing witness. Shape
→ stop; bug and witness → fix - The same family appeared two rounds running → change shape without waiting for a third. The
criterion is whether you placed the rule where a rule added later is protected automatically
(PR #53 closed it by normalizing ahead of the structural decision) - The pin was broken in a different shape every time → looks like the family sign but closes
differently: normalization will not close it. The pin is in the wrong place — the rule has
no single definition. Criterion: "can this test claim set identity, or can it only produce
rejection samples?" If only samples, stop adding samples and move the definition to one place - A mechanism you fixed one round ago is eaten together with the fix → your fix granularity is
too fine (PR #53 closed it by moving from two counters to a stack of construct kinds) - The same mechanism keeps being broken for three rounds or more → suspect that the question
the rule is trying to answer cannot be answered at this level of analysis; change to a weaker
question that can be. If the simple and the complex version give identical measured diffs, the
complexity bought nothing. When the defects are all ONE shape rather than a spread, reach for
this before the five-round "the shape of the rule is wrong" count below — on PR #98 it was the
correct read at three. Changing the question is a shape change, so it is split-or-ask, and
it buys no smaller review surface: the replacement mechanism drew two more rounds of HIGH
findings, one of them a fail-open regression againstorigin/main
(references/class-descent-log.md) - A reviewer said "it is environment-dependent" → do not close it with a mock on the test side.
Ask first what happens in production on that environment (atmofab-enforcement-changerule 2
owns this) - You rebuilt the design and tests carrying the old mechanism's name remain → test names are
read as evidence that the mechanism is still protected. Do not delete them; annotate at the
head of the group what they pin and what they do not - You extracted a predicate to one place and shared it → the call sites need their own pin.
The predicate's test and the "the call site writes the expected value" test are different
things, and the latter can only be written by reading the artifact - A change has "mirrors of the same predicate" and you fixed one → there can be three mirrors.
grepfirst for prose saying "mirrors", "cannot disagree", "same decision" — mirrors usually
announce themselves in a comment - That mirror's agreement test is written as a reconstructed copy of the real thing → it
cannot see a disagreement structurally. Extract the real thing as a function and call it from
both the body and the test - A witness test's probe value contains the implemented value as a substring → the assertion is
automatically true via another clause ("cmake"contains"make"). Assert inside the test
that the probe has the property it needs - Your assertion searches a TOOL'S OUTPUT for a code, a name, or a marker → check what else
that output carries. A linter, a compiler and a test runner all ECHO THE SOURCE LINE under the
diagnostic, soassertIn("C122", stdout)matches the very! allow(C122, …)comment whose
suppression the test is meant to disprove. On PR #116 that made the single behavioural witness
for a security fix pass with the fix REVERTED, leaving one string equality on the argv between a
reproducedleaf shortcutand a green suite. Parse the structured form (the diagnostic
line'spath:line:col: CODEshape, or--output-format json) and add the negative control:
the same input WITHOUT the mechanism must lose exactly what the mechanism was supposed to keep.
This isatmofab-enforcement-change's surface 5 — caller-controlled data mixed into a
classification channel — asked of a TEST rather than of a gate - You appended a test class to the END of a file → check where the
if __name__ == "__main__": unittest.main()guard sits. A class defined below it is collected by pytest
(CI) and bypython3 -m unittest <module>, and is silently absent under the module's own
entrypython3 -m <module>— the instrument a maintainer reaches for when one file is in
question. Issue #269's coupling class shipped that way for six commits and a disclosure
reviewer found it by running all three entries and comparing the counts. Criterion, one
command pair:python3 -m tools.tests.<module> 2>&1 | grep '^Ran'againstpython3 -m pytest tools/tests/<module>.py --co -q | tail -1; the two numbers agree or the
guard is in the wrong place. This is the file-position case of "name any script, harness or
fixture generator the branch committed as review surface" under Stopping conditions - You added a prose pin: construct the document SAYING THE OPPOSITE and run it → a pin that a
document mentions the rule is not a pin that it states the rule. On PR #116 three leaf-read
contracts were held to not CARRYING a forbidden directive and to citing where the rule lives;
replacing each prohibition with its exact reversal ("… is the accepted way to clear a stubborn
style finding") passed 1294 tests. The reversal is the mutation for a prose pin, and the
literal you require should be DERIVED from the code (the flag, the constant) so a rename breaks
both together - You measured a family and reported the conclusion → ask whether the family could have come
out the other way. Two checks, and the second is the one that finds things. (a) Name a member
for which the measurement could have failed; if you cannot, you measured a family that
structurally cannot answer, however many members it had — 48 spellings, 8 spellings and 8
spellings each did this on PR #98. (b) Is the generated spelling one the ACCEPTING PARTY takes?
The accepting party is whatever produces the input in production, and it is often not the code
under test: for a wrapper table it is bash (timeout cp a bparses fine andtimeout(1)then
refuses it, so the row is inert), for a detector fed arbitrary argv it is the detector itself, for
a source-text scan it is Python's grammar. Check (a) cleared a real defect on this repo's own
corpus that (b) caught — a restatement scan generating one quote style where the neighbouring
test loops both. This is the generated-family case of "a hand-built fixture can test a shape
that does not exist" above; that bullet is the same rule for a hand-written probe.
Carve-out: a loop asserting a property every member is SUPPOSED to have (assertTrue(name.strip())
over a curated constant) is a future-guard, not a measurement — no current member can fail by
construction and that is the point. The checks apply when you are reporting a conclusion FROM the
family. And the family can generate a degenerate member:'verdict.json'is a substring of'aggregate_verdict.json'in the same tuple, so the substring sign above and this one compose —
check members against EACH OTHER, not only against the implementation. The remedies differ
(assertNotIn(implemented_id, probe)vs. widening the family), so name both when they overlap.
Episodes, including a 13-family sweep of this repository's own tests:references/mutation-testing.md
Finally
Judge whether this skill itself, scripts/mutation_check.py, or memory needs updating. If this
loop showed a procedure was missing, gave a false positive, or failed to fire when it should have,
tell the user rather than rewriting silently. If you judge no update is needed, say so in one
line.