ADR 0003 — gatehouse owns the merge
- Status: accepted (2026-09-06)
- Supersedes the merge-queue half of ADR 0002; everything else in 0002 stands
- Applies to:
ci/merge-queue.toml,ci/required-checks.txt,crates/ci-spec/**, branch protection onmain, the merge-queue ruleset
Context
ADR 0002 made the merge queue a modelled system: the constants that govern it are pinned in
ci/merge-queue.toml, the capacity theorem reasons about those constants, and
cargo xtask ci-spec live-parity reds when the pins and GitHub’s settings drift. What it did
not change is the thing underneath: a verdict is a row in GitHub’s database saying that some
runner reported success on some commit. Nothing about that row can be checked afterwards, so
the queue re-runs every gate on every group — which is why the pool saturates.
gatehouse replaces the verdict, not the runner. A gate declares its inputs, its environment, its capabilities and its command; running it mints a receipt signed inside the sandbox and appended to a transparency log; the queue verifies receipts and runs only what it has no receipt for. The receipt is checkable by anyone with the verifier and the log — the property GitHub’s row does not have.
Decision
gatehouse’s queue merges main. GitHub’s merge queue is switched off, and the single
required context gatehouse/required carries the roll-up of every Required gate at the tree
under review. The other contexts stay on their workflows and stay required; they move to
gatehouse gate by gate, each after its cold wall time is measured, and never before.
Three consequences are load-bearing, and each is checked:
-
ci/merge-queue.tomlnames an owner.owner = "github"means a ruleset carries themerge_queuerule that enforces the queue;owner = "gatehouse"means no active ruleset may carry one.live-paritydecides this in both directions: GitHub’s queue still running under a gatehouse pin is two queues merging one branch, and no queue at all under a GitHub pin means nothing builds a group. With a gatehouse pin the check is over every ruleset, not the pinned id, because a queue re-enabled under a new ruleset is exactly the drift that would otherwise go unseen. -
strictis true. Under GitHub’s queue, requiring a branch to be up to date before merging forced a rebase before every merge and was half of the fifteen-hour stall of 2026-09-04. Under gatehouse’s it is the opposite: it is what makes the tree a receipt was verified at the tree that actually gets merged, without building speculative merge trees for a queue one entry deep. The pin and the setting move together, andlive-paritycatches either moving alone. -
The merge is asserted after the fact. gatehouse merges through the API and then reads the merged commit back by content: if
tree(main)is not the tree its receipts were verified at, the queue pauses and a human resumes it. A merge nobody verified is a stop, not a warning.
What this does not claim
The capacity theorem (ci/lean/CiSpec/Capacity.lean) is about a queue with build concurrency
one and no competing runs. gatehouse’s queue has that shape, so the theorem still applies —
but it is now a claim about our queue, and the constants in ci/merge-queue.toml describe
that queue. Nothing here proves the gates themselves are right, and nothing here changes what
a Required gate is; it changes who decides that a Required gate held, and whether that decision
can be checked later.
Rollback
Restore the branch-protection contexts and re-create the merge_queue rule, then set
owner = "github" and strict = false. It is two API calls and a three-line revert, and the
required contexts on the workflows never moved, so nothing else has to come back with it.