6 min read1,386 words
Spore: Merge Conflicts as Queued Work for Agent Swarms
A Git integration layer on Cloudflare Artifacts where no agent ever stops on a merge conflict: clashes are predicted from real pushes, routed, and resolved by other agents.
Spore lets a swarm of agents change one codebase without any of them waiting. Every agent works in its own fork, pushes early, and submits when it is done. Agents learn which others are changing the same lines while they work, from what was actually pushed rather than from claims or locks. Clean changes that pass the tests merge into trunk straight away. Every clash becomes a queued record that a resolver agent fixes while the original agent moves on. A person sees only what no agent can settle.
I built it in response to Cloudflare's call to build the next Git platform on Cloudflare. Cloudflare's Artifacts supplies Git-compatible repositories that can be created and forked programmatically. The challenge was the layer above that: how agents coordinate, and how their changes get reviewed and merged.
Source on GitHub · Design notes
The Bet#
At swarm scale, conflicts are normal traffic. Spore treats them as work to route and to see coming, not as errors to stop on.
- The GitHub model: a conflict halts the author until someone rebases. With thousands of agents, most of the swarm would be waiting.
- The lock model: agents claim files up front, overlapping claims block, and one winner merges while the others are abandoned. Claims are guesses. Two agents editing different functions in one file still collide, and the losing work is thrown away.
- Spore: coordination comes from what agents actually push. Git decides what conflicts, down to the line. Agents are warned while they work, clean changes merge immediately, and every real clash is kept as a record that a resolver turns into a fix. No change is discarded and no author waits on a conflict.
Five Steps, Each Costlier Than the Last#
Each step catches what the earlier ones could not:
- Warn. Every push is trial-merged against the other agents' in-flight work. Agents changing the same lines learn each other's names and goals before either submits.
- Go next. A warned agent can wait for the others, rebuild on the new trunk, and merge cleanly.
- Ask, then drop or hand over. When a task's first push lands on lines another agent got to first, a coordinator asks each of them whether its change there is required. An optional change is dropped from its branch. If several agents need theirs, the one that started first continues and the others hand their whole tasks to it, so one agent makes the changes in sequence. A verdict is carried out only if the tests pass.
- Resolve. A clash that reaches trunk becomes a record. The resolver asks a model to rewrite the conflicted file so both goals hold, and the fix merges only if every goal's test passes.
- Ask a person. Goals the model judges contradictory go straight to a person, as do records the resolver gives up on. The person keeps trunk or takes the incoming change.
Who Is Touching This?#
The warning system is what lets agents avoid clashes instead of only cleaning them up afterwards.
Each fork gets an Artifacts event subscription that delivers its pushes to a Queue. The queue consumer hands each push to the integrator, which fetches the commit and lists every change: file, line range, and the enclosing function taken from git's hunk context. It then test-merges that commit against every other in-flight agent touching the same files with git merge-tree, which needs no checkout. A predicted conflict is recorded as a warning on both agents. Warnings are advice, never locks.
Because git decides, two agents editing different functions of the same file are never warned about each other. Any agent can also ask directly:
GET /touching?path=src/flags.js&symbol=setFlag
→ agents in flight on that function
→ conflicts waiting on it
→ recent merges, with their goalsThe Integrator#
There is one integrator per repository: a Durable Object with its own container. It takes submits one at a time and either merges them into trunk or files a conflict record. Trunk only moves forward.
submit(sha)
↓
sync trunk, fetch fork, three-way merge (in the container)
↓
├─ nothing new → already_in_trunk
├─ clean merge → run tests (including every agent's goal test)
│ ├─ pass → push merge commit to trunk → merged
│ └─ fail → record (kind: tests) → queued #N
└─ textual clash → record (kind: merge, blamed trunk task) → queued #NEvery agent ships a goal test with its change at test/goals/<task id>.test.js, so each later merge is checked against every earlier goal too. That turns out to be the key to trusting the resolver: a model that quietly picks one side of a contradiction fails the other goal's test, the record reopens, and a second failure escalates it to a person.
The integrator is the only writer of trunk. It mints a trunk write token for ten minutes per merge. Agents and resolvers only get write tokens for their own forks. Fork code runs its tests as an unprivileged user, git hooks are disabled, and a test run is killed after five minutes. A warm merge and test run on the sample project takes about 3 to 4 seconds.
The Resolver#
A queued conflict sets an alarm on the integrator's Durable Object, and the alarm works through the queue until it is empty. The resolver works in its own working copy in the container, outside the merge queue, so merges never wait on a model call. It forks current trunk, fetches the incoming change, and merges with conflict context from the common ancestor. Workers AI then gets both goals and the file with its conflict markers. It either rewrites the file so both goals hold, or answers ESCALATE: <reason>.
The fix is an ordinary submit, so it goes through the same merge and the same tests as any other change. One model decision takes about 3 to 25 seconds.
Separately, when two in-flight tasks clash, the integrator asks Workers AI in the background whether one change could satisfy both goals at all. If it couldn't, both cards say so, and a record between them goes straight to a person without a wasted resolver run.
The Board#
The dashboard is a single live page served by the Worker, fed by a hibernating WebSocket from the integrator.

- In Progress: one card per agent, with the functions its tasks change and who it would clash with. The coordinator's question and verdict appear on the card.
- Review: submitted tasks while they merge, and conflict records with both goals. An escalated record shows the reason and the buttons Keep trunk and Take incoming.
- Done (Trunk): each merge is a stop on the trunk line. A resolved conflict curves back onto it.
- Record view: the trunk side, the incoming side and the resolver's fix, side by side.
The Demo Run#
The video records a real run against a deployment. Nine scripted agents change a small feature flag service. Their edits are fixed so runs repeat, but the integrator, push events, merges, tests and model calls are all live:
- Three agents changing the same lines of
setFlagare warned about each other, and so are two setting the rate limit. The agent changinglistFlagsin the same file is not. - One
setFlagagent goes next: it waits for the other two, rebuilds on the new trunk, and merges cleanly. - A late agent starts on the health check another agent is already changing. Both say their change is required, so the late task is handed over and done in sequence.
- Seven changes merge, and two clashes become records.
- The resolver fixes one, and both goals' tests pass. It escalates the other, two rate limits that cannot both hold, to a person.
Built on Cloudflare#
| Component | Product |
|---|---|
| Trunk and per-task forks, repo-scoped tokens | Artifacts |
| Push detection | Artifacts event subscriptions + Queues |
| Integrator state, serialized merges, resolver alarm | Durable Objects (SQLite) |
| Merges, inspections and test runs | Containers |
| Conflict resolution and contradiction checks | Workers AI |
| Gateway API and dashboard | Workers |
Artifacts has no server-side merge, so the container does the git work. Artifacts push events can only be subscribed per repository, and the binding cannot create subscriptions, so the gateway subscribes each new fork through the REST API when it creates the task.
Limitations#
- There is one API key for every caller, so claim ownership protects against mistakes, not against a dishonest client.
- Each deployment has one trunk repository, and each repository has one resolver.
- Merges are serial per repository. Batching queued submits and bisecting on failure would be the upgrade for larger repos.
- A handover moves a whole task, never part of one.