How I Stopped My AI Agents From Fighting Over the Same Ticket
A dead-simple locking pattern for anyone running more than one coding agent — no database, no infra, just git.
One agent is magic. Five agents in parallel — plus a co-founder running their own swarm — is a different animal. The failure mode:
- You keep a backlog of GitHub issues.
- Each free agent runs
gh issue list, grabs the top ticket, starts coding. - Two agents grab the same ticket because they looked at the same moment.
- Two branches, two PRs, same files → merge conflict, wasted tokens, wasted time.
It’s not an AI problem. It’s a distributed-systems problem in a hoodie: many workers, one shared queue, no coordination. The fix is a lock — and you don’t need Redis for it.
A label is NOT a lock
“Just add an in-progress label so others skip it” doesn’t work:
Agent A: list → #640 free ✓
Agent B: list → #640 free ✓ ← B read before A wrote
Agent A: add label
Agent B: add label ← idempotent, B never notices
→ both now "working" on #640
Labels, assignees, columns are all read-then-write. To actually prevent the race you need one atomic operation where exactly one agent wins and the rest provably fail (compare-and-swap).
The trick: your git remote is already a lock server
Creating a ref on a git server is atomic. Two machines pushing the same new tag → the server accepts one, rejects the other. That’s your mutex.
git tag -a claim/640 -m "agent=asif-laptop nonce=$RANDOM"
git push origin refs/tags/claim/640 # ← the atomic gate
- Push succeeds → you own #640. Go code.
- Push rejected → someone beat you → pick another ticket.
Works across every machine and agent, because they share one origin. Zero new infra.
Gotcha: use annotated tags with a nonce. Lightweight tags on the same commit can hash-collide into a false “up-to-date” success where both agents think they won.
Tested live: A: WON (ref created) / B: REJECTED — exactly one winner ✓
The full loop
1. pick next eligible ticket
2. CLAIM atomically ── lost? → back to 1
3. spin an isolated git worktree (no file collisions)
4. run the agent …heartbeat every 5 min
5. release: --done or --blocked
↺ a reaper frees claims with no heartbeat past a TTL (skips open PRs)
- Worktrees keep parallel agents off each other’s files.
- Heartbeat + reaper = crashed agents self-heal; live ones never get interrupted.
- Mirror the claim to a label + Project board after winning — the label is the display of the lock, not the lock.
Bonus: a human design gate
Going design-driven? Add two labels: design:required and design:approved. Agents skip and refuse any design:required ticket until a human adds design:approved. Plumbing runs fast; anything users see waits for human sign-off. Vibe coding with guardrails.
Takeaway
If you run more than one coding agent, make “picking a ticket” an exclusive act. A label isn’t a lock — you need atomic compare-and-swap, and your git remote already gives you one: push a claim/<id> tag, first writer wins, the rest get rejected. Add heartbeats, mirror to a board, gate design work behind human approval. No Redis required.