Coding Agents Sharing One Branch: 2,818 Merges in 30 Days
Twenty-plus agents merge into the same integration branch, about 94 pull requests a day. Here is what broke when they collided, which files they fight over, and the rules and checks that made it work.
Our coding agents all merge into one integration branch of one Rust monorepo. Over the 30 days to 22 October, 2,818 agent pull requests were squash-merged into it, an average of 94 a day, with a peak of 197 on 3 October. There were merges on every one of those days.
Show the data
| PRs | |
|---|---|
| Sep 23 | 29 |
| Sep 24 | 20 |
| Sep 25 | 62 |
| Sep 26 | 63 |
| Sep 27 | 55 |
| Sep 28 | 40 |
| Sep 29 | 41 |
| Sep 30 | 42 |
| Oct 1 | 93 |
| Oct 2 | 103 |
| Oct 3 | 197 |
| Oct 4 | 182 |
| Oct 5 | 79 |
| Oct 6 | 123 |
| Oct 7 | 148 |
| Oct 8 | 117 |
| Oct 9 | 101 |
| Oct 10 | 110 |
| Oct 11 | 163 |
| Oct 12 | 153 |
| Oct 13 | 170 |
| Oct 14 | 125 |
| Oct 15 | 146 |
| Oct 16 | 41 |
| Oct 17 | 87 |
| Oct 18 | 75 |
| Oct 19 | 76 |
| Oct 20 | 65 |
| Oct 21 | 51 |
| Oct 22 | 61 |
At that rate, the usual advice about merge conflicts (talk to your teammates, keep branches short) does not apply. Agents do not talk to each other, and a branch that is an hour old may already be dozens of merges behind. Everything that kept the branch healthy had to be a rule the agent follows on every task or a check the tooling enforces.
Which files they fight over
Conflicts concentrate on a few files that every change has to touch. Counting the pull requests that touched each file over the same 30 days:
Show the data
| Pull requests | |
|---|---|
| Cargo.lock | 207 |
| An app runtime script | 147 |
| Terminal UI entry point | 120 |
| CLI subcommand list | 111 |
| Data engine internal API | 94 |
| Deploy snapshot test | 77 |
| CLI reference docs | 76 |
| Deploy service list | 75 |
The pattern is consistent. The lockfile changes whenever any crate gains a dependency. The CLI entry point holds one central list of subcommands. The deployment code holds one list of services, and a snapshot test is generated from that list. Every agent adding a command or a service edits the same lines, and two of them doing it at once will conflict even when their work is unrelated.
What went wrong first
Most of these happened in the first two months, when a handful of agents grew to twenty and the tooling had not caught up.
- Merges the forge reported but never landed. In July, a burst of pull requests showed as merged while their commits were missing from the branch. Concurrent merges were each computed against a stale tip and overwrote one another. Our history has 97 commits whose titles mark them as re-landing lost work.
- Work committed to already-merged branches. Our forge squash-merges and never re-opens a merged pull request. An agent that kept pushing to its branch after the merge produced commits that nothing would ever pick up. About 13 branches lost work this way before there was a rule against it.
- Conflict markers merged into the branch. In August, a stray
<<<<<<<marker in a test file made it into the integration branch and broke every UI build until the next fix. - Clobbered changes. One pull request resolved a conflict by keeping its own version of a file and erased type changes from a pull request merged shortly before it.
- A stale lockfile after a run of merges. Each pull request’s lockfile was valid on its own, but several landing together left the merged lockfile out of date, and every build that required an exact lockfile failed.
- Accidental bulk commits. An automated commit using
git add -Aswept in a 424MB binary, and another added 2,416 build artifacts. Together they made up about half of the repository’s pack size at the time.
The rules every agent follows
These live in the instructions every agent receives at the start of every session, so they apply whether or not the agent has seen a failure before.
- Start every task on a fresh branch from the current tip. Branching from a stale local copy caused chains of pull requests that conflicted with each other.
- Never commit to a branch whose pull request has merged. After a merge, the next change gets a new branch.
- One coherent pull request per task. Splitting a change into many small pull requests multiplies the chances of conflict on shared files. Splitting is only worth it when two parts touch the same registry file and need to land in order.
- Serialize changes to shared registries. Two pull requests that edit the same central list land one after the other, the first one first.
- Stage files by name. Never add everything in the working directory.
- No parallel working copies. Agents that split work into sub-tasks share one checkout and divide it by file, so two helpers never edit the same file.
The checks that enforce them
Rules in a prompt are followed most of the time. The failures that cost the most are now also blocked by our command-line tool, which agents use instead of raw git for these steps.
- Branch creation refuses a dirty working tree, and if the local copy of the integration branch has diverged, it saves it to a backup branch instead of overwriting it.
- Opening a pull request with an empty diff is refused. An empty diff almost always means the work already landed under another pull request.
- A merge is pinned to the exact commit that was tested. The merge request carries the expected head commit, so a push after CI or review cannot be merged under the earlier result.
- CI results are discarded if the head moved during the run. The tool posts a comment saying so and the pull request has to be tested again.
- Every merge is verified after the fact. The tool checks that the merge commit is actually on the integration branch, and if it is missing, it says to re-land on a fresh branch rather than push to the merged one.
- Lockfile freshness is checked before a build, so a stale lockfile shows up as a clear error instead of a confusing builder failure.
The lost-merge problem is the clearest result: 97 re-land commits before the merge verification and pinned-head checks, none in the last 60 days. In the same 60 days, only one pull request was reverted. Agents fix forward: when a merge breaks something, the next pull request repairs it.
What we would do from day one
- Make the merge tool check that the merge landed. Do not trust the forge’s word for it under concurrency.
- Pin CI status and merges to an exact commit, and throw away results for any other commit.
- Find your registry files early (the lockfile, central lists of commands, services or routes) and either split them so separate changes touch separate files, or serialize changes to them.
- Put the branch rules in the agent’s standing instructions, then add a tool check for each one that an agent breaks anyway.