The day in numbers
On 24 October, main took 38 squash merges. They came from four lanes running in parallel: clinical release blockers, the clinical data platform, the arcade's phone work, and CI and test debt. Each lane is a coding agent working through its own queue of issues. I set the queues, make the product calls, and press the buttons only a person should press.
That throughput did not come from the agents being fast. It came from a rule about how their work reaches main, and the rule came from a bad evening the day before.
What stacking cost
That evening I let one wave of work land as a stack: each pull request based on the branch of the one below it, so they would merge in order. It looks tidy. It went badly, for reasons that are specific to this repository's setup and are probably true of yours:
- Nothing above the bottom of the stack got CI. The workflow runs for pull requests into
main. A pull request into another branch ran no gates at all, so the stack was green the way an empty test suite is green. - Squash merges into intermediate branches leave add-versus-nothing conflicts that have to be resolved by hand, and every resolution is a chance to drop someone's change.
- Closing keywords only fire on a merge into the default branch, so issues stayed open after their work had merged.
- The history stopped describing itself. When the stack collapsed, the commit on main titled for Dataset-JSON, CSV and XPT exports also carried the arcade's stage-fitting layout and its tests. Anyone bisecting a layout regression later lands on a commit whose message is about clinical exports.
The rule: one slot
Since then every pull request is cut from main and targets main. Work stays in draft until its quick checks pass. Only one pull request at a time may be marked ready: the one holding the slot. Whoever takes the slot merges the current main into their branch, reruns the fast checks if main moved, pushes, marks it ready and turns on auto-merge once. When it lands, the next one moves up.
A few files are touched by nearly every feature: the command palette, the navigation data, the sitemap, the route metadata, package.json, the CI workflow and the README. Those take a lock in the lane file before anyone edits them, so two lanes never find the conflict at merge time.
It is a merge queue, written in Markdown, which every agent reads before any pull request action. It is slower per pull request than letting everything race. It is much faster per day, because almost nothing has to be done twice.
What the slot does not protect
Serialising merges does not serialise resources. Two pull requests that day, Duck's Halloween mode and the new study data page, each passed every gate on its own and failed together. The cold type check needed about 2.54 GB of live heap, and CI's budget was 2,560 MB. Neither change was large. The program was simply around 5,800 files by then, and those two happened to be the pair that crossed the line.
Both ceilings went up by 512 MB, with the CI budget still 512 MB below the production build's, so growth still fails a pull request before it can reach a deploy. Before raising anything, the obvious suspects were measured. Leaving the tests out of the program dropped memory from about 3.0 GB to 2.0 GB. Stubbing a 7.6 MB icon declaration file down to the 262 icons actually used made no difference that rose above the run-to-run noise of about 300 MB. That second result saved a refactor nobody needed.
A budget gate is a shared resource. Parallel lanes spend it together, and the lane that crosses it is rarely the one that grew it most.
Two smaller lessons
Push everything before you turn on auto-merge. The metric registry merged while its governance commit was still being written, because auto-merge does exactly what it says. The follow-up became its own pull request from main. Now a branch is finished before auto-merge goes on, or the next change is a fresh pull request.
A shared types file is a serial resource too. Every platform pull request added to the same types file and the same table in the architecture decision record. One evening I marked three of them ready and turned on auto-merge for all three at once. Each merge forced the other two to merge main again and resolve the same two files. Resolving additively (keep both type blocks, keep both table rows, regenerate the docs) made it safe. Following my own rule would have made it unnecessary.
What I would tell someone starting
- Parallelise the building, not the merging. Agents in separate lanes are cheap; merges that race are not.
- Do not stack branches unless CI runs on every base. Check that before you trust a green tick.
- Write the lane rules where the agents read them before acting, and keep each edit small and dated.
- Treat every budget CI enforces, whether memory, time or bundle size, as shared by every lane, and measure before you raise one.
- Name the files everyone touches, and lock them.
What Agent-First Tooling Actually Requires covers the guardrails each agent works inside. This post is about what happens when several of them work at once.