35 Worktrees, 12 Agents, Zero Merge Conflicts
Parallelism gets agents working at the same time. Choreography is what keeps their work from eating itself alive when they finish.
View companion repoTwelve AI agents. Thirty-five git worktrees. One codebase modified in thirty-five places at once. The merge took ninety seconds. Zero conflicts.
That was the second attempt. The first? Twenty-three conflicts. Three hours of manual untangling.
Post 6 covered how to run 194 parallel agents with zero conflicts at the worktree level — spinning up worktrees, scoping tasks, keeping agents from stepping on each other during execution. This post is about what happens after they all finish. Parallelism gets agents working at the same time. Choreography is what keeps their work from turning into a three-hour merge disaster when they’re done.
The Three-Hour Merge From Hell
The project looked simple: build a three-platform auth system (iOS, web, API) with agents in parallel. Across 23,479 sessions (4,534 human-initiated, 18,945 agent-spawned), I’d used TaskCreate 2,182 times, SendMessage 1,720 times, TeamCreate 128 times. I understood agent orchestration. I didn’t understand merge orchestration.
I spawned twelve agents, each in its own worktree. The coding took four hours. Every worktree compiled in isolation. Every feature worked when tested independently.
Then I started merging.
Twenty-three conflicts across shared files. package.json modified by six agents adding dependencies. types.ts extended by four agents adding interface definitions. The API routes index touched by three agents registering endpoints. The code was clean. The coordination wasn’t.
During manual resolution, I introduced two regressions by picking the wrong side of a conflict in a file I hadn’t fully read. One was a type definition where Agent 4’s version had a field Agent 7’s code depended on — I kept Agent 7’s longer version. The other was a dependency version mismatch: I took Agent 2’s package.json additions but Agent 9 had specified a different version of the same package, for reasons the build broke to remind me of twenty minutes later.
“Twelve agents writing into the same codebase produce twelve correct walls and one ruined floor: each piece works in isolation, the seams are what fail.”
The Choreography Problem
Post 6’s parallelism answer — give each agent its own worktree and task scope — is necessary but not sufficient. Two problems remain the moment execution ends:
- 01Which files can each agent touch? Agents working in parallel reach for shared infrastructure files — package.json, type indices, route registries — because that’s where integration happens. Without explicit ownership, everyone touches everything.
- 02In what order do branches merge? Dependencies run one way. Auth defines types that media consumes. Media defines endpoints that the web client calls. Merging in the wrong order produces a codebase that compiles after all merges complete but fails at intermediate steps, making regression bisection impossible.
The choreography layer answers both before a single agent writes code.
File Ownership as the Core Primitive
Before any agent writes anything, every file in the project gets assigned to exactly one agent using glob patterns. Here’s the ownership matrix from the 35-worktree auth build:
The agent-integrator role is the insight that makes this work. Every multi-agent project has shared files that no feature agent should own: package.json, the types index, the route registry, CI config. Those belong to a dedicated integration agent that runs last. It reads feature agent outputs (dependency manifests, type declarations, route definitions) and assembles the shared files. No feature agent touches package.json directly — dependency additions go through a manifest the integrator consumes.
The Python implementation in the companion repo:
Validation runs at task-assignment time, not merge time:
The violation fires before the agent writes anything. Not “you have a merge conflict” but “you’re about to create one.” Prevention beats detection by hours.
I also built an owner_of method that resolves any file path back to its owning agent, and an unowned_files scanner that catches coverage gaps before work begins. Unowned files are how conflicts sneak in — they sit in no-man’s-land until two agents both decide they need to modify them.
Conflict Prediction: Catching Problems Before They Exist
The ownership matrix prevents intentional overlap. Conflict prediction catches accidental overlap — where two agents modify the same file despite the ownership rules, usually because a glob pattern is too broad or a shared utility wasn’t identified during planning.
The predictor compares the diff output of every worktree pair:
With thirty-five worktrees, that’s 595 pairs to check (35 choose 2). It runs in under two seconds because it only compares file paths from git diff --name-only, not contents. During the thirty-five-worktree merge, this prediction pass caught two issues: a shared utility function modified in two worktrees (an ownership violation that slipped past initial validation because the glob pattern covered two overlapping directories) and a type rename in one worktree that broke imports in another.
Both got fixed in minutes by adjusting ownership boundaries. Buried in a wall of conflict markers during merge, they would’ve cost an hour each to untangle.
Topological Merge Ordering
File ownership prevents conflicts. But even without conflicts, merge order matters.
Here’s the algorithm. Five steps:
- 01Build a dependency graph where each node is a branch and edges point from dependency to dependent (auth → web means web depends on auth, so auth merges first).
- 02Compute in-degree for every node — the count of branches that must merge before it can.
- 03Initialize a queue with all zero-in-degree branches (no dependencies, safe to merge immediately).
- 04Pop a branch from the queue, merge it, then decrement the in-degree of everything that depended on it. Add newly-zero branches to the queue.
- 05If any branches remain after the queue empties, they form a cycle — a circular dependency that must be broken before merging can proceed.
The Python implementation:
The circular dependency check isn’t optional. On one build, Agent 5 declared a dependency on Agent 8, and Agent 8 on Agent 5. Neither was wrong — they had genuinely interdependent components. The fix was extracting the shared interface into a new foundation branch owned by a third agent. The topological sort caught this at planning time. Without it, I’d have discovered the cycle mid-merge when one branch couldn’t compile without the other.
The merge order reads bottom-to-top on the dependency graph, top-to-bottom in execution: database schema first, then auth and media services, then platform clients, then the integrator last. Each layer depends only on layers below it. The integrator runs last because it consumes outputs from every other agent.
The Full Merge Choreography
execute_merge_choreography ties ownership, prediction, and topological ordering into a single automated pipeline. It merges each branch in dependency order and runs a build check after every merge:
The run_build_check() after each merge isn’t optional. If the auth service breaks the build, the media service shouldn’t merge on top of a broken foundation. Each merge step produces a working codebase or rolls back and reports the exact component that caused the failure.
When a conflict does slip through (usually shared configuration the ownership matrix didn’t fully capture), _resolve_with_ownership handles it. If the file belongs to the currently merging agent, take their version. If it belongs to an agent whose branch already merged, keep the merged version. If nobody owns the file, abort and report. This handles ninety percent of residual conflicts without me touching anything.
The 9-PR Session and the Ripple Rebase
The ownership model scales. A single session where I created and merged nine pull requests (#8 through #16), managing twenty-one active worktrees. Each worktree had its own branch, each branch owned distinct files, each merge followed topological order.
During this session I discovered the “ripple rebase” pattern. When you merge PR #10, branches #11 through #16 all need rebasing against main. The ripple approach rebases in dependency order: if #12 depends on #11, rebase #11 first, then #12 against the rebased #11. Each conflict resolved once, in the right order, propagating cleanly.
Per-file conflict resolution recipes reinforced the ownership model. When a conflict appeared in shared configuration, the strategy was documented per file type:
- 01package.json conflicts: Take the union of dependencies. If version conflicts exist, take the higher version.
- 02tsconfig.json conflicts: Take the more permissive compiler options.
- 03index.ts route files: Concatenate the registrations in alphabetical order.
- 04.env.example conflicts: Take the union of environment variables.
I wrote each recipe when I created the ownership matrix, not when the conflict appeared at 2 AM. No judgment calls during merge.
The expires_at War Story
Three bugs. Two platforms. Six hours of debugging. Four-minute fix.
The authentication system generated JWT tokens with an expires_at field. The API returned 2026-03-06T14:30:00.000Z (ISO 8601 with fractional seconds). The iOS app parsed this with Swift’s ISO8601DateFormatter. The web app parsed it with Zod’s z.string().datetime(). Both worked in isolation.
In production, the API sometimes returned 2026-03-06T14:30:00Z (no fractional seconds) and sometimes 2026-03-06T14:30:00.000Z (with). The difference depended on whether the millisecond component was zero. Swift’s default ISO8601DateFormatter doesn’t handle fractional seconds. Zod’s default datetime validation rejects timestamps without fractional seconds when configured to expect them.
Bug 1: iOS token expiry check fails silently when fractional seconds are present, treating every token as expired. Bug 2: Web app rejects valid tokens when fractional seconds are absent. Bug 3: token refresh loop between iOS and web. Each platform’s “valid” format is the other’s “invalid.” The bugs locked into each other.
Here’s the fix. Four minutes. Add precision to the OpenAPI spec:
This shows what file ownership can’t catch. Ownership prevents file conflicts. It doesn’t prevent semantic conflicts. The expires_at bug involved three agents modifying three different files — no overlap, no merge conflict, no ownership violation, but code that broke at the protocol level. Ownership handles mechanics. Only precise specs handle semantics.
The Numbers
The thirty-five-worktree merge went from 150 minutes of sequential development to 32 minutes of parallel build with orchestrated merging — 4.7x speedup. The three-platform auth build went from four days to fourteen hours.
Across 23,479 sessions: 128 TeamCreate calls spawning coordinated agent groups, 1,720 SendMessage calls for inter-agent communication, 2,182 TaskCreate calls dispatching work. The 18,945 agent-spawned sessions represent work that would’ve been sequential without the orchestration infrastructure. The merge orchestrator is what makes that parallelism safe.
Here’s the CLI:
What Breaks This Pattern
The ownership model isn’t universal. Here’s what I’ve hit.
Shared configuration that doesn’t decompose. A monorepo’s root tsconfig.json affects every agent’s compilation. The integration agent pattern handles this but adds a serialization point. Feature agents finish before the integrator assembles shared config. For heavy shared configuration, that serialization becomes the bottleneck.
Implicit runtime coupling. Agent A adds a global event listener. Agent B adds another. Neither imports the other’s code, but they interact at runtime through the event bus. The ownership validator can’t catch what it can’t see in the import graph. These bugs surface during integration testing, after the merge. I haven’t found a way to catch them statically.
Late-discovered scope expansion. Agent 3 discovers mid-implementation that it needs to modify a utility owned by Agent 7. The ownership system correctly blocks this. The fix requires human intervention to reassign ownership. Happens on one in five multi-agent builds — thirty minutes up front still saves three hours of conflict resolution, but doesn’t eliminate the fifteen-minute reassignment conversations.
Branch drift. The longer an agent works in its worktree, the more the integration branch diverges. The thirty-five-worktree merge worked because I scoped each task to under ninety minutes. At four-hour agent tasks, drift accumulates enough to produce integration failures even with clean ownership. Keep it short.
Specs that lie. The expires_at story is spec imprecision. “date-time” isn’t a contract. “RFC 3339 with mandatory fractional seconds matching pattern X” is. File ownership prevents mechanical conflicts. Only precise, enforced specifications prevent semantic ones.
What I’d Build Differently Now
Thirty minutes drawing ownership boundaries up front beats three hours of conflict resolution later. The system isn’t complicated: an ownership map, a topological sort, a build-verify loop. The Python implementation is under 400 lines.
The real lesson is the mental model. Multi-agent parallel development isn’t “give everyone the codebase and merge at the end.” It’s “partition the codebase, enforce the partitions, order the integration, verify at every step.” Parallelism gets you the speed. Choreography gets you the merge that doesn’t cost you everything you saved.
“Ninety seconds versus three hours, on a run that scaled from collapse-at-five worktrees to thirty-five running in parallel without a single conflict.”