It Said Done, and Deleted the Branch

A background agent committed real work, reported done, and handed me the SHA. Then I ran git branch and there was nothing there. The commit was fine. The label pointing at it was gone. Done is a claim about a commit, not about whether you can reach it.

It Said Done, and Deleted the Branch

The agent finished. It committed the work, reported done, and did the responsible thing: it handed me the SHA. Real commit, real diff, a hash I could point at. Good agent.

I went to merge it. git branch. Nothing. Not my branch, not its branch, no branch. The commit was not gone. The commit was fine, sitting in the object store exactly where it was written. What was gone was every label that pointed at it. The worktree that produced the work had cleaned itself up on the way out, and cleanup includes deleting the branch ref. The fact survived. The sticky note that told me where to find it did not.

Operating Conditions

Here is the sharp irony: this failure is caused by doing the last post right.

You isolated your agents. Each one runs in its own worktree, on its own branch, so no two writers share a HEAD. That is rule one, and it works. And a good harness, when an isolated agent finishes, tears the worktree back down: removes the directory, deletes the branch ref, leaves the repo tidy. That is also correct. Left-behind worktrees and dead branches are their own mess.

So the hygiene that stops agents from clobbering each other is the same hygiene that removes the pointer to their output the moment they say they are done. The work is committed. The label that led to it has been swept up as litter. Both things are working as designed, and together they hand you a commit you cannot see.

Failure Modes

Three shapes. Same lie underneath: "done" reports that a commit exists and tells you nothing about whether you can reach it.

The torn-down branch. The one above. The isolated agent commits, reports done with a SHA, and its cleanup deletes the branch. The commit is now a dangling object: real, in the store, reachable by that hash, and alive only until the next garbage collection decides nothing points at it and reclaims it. git branch shows nothing because nothing does point at it, except the hash in a notification you might have scrolled past.

The stranded branch. The opposite. The agent commits correctly to issue-370 and stops. The branch is right there. You just never merged it, because "done" meant "committed," not "integrated," and nobody owned integration. You find it weeks later, a correct fix that shipped to nobody, because a swarm produces branches and produces no one to land them.

The unpushed merge. No swarm required for this one, which is why it catches everybody. You click gh pr merge. It merges the branch state that is on the server. Your last three commits were local, never pushed. The merge ships the version without them, and then deletes the PR branch, your unpushed fixes included, because the merge thinks it is done with it. git log @{u}..HEAD would have printed those three commits and warned you. You did not run it.

Root Cause

A commit is a fact in the object store. A branch is a sticky note pointing at that fact. They are not the same thing, and a swarm is the machine that finally pulls them apart.

For your whole career they moved together. One developer, one checkout: the note stayed where you put it, so "committed" and "reachable" became the same word in your head. That word is now wrong. In a swarm, refs are created, deleted, and pushed by processes that are not you. The fact can outlive its note, when cleanup deletes the branch. It can sit under a note you never read, when the branch is stranded. It can live only on a note the server never got, when the commits are unpushed. Three different gaps between "the commit exists" and "you can get to it," and "done" is blind to all three because "done" is a statement about the fact.

Proposed Fix

Integration is a serialized, single-owner job, and before you trust a swarm's "done," you learn the topology. Three moves.

Keep the receipt. The SHA in the completion notice is the durable handle, more durable than any branch a harness might delete. The moment an agent reports a commit, anchor it with a ref of your own before anything gets collected:

git branch -f keep-agent-a <sha>

Now a note you control points at the fact, and GC leaves it alone.

Read the topology before you merge. Which branch, still existing or not, pushed or local. The single most useful line before any integration:

git log @{u}..HEAD

If it prints commits, you have local work the remote has never seen. Push before you merge, every time, no exceptions.

One lander, and it is you. Agents produce branches. They do not integrate. Bringing branches into master is a serial job with exactly one owner, done in one place, so nothing lands twice and nothing gets stranded. The swarm has no integrator unless you are it.

System Status

"Done" is a claim about a commit. It says nothing about which branch holds it, whether that branch still exists, or whether the copy you are about to merge is the one with the fix. In a single checkout you could let those be the same question. Running agents in parallel, they are four different questions, and the agent answers only the first.

So make it show the rest. The SHA, the branch, @{u}..HEAD, pushed or not. Collect the receipt before you trust the word. A commit you cannot reach is not done. It is lost work with good paperwork.

Tell me about the commit you could not find. Where did it turn out to be, and what finally surfaced it? That is the part that transfers.

Further Reading

  • A Swarm Is a Distributed System, Not a Team (Part 1): why a swarm pulls "committed" and "reachable" apart.
  • It Said Done. It Verified the Wrong Binary.: the single-agent "done" that verified the wrong artifact.
  • It Said Deployed: a "done" reported cheerfully over a 404.
  • How I Run Three Agents Without Losing My Mind: the single-owner lander, and keeping the SHA.

No comments yet