The AC gate: what should block a merge
A gate that blocks everything gets disabled in a fortnight. A gate that blocks nothing was never a gate. The interesting design is what happens in between.
Every team that turns on a criteria gate goes through the same two weeks.
Day one, the gate blocks a merge because a checkbox is unticked. Correct behaviour, everyone is pleased. Day three, it blocks a one-line copy fix, because the ticket has five boxes and four of them are about a part of the feature that shipped last sprint. Day six, it blocks a hotfix at 11pm, and someone with admin rights uses the override, and the override works, and now everyone knows the override works. Day ten, it blocks a merge where all the criteria are genuinely met but nobody ticked the boxes because the boxes were ticked on a different ticket. Day fourteen, the gate is set to advisory, which is a polite way of saying off.
The gate did not fail because gating is wrong. It failed because it was designed around a single question, “are all criteria met”, when the actual question is “is the cost of stopping this change lower than the cost of letting it through”. Those two questions have different answers most of the time.
Start with what a gate is for
A gate exists to prevent a specific bad outcome, and you should be able to name it. “Enforcing quality” is not a bad outcome, it is a slogan.
The bad outcome that criteria gates genuinely prevent is this: work that is incomplete in a way that nobody will notice until it matters. Not incomplete work in general, which is fine and normal and the whole point of iterative delivery. Incomplete in a way that is invisible, because the board says done, the PR is merged, and the only record of the gap was a checkbox nobody looked at.
Framing it that way immediately tells you something important. If the incompleteness is visible, you do not need a gate. If a criterion is unmet and the ticket clearly says so and the ticket is not marked done, the system is already honest. The gate is only earning its keep when it prevents a false signal of completeness.
Which suggests the most important design decision in the whole area, and it is one most teams get wrong: the default response to unmet criteria should not be blocking the merge. It should be refusing to mark the work done.
Blocking
- Stops the code from landing
- Interrupts a person, holds a branch
- Applied when patience is thinnest
- Generates override pressure
- Should be rare
Routing
- Code lands, the record refuses to advance
- Ticket moves to review, not done
- Nobody is interrupted
- The system now says something true
- Costs nothing at the moment of merge
Blocking versus routing
These are two different interventions and conflating them is the root of the day-fourteen problem.
Blocking stops the code from landing. It is expensive: it interrupts a person, it holds up a branch, it creates pressure to override, and it is applied at the moment when the developer’s context is most loaded and their patience is thinnest. Blocking should be rare and should be reserved for things where landing the code is itself the harm.
Routing lets the code land and refuses to advance the record. The pull request merges. The ticket moves to review rather than done. Nothing is interrupted, nobody is blocked, and the system of record now says something true: the change is in, the definition of done is not fully satisfied, a person should look. That is the honest state and it costs nobody anything at the moment of merge.
Almost everything people want from a criteria gate is better served by routing than by blocking. An unticked box is not usually a reason to prevent a merge; it is a reason to not believe the ticket is finished. GroundTruth’s default is exactly this: a merged pull request whose criteria are unmet moves the ticket to review rather than done, which is the cheap intervention that preserves truth without generating override pressure.
Once you separate the two, the design question gets much easier, because the blocking list can be short.
What should actually block
My list, and I would defend it as roughly the maximum rather than the minimum.
Criteria explicitly marked blocking on this ticket. From the earlier article: the specifier marks two or three criteria as the point of the feature. Those, and only those, stop a merge. This works because the marking is a deliberate act by a person who understood the stakes, rather than a uniform rule applied to every box.
Criteria in the standing high-risk categories. Authentication, authorisation, payment, personal data, deletion. If a ticket touches these and has unmet criteria in them, block. The list should be short, explicit, and owned by someone senior, and it should be the same list across all teams so nobody has to remember which rules apply where.
Negative-space criteria. “No change to the public API shape”, “no new dependencies”, “feature stays behind the flag”. These are cheap to check mechanically, and a violation means the change did something outside its declared blast radius, which is the highest-signal failure available. Block on these without hesitation.
Structural failures rather than content failures. No criteria at all on a ticket that has a merged pull request. No linked ticket on a non-trivial PR. These indicate that the process was skipped rather than that the work is incomplete, and they are worth blocking because they are trivially fixable in thirty seconds.
That is the list. Notice it does not include “all criteria are ticked”. A ticket with eight criteria where six are met and two are hygiene items is a perfectly reasonable thing to merge, and should route rather than block.
What should route, and what should be silent
Route to review: any unmet non-blocking criterion, any criterion classed unverifiable, any criterion where the automated check was inconclusive. The signal is “someone should look”, the cost is one workflow state, and nobody’s evening is ruined.
Be silent: everything else. This is underrated. A gate that produces a comment on every pull request trains people to ignore its comments, and once they ignore them, the blocking ones get ignored too, right up until the point where they are experienced purely as an obstruction. Noise does not just waste attention, it actively destroys the credibility of the signal you care about.
The rule of thumb I use: if a message does not change what anyone does, do not emit it. A pull request where all criteria are met should get nothing at all, or at most a status check that is green and unobtrusive. Congratulating people is a cost.
The override, and how to build it properly
Every gate needs an override. Systems without an override get bypassed structurally, which is worse: people stop linking tickets, they split PRs to avoid triggering it, or they merge to a different branch. Design the escape hatch or you will get one you did not design.
The properties that matter:
Available to the person who is blocked. An override requiring someone else’s approval at 11pm is not an override, it is an outage. This is counter-intuitive and I hold it firmly: the value of the override is not the friction, it is the record.
Requires a written reason. One sentence, free text, mandatory. This does about ninety percent of the work, because writing “overriding: hotfix for the payment outage, ticket ABC-123 to follow” takes ten seconds and makes the act deliberate rather than reflexive.
Creates a durable, visible artefact. A comment on the PR, an entry in a log, ideally a follow-up ticket generated automatically. If the override leaves no trace, you have no way to know whether the gate is working.
Is counted, and the count is reviewed. Not to punish. The override rate is the single best diagnostic you have about gate design. A gate overridden twice a quarter is calibrated. A gate overridden twice a week is miscalibrated and is being kept alive by other people’s goodwill, which is a depleting resource.
That last point deserves emphasis because it inverts the usual instinct. When overrides are high, the correct response is almost never to make overriding harder. It is to make the gate block less.
The override rate is the single best diagnostic you have about gate design. Twice a quarter is calibrated. Twice a week is miscalibrated and is being kept alive by other people's goodwill, which is a depleting resource.
When overrides are high, the correct response is almost never to make overriding harder. It is to make the gate block less.
A worked configuration
Concretely, here is a policy I would be comfortable defending to both an engineering team and an auditor.
BLOCK the merge when:
- a criterion tagged BLOCKING is unticked
- the ticket has any unticked criterion AND the PR touches paths in
/auth, /billing, /permissions, or any migration that drops a column
- a declared negative-space criterion is violated (new dependency,
public API shape change, flag not respected)
- a PR of more than 20 changed lines has no linked ticket
- the linked ticket has zero acceptance criteria
ROUTE ticket to review (do not block) when:
- any non-blocking criterion is unticked
- any criterion is classed unverifiable and has no human sign-off
- semantic verification returned below-threshold confidence
SILENT when:
- all criteria met, or the only unmet ones are explicitly deferred with
a linked follow-up ticket
OVERRIDE:
- self-service, one-sentence reason required, posts to the PR, opens a
follow-up ticket assigned to the overrider, counted weekly
Four blocking conditions. Three of them are structural rather than judgemental, which is deliberate: structural rules are unambiguous, so people do not argue with them, and they do not create the “the gate is wrong about my work” resentment that kills adoption.
Rolling it out without the day-fourteen death
Start in routing mode only, with no blocking at all, for two weeks. Watch how often the ticket would have been routed. That number tells you the shape of your problem before you inflict anything on anyone.
Turn on the structural blocks first. No linked ticket, no criteria at all. These are uncontroversial and fast to fix, and they build the habit of the gate being something you clear rather than something you fight.
Add the high-risk paths next, with the path list agreed in a room rather than imposed.
Add BLOCKING tags last, because they require the specifier habit from the previous article to exist first. A blocking tag on a ticket where nobody thought about which criteria matter is just a uniform gate with extra steps.
And publish the override rate from the first week, so that when it rises, the conversation is about calibration rather than about compliance.
Where this breaks down
Routing only works if anyone looks at the review column. The entire argument for routing over blocking rests on “moves to review” being a meaningful state that a person acts on. On plenty of teams, review is where tickets go to accumulate. If nothing pulls from that column, routing is functionally identical to doing nothing, except that it feels responsible. Before preferring routing, be honest about whether your team has a review ritual that actually runs. If it does not, the choice is not routing versus blocking, it is blocking versus nothing.
Self-service override is genuinely risky in regulated contexts. I argued for it above and I stand by it for most teams, but if you are in an environment where a control has to be demonstrably effective to an auditor, “anyone can bypass this by typing a sentence” is a control that may not survive scrutiny. There the answer is a narrower blocking list with no override rather than a broad list with an easy one, and that is a real constraint rather than a failure of nerve.
Blocking on unticked boxes assumes the boxes mean something. Everything here inherits the honesty problem from earlier in this series. If your team ticks boxes in bulk before opening the PR, the gate passes every time and prevents nothing, while producing an audit trail that says the definition of done was verified. A gate built on dishonest inputs is worse than no gate, because it converts a known unknown into a false certainty. No configuration fixes this.
And there is a decent case that criteria gates are the wrong control point altogether. Tests, type checks, contract tests, canary deploys and fast rollback all act on the actual code rather than on a description of it, and they cannot be satisfied by editing a ticket. A team with strong automated verification and ten-minute rollback might reasonably decide criteria gates add process without adding safety, and put the effort into the pipeline instead. I think criteria gates earn their place because they catch the class of problem tests cannot, which is “this does not do what was asked”, not “this is broken”. But if you have limited appetite for process, and you must choose, a good test suite beats a good gate.
The takeaway
Design the gate around the outcome you are preventing, which is invisible incompleteness, not incompleteness itself. That single reframe does most of the work, because it moves the default response from blocking the merge to refusing to mark the work done.
Keep the blocking list to four or five conditions, weighted toward structural rules and declared high-risk paths rather than blanket criteria coverage. Route everything else to review and stay silent on the rest. Make the override self-service, written, logged and counted, and treat a rising override rate as feedback about your gate rather than about your team.
A gate people clear without thinking about it, that stops the two things that would really hurt, is worth more than a comprehensive one that gets switched to advisory in a fortnight.
The next piece looks at the technique that promises to make more of this checkable automatically: verifying prose criteria against the actual diff, what it genuinely does well, and the specific places where a confident answer would be a lie.