Local proof is the gate; continuous integration is the backstop
Our merge rule stated plainly, with local verification as the real gate, red checks as a hard stop, and absent checks handled as a recorded coverage gap.
Development track
A check that never ran tells you nothing about the code in front of you. In a working pipeline that is easy to get wrong, because most systems give a check only two visible states: green or red. A check that was never configured, a check that could not start, and a check that ran and failed all arrive looking similar, as an absence of green. If the merge rule treats every absence of green the same way, two bad outcomes follow. Good work stalls behind a check that does not exist. Or people learn to wave work past red checks, and the checks stop meaning anything.
Our delivery train is a pipeline where agents design, build, review and verify software changes from a task queue. It merges a steady stream of small changes, so its merge rule gets exercised often. This post states the rule we wrote down after a stall, and the reasoning behind its three states.
What happened
The train had a merge gate, and the gate had a hole in its definition. Everyone agreed on the easy case: a change with failing checks does not merge. What the rule had left undefined was the case where checks were absent. There was no failing result to point at, and no passing result to rely on. Work in that state had no stated path forward, so it waited. The next task waits behind the stalled one, and the gate that was meant to protect quality becomes the reason nothing moves.
We also saw the opposite pressure in a concrete case. A product change reached the point where its pull request sat blocked behind a readiness check that kept failing on the same code. The automated conductor that moves work through our queue stopped on that repeated failure, and the item was parked in the backlog on November 1, 2026. That was the rule working as it should for the red case. A readiness check that fails repeatedly on the same head is a real signal about that change, and the correct response is to stop the merge and fix the cause. The problem case was absent, where treating no result as a failed result produced a stall with no signal attached.
The timeline
The timeline, as a table:
The same timeline in text form:
- October 6, 2026: The direction of travel is recorded: local verification is the real gate, and continuous integration is the lightweight backstop.
- After a stall in the train: The merge gate is rewritten to define the absent case: absent checks are a coverage gap, handled by review and local validation, with the gap recorded and a coverage ticket filed.
- By the current doctrine: Every evaluation gate must define its absent state, and the build-lane test contract sets what local proof consists of.
- November 1, 2026: A product change whose pull request sat blocked behind a repeatedly failing readiness check is parked in the backlog after the conductor stopped on the repeated failure.
What was actually wrong
The first error was a category error. A failed check is evidence: the code was exercised in a stated way and did not meet the stated condition. An absent check is a statement about our tooling: the code was never exercised in that way, so the result says something about coverage and nothing direct about whether this change works. Reading those two results as the same verdict gives the tooling gap the authority of a code failure. Fix the code is the right response to a failure. It is a confused response to a check that never ran.
The second error was leaving the absent case undefined. A gate with no defined absent state defaults to waiting, because waiting looks safe. Our eval doctrine puts the point in one line: an unsatisfiable gate is a stall, not strictness. A condition that cannot run cannot be met, so holding work against it tests nothing except how long the queue can sit still.
We should admit a gap in our own record here. We have no clean count of how many stalls absent checks caused before we rewrote the rule, and no sourced figure for the time they cost. Nobody counted them as a category while they were happening, because the category itself was the missing piece. A post that supplied a tidy number for that cost would be inventing the measurement it claims to report.
What we changed
The merge rule now names three states, and each state has its own action.
The three states, as a table:
The same three states in text form:
- Checks pass: the change was exercised by the configured checks and met their conditions. Merge can proceed, on review.
- Checks fail (red): the change was exercised and did not meet a stated condition. Hard stop. The change does not merge, and nobody overrides the result.
- Checks absent or could not run: the change was not exercised in that way, so there is no verdict on the code from that check. Merge on review passes plus local validation, record the gap, file a coverage ticket, keep moving.
Local proof is the gate. Before a change is proposed for merging, it has to be verified where it was built. Under our build-lane test contract, that proof consists of a contract test for each touched boundary, a vertical slice that exercises the change end to end at its own level, and one Compose smoke of the production artifacts as a merge gate. Local proof runs the affected lanes only. The full suite runs in continuous integration instead, because running it locally on every change would turn verification into a long ritual that agents and humans both learn to avoid.
Continuous integration is the backstop. It catches what local proof missed, at the level of the whole repository, after the change has passed the checks closest to the work. A continuous integration failure after green local proof is a parity gap between the two environments. We record it, fix the cause, and file that fix as a separate ticket. We do not hold the feature ticket hostage to a gap in the environment that produced the signal, because the feature and the parity problem are different pieces of work.
Two smaller rules protect the main one. A rerun after a flaky result only confirms an observed flake fix. Rerunning a red check until it turns green by chance would convert a real signal into noise, so a rerun has to be tied to a specific fix for a specific observed flake. And a failed check becomes work in the queue, with its cause attached, rather than a reason for the whole train to stand still around one change.
The same three states as a list, matching the chart:
- Pass: merge can proceed on review.
- Fail (red): hard stop, never merge.
- Absent or could not run: merge on review plus local proof, gap recorded, coverage ticket filed.
Figure 1. The three states of our merge gate and the action attached to each. Look at the bottom row: the absent state has its own action, separate from both passing and failing.
What it cost
The absent state is cheaper in waiting time and more expensive in bookkeeping. Every gap the train merges past has to be recorded, and a coverage ticket has to be filed, or the exception becomes the norm and coverage never improves. That record keeping is real work, and it falls on the same queue the rule is trying to keep moving. We accept that cost because the alternative hides the gap entirely. Unrecorded missing coverage gets noticed later, usually when a check that everyone assumed existed turns out never to have run on the change that broke something.
The rule also asks more of local proof. If continuous integration is the backstop rather than the gate, the verification done before the merge request carries the weight, and a thin local proof is a weak gate rather than a minor shortcut. Writing the contract tests and keeping the production artifact smoke working are ongoing costs, paid on each change that touches a boundary.
What we take from it
The useful question to ask of any gate is what happens in each of its states, including the state where it produces no result at all. If a gate cannot answer that last question, it will answer it by default, with a stall, and the stall will look like caution. Naming the absent state turned a vague discomfort about blocked work into a rule we could apply the same way twice: red stops the merge, absent starts a different and stated process, and pass lets review decide.
If you run checks around agent written code, the transferable version is small. State your three states before you need them. Keep red absolute, because a red result that can be argued away is a decoration. Give absent its own path, built on the strongest verification you actually ran, and write the gap down where it will become work later.