The file-disjoint rule: how to dispatch 10 LLM coding agents in parallel without merge pain
Parallel coding agents don't bottleneck on compute — they bottleneck on the merge. Two agents touching the same file cost more than one agent doing both edits. The fix is a partitioning rule we wish we'd adopted earlier.
AppX team ·
We run an AI coding team — a coordinator that fans tasks out to 5-12 LLM agents (Claude Code, Codex, aider, whatever) and merges the results back into one branch. The first thing you discover is that the bottleneck is never compute. Anthropic and OpenAI will happily sell you parallel tokens. The bottleneck is the merge.
If two agents touch the same file, you get a three-way merge between two LLM diffs and a base. The conflict markers don't resolve cleanly — both sides rewrote imports, both sides added a helper, both sides reformatted a closing brace. A human (or worse, a third agent) now has to re-read both diffs, reconcile intent, and re-run tests. You just paid for two agents and got the throughput of one and a half.
The rule that fixed this for us is embarrassingly simple: partition the work by file. If you can guarantee agent A's write-set and agent B's write-set are disjoint, you can run 10 agents in parallel and the merge is a concatenation. No conflict resolution, no re-cost, no taste arbitration.
The dispatch shape
flowchart TD
T["Task"] --> SP["Split by FILE, not by feature"]
SP --> A1["Agent 1 · files a, b"]
SP --> A2["Agent 2 · files c, d"]
SP --> A3["Agent 3 · files e, f"]
A1 --> MG["Merge"]
A2 --> MG
A3 --> MG
MG --> OK["No overlap → no conflicts"]
style OK fill:#13241a,stroke:#2ecc71,color:#d7f7e3
The coordinator's job is to take a task list and produce a partition. Pseudocode:
type Task = { id: string; description: string; files: string[] };
function partitionDisjoint(tasks: Task[]): Task[][] {
const buckets: Task[][] = [];
const owned = new Map<string, number>(); // file -> bucket index
for (const t of tasks) {
const collisions = new Set(
t.files.map(f => owned.get(f)).filter(i => i !== undefined),
);
if (collisions.size === 0) {
// New bucket — no file overlap with anyone running
const idx = buckets.push([t]) - 1;
t.files.forEach(f => owned.set(f, idx));
} else if (collisions.size === 1) {
// Single owner — bundle into their bucket, they run both tasks
const idx = [...collisions][0]!;
buckets[idx].push(t);
t.files.forEach(f => owned.set(f, idx));
} else {
// Multi-collision — serialize. Don't dispatch yet.
t.deferred = true;
}
}
return buckets.filter(b => b.length > 0);
}
Three rules fall out of this:
- Zero overlap → fan out. Each bucket becomes one agent. 12 buckets = 12 parallel agents.
- Overlap with one existing bucket → bundle. Two small tasks on the same file become one agent doing both. The agent reads the file once, applies both changes, writes once. Strictly cheaper than spawning two agents that race.
- Overlap with multiple buckets → serialize. The task touches a hot file that several agents need. Hold it. Run it after the first wave finishes, when the file is stable.
The bundling rule (#2) is the one that surprises people. The intuition is "more agents = more parallel = faster." Wrong. Two agents writing to the same file is negative throughput versus one agent doing both edits, because you pay for the merge.
When this fails
The file-disjoint rule has three real failure modes. None are theoretical.
Shared types files. If you have a types.ts or schema.ts that every feature touches, partition collapses. Every agent wants to add one field. You can't bundle ten tasks into one agent because the agent's context window fills up with unrelated work and it loses the plot around task six.
The mitigation is to split shared type files aggressively. One file per domain, re-exported through a barrel. The barrel itself is hot — see below — but the underlying domain files are cold.
Barrel exports (index.ts). Every new module wants a line in the barrel. If you have ten agents adding ten modules, all ten want to edit index.ts. We treat barrel files as a coordinator-owned resource: agents write their module, the coordinator appends to the barrel in a final serialized step. Don't let the agents touch it.
Refactoring a shared interface. If the task is "rename User.email to User.primaryEmail across the codebase," there is no partition. Every call site has to change in lockstep. This is the case where you serialize: one agent, full context, atomic commit. Don't try to be clever — the merge from two agents doing a rename across overlapping files is unrecoverable in practice.
The smell is: if the task description contains the word "across" or "everywhere," it's serial.
How we structure tasks to be partitionable
Most of the work the coordinator does is upstream of dispatch. Before any agent runs, we produce a per-task file manifest. The plan step is mandatory — an LLM that's allowed to discover files mid-task will inevitably wander into hot files and break the partition.
Concretely, every task in the queue looks like:
{
id: "add-export-csv-button",
description: "Add CSV export button to reports page...",
files: {
write: ["src/pages/reports/ExportButton.tsx",
"src/pages/reports/index.tsx"],
read: ["src/lib/csv.ts", "src/types/report.ts"],
},
}
The write set is what the partitioner uses. The read set is informational — multiple agents can read the same file without conflict. If an agent tries to write a file outside its declared write set, the coordinator rejects the diff and re-queues the task. This sounds harsh; it's the only thing that makes the system stable past 4 or 5 concurrent agents.
Takeaways
- The merge is the bottleneck, not compute. Two agents on one file is slower than one agent doing both edits.
- Partition tasks by their write set, not their description. Disjoint writes = safe parallel.
- Bundle multiple small tasks on the same file into one agent. It's strictly cheaper.
- Serialize anything that touches "shared" surface — types, barrels, cross-cutting renames.
- Require an explicit file manifest per task before dispatch. Reject diffs that escape the manifest.
- If you can't predict the write set ahead of time, you can't parallelize safely. Plan first, fan out second.