Plusieurs agents, un seul dépôt

Chaque agent prend une tâche que les autres voient prise. Le claim est une ref git : le deuxième agent qui tend la main vers la même tâche est refusé, avec le nom du détenteur, et celui qui travaille à côté apprend quels fichiers il partage.

deux worktrees d’un même dépôt
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

Ce qui casse sans lui

Faites tourner deux agents de code sur un même dépôt et les pannes sont silencieuses. Aucune erreur : le travail se télescope, c’est tout.

  • La même tâche, deux fois

    Les deux agents lisent le même backlog et prennent le même premier élément. Aucun ne sait que l’autre a commencé.

  • Les mêmes fichiers, réécrits

    Deux tâches ont l’air indépendantes et touchent le même module. La seconde branche défait la première, et c’est au merge qu’on le découvre.

  • Un claim qui expire en silence

    Une session meurt en route. La tâche dit toujours « en cours », personne n’est dessus, et personne ne sait ce qui a été tenté.

Un tree et une identité par agent

Donnez à chaque agent son propre working tree, un git worktree ou un clone, sur une branche tirée fraîchement de la branche par défaut, et sa propre identité dans ANK_AGENT. ank status signale l’écart avec la branche par défaut, parce qu’une base périmée rend rouge ailleurs un tree qui était vert.

Tous les worktrees d’un dépôt partagent refs/ank/ : le compare-and-swap de git tranche la course entre eux, avec un seul gagnant. Des clones séparés sont arbitrés par un remote nommé origin, que ank init configure pour transporter refs/ank/*.

Sans ANK_AGENT, l’identité retombe sur <user>@<hostname> : deux terminaux sur une même machine sont un seul agent pour les refs. Un second claim sous le même nom est refusé, et le refus explique comment donner son propre nom à la session.

git worktree add ../svc-rate -b task/rate
export ANK_AGENT=codex:rate
deux sessions, un seul nom
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

Un claim est une ref git

ank claim fait trois choses à la fois, et aucune n’est un champ qu’on peut modifier à la main.

  1. La tâche passe en in_progress

    Et claim positionne HEAD, donc les commandes suivantes n’ont pas besoin d’id. Un claim à la fois par identité.

  2. Une ref apparaît sous refs/ank/claims/

    C’est elle qui arbitre deux agents qui visent la même tâche. Chaque worktree la voit, et le remote la porte jusqu’à chaque clone.

  3. Le critère est figé par hash

    Le hash part dans l’enregistrement du claim, hors de portée de l’éditeur. Assouplir le critère pour que le travail passe ne débloque rien : done compare et refuse.

Un bail, renouvelé par le travail

Un claim dure 30 minutes et se renouvelle avec ank context, avec ank show sur la tâche que vous détenez, et avec ank log quand il écrit. Pas de heartbeat à penser : noter ce que vous avez appris, c’est ce qui garde le verrou. Un claim expiré n’est plus vivant, et n’importe qui peut prendre la tâche.

Ne jamais laisser un claim expirer en silence

ank release --reason rend la tâche et consigne pourquoi dans son log, où le prochain détenteur le lit avec ank log <id> avant de refaire ce que vous avez déjà tenté. ank done ne supprime pas la ref : il la transforme en trace d’achèvement qui nomme le commit et la branche, pour qu’aucun autre tree ne prenne la tâche en attendant le merge.

ce que contiennent les refs
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

Un claim nomme les scopes qu’il recoupe

Chaque tâche porte un scope, un ensemble de globs. Prenez une tâche dont le scope recoupe un claim vivant : ank la prend quand même, avec un avertissement qui nomme le détenteur, sa tâche et les chemins partagés. C’est un fait à lire, pas une erreur à refuser : savoir si deux tâches peuvent partager un fichier est un jugement, et il revient à celui qui prend la tâche.

La règle que les skills enseignent aux agents en découle : prendre la tâche qui ne peut pas entrer en collision, ou n’en prendre aucune. Quand rien d’ouvert n’est à la fois débloqué et dégagé, une session au repos coûte moins cher que deux agents qui réécrivent le même périmètre.

un scope est src/auth/**, l’autre nomme 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

Voir qui détient quoi

Avant de prendre une tâche, un agent lit trois choses : ce qu’un autre agent détient, ce que son propre claim recouperait, et ce que blocked_by impose comme ordre.

ank status

La branche, l’identité, le claim détenu et son expiration, et chaque claim détenu ailleurs.

~/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

Le DAG des blocked_by, indenté sous ce qui bloque. Une tâche sans bloqueur est une racine, et elle est prête.

~/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

Un dashboard interroge ank status et ank find. Pas context ni show, qui renouvellent un claim, et pas check, qui élague les refs périmées et donc écrit.

Le parallélisme se déduit, il ne se déclare pas

  • blocked_by est le seul ordre

    Les tâches dont les bloqueurs sont terminés sont prêtes ensemble, et ank context les liste, celles qui débloquent le plus en premier. Sans blocked_by, l’ordre est une fiction.

  • Une branche par tâche

    ank done prouve la tâche dans le tree où elle a tourné. Le merge, c’est l’affaire de git ; un changement fait de plusieurs tâches reçoit une tâche d’intégration, bloquée par chaque partie, avec ses propres vérificateurs.

  • Git garde ce que git fait bien

    Aucun verbe ank ne crée de worktree, ne nomme de branche ni n’en merge. Tâches, claims et preuves sont le plan d’ank ; branches et historique restent à git.

Installer ank

Deux commandes. Linux, macOS et Windows, avec git 2.34 ou plus récent.

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