A gate is a check that either passes or stops the run. Gates are deterministic, they cost nothing to run, and each exists because of a specific way that a pipeline made of model calls fails quietly.
They share a shape: something arrives looking like a valid result, and the only way to know it is not is to check a property its producer never asserted.
wraps every model call
A model call returning a success status has not necessarily returned an answer. It may have been cut off at its token ceiling, in which case the text is real but incomplete — and a truncated patch is the worst possible outcome, because applying it modifies some files, fails on the rest, and leaves the working tree in a state no retry can reproduce.
It may have returned nothing at all. Give a reasoning model too small an output budget and it will spend the entire allowance thinking, then return empty visible text — billed in full, with a success status. This is the failure that most resembles success.
When that happens there is often a field alongside the empty one containing the model's reasoning, and it is extremely tempting to fall back to it. That field is not an answer. Substituting it produces confident nonsense that reads like a response, so the gate refuses it explicitly rather than by omission.
A refusal also arrives as a success with content attached, so the reason the model stopped must be checked before the content is read at all. And the gateway attaches an advisory header naming the fault class when it recognises one — not reading it discards a diagnosis that has already been made for you.
after planning, before any implementation is paid for
The planning stage emits a task graph naming files, symbols and dependencies. In a conventional pipeline nothing checks it, and every later stage treats it as ground truth — so a wrong plan is discovered not at planning time but at integration time, after the expensive stages have already run on it.
The failure to design against is not malformed output. Models rarely emit broken JSON. They emit valid JSON describing a task ordered before the task it depends on, or an interface that does not exist, or a file path that was plausible and is not real.
Three checks, in increasing cost. The shape check is a schema, and it doubles as the place where a planning response that hit its token ceiling gets caught, because a truncated plan is not a valid one. The structure check is a topological sort: a dependency graph either has a valid ordering or it contains a cycle, and no judgement is involved. The reference check requires that every file and symbol named in the plan actually resolves.
That last check is the reason layer 3 exists. Confirming a symbol is real is not a question text search or structural search can answer — they can tell you a string appears somewhere, which is a different claim.
One detail matters more than it looks: if no symbol index is supplied, the gate records a warning saying the check did not run. A gate that was skipped is not a gate that passed, and that difference has to survive into the record.
every iteration of the test-and-fix loop
The usual rule for escalating a repair loop to a stronger model is a counter: try twice, then escalate. A counter conflates faults with nothing in common. A flaky sandbox, a rate limit, a model making steady progress and a model that has completely stalled all increment it identically.
It is blind in both directions. If the first attempt moves the failure from one exception type to another, the model is making progress and escalating is premature. If it emits a byte-identical patch twice, the session has collapsed and escalating is already overdue.
The signal used instead is a fingerprint of the failure itself — exception type, failing test, assertion line, and the traceback with volatile details like memory addresses and temporary paths normalised away — paired with a fingerprint of the attempted patch. Comparing consecutive iterations yields four distinct states rather than a number, and only one of them means this model is not going to get there.
Transport faults are deliberately excluded from that comparison. A rate limit is not evidence about the model's reasoning, and letting one escalate a model tier is how a cost-saving tiered runner silently becomes a rate-limit router — paying more for capacity reasons while believing it paid more for difficulty.
This is not theoretical. In one corpus on this machine, 61 per cent of consecutive failures were byte-identical to the previous attempt, and those calls accounted for 22.5 per cent of all spend while changing nothing whatsoever.
around every attempt that modifies files
A retry only means something if it starts from the same world. Otherwise the second attempt runs against a tree the first partially modified, and the failure it produces cannot be attributed to either of them.
So the tree is snapshotted before each attempt and restored on failure. Patches are checked in a dry run before being applied for real, because applying a diff is not atomic across files: without the check it can succeed on the first files and fail on a later one, leaving a tree that is neither the old state nor the new. Fuzzy application is never used — it is the mechanism by which a diff that does not quite match gets absorbed anyway, corrupting adjacent logic while reporting success.
The snapshot only captures files the repository already tracks, which leaves a hole: an attempt that creates a new file and then fails leaves it behind, so the next attempt does not start from an identical state after all. The gate closes it by recording which untracked files existed at snapshot time and removing only those that appeared afterwards. Deleting all of them would be easier and would also delete work that had nothing to do with this attempt.
One limit is worth stating plainly, because the alternative is implying a guarantee that does not exist. Model calls are not reproducible, even at fixed sampling and especially across vendors. Retrying the same work is not a repeatable transaction. The only thing that can be made reproducible is the starting state, and that is exactly what this provides — no more.
Stage 1 is the first model call in the pipeline and the only one whose output every later stage treats as ground truth. That asymmetry is why the contract validator sits directly behind it. A wrong plan is not expensive here; it is expensive four stages later, after implementation has been paid for.
Two constraints are deliberate. The stage must not write code — planning and implementing are different jobs, and a model asked to do both produces code shaped by whatever it wrote first rather than by the structure it should have chosen. And its output is a contract rather than prose: the schema goes into the prompt and is checked on return, so a plan that does not parse is a failed call rather than something a later stage has to interpret.
Effort is set high here on purpose. This is one call per run and a small share of total spend, and it is the call whose mistakes travel furthest. That is the opposite trade from the repair loop, where the same setting would be paid dozens of times for work that does not reward deliberation.
These counts come from the gates' own test runs. A gate that has never rejected anything is a gate nobody has tested.
| gate | records | count | outcomes seen |
|---|---|---|---|
| response validator | rejected a model response | 4 | empty_content, error_body, refusal, truncated |
| contract validator | rejected a plan | 3 | cycle, schema, unresolved |
| loop controller | classified an iteration | 15 | no_output, progress, resolved, stalled, thrashing |
| workspace | checkpoint / restore / patch | 18 | — |