How to Merge Work From Parallel AI Agents
Five agents finish. Five branches. Each one passed its own tests in its own worktree. This is the moment a parallel workflow either pays off or quietly gives back the time it saved.
Integration is its own phase, with its own failure modes, and two of them catch people repeatedly:
- Branches that each merge cleanly into
maincan still conflict with each other. Testing every branch againstmaintells you less than it appears to. - Green plus green does not equal green. Two branches that each pass the full suite can produce a merged
mainthat fails, without a single merge conflict.
This guide covers what happens between “the agents are done” and “it’s on main” — assuming you have already split the work into independent tasks and given each one its own worktree.
Check which branches conflict with each other, before you merge any
Start with the check that costs nothing. git merge-tree performs a real merge and reports the result without touching your working tree, your index, or HEAD. (It does write the merged tree and any conflicted blobs into the object database — it just never moves your branch or your files.) It exits 0 for a clean merge and 1 when there are conflicts.
Ask which branches merge cleanly into main:
for b in task-a task-b task-c; do
git merge-tree --write-tree --name-only --no-messages main "$b" > /dev/null \
&& echo "clean: $b" \
|| echo "conflict: $b"
done
On a small repository where task-a and task-b both rewrote the first line of the same file and task-c touched something else entirely, that loop prints:
clean: task-a
clean: task-b
clean: task-c
Every branch merges cleanly into main. Now ask the other question — how the branches relate to each other:
for a in task-a task-b task-c; do
for b in task-a task-b task-c; do
[[ "$a" < "$b" ]] || continue
git merge-tree --write-tree --name-only --no-messages "$a" "$b" > /dev/null \
&& echo "clean: $a + $b" \
|| echo "conflict: $a + $b"
done
done
conflict: task-a + task-b
clean: task-a + task-c
clean: task-b + task-c
The conflict was invisible in the first check and obvious in the second, because merging task-a moves main out from under task-b. Three branches produce three pairs; five branches produce ten. Running the matrix takes seconds and turns integration from a sequence of surprises into a plan.
Three caveats worth holding onto. git merge-tree in this form needs Git 2.38 or newer — the output above was checked on Git 2.43. It detects textual conflicts only, so it cannot tell you that two branches both compile and then disagree at runtime, which is the failure mode the rest of this article is mostly about. And the loops above treat any non-zero exit as a conflict, so a typo in a branch name shows up as conflict: rather than as an error — check the spelling before you trust a surprising result.
Step 1: Take stock before you merge anything
Before the first merge, get the footprint of each branch:
git diff --name-only main...task-a
The three-dot form lists what the branch changed relative to where it diverged, rather than every difference between the two tips. Do this for each branch and you have the map: which branches are narrow, which are wide, and which pairs overlap.
Combined with the conflict matrix, this tells you the shape of the integration before you are committed to it. If two branches overlap heavily, that is a decision to make deliberately, with the diffs in front of you, rather than halfway through a merge.
Step 2: Choose the merge order
Merge order is a decision, not an accident. Every merge invalidates the base of every branch you have not merged yet, so you are choosing who pays the rebase cost. A reasonable default ordering:
- Anything that changes a shared contract. Types, schemas, API shapes, shared interfaces. Everything else consumes these, so they belong on
mainfirst. This is the merge-time half of the contract-first pattern. - The branch with the widest file footprint. Land the sprawling change while the others are still small and easy to rebase, rather than rebasing it later against everything else.
- Narrow, isolated branches. These are cheap in any order.
- The most speculative work last. If something is going to be abandoned, you want to find that out before it has been rebased three times.
Step 3: Merge one branch, then re-establish the baseline
Merge one branch. Then run the full suite on the merged result, not on the branch you just merged.
git merge --no-ff task-a
npm test
--no-ff keeps each integration as a single merge commit, so a bad one is a single git revert -m 1 <merge-sha> rather than an archaeology exercise across five interleaved histories.
Running it on the result rather than the branch is what catches semantic conflicts — changes that merge without a single marker and are still wrong together:
- One branch renames a helper and updates its three callers; another branch adds a fourth caller under the old name. Different files, clean merge, broken build.
- Two branches each register a route, a CLI flag, or an event handler under the same name. Both files merge; at runtime one silently shadows the other, or both fire, depending on the framework.
- One branch tightens a validator; another relies on the input the validator now rejects.
- One branch bumps a dependency to a version whose behavior the other branch’s new code depends on not changing.
- Two branches each add a database migration. Both files land; the order they now run in is not the order either agent assumed.
None of these are Git’s problem. Git merged the text correctly. They surface only when the merged result is exercised, which is why the loop is merge, verify, then merge the next one rather than merge-everything-then-test. When five merges land before the first test run, you know only that something among five changes is wrong.
If the integrated result is red, fix it or revert it now, while it is one merge commit and the context is fresh.
Step 4: Rebase the remaining branches onto the new main
Once main has moved, every other agent’s worktree is working from a stale base. Bring each remaining branch forward before you merge it:
git -C ../task-b rebase main
Pause the agent working in that worktree, and let it commit or stash first. Changing the branch underneath a running agent produces confusing failures and, occasionally, lost work.
Then the question that actually matters: who resolves the conflict?
| Conflict type | Example | Who should resolve it |
|---|---|---|
| Textual and mechanical | Both branches added an import or an entry to the same list | The agent that wrote the branch, in its own worktree |
| Same-function, different intent | Both branches rewrote the same method for different reasons | You decide the target behavior, then an agent implements it |
| Semantic, no marker | Renamed symbol, duplicate route, incompatible validation | You — this is a design decision, not a merge |
Handing a mechanical conflict back to the agent works well, and works best when you give it the new base and the failing test rather than a description: “You are on task-b, rebased onto the updated main. npm test now fails with X. main renamed formatUser to formatUserLabel. Update this branch to match; do not revert the rename.”
Handing a semantic conflict to an agent tends to produce a plausible resolution that discards one side’s intent. That is the class of change worth reviewing carefully regardless of who wrote it.
The files that usually collide
Some files conflict on almost every parallel run, and for most of them merging the text is the wrong move:
| File | Why it collides | What to do |
|---|---|---|
Lockfiles (package-lock.json, poetry.lock) | Every branch that touches dependencies rewrites large regions | Resolve the manifest, then regenerate the lockfile |
| Generated code, snapshots, compiled schemas | Two branches regenerate the same artifact from different sources | Resolve the source, re-run the generator |
| Migration directories | Both agents add a new file; ordering is implied by name | Renumber or re-timestamp after merging, then verify the sequence runs |
Barrel files and central routers (index.ts, route tables) | Every feature appends to the same list | Take both sides; it is usually an additive conflict |
CHANGELOG.md, i18n catalogs | Everyone appends at the same place | Take both sides, then sort |
The general rule: for anything with a generator, resolve the input and regenerate the output. Hand-merging a lockfile produces a file that describes a dependency tree no tool would have produced.
Reuse conflict resolutions with git rerere
Parallel work makes you resolve the same conflict repeatedly, because several branches each get rebased onto a main that keeps moving. Git can remember:
git config --global rerere.enabled true
rerere (“reuse recorded resolution”) records how you resolved a given conflict and replays that resolution the next time it sees the same one. For a five-branch integration where three branches all collide with the same barrel file, it converts three identical resolutions into one.
It reapplies a resolution without asking you to confirm it, though. Git notes Resolved '<file>' using previous resolution. and still stops with the path unmerged so you can inspect it and git add — but a resolution you got wrong the first time comes back wrong. Keep reviewing the result rather than trusting that a familiar conflict resolved itself correctly.
When to throw a branch away instead of merging it
There is a cost asymmetry that is easy to miss: re-running an agent task is cheap, and your merge attention is not. A branch that needs forty minutes of conflict surgery is often cheaper to delete and re-run as a fresh task from the updated main, where the agent starts from code that already contains everything the other branches added.
Signals that a branch is worth discarding rather than rescuing:
- The conflicts span design decisions rather than text.
mainhas moved enough that the branch’s approach no longer matches the surrounding code.- The branch is one of several attempts at the same task, and another attempt is closer — the normal outcome when you are racing agents on one task.
- Resolving it requires you to reconstruct what the agent intended.
Re-running a task does cost tokens, and running redundant work is one of the things that makes parallel agents more expensive than sequential ones. Weigh that against the review time — but do weigh it, rather than treating every finished branch as something that must be landed.
Make the next integration cheaper
Most integration pain is created earlier, in the split. A few habits that pay off:
- Merge as things finish, not in a batch. Five branches merged over an afternoon are five small integrations. Five branches merged on Friday are one large one.
- Keep branches short-lived. The conflict surface grows with how far the branch’s base has drifted.
- Give hot files a single owner. If three tasks all need to touch the central router, that is one task, or a contract change that lands first. The same applies to shared instruction files —
AGENTS.mdis a contract every agent reads. - Use an integration branch for large fan-outs. For more than about four branches, merge them into a staging branch, verify the combined result there, and merge the verified result into
mainonce.mainstays releasable throughout.
The deeper fix is upstream: tasks that split along seams rather than steps produce branches that merge in any order.
Where Parallel Code fits
Disclosure: Parallel Code is our product. Everything above is plain Git and works the same whether you orchestrate agents with a GUI, a terminal multiplexer, or several shells.
Parallel Code gives each task its own branch and worktree automatically, so the integration phase starts from the structure this article assumes: one reviewable branch per task, each with a diff against its base. Review happens on the diff before anything is merged, and you merge one task at a time from the app rather than tracking which worktree is where.
What it does not do is remove the verification loop. Re-running the suite on the merged result, deciding the merge order, and judging semantic conflicts are still yours — worktree isolation prevents concurrent edits to one checkout, not incompatible changes to one codebase.
- Install from the latest release
- Run the conflict matrix before you start merging
- Merge one branch, verify the result, then rebase the rest
Frequently asked questions
Do git worktrees prevent merge conflicts?
No. Worktrees prevent two agents from writing to the same working copy at the same time. Each agent still edits its own copy of shared files, and those edits still have to be reconciled at merge time. Isolation solves the checkout problem; task boundaries solve the conflict problem.
In what order should I merge agent branches?
Shared-contract changes first, then the branch with the widest file footprint, then narrow isolated branches, then anything speculative. Each merge moves main and forces the remaining branches to rebase, so you are choosing which branches pay that cost.
Should I let an AI agent resolve merge conflicts?
For mechanical conflicts — duplicate imports, both-added list entries, formatting collisions — yes, and it works best if you hand the agent the rebased branch plus the failing test rather than a description. For conflicts where two branches implemented incompatible designs, decide the outcome yourself first; an agent will otherwise pick one side plausibly and silently drop the other’s intent.
Why did my tests pass on every branch but fail after merging?
Because each branch was tested against the old main, not against the other branches. Renames, duplicate registrations, tightened validation, and migration ordering all merge cleanly and fail together. Run the full suite on each merged result, not on the branches.
Is an integration branch worth the extra step?
For two or three branches, usually not — merge them into main one at a time and verify after each. For larger fan-outs it is worth it, because it keeps main releasable while you work through a combined result that may need several fixes.
How do I check whether two branches conflict without merging them?
git merge-tree --write-tree --name-only --no-messages <branch-a> <branch-b> performs the merge in memory and exits 1 if there are conflicts, without touching your working tree, index, or HEAD. Requires Git 2.40 or newer.
Integrate as you go
The reason integration feels disproportionately painful after a parallel run is that it gets treated as cleanup rather than as a phase with its own plan. It has one: check the matrix, pick an order, merge one branch, verify the result, bring the rest forward.
Do that with two branches until the loop is routine. The habits that make it work — verifying merged results rather than branches, deciding order deliberately, and discarding a branch when rescuing it costs more than re-running it — are the same at two branches and at ten. Only the arithmetic changes.