Kai Hirota

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.

Change flow: an agent pushes to its fork and gets a reply at once; the integrator merges and runs the tests; a clean, passing change advances trunk; otherwise the conflict is queued, a resolver claims it and its fix takes the same path, and contradicting goals or two failed fixes go to a person.

Five Steps, Each Costlier Than the Last#

Each step catches what the earlier ones could not:

  1. 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.
  2. Go next. A warned agent can wait for the others, rebuild on the new trunk, and merge cleanly.
  3. 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.
  4. 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.
  5. 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 goals

The 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 #N

Every 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.

Components: agents push to their own Artifacts forks and call the gateway Worker; push events reach the integrator Durable Object through a Queue; the integrator holds in-flight changes, the coordinator, the conflict queue and the resolver; its container fetches forks, merges and tests, and is the only writer of trunk; the resolver calls Workers AI; the dashboard shows live state.

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.

The Spore board near the end of a run: Done (Trunk) with a resolved change rejoining it, and in Review an escalated conflict between two rate limits waiting for Keep trunk or Take incoming.

  • 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:

  1. Three agents changing the same lines of setFlag are warned about each other, and so are two setting the rate limit. The agent changing listFlags in the same file is not.
  2. One setFlag agent goes next: it waits for the other two, rebuilds on the new trunk, and merges cleanly.
  3. 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.
  4. Seven changes merge, and two clashes become records.
  5. 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#

ComponentProduct
Trunk and per-task forks, repo-scoped tokensArtifacts
Push detectionArtifacts event subscriptions + Queues
Integrator state, serialized merges, resolver alarmDurable Objects (SQLite)
Merges, inspections and test runsContainers
Conflict resolution and contradiction checksWorkers AI
Gateway API and dashboardWorkers

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.