Escalation rules and the bill
When should a stuck task move up to a stronger model, and what stops either that decision or the review spend from being a mood? Write the rule down as a table, count failures only from receipts the tools wrote, and make the money visible with a budget wrapper around each expensive command. This essay is about those three pieces and about how much each one actually enforces.
It is the third in a series about a small open-source harness, pinned throughout to one commit of Agent Harness, 504d930; the guide to the whole flow is HARNESS.md. The first essay covered review gates and the second covered the spec and the merge gate. Here a review keeps blocking a change, and I want a rule for what happens next. The site has an earlier essay on bounding spend before dispatch, Budget agent work before dispatch, then reconcile what actually happened.
Two failures that look opposite
A review blocks a change. The author fixes it, and the review blocks it again for the very same reason. Then a third round. Same model, same effort, same finding. Nobody decides to try something different, because nothing in the setup says when.
The reverse failure is just as common: the strongest, slowest model gets spawned to fix a typo. One has no upgrade rule and the other has no downgrade rule. A ladder fixes both.
"Why not use the strongest model for everything?" Because that is expensive, slow, and blind to its own habits. And the model that keeps failing is the worst judge of its own failure, so it should not be the one who asks for a stronger model.
The ladder is a table
A rung is a model family plus an effort level. Four rungs write at rising effort, then one rung reviews only, and the top writing rung needs the user's permission. The real table agrees with that description; I asked it. This is the real lookup, run with a throwaway home directory and no model launched:
== 1. the tier table: which rung is a model and effort?
sonnet - -> R0 writable=True
opus medium -> R1 writable=True
opus high -> R2 writable=True
opus xhigh -> R3 writable=True
opus max -> R4 writable=False
writing rungs: ['R0', 'R1', 'R2', 'R3', 'R5']
next writing rung after R3: R5
shipped mode: warn failures per rung: 2 ladder limits: {"max_review_launches": 8, "max_rung_climbs": 3, "max_total_review_usd": 60}
Rung R4 reviews only, so the climb skips it. The numbers are settings in the table file, not a mood: two failures per rung, at most three climbs, and the shipped review spend ceiling of 60 dollars as a default. An unknown model, mystery-model high, came back as None, and the design says an unknown rung resets to the lowest and records why. The reviewer sits one rung above the author, from the other harness if possible, which is the same-blind-spots argument from the second essay:
author R1, wrote with the claude harness:
{'rung': 'R2', 'harness': 'openai', 'effort': 'high', 'cross_harness': True, 'why': 'one rung above the author; cross-harness preferred'}
Failures come from receipts, not from the agent
A receipt is a record a tool writes when a run finishes. Only an executed final receipt with at least one real provider launch counts, and a block counts only if it names findings with real ids. Quota errors, timeouts and zero-launch attempts count for nothing, because they say nothing about the code. I fed the real classifier six synthetic receipts that I wrote:
== 3. only executed machine receipts count
review BLOCK with a named finding -> FAIL
review BLOCK, no named finding -> NOT_COUNTED
review BLOCK, quota failure -> NOT_COUNTED
review, zero provider launches -> NOT_COUNTED
review ACCEPT -> PASS
a plain claim in prose -> NOT_COUNTED
Then the rung itself, which is a pure function of the failure events. Replay the same events and you get the same rung:
== 2. the rung is a pure function of failure events
no events -> R0 (failures at rung 0, climbs 0)
same finding twice, same commit, no fix -> R0 (failures at rung 1, climbs 0)
same finding twice, fix claimed between -> R1 (failures at rung 0, climbs 1)
two different findings -> R0 (failures at rung 1, climbs 0)
eight rounds of one finding -> R5 (failures at rung 0, climbs 4)
That table answers the objection that a reviewer repeating the same bug at the same commit would rack up failures unfairly: it counts once until a fix is claimed. A different finding resets the count. Eight rounds of one finding reach the top writing rung and skip the review-only one. The real ledger also caps climbs at three and reports blocked_by: ['max_rung_climbs'] with permission needed True once it arrives at R5, so in practice it would stop sooner and wait for a person. I ran that ledger path with fixture receipts shaped like spec-review records. They are not real reviews, and the outage filter exists for review and test receipts, which I did not run.
The toy versions are small. The ladder toy is 82 lines with 11 tests, and I planted eight bugs, each removing one rule; the suite failed every time. The second toy, which adds the spawn gate and budget, has 21 tests and eleven planted bugs; again every one failed the suite. A test that cannot catch a planted bug will not catch an unplanted one.
At the door: the spawn gate
A table in a file does nothing by itself. The enforcement point is a pre-tool hook, meaning a script the harness runs just before an agent action. It checks each agent, task or workflow spawn against the ledger. I called its decision function directly with a scratch ledger:
== 4. tier_gate.decide_spawn (the ladder at the spawn door)
allow no model named, task at R0 rung R0 applied (no model named) -> sonnet medium
allow sonnet requested, task at R0 rung R0 matches
allow opus high requested, task at R0 requested opus high is above rung R0; downgraded -> sonnet medium
deny opus max (review-only) to write rung R4 (opus max) is review-only; writing spawns use R0
allow sonnet, task at R2, with a stated reason below rung R2 with recorded reason
allow a reviewer for the R2 task rung R3 applied (no model named) -> opus xhigh
A request above the rung is quietly downgraded. A request below the rung is denied unless the prompt carries a recorded reason, and the top rung without recorded permission is denied too. I am leaving out two rows whose printed text was cut off by the lab's column width.
The caveats matter more than the demo. The shipped table is in warn mode, so the hook reports what it would deny. Switching to enforce is a separate act. A fork keeps its parent's model, which the hook cannot see, so the real code refuses a fork at a raised rung. I called the decision function; I did not run the hook with real payloads, the shell launch-line checks, the user-permission approval, or the rescue helper that launches a model.
The bill: a wrapper with a memory
Review spend is the other half. The usage guard wraps a command, captures repository state, and records start and final receipts. I ran it eight times on a scratch repository:
== 5. usage_guard: finite, opt-in receipts (CLI, scratch repository)
exit 0 first focused run COMPLETED/NONE
exit 2 identical run again, no reason NOT_STARTED/BUDGET_EXHAUSTED Identical invocation already reserved; use evidence or one justified repeat
exit 0 identical run, one justified repeat COMPLETED/NONE
exit 2 identical run, a second repeat NOT_STARTED/BUDGET_EXHAUSTED Identical invocation already reserved; use evidence or one justified repeat
exit 2 full run with no gate and no reason NOT_STARTED/INPUT_CHANGED Full validation needs a gate or explicit investigation/native reason
exit 0 full run, declared finite investigation COMPLETED/NONE
exit 3 a command that outlives its timeout INTERRUPTED/TIMEOUT
exit 3 a run that dirties the worktree COMPLETED/INPUT_CHANGED
The same invocation twice is refused, one justified repeat is allowed, and a second repeat is not. A full-suite run needs a gate or a stated reason. A command that outlives its timeout ends interrupted rather than silently running. One quirk I noted: the refused full run carries the code INPUT_CHANGED even though the message says a gate or reason is needed. Command arguments are opaque by default, so a run's identity comes from its purpose, kind and adapter. My lab gave each run its own purpose; reading the code, two different commands with the same purpose would be refused as identical, but I did not run that case.
What this is, and is not
The documentation says the threat model is careless or hurried agents, not adversarial ones, and that an agent that can edit the installed hooks can defeat the gates. The usage guard is opt-in, and its own source says receipts are local evidence, not attestations and not enforcement for anything outside the helper. A rung table that is not enforced is a policy document.
What I would copy first costs no tooling: count failures from receipts, never from the agent's report, and pick the retry number before you start so changing tactics is not a mood. A list of findings and a rule about who reviews whom is a fine first version.
What I did not run
- No model was launched. Every row above is a table lookup, a function call or a wrapped scratch command.
- Fixture receipts. The ledger rows come from receipts I wrote, shaped like spec-review records.
- Not exercised: the enforcing hook with real payloads, shell launch-line checks,
approve-top,reset, the rescue helper, recording real review or test receipts, and the usage guard's gate, reuse and reconcile actions. - No savings claim. I did not measure spend, latency or quality under the ladder, and I am not claiming it saves money. The only number I quote is a shipped default.
Take one task's failure events, write them as a list, and replay them through your own rung table. The next essay is about the guards that sit at the same door and what they do when they cannot tell: Guards that say which way they lean.