The Backlog Is the Agents' Memory
Aug 17, 2026 · 10 min read

In June I audited 129 open issues in one of my repositories against the code that actually existed. 29 of them were already shipped. 23 more were half done. Nobody had closed them, because almost none of the commits had named a ticket, so there was nothing for the tracker to notice. Nine Claude Code agents ran that audit in parallel; by hand it would have meant reading 129 issue bodies against the code.
Amit Kothari published the fast version of the same failure on August 10: 23 concurrent Claude Code sessions on one machine, six of eight units in one job built twice, both copies merged. His duplicates took a morning. Mine took weeks, one session at a time, and the fix could not be a directory on one machine. Same failure, different clock.
This post is the slow version. Two monorepos, one owner, three months, most commits written by agent sessions that start with no memory of the last one. What follows is the sequence as it happened, with the numbers as they were measured.
What happened
The first one I caught was three days old. On a Friday in May a session built a correlation job for one customer's sensor setup, and an admin page for it the next morning. By Saturday afternoon the page had been generalised. By Sunday the job was gone, 792 lines, replaced by a generic analytics runner that did the same work for any sensor. Nobody had done anything wrong. Each step was the reasonable thing given what its author could see, and what none of them could see was the other two.
I put two things into CLAUDE.md that month: a list of platform concepts to reuse before inventing, and a paragraph about tickets. Every commit ends with Refs #<id>. Starting work moves the issue to in-progress, finishing moves it to review. I had a feeling the board had drifted from the code. I did not have a number.
The number came three weeks later, when I stopped trusting the board and had it checked. Nine agents took the 129 open issues between them and compared each with the code. Twenty-nine were already shipped. Twenty-three were half done. Forty percent of what I thought was my backlog was fiction, for a dull reason: before that paragraph, eight of 633 commits had named a ticket. There had never been anything for the tracker to reconcile against, and the sessions that shipped those features had ended without telling anyone.
The paragraph itself worked better than I expected. Agents went from naming an issue in one percent of commits to 88, and to 95 after I added rules for epics and follow-ups. No hook, no CI check. Text, read at session start. Which is why the next audit stung. Nineteen of the 26 issues I checked in July had shipped and were still open. This time I found the mechanism instead of the symptom: the repo's default branch is main, merge requests land on dev, and GitLab only fires Closes #N on the default branch. Every trailer had been inert since the day I wrote the rule. The agents had complied at 88 percent with a rule whose payoff never fired. Two of the nineteen were also wrong; the trailer claimed more than the diff delivered, so closing stayed a judgement rather than a script.
Then I opened the sibling repository, same owner, same stack, and found the same disease untreated. 150 of 199 open issues had no parent. Three of them were titled Epic: and used as containers, which GitLab does not allow, and a 44-issue import had never been given a home. Nearly half the design specs named no issue; five described work that had shipped without ever having one. I had fixed all of this next door a month earlier and never carried it over. While the cleanup ran, a parallel session filed two follow-ups from a review, at 12:19 and 12:20, with no epic and no label. The rule was being broken in the repo where I was writing it. The same afternoon a git checkout -b in the shared clone dragged a live implementation session onto my docs branch.
Within the week the rules stopped being prose. A test now fails any spec without an issue line. A hook refuses to open a worktree unless it is named after an open issue, and marks the issue in progress on the way in. Another puts the board in front of every session as it starts, and a third appends Closes to the merge request if the agent forgot. Eleven weeks from the first paragraph to the first hook.
What got built twice
The audits measure the symptom. The cost is work done twice, and it came in four shapes, each one a thing the session could not see.
The same file on two branches. Sequential Liquibase changelog numbers have no allocator, so two branches both reached for the next number and both produced a db.changelog-071.yaml, same filename, different changesets, unmergeable. Neither branch could see the other's intent until the merge failed. Filenames are UTC timestamps now, and ChangelogNamingTest fails the build on 074.
The same concept under two names. Alert and Event were one thing carried by two entities for 73 days, because the session that built the alerting engine in March could not know that events would grow a lifecycle in May. Dropping Alert took a day; the reuse list now carries the line "fired instances are persisted as Event + EventLifecycle, not as standalone Alert rows" so the next session does not rebuild it.
The same seam in two repositories. Two loaders for one embed seam, written independently, with incompatible protocols, because nothing either repo could read said the other had one. A spec that named both ended it, and the 125-line copy was deleted.
The same runtime in two trees. Two copies of the gateway firmware, one legacy, one in the new product layout, and eight shared files that had quietly diverged. A product overlay resolved to the copy without the drivers it needed, so the product broke. Removing the shadow took 5,056 lines in one commit and 19,647 across 182 files in another.
None of these came from 23 sessions racing for the same file. They came from one session at a time not knowing what the last one had done, or what the session in the next worktree was doing right now.
Why it generalises
The tracker is the memory
An agent session has no memory of the last one, so whatever is not in the tracker, the repo or an explicit handoff does not exist for the next session. When humans did most of the work, a stale board was annoying, but the person who built the thing remembered building it. When agents do most of the work, the person who built the thing is gone at the end of the session, and the next one reads the board. Every gap in ticket hygiene becomes duplicate work one session later. For agents the tracker is the shared memory, and the only one.
Prose gets you 88 percent
One paragraph in CLAUDE.md moved commit-to-issue linkage from 1.3 to 88 percent, and later to 95. That is better compliance than most human teams manage, and it is still where the duplicates live: 8 of the 166 commits after July carried no reference, from sessions that had the rule in context, and the worktree rule had to be restated in session memory because, in my own words at the time, agents "always overrule" it. The rules here stayed prose for eleven weeks before they became tests and hooks, and each conversion has an incident number attached. If you are going to write the rule anyway, plan the hook that enforces it.
Read back every write
GitLab returned 200 to a parent_id it silently ignored, which would have left eight top-level epics orphaned had the cleanup script not re-read them. The epic-issues endpoint takes an issue's global id, not its project iid, and on gitlab.com an issue with global id 262 really does exist in somebody else's project, so posting the iid attaches a stranger's issue to your epic and reports success. A PUT returned HTTP 500 having already applied the mutation, which is why every script in that toolchain is idempotent, defaults to --dry-run, and never tries to undo a batch. Scoped labels of the same scope passed together dedupe silently to the last one. add_labels creates a label that does not exist instead of failing. A label delete would have stripped 174 merge requests. Silent success is the failure mode of every tracker API, and an agent that reports "done" off a 200 is exactly how the board drifts.
Silence in a spec is a lie
The rule that stuck allows exactly three answers on a spec's **Issues:** line: an issue number, None yet for work not sliced into issues, or none, shipped untracked (<refs>) for work already merged that never had a ticket. An invented number is not allowed, and neither is a blank. Five specs turned out to be in the third state, and naming that honestly is what let the tracker and the specs agree again.
Handoffs are about what not to reopen
Session memory outside the repo grew to 58 one-fact files in one project, indexed by one-liners. The entries that earned their keep were negatives: "fetch before analysis, the local clone was 636 commits behind while I gave repo-wide advice"; "name tickets, don't number them, bare numbers carry zero meaning across sessions"; "search the tracker before declaring a gap, the roadmap lives in issues, not in code". The best handoff document I have from these three months opens with "READ THIS BEFORE TOUCHING #303, the plan is wrong", records that the plan "has been wrong four times out of four in this area", ships a nine-row tracker-state table, a known-flaky test baseline with "do not chase it", and a five-step suggested opening move. It is untracked, so it exists on one laptop. Meanwhile a 4.1-million-token knowledge graph of the same codebase, built to onboard sessions, is referenced in zero repository files and is 526 commits stale.
The lock has to live where the sessions live
Kothari's answer is an atomic mkdir per lane, and for 23 sessions on one host in one morning it is the right answer: one syscall the kernel guarantees exactly one caller wins. It cannot see my other laptop, or a session from last Tuesday. Sessions spread over machines and weeks need a lock in the tracker, and the tracker's labels are last-write-wins, not atomic. What is atomic is git push -u origin <iid>-<slug>: the second session to claim the same branch name is rejected by the remote ref. Do that at the gate, when the worktree opens. His rule for the message layer applies to the board snapshot word for word: absence of an in-progress issue is not evidence nobody is working on it, same as absence of a socket. Use it to rule out, never to rule in.
What to check
- Does your default branch match your merge target? If MRs merge to
devand the default ismain, everyCloses #Nyou have ever written is inert. Either change the default, add a close step to the Stop hook, or accept that closing is manual and audit for it. - Do your specs name their issues, and do your issues link their specs? Grep for the header line. Count the blanks. Every blank is work that will look abandoned to the next session.
- Can a follow-up survive a merge? If review findings live only in MR threads, they are lost the day the MR merges. Promote before merge, label them, attach to the epic.
- Does the session start with the board? Fifteen in-progress issues, capped and cached, without a model call. If the agent has to go looking, it will not.
- Does the branch name carry the issue?
<iid>-<slug>makes the gate trivial, makes the Stop hook trivial, and makesgit worktree lista status report. - Do you push the branch at the gate? That push is the only atomic claim in the whole flow, so use it as one.
- Do you search the tracker before declaring a gap? Code state alone will tell you a capability is missing while an open issue, an in-flight MR and a committed spec say otherwise.
- Do you name tickets, not number them? "per-call routing (#277)" survives a session boundary. "#277" does not.
- Does the handoff say what is closed? Date it, list tracker state, list the test baseline with its known failures, and say what not to reopen. Then commit it, so it exists on more than one machine.
- Which layer are you missing? Kothari's order holds: a lock first, because it is the only thing that stops the duplicate; declarations second, because they stop the collisions you can predict; live observation third, because it reveals the ones you cannot. A ten-minute reconciler that checks worktrees for the same issue, branches for overlapping files, and in-progress issues for near-identical titles is layer three. It is cheap, it should only ever flag and label, never close, and it comes after the gate.
The morning I audited 129 issues, the tracker was three weeks into having a rule about it. The rule was right, and it was prose, and prose got 88 percent. What held the rest was mechanical: a hook that refuses to open a worktree without an issue, a test that fails a spec without one, and a push that makes the second claimant lose. Agents forget. The backlog has to remember, and it only can if something makes the agents write to it.