What a dozen agents taught us about git.

Put a dozen agents on one repo and git does exactly what it was built to do: it lets every one of them branch, and hands you the collisions at the end. We built cinder for Cloudflare’s git platform challenge to find out what has to change, and most of the answer turned out to sit around git, not inside it.

The life of a change in cinder

Git isn’t the problem. Coordination is..

Git handles concurrent work fine when the people doing it talk to each other. A team of humans says “I’m in the billing module this week” at standup, and most collisions never happen. Agents don’t have a standup. Each one branches off whatever main looked like a minute ago, and git hands you the conflicts at the end, when they cost the most.

That cost is real in two ways. Somebody spends an afternoon reconciling two diffs nobody on the team wrote. Or another agent gets assigned to resolve it, burns tokens, and sometimes gets it right. Neither scales past a handful of agents, and the demo we built for the challenge files twelve intents at once, several of them written to overlap on purpose.

So we moved the hard part to the front. Before an agent writes anything, cinder predicts which functions the change will touch and leases them. A second change that needs the same function waits, then starts from the first one’s result instead of racing it.

Leases, not conflicts

The surprise was how much the size of the lock mattered. Files are too coarse, because every change touches index.js and the whole repo ends up in one line. Lines are too fine, because a change you haven’t written yet has no line numbers. Exported functions and types turned out to be the right unit: a model can predict them from a sentence of intent, and they’re stable enough to hold a lease on.

Leases don’t remove every collision, so three things back them up:

  • Seams land first. When two intents both need a new shared function, that function goes in as its own small change, and both build on it. Two agents inventing the same helper in two places was the most common collision we saw before this.
  • The likely conflicts move first. When the target branch moves, the changes most likely to conflict get rebased first, while the conflict is still small.
  • A resolver gets both intents. Anything that still conflicts goes to a resolver agent, which is handed what each change was trying to do, not only the two diffs. Resolving a conflict without knowing either side’s goal is how you get code that compiles and does neither.

The lease also gets renegotiated when the real diff outgrows it. A prediction is a starting point, and the agent touching something it didn’t lease is a signal, not an error.

A model’s guess is worth nothing until it’s graded.

Leasing before code exists means asking a model. So does estimating risk, deciding whether two changes will collide, and deciding whether a person needs to look. Our first instinct was to trust those answers. That was the wrong order.

Every guess gets graded

Every prediction is now stored with the change: the question, the answer, the probability the model gave, and the model version. When reality answers, an outcome gets attached. The change touched functions outside its lease. The merged change got reverted. The flaky test passed on rerun. Each of those is a mark against or for a specific judgment.

From those pairs, each repo fits its own calibration for each kind of question, so “80 percent likely” comes to mean 80 percent on that repo. A judgment only gets to decide on its own once it has enough outcomes behind it. Until then it falls back to the safe default for that kind of question, which for risk means a reviewer agent checks the work. A new model version starts a new series from zero, because the old record was earned by a different model.

The other lesson: ask narrow questions. “Is this safe?” gets a confident paragraph. “Which of these functions will change?” gets an answer you can mark right or wrong. If you can’t grade the answer, you can’t calibrate it, and you’re back to trusting the confident paragraph.

Review is the bottleneck, so spend it carefully.

Nobody reads every diff from a dozen agents. Pretending you will just means the reviews get shallow. The question shifts from “was this reviewed?” to “does this one need a person?”

In cinder that question gets answered per change. Low risk merges once its checks pass. Medium risk gets a reviewer agent. High risk waits for a person. A README edit is documentation only, so it merges on green without anyone looking. A risky change to auth code doesn’t. The score itself is one of the graded judgments above, so the routing gets better as the repo builds a record.

Checks shift too. Green on a branch means little when ten changes have landed since it branched. So cinder verifies the merge result, the exact tree the target would have after the merge, and merges through a queue per target branch. When a check fails, the agent that wrote the change gets the failure back and repairs it, within a fixed number of rounds and a budget, before anyone else is involved.

Credentials first, capabilities second.

An agent holding a repo token holds it for every instruction it reads, including the ones planted in an issue comment or a README it was asked to summarize. The usual answer is a narrower token. We went further and took the token away.

Jobs never hold a token

In cinder, jobs never see a token. When git inside a job asks for a file, the request goes out through cinder, which attaches read access scoped to that job’s repos and gone in about a minute. There’s nothing in the job’s environment worth stealing.

Writes come back as changes, not pushes. Nobody pushes to the target branch, people included. A push goes to refs/for/<branch> and opens a change in its own fork. The merge commit is signed by cinder with a key the runners never see, so “this landed on main” always means “this passed verification”.

Most jobs don’t need the history.

Only what it reads

A full clone downloads every version of every file. Our test repo has about a thousand files, and its history is more than ten times their size. An agent fixing one function needs almost none of it.

So cinder’s jobs clone commits and trees only, and fetch a file’s contents the moment something reads it. That took our test repo from 44 MB to 328 KB per job, and reading one file afterwards fetched one object. At dozens of jobs an hour, that’s the difference between working and downloading. It’s plain git, too: partial clone has been in git for years, and you can measure it on your own repo in a minute.

blobless-clone.sh
# Clone the same repo twice and compare. Any git server that supports
# partial clone works (GitHub, GitLab and Artifacts all do).
url=https://github.com/<you>/<your-biggest-repo>.git

git clone --quiet "$url" full
git clone --quiet --filter=blob:none --no-checkout "$url" blobless

du -sh full/.git blobless/.git

# Read one file from the blobless clone: git fetches that one blob, nothing else.
git -C blobless show HEAD:README.md > /dev/null
git -C blobless count-objects -v | grep in-pack

Rules beat models.

We built most of cinder with agents, under strict rules. Every change stays under a fixed size. Every acceptance item gets a test that fails without the change. No dependency gets added unless it’s on an allowlist only the owner edits. And the verifier is never the author. Those rules did more for quality than any choice of model.

Agents wrote the middle well. The bugs that hurt lived at the edges, where our code met the platform: a list call that silently stopped at the first page, two changes racing for the same fork name. No test caught those, because the tests used our idea of the platform, not the platform. Budgets were real too. A token cap paused our agents right before a recording, which is exactly what it’s for.

Keep it git.

Any git client works with cinder. Each merge commit carries Cinder-Change and Cinder-Agent trailers, pointing back to its change and the agent that wrote it. Every change’s record, its intent, its events and every prediction made about it with whether it was right, is written as commits under refs/cinder/changes/, next to the code. If cinder went away, the history would still explain itself.

Everything cinder runs on, all Cloudflare

It all runs on Cloudflare. Artifacts holds the repos and one fork per change. Durable Objects hold the leases and the merge queues. D1 and R2 keep the records and logs, and Workers AI runs the judges. cinder is a challenge entry, not a hosted service: the code is open, Apache-2.0, and deploys to your own account.

Where it could go.

cinder is a demo, but the pieces add up to a platform. Leases keep agents out of each other’s way. Graded judgments decide how much review a change gets. Merges are signed by the platform, never by whoever wrote the code. Built out, that’s a delivery system where agents ship on their own by default and the guardrails live in the structure itself. If you’d like to talk about making that real, reach out at [email protected].

Code: github.com/brinkercode/cinder