← Graftv0.2.0Raw dataDeploy
Field report · Graft v0.2.0 ·

Graft vs Git under an agent swarm

Many agents built one web app in the same repository at the same time, once through Git and once through Graft, a server-side source control system for agents. Below: how Graft works, what changed in the bug scan, and the measured timing difference from scripted swarms of up to agents and from real Claude agents.

How Graft works

Git assumes every client keeps a full copy of history and works offline. Agents are always online, they make small changes, and many of them land on the same branch at once. Graft moves the work to the server. Agents read and search through an API, send a change with the revision they read, and the server creates the commit, rebases it if the branch moved, and lands it. Nothing is cloned, pulled or pushed.

Claude agents graft_* MCP tools, no checkout graft CLI workspaces sparse, fetch on demand People web viewer node cache ~/.graft/cache graft server one container · SQLite + blob dir JSON API commit+land · read · search · diff · blame · log · manifest · blobs changesets immutable write-ahead log only paths changed vs parent content-addressed id refs one row per branch moved only by compare-and-swap no lock, no merge queue derived data manifests, trees, blame, history computed from parents, cached checkpoint every 64 generations blobs sha-256 addressed, gzip written once, never change any tier may cache them read · commit + land blobs/get commit · content inline log · diff · blame
One server, three kinds of client. Agents never hold a copy of the repository: they ask the server. The CLI exists for agents that need real files to run tests, and it shares one content-addressed cache per machine, so a second workspace downloads nothing.

Changesets and derived data

A changeset records only the paths that changed relative to its first parent, each pointing at a blob hash. Commit cost depends on the size of the change, not the size of the repository. Everything else (the full file tree, blame, path history) is derived from parents when it is first asked for, then cached. Full trees are checkpointed every 64 generations, so a cold server never walks more than 64 changesets to rebuild one.

gen 64 · 9a1f… checkpoint: full path→blob map stored as one blob gen 65 · 4c07… app.js → b31e… index.html → 77d0… gen 66 · e8b2… pages/orca.js → 0f9a… styles.css → c4d1… gen 67 · 51aa… CHANGELOG.md → 2e6b… old.txt → deleted parent parent parent manifest(gen 67) = checkpoint(64) + changes 65 + 66 + 67, cached in memory
Commits store deltas; trees are derived. Creating gen 67 writes two rows. Answering "what is the tree at gen 67?" starts from the gen-64 checkpoint and replays three short change lists. Blame is derived the same way, one file version at a time, and cached per version.

The two loops an agent runs

The biggest difference is where the retry loop runs. When two agents race to the same branch, a Git agent loses the push, fetches, rebases locally, possibly resolves a conflict, and pushes again: every turn of that loop is several Git processes plus a round trip to the remote. A Graft agent sends one request. If the branch moved, the server rebases the change in memory and retries the compare-and-swap itself, in microseconds. Only a real textual conflict comes back to the agent.

git agent every box is a git process; every dashed edge crosses to the remote pull --rebase edit files add + commit push landed pull --rebase conflict:abort · reset · re-apply accepted rejected rebased: push again conflict graft agent (MCP) one request; the retry loop lives inside the server read files at C decide edits graft_commitbase = C, changes textual conflictnothing landed; markers returned landed inside the server, in memory write changeset (draft) branch head == C ? 3-way merge per filebase C · head · change compare-and-swap reffast-forward or rebased copy 1 call no: rebase yes clean lost the race? rebase onto the new head and swap again conflict swapped re-read, retry
Where the retry happens is the whole difference. The dotted blue loop on the right (a lost compare-and-swap means rebase onto the new head and swap again) never leaves the server. The orange loop on the left is paid by the agent, in processes and round trips, every time another agent landed first.

Workspaces for agents that need files

Agents that run tests need real files. graft clone --sparse src/ asks the server for the file list under the paths it needs, fetches only the blobs the machine's cache is missing, and copies them into place with copy-on-write clones where the filesystem supports it. graft commit sends small changes inline with the commit request. After a fast-forward there is nothing to fetch; after a rebase the server returns the list of paths that differ, and only those are updated.

graft server manifest(prefix) blobs/get (immutable) diff paths-only node cache shared by every workspace workspace A--sparse src/ workspace B--sparse docs/ tests/ workspace Cfull checkout missing blobs only CoW clone graft commit: content inline, lands in one call
Data moves once per machine. Three workspaces of the same repository fetch each blob once. A sparse workspace never asks for paths outside its list; graft add-sparse pulls more later. Committing a new file that already exists upstream outside the sparse list is refused (one of the bug-scan fixes).

The experiment

Every agent owns one page of a small hash-routed web app, the Field Guide, and lands three changes in order. The first creates its page module; the other two touch files that every agent edits. Two of those shared files take insertions at sorted positions, so concurrent edits usually land on different lines. The other two take everyone's additions at the same spot, so they produce real textual conflicts in both systems.

step 1 · scaffold step 2 · register step 3 · changelog pages/<slug>.js new file, owned by one agent: never conflicts styles.css everyone appends above /* pages:end */ app.js import + route, sorted: usually different lines index.html nav link, sorted by label CHANGELOG.md everyone inserts under ## Unreleased hot spot: same insertion point → conflicts shared, but usually merges cleanly Agents think between steps (scripted: random 0.4–1.6 s; real agents: actual model time), sync before each edit, and must land each step before starting the next.
The same workload for every backend. When a step conflicts, the agent does what a careful agent would: take the latest version of the file, re-apply its own addition, and try again. At the end, every finished app is checked for lost work: checks per page (file, import, route, nav link, CSS, changelog line) and no leftover conflict markers.

Three backends ran the identical scripted workload: graft over the HTTP API (what MCP agents use), the graft CLI with a workspace per agent, and git with a clone per agent pushing to a local bare repository. Everything ran on one Mac with no network, which favors Git: a real remote adds round-trip time to every turn of its retry loop.

Results: scripted swarms

Time from "ready to commit" to "on main". The y axis is logarithmic: each gridline is ten times the one below. Graft over the API stays in the low milliseconds at every swarm size because a lost race costs the server one in-memory rebase. Git's median and tail both climb with every doubling of agents, because each lost race costs the agent another pull, rebase and push.
Wall clock to finish the whole app. Includes the agents' think time (about 3 s per agent), which is the floor every backend shares.
Source-control time per agent. Sum of every VCS operation an agent ran: how long it spent waiting on source control instead of working.
All scripted runs. Conflicts count textual conflicts the agent had to handle. Retries also include Git push rejections that rebased cleanly. "Verified" means every page was complete in the final app.

What agents look like, on one time axis

Each row is one agent; time runs left to right and all three panels share the same scale. Gray is the agent thinking. Everything colored is time spent in source control.

thinking read / sync commit / land push pull --rebase conflict recovery conflict
The colored area is the cost of coordination. In the Graft API panel the commits are thin slivers between thinking blocks. In the Git panel the colored loops of push, rebase and recovery stretch out the time to finish.

Where the time goes, per operation

Per-operation latency at agents. Medians per call and total time across the swarm.

Results: real Claude agents

Source-control time per agent. Each agent built the same page on both teams, so each row compares like with like. Measured passively: Git's built-in trace2 timing log on one team, GRAFT_TRACE on the other.
How long each agent took, start to finish. Includes model time, reading and editing, so source control is only part of it. Same page, same model, same instructions; only the source-control loop differs.
Every source-control command the real agents ran. Colored by operation; a gap between bars is the agent thinking, reading or editing. Failed commands are outlined.
Per-operation totals for the real teams.

Bug scan

Before the experiment, every module was read for defects, and every CLI command and MCP tool was exercised against a live server, including the error paths. Each fix below is pinned by a regression test; the suite went from 11 to 18 tests, all passing.

Found and fixed. Severity reflects the worst realistic outcome: high means data loss or a server outage, medium a wrong result or a crash on bad input, low a cosmetic or performance issue.

Method and limits

  • One machine (), all processes local. Git pushed to a local bare repository and Graft ran as a separate server process; neither paid network latency.
  • Scripted agents think for a uniformly random 0.4–1.6 s before each step. Real agents ran on Claude Haiku in two teams at the same time, so both teams saw the same machine load.
  • Git ran with a hermetic configuration: no global config, hooks or commit signing. The Git loop is the standard agent loop: pull --rebase before editing, push, and on rejection pull --rebase again; on a rebase conflict, take upstream and re-apply.
  • The Graft CLI pays roughly 100 ms of Node.js startup per command. That is most of its gap to the API path, and it widens the window in which another agent can land first.
  • Both systems treat two insertions at the same line as a conflict. A union merge for append-only files (Git's merge=union) would remove most conflicts for both, and is a natural next feature.
  • This is Graft, an independent implementation of ideas Anthropic published about its internal system, Scion. No Scion code or numbers are used here.

Reproduce

npm install && npm run build && npm test
node dist/src/swarm/dashboard.js            # live view on http://localhost:7300
swarm/run-matrix.sh 8 16 32 48              # scripted swarms, all backends
node dist/src/swarm/export.js               # → docs/data/results.json
node docs/build-report.mjs                  # → docs/graft-swarm-report.html