Several agents, one repository

Each agent takes a task the others can see is taken. The claim is a git ref, so the second agent to reach for it is refused by name, and the one that edits next door is told which files it shares.

two worktrees of one repository
ANK_AGENT=claude:auth ank claim 0e7d
claimed TASK-0e7de9c55b80 migrate-auth-to-opaque-sessions -> HEAD

cd ../svc-rate
ANK_AGENT=codex:rate ank claim 0e7d
error[4]: TASK-0e7de9c55b80 held by claude:auth (expires in 30m)
  -> ank claim TASK-20e993387d9d   (another ready task in this scope)
ANK_AGENT=codex:rate ank claim 20e9
warning: claude:auth holds TASK-0e7de9c55b80, overlapping on src/auth/login.ts
claimed TASK-20e993387d9d rate-limit-the-login-route -> HEAD

What goes wrong without it

Run two coding agents on one repository and the failures are quiet ones. Nothing errors; the work just collides.

  • The same task, twice

    Both agents read the same backlog and pick the same first item. Neither knows the other started.

  • The same files, rewritten

    Two tasks look independent and touch one module. The second branch undoes the first, and the merge is where you find out.

  • A claim that lapses in silence

    A session dies halfway. The task still says in progress, nobody is on it, and nobody knows what was tried.

One tree and one identity per agent

Give every agent a working tree of its own, a git worktree or a clone, on a branch cut fresh from the default one, and an identity of its own in ANK_AGENT. ank status names the drift from the default branch, because a stale base turns a green tree red elsewhere.

Every worktree of a repository shares refs/ank/, so git’s compare-and-swap settles a race between them: one winner. Separate clones are arbitrated through a remote named origin, which ank init sets up to carry refs/ank/*.

Without ANK_AGENT, the identity falls back to <user>@<hostname>, so two terminals on one machine are one agent to the refs. A second claim under the same name is refused, and the refusal says how to give the session its own.

git worktree add ../svc-rate -b task/rate
export ANK_AGENT=codex:rate
two sessions, one name
ank claim 51c2
error[7]: human:marie holds a live claim on TASK-820d259af6a7 (expires in 30m)
  -> ank release --reason "<why>"   (a second session on this machine sets its own ANK_AGENT)

ANK_AGENT=human:marie-2 ank claim 51c2
claimed TASK-51c2a0f6d418 say-in-the-readme-what-a-session-is-now -> HEAD

A claim is a git ref

ank claim does three things at once, and none of them is a field anybody can edit by hand.

  1. The task moves to in_progress

    And claim sets HEAD, so the commands that follow need no id. One claim at a time per identity.

  2. A ref appears under refs/ank/claims/

    It is what arbitrates two agents reaching for one task. Every worktree sees it, and the remote carries it to every clone.

  3. The criterion is frozen by hash

    The hash goes into the claim record, out of the editor’s reach. Softening the criterion to make the work fit unblocks nothing: done compares and refuses.

A lease, renewed by working

A claim lasts 30 minutes and is renewed by ank context, by ank show on the task you hold, and by ank log when it writes. There is no heartbeat to remember: writing down what you learned is what keeps the lock. An expired claim is not a live one, and anyone can take the task.

Never let a claim lapse in silence

ank release --reason hands the task back and records why in its log, where the next holder reads it with ank log <id> before repeating what you tried. ank done does not delete the ref: it turns it into a completion record naming the commit and the branch, so no other tree takes the task while the merge is pending.

what the refs hold
git for-each-ref refs/ank/
fc6c72c07eaee7d0e1d31f38316d8d5304382d44 blob	refs/ank/claims/TASK-0e7de9c55b80
e671470149b55366650f6da572866a25f33315c5 blob	refs/ank/claims/TASK-20e993387d9d

ank release --reason "the criterion names a test that does not exist yet"
released TASK-820d259af6a7 -> open

A claim names the scopes it intersects

Every task carries a scope, a set of globs. Claim a task whose scope intersects a live claim and ank takes it anyway, with a warning naming the holder, their task and the shared paths. It is a fact to read, not an error to refuse: whether two tasks may share a file is a judgement, and it stays with whoever claims.

The rule the skills teach agents follows from it: take the task that cannot collide, or take none. When nothing open is both unblocked and clear, an idle session is cheaper than two agents rewriting one perimeter.

one scope is src/auth/**, the other names src/auth/login.ts
ANK_AGENT=codex:rate ank claim 20e9
warning: claude:auth holds TASK-0e7de9c55b80, overlapping on src/auth/login.ts
claimed TASK-20e993387d9d rate-limit-the-login-route -> HEAD

See who holds what

Before claiming, an agent reads three things: what another agent holds, what its own claim would intersect, and what blocked_by orders.

ank status

Branch, identity, the claim you hold and when it expires, and every claim held elsewhere.

~/svc-rate
ank status
branch task/rate (default main)
drift 1 entity file(s) differ from main (git merge main)
identity codex:rate (ANK_AGENT)
claim TASK-20e993387d9d Rate-limit the login route
  expires 2026-10-02T08:59:19Z
elsewhere 1 claim(s) by other agents
  TASK-0e7de9c55b80 Migrate auth to opaque sessions (claude:auth until 2026-10-02T08:59:18Z)
perimeter the scope of TASK-20e993387d9d, 0 constraint(s)
queue 0 proposal(s), 0 finished elsewhere
corpus 0 fault(s), 4 signal(s)

ank graph

The blocked_by DAG, indented under what blocks it. A task with no blocker is a root, and ready.

~/svc
ank graph
.

TASK-0e7d  [claimed:claude:auth] Migrate auth to opaque sessions
└── TASK-357b  [open] Say in the README what a session is now
TASK-20e9  [claimed:codex:rate] Rate-limit the login route

3 task(s), 2 root(s) — indented under what blocks them

A dashboard polls ank status and ank find. Not context or show, which renew a claim, and not check, which prunes stale refs and so writes.

Parallel work is derived, not declared

  • blocked_by is the only order

    Tasks whose blockers are finished are ready together, and ank context lists them, the ones that unblock the most first. If there is no blocked_by, the order is a fiction.

  • One branch per task

    ank done proves the task in the tree it ran in. Merging is git’s business; a change made of several tasks gets an integration task, blocked by each part, with verifiers of its own.

  • Git keeps what git does well

    No ank verb creates a worktree, names a branch or merges one. Tasks, claims and proofs are ank’s plane; branches and history stay git’s.

Install ank

Two commands. Linux, macOS and Windows, with git 2.34 or newer.

npm install -g @haksolot/ank
npx skills add haksolot/ank