Loops Are Not a Way to Avoid Judgment

What running a brownfield migration on an agent loop actually took: four gates, a stateless driver, an adversarial skeptic, and where human judgment still had to sit — and why the bottleneck moved from attention to judgment.

What I described in How I Use Claude Code was agentic engineering, the term Andrej Karpathy suggested. I plan an architecture to give better specs, review every diff like a senior reviewing a junior’s PR. It works — better specs give better output. But the problem is I am the bottleneck. Every time it commits, it needs my real-time attention. Review time replaced typing time; the ceiling didn’t move, it relocated onto me.

And that’s the reason why loop engineering came out.

Why loops, and why now

Boris Cherny, head of Claude Code: “I don’t prompt Claude anymore. I have loops running that prompt Claude… My job is to write loops.” Addy Osmani names the job: loop engineering is designing the system that prompts your agents instead of prompting them yourself.

I came at this from a lightning talk by Valerii Iatsko, a Google engineer who has been hardening loops one failure at a time. He starts where the technique started.

Ralph is one line in its purest form:

while :; do cat PROMPT.md | claude-code ; done

The driver consults no state, has no stop condition, and no gate. It worked, which is the surprising part and the reason anyone took it seriously.

What it costs is PR #513, a run he cites. August 2025: a team pointed a Ralph loop at their own frontend, and six hours produced a clean +19,392/−1,367 across 131 files that never shipped. Nothing in the loop asked whether the last unit had landed before it started the next, so main kept moving while the diff sat, and the conflicts outgrew the refactor. His fix sizes the loop to the constraint that actually binds — one diff per night, reviewable over coffee, merged the same morning: “waking up to one small refactor every morning is better than waking up to none, and better than waking up to 50.” The bottleneck was never the agent. It’s how fast humans absorb change.

Why is this a thing now and not a year ago? Two things changed, and only one of them is tooling.

The models crossed a threshold — but not the one people usually name. The capability that matters isn’t raw intelligence on a hard problem; it’s sustained coherence over a long horizon. A year ago, an agent left alone for an hour drifted. The current frontier models are documented and sold on the opposite: long autonomous runs, overnight refactors that complete without human correction, single requests that run fifteen minutes because the model is gathering context, building, and verifying its own work before it answers. That is the precondition for a loop: an iteration only means something if the agent can hold a goal long enough to finish a unit of work.

The second thing is that the pieces ship natively — and that loop had none of them. Everything that made a bare loop safe was something you built yourself and maintained forever. Now automations, worktrees, skills, connectors, sub-agents and state on disk are all in the box in both Claude Code and Codex, and a stop condition is a primitive rather than something you invent: /goal runs until a written condition holds, with a separate model checking doneness after every turn — against the transcript, not the work. Loop design became tool-agnostic, and the barrier dropped.

I want to be careful about which of these is load-bearing, because the evidence cuts against the one everybody reaches for. Go back to the six-hour run. It happened on a model a generation behind, in the era when an agent left alone for an hour drifted — and it didn’t need to hold out, because a Ralph loop never asks one session to remember anything. Each iteration wakes up fresh, reads the plan off disk, and writes the next one back. The continuity lived in files rather than in any model’s context — which is the one thing Ralph got exactly right, and the reason a bare loop worked at all. And the agent side still didn’t fail: nineteen thousand correct lines, with every failure in what surrounded them.

Capability was the precondition; it was never the differentiator. Everything that broke — for them then, and for me since — was harness. Osmani stacks the layers in one line: harness engineering sits under loop engineering, which sits under the factory model — and the layer that keeps failing is the bottom one.

The trend comes with a warning label, and it’s the honest half of this essay: loop design is harder than prompt engineering, not easier. The leverage point moved.

flowchart LR
  subgraph A["agentic engineering"]
    a1[spec] --> a2[agent implements] --> a3["human reviews EVERY diff<br/>(real-time attention)"] --> a2
  end
  subgraph B["loop engineering"]
    b1[design the loop + gates once] --> b2["loop: read state → build →<br/>check → record"] --> b2
    b2 --> b3["human judgment at DESIGNED gates:<br/>decisions · verdict · merge"]
  end
  A -->|"the bottleneck relocates"| B

What we actually built

The context, briefly. Cusflux ingests tariff data from seventeen countries, each with a legacy crawler pipeline, and we are migrating them onto a shared collection framework: one crawler runtime and one CLI for all seventeen. This is exactly the work to point a loop at: recurring and checkable. Whether it was bounded took two iterations to find out. There are four gates, in fixed order:

  1. unit tests
  2. a small live crawl against the real government site
  3. a diff against the production database
  4. an adversarial re-verification in a fresh session.

They appear throughout as gates (a), (b), (c) and the skeptic, and appendix A has them verbatim.

“The model is just the engine. The harness — tools, memory, permissions, sandboxes, tests — is the car you build around it so it can do real work safely.” That is Osmani’s decomposition, and it is a parts list: an agent is “a model plus a harness of files, tools, memory, skills, sandboxes, permissions, observability, and recovery.” Ours took its shape from the work.

The work is brownfield. Seventeen legacy pipelines whose real behavior nobody wrote down — “the system behavior you have to audit doesn’t live in the code. It lives in the scars.” That is why the final gate diffs against the production database rather than against a specification. The legacy pipeline’s real behavior is only recoverable from what it actually loaded, because the document was never written.

The corollary fell out of our own gates: in brownfield, verification has to be evidential rather than declarative. You cannot check the work against a stated intent. You can only check it against another observation of reality.

Trust lives in repo artifacts, not in any agent’s memory. Every loop iteration starts fresh and reconstructs state from what got committed — the playbook (the migration procedure, including the rules a loop may not edit), the status table (one committed row per country, the machine-readable answer to “how far along is this”), the brief (a country’s written record of what its data does and every decision taken about it), and the ledger (a timestamped log of who spent attention on what). Appendix B shows each as it exists on disk.

That is not a limitation we worked around; it is what makes an iteration’s “done” claim checkable at all. Nothing persists in anyone’s context window.

The stronger reason is answerability, and it has to be structural rather than reconstructed. With long-horizon agents, hour-scale decisions are decisions, “and not all the decisions are going to be recorded. You can’t trace them all back to input tokens” — and reconstructing the chain afterwards would cost “hundreds or even thousands of human hours,” which is a polite way of saying it never happens. So committing the state isn’t only how a stateless loop resumes. It is the only reason anyone can answer for what the loop did. Design that in at the start or you don’t get it at all.

Around that sits an outer loop, and ours is deliberately dumb. It is a small bash driver; the intelligence is in the gates and the state. It never merges, never loads to production, never runs the skeptic — it stops. Here is its entire stop condition:

# scripts/migration_loop.sh — the two-signal stop condition
migrated() {
  grep -Eq "^\|\s*$COUNTRY\s*\|[^|]*\|\s*migrated\s*\|" "$WT/docs/migration/status.md" \
    && (cd "$WT" && python scripts/check_migration_status.py --check)
}

A textual claim in the status table AND a machine check independent of the session — though not of what the session committed — must agree. Either alone is the unverified “done” the playbook warns about.

Enforcement rides tooling, not prompts. The loop’s never-load and never-push rules are CLI denylists on the invocation itself (--disallowedTools "Bash(* --load*)" "Bash(git push *)"), because project settings are silently ignored in headless runs. A polite sentence in a prompt is not enforcement — Iatsko’s phrasing, “enforced by tooling, not a polite sentence,” coined after a loop he knew of deleted the tests to go green. A deny pattern is a sentence of a kind too: --l matched no deny rule, still matched the allow, and reached the loader, and the rule had held until then only because a person was reading command lines before they ran. The never-load rule now lives in the program as well, where spelling cannot reach it.

The maker is designed never to be the checker, and it is no longer even the checker’s model. Verification runs as /verify-migration in a fresh context with no shared memory, while the loop’s maker runs on a cheaper model (sonnet-5) and the skeptic and interactive sessions stay on the stronger one. That is a --model flag, and it is the only place I spend tokens on a second opinion. Designed is the operative word; what the design achieved at the merge is recorded below. The skeptic’s own instructions, quoted from the skill file:

You are the skeptic, not the maker. Your job is to DISPROVE this migration. […] A model grading its own work gets agreeable — you are not grading your own work; act like it.

The four gates are not redundancy. They are ordered so that each one checks what the previous one was willing to accept, and every layer has at least once caught something the layer beneath it passed.

That arrangement is what makes the loop a feedback structure rather than a pipeline. Failing (a) or (b) routes work back into the loop with the failure recorded as an artifact, so the next iteration starts from a written finding rather than from someone’s memory of what went wrong. Failing (c) or the skeptic routes it to me, in writing; the loop’s half ends before either runs.

Getting there took longer than the forward path did. Read state, choose, run, check, record — that part automated early and worked. Every return path ended in a human noticing. Documentation drifted until somebody read it. A run grinding itself to death against a site that had changed underneath it held that fact in its own retry counters, where nobody was looking. So most of what I built was not the loop; it was putting a mechanism on each of those paths where a person used to be — a checker that derives the status table from the code and fails the build when the two disagree, a breaker that halts a run whose failures are correlated instead of letting it burn through its retries, and the verdict gate that tracks HEAD, under the misconceptions. The diagram below is mostly those return paths; the forward path is the boring part.

The checks are Quality — everything installed before the system is let loose, whose actual job is to produce evidence. The decision that evidence licenses is the Verdict, and it lives at the boundary rather than inside the loop: “The model may write the line, but the Verdict is mine.” The part I had wrong is that a Verdict is six-valued, not two — ship, block, redirect, narrow the response, add a guardrail, reject. The merge below needed a seventh the list does not carry: proceed over a failed blocking check, with what was lost written down. Looking back at that day with the list in hand, the go-aheads were the easy part. Every ruling that touched the data was one of the middle four: a rate-shape ruling that added a guardrail, a status row flipped to blocked, a crawl deliberately narrowed in scope. An approve/reject model of the boundary would have described none of them.

Read it for the shape, not for the individual boxes:

flowchart TB
  subgraph loop["outer loop — migration_loop.sh (stateless per iteration)"]
    A["fresh agent<br/>claude -p /migrate-country"] --> B{"gates (a)+(b):<br/>tests green + small live<br/>multi-prefix run"}
    B -->|not yet| A
    B -->|"blocked on a genuine<br/>human decision"| J["record it (brief + status<br/>+ ledger) and STOP"]
    B -->|green| C["flip status.md row<br/>(same commit as evidence)"]
    C --> D{"two-signal stop:<br/>row says migrated AND<br/>check_migration_status --check"}
    D -->|"no, iter < MAX"| A
    D -->|yes| E["exit 0 — handoff line to human"]
    D -->|MAX_ITER| F["exit 1 — for deferred-gate-(c)<br/>countries this IS completion:<br/>build half done, ops track next"]
  end
  J --> K["human rules (written,<br/>dated, in the brief)"] --> A
  E --> G["skeptic: /verify-migration<br/>fresh context, prove-wrong"]
  F --> G2["ops track: full crawl +<br/>gate (c) prod diff"] --> G
  G --> H{"check_skeptic_verdict.py:<br/>newest verdict PASS<br/>at branch HEAD?"}
  H -->|no| G
  H -->|yes| I["human: merge<br/>('automate the merge last,<br/>once it's boring')"]
  X["return paths (mechanized, not noticed):<br/>circuit breaker · requeue CLI ·<br/>attention ledger check · judge-edit FAIL"] -.-> loop

The misconceptions

Building this cured me of the following beliefs:

“Loop engineering means removing the human.” The recipes that work contain human gates by design. China’s build ran in a day, through four designed human decisions in sequence: the proposal run (drafted the brief, built nothing, waited for my rulings on eight open questions about the data), the launch go-ahead, a mid-run blocking decision I’ll come back to, and the crawl-start order. The key is not whether humans appear but where judgment is spent — at designed, batched gates versus ad-hoc alarm-watching.

“Write the rule down and the loop will follow it.” A rule nothing enforces is a preference. This is why ours still leaked wherever enforcement was a sentence. “Merge only on PASS” lived in a plan file, in prose. Indonesia was the pilot, and its recorded skeptic verdict was FAIL; the branch kept moving underneath it: the cures, the PR Contract and the status flip all landed on top of it. Nothing was violated; a sentence has no way to notice. The fix is mechanical — verdicts now carry the SHA they judged, and the gate fails closed when HEAD moves:

# scripts/check_skeptic_verdict.py — verdicts now track HEAD
elif not head_sha.lower().startswith(judged_sha):
    violations.append(
        f"judged SHA {verdict.judged_sha} != HEAD {head_sha} — commits "
        f"landed after the verdict, re-run the skeptic"
    )

In the pilot, every blocking rule held and every advisory rule broke. The attention ledger was a written instruction to log as you go, and it stopped partway through the pilot. The status table drifted from the branch updating it and stayed wrong until the skeptic caught it. Careful is not a mechanism, and the fix was never to write the rule more firmly: the ledger’s presence is a merge check now, and it fails closed on absence.

From the other direction: on china’s first day a blocking rule failed closed and stopped every iteration cold. The stop was correct. The trigger was not what the rule was built for — the ledger was there, and the sandbox could not follow the symlink to it — which is its own lesson: a blocking check and the ability to satisfy it are separate deliverables.

“‘Done’ means done.” Done is a claim, not a proof — Osmani’s line, and two receipts. On indonesia, the production diff gate caught real crawl loss that the run’s own validation had accepted: 2,173 rows, gone from five headings. A heading is one four-digit product category, the unit we crawl in; a country has roughly 1,300 of them, each holding tens to hundreds of tariff rows. And on china the skeptic reproduced every number of a technically-perfect migration independently, then failed it anyway — on record-keeping rather than on data.

What it produced, and what it cost

Two of my four gates wait on a person to start them, and that alone puts them on the wrong side of the boundary. Quality belongs inside the loop, where it produces evidence; the Verdict is the decision that evidence licenses. Gate (c) and the skeptic are Quality, on different grounds. The diff is mechanical and re-runnable. The skeptic is not — its core is judgment: it must find each removed row’s explanation and rule on whether it is one, and it constructs adversarial records by hand — but it was built to share no memory with the work, and the maker never being the checker is the other way a check earns its place inside. Both of them sit outside, waiting for a person to start them. So what reaches my boundary is not evidence awaiting a decision. It is an unstarted evidence-production job. I am not rendering a verdict there; I am hand-cranking the machine that makes the thing I would render one on.

What is inside on weak terms. The loop runs the unit tests and the small live crawl, but it runs them in the session, so the model is what decides they passed. The check between iterations is a stop condition, not a quality check: it answers is it finished, never is it right. It is genuinely independent — it runs in the driver after the turn ends, and short of the iteration cap it is the only thing that ends the loop — but everything it reads was written by the iteration it is judging. It confirms that the files exist and that the status table is not lying about them, and nothing more. It never runs a test. A green suite, an unrun suite and a red suite look identical to it.

Inside that boundary, on indonesia, one iteration produced a full migration and four commits in about two hours, and the next woke, verified it, and correctly stood down. I am not publishing a rate for that: build time is a property of each country’s site rather than of the loop.

Judgment arrived in one batch, which is the only reason any of this pays. On china, a validator refused 38 rate strings — currency-and-unit text no shared pattern could adjudicate without guessing. We had already made that validator un-editable without a written human decision, and the loop did what such a rule demands: wrote up both options with a recommendation, marked the country blocked, and stopped. Its own log line was “stopped rather than inventing the decision.” The next iteration woke, re-derived the situation from committed files alone, found no ruling, and stood down in two minutes — and in that same session found and fixed a real handler bug, its ledger drawing the distinction itself: “a real bug (not a business decision).” The maker knew which side of the line it was standing on.

I read all thirty-eight and ruled once; the loop implemented the ruling and revalidated against already-collected data without re-crawling anything. Thirty-eight is a batch one person reads in a sitting. Set it beside the 19,392 lines this essay opened with and the whole argument is in two numbers, sized either side of what a human absorbs in one go. The same design produced 114,075 changed rates at indonesia’s final gate. The diff that produced them runs itself; a person only starts it. What has never been automated is the sorting of those rows into explained categories in writing before anything ships — that is the judgment the evidence exists to feed, not the evidence.

Judgment also entered where I had built no door for it. A person enters a run four ways — shaping it before it starts, steering it while it runs, taking a hand-off mid-flight, stopping it at the end — and I had designed only the last. Indonesia’s multi-day crawl needed a human to put hands on it while it was running, on five separate occasions. Two were defects a signal caught, fixed mid-flight without re-crawling: steering, and gates doing their job. One was a nineteen-hour false outage that turned out to be the source’s own frontend deploy; at the time it was the harness meeting the world, and it is the one case a design change has since shortened — the queue’s own attempt counters now trip a circuit breaker, so a burn is a signal the driver can act on rather than something a person notices. Two were neither: a machine switch, which is a hand-off, and a shutdown lifecycle nobody had characterized. No gate shortens those two. What shortens them is building the doors instead of discovering them.

The worst defect passed every gate. China’s full crawl reported every unit done, nothing dead, validation passing — and was missing 1,632 rows. The handler had looked at those pages and found nothing on them, and it records that the same way whether the source is genuinely empty or the page had not finished rendering: as a finding rather than as an error. Nothing retries a finding. What found it was crawling the whole country a second time and diffing the two corpora — and the second corpus existed only because it had been produced for an unrelated measurement. There is a check for this now: a country has to declare in advance which parts of its scope are legitimately empty, and an undeclared empty kills the run. It exists because of this, not before it.

The last gate blocked on two rows nobody could account for, and on a ledger that had gone quiet. At china’s merge the skeptic re-executed every figure from disk rather than reading my summary, down to a census of all 500,484 accepted rate strings, and reproduced every one of them. It returned FAIL anyway. The production diff showed 65 description rows removed, and for two of them the branch carried no explanation that would survive the merge; the class covering them claimed fourteen codes and listed twelve. Unexplained removals block. The data was probably fine, and nothing established that it was, so I cured it: the attribution moved into the brief, where it survives the merge. Nothing about the data was waived. The second finding was the attention ledger, stale by eleven commits and three recorded decisions, and that one I waived in writing — the first use of the escape hatch the ledger rule was designed with — at a price the waiver had to state: the supervision cost of this migration is unmeasured, permanently. The re-run passed.

Both merge verdicts opened by disclosing that the session writing them had authored commits under audit: “The skeptic role assumes an independent session; this one is not.” Every command was re-run from scratch, and the verdict still told the reader an independent run would be worth more. The maker is never the checker holds between the loop’s iterations, where a fresh session re-derives the last one’s work from committed files, and fails within one, where the model runs its own gates and decides they passed. At the merge it is a disclosure of failing to achieve independence rather than independence.

Human is always in the loop

Loop engineering didn’t remove me from the loop. It moved me out of the inner one. Agents run investigation, implementation and verification. The engineer owns what surrounds it. Inside the boundary there is capability; outside it there is agency — deciding, verifying, approving, owning — and evidence is the only thing that should cross; in this harness, an unstarted job crosses instead.

Named that way, the four places I still am become obvious, and they describe my week far better than “review” does.

  • Constraints: the playbook, the invariants, the rules the loop cannot edit.
  • Sampling: how much of the output the gates and the skeptic actually look at.
  • Audit: what evidence gets kept and whether the ledger is worth anything.
  • Ownership: which part of the production boundary is mine — the Verdict, and the merge.

Everything between those gates got verifiable instead of trusted. That is the entire trade.

There’s a guard on the other side, too. The faster a loop ships code I didn’t type, the faster comprehension debt grows — so the PR Contract stays human-written — a fixed questionnaire on every migration pull request, asking what the data does, which numbers moved and why, and what you would check first if it broke — because writing the answers is the comprehension test. It’s the one gate automation wouldn’t speed up but destroy.

Two footnotes china added, both about trust rather than speed. A synced copy of state is not the state — I pronounced a healthy crawl dead from three locally-true artifacts on a machine it wasn’t running on, and what settled it was the queue’s own counter ticking. And the harder one: a green gate is evidence about the run, not about the world. Every check passed on the corpus missing 1,632 rows — the one above — because all of them were downstream of a step with no way to know it had failed. Verification has to reach a second independent observation, or it only confirms that the pipeline agreed with itself.

One last thing, because this essay nearly didn’t ship on time either. I had been holding it for a number — the supervision cost that would let me price the loop instead of describing it. That number was never going to arrive, and it took building the entire instrument to find out why: the comparison it existed to complete had no other side, and never had. PR #513 is a warning about diffs, but it generalises past diffs. Work that waits for one more thing dies of the waiting, and main keeps moving the whole time.

The bottleneck didn’t disappear. It moved from my attention to my judgment. Loop engineering is the practice of spending that judgment where it matters.


Appendix A — the four gates, verbatim

Every “gate” in this essay is one of these. They are quoted from the migration playbook the loop reads at the start of each iteration, and they run in this order; failing (a) or (b) routes work back into the loop, failing (c) or the skeptic routes it to a person.

Phase 4 Gates (blocking, in order)
  (a) test suites green (`python -m unittest discover tests/collection`
      + country tests);
  (b) small live artifact-only run: `collection run --country {alias}
      --year YYYY --scope <small> --max-workers 1` (never `--load` during
      migration). Make <small> span several different HS4 prefixes /
      chapters, not a single one — one prefix exercises one code path.
  (c) DB coverage diff — the acceptance gate: `python
      scripts/diff_run_vs_db.py --run-id … --country {alias}`
      — read-only comparison of the accepted artifact against the CURRENT
      `{alpha3}_fta_rate`/`{alpha3}_hscd_desc` rows, keyed on `(hscd_num,
      hscd_seq[, fta_nm])`: hscodes **added** (artifact-only), **removed**
      (db-only), rates/descriptions/**year** changed — a git-diff-style
      view against what the legacy pipeline actually loaded.

Gate (c) is the one with no specification to check against — it diffs against the production database because, in brownfield, that is the only surviving record of what the legacy pipeline really did. Its output is buckets: rows added, removed, rates changed, descriptions changed, year changed. Unexplained removals and rate changes block the merge; every other bucket is recorded with its count. That is the 114,075-row task described above.

The fourth gate is the skeptic — /verify-migration, run in a fresh session with no shared memory of the work. It re-executes every check from artifacts on disk rather than reading the summary, and posts a verdict of PASS, FAIL or BLOCKED. Its instructions are adversarial by construction:

You are the skeptic, not the maker. Your job is to DISPROVE this migration.
[…] A model grading its own work gets agreeable — you are not grading your
own work; act like it.

Appendix B — the artifacts a loop iteration reads and writes

The thesis “trust lives in repo artifacts, not in any agent’s memory” is only meaningful if you can see the artifacts. All four are plain text in the repository.

The status table (docs/migration/status.md) — one row per country, and the thing the loop’s stop condition reads. Its state column is the claim; a separate checker verifies that claim against the code.

| country   | pkg      | state       | measured | priority | family    | alias | … |
| --------- | -------- | ----------- | -------- | -------- | --------- | ----- | … |
| china     | china    | migrated    | waived   | done     | hs4-crawl | chn   | … |
| india     | india    | not_started | -        | 1        | hs4-crawl/retry | -  | … |

The completion predicate (scripts/migration_loop.sh) — two signals that must agree, both read from what the iteration committed, neither from the session that committed it. A textual claim alone is the unverified “done” the playbook warns about.

migrated() {
  grep -Eq "^\|\s*$COUNTRY\s*\|[^|]*\|\s*migrated\s*\|" "$WT/docs/migration/status.md" \
    && (cd "$WT" && python scripts/check_migration_status.py --check)
}

The attention ledger — built as the instrument for measuring what the loop costs in human attention; today the migration’s handoff record, for the reason at the end of this entry. Four columns; machine rows are appended by hooks, human rows carry ? until the human answers. It lives in two places and the split is the point. While a migration runs it is an untracked working file (plans/{country}-attention-log.md), because hooks fire on session start, session end, turn end and every commit, and a tracked live ledger would leave the tree permanently dirty and turn concurrent worktree appends into merge conflicts. Before the skeptic runs it is published to docs/migration/attention/{country}.md and committed on the branch, and the merge check now reads only the published copy — in its own words, the working file “is not evidence anyone but its author can check.” That is the newest part of the harness. Before it, the artifact this thesis rests hardest on was the one a reader could not open.

This is a real pair of rows from china’s log, the second one being the D9 ruling this essay keeps returning to:

| date · time         | who              | minutes | what                        |
| ------------------- | ---------------- | ------- | --------------------------- |
| 2026-07-23 · 15:19  | claude (sonnet-5)| 1       | session start — loop iter…  |
| 2026-07-23 · 16:42  | jordan           | ?       | D9 RULED (both parts): …    |

Every one of china’s seventeen human rows still reads ?, and every one of indonesia’s seven reads ? (reconstructed). That is the finding, not an oversight in this appendix. The merge check no longer blocks on it either: the minutes are reported as a note, and the only thing that fails is the ledger being absent or not covering the commits. When the criterion that consumed a number is retired, the gate enforcing it has to be disarmed too, or it blocks the next migration for a reader who no longer exists.

The country brief (docs/countries/{country}.md) — where a migration’s decisions live after merge, and the reason one of the skeptic’s FAIL findings was a record-keeping failure rather than a data failure: the table explaining two removed rows existed, but in a git-ignored directory, so it would not survive the merge for any future reader.