De la commande au daemon : comment j'ai confié ma backlog à des agents

Au départ, une commande lancée à la main pour faire coder un ticket par un agent. Six semaines plus tard, un programme qui pioche dans ma backlog GitLab jour et nuit. Récit d'une dérive vers l'autonomie : une mouture qui coince encore, et que le passage de Claude à la facturation au token rendra bientôt coûteuse.

Projet rattaché Granit Golem Sur Azure, le prix d'un health-check explose : de quelques centimes à plusieurs euros pièce. Aucun outil self-hosted ne coche mes cases, alors je l'écris moi-même. Au passage, je lâche Claude Code à fond pour voir : le projet 'rikiki' vire en plateforme SaaS industrielle, et je passe un mois à éponger le bazar généré.

J’ai un projet, Granit Golem, et une backlog GitLab qui ne désemplit pas : une file de tickets, autant de petites tâches à coder. L’idée me trottait dans la tête depuis un moment. Et si je n’avais plus à les coder moi-même ? Des agents Claude (des assistants logiciels qui écrivent le code tout seuls) pourraient piocher dans cette file et le faire à ma place pendant que je dors.

Le premier réflexe, c’est de lancer un agent par ticket, à la main. Ça marche pour deux ou trois.

Un soir, gourmand, j’en ai lancé quatorze d’un coup, persuadé d’avoir trouvé le raccourci. Une heure plus tard, c’était la cohue. Deux agents réécrivaient le même fichier et s’écrasaient l’un l’autre. L’API saturait. Et moi, au lieu de coder, je passais la soirée à surveiller un tableau de bord clignotant.

Distribuer le travail à la main ne tenait pas la distance. Il fallait que quelque chose s’en charge. Ce quelque chose, c’est devenu Forge. Pas d’un coup : chaque fois que rester dans la boucle me pesait, j’en retirais un peu plus.

Six semaines, de la commande au daemon autonome

Avril · la commandesupervisé

  1. /dispatch-tickets

    lancée à la main, dans le chat

  2. validation humaine

    je dis go à chaque fournée

11 mai · le pipelinele pivot

  1. agents headless

    ils codent sans supervision

  2. mode auto

    lit la backlog tout seul

12 mai · le daemonsans moi

  1. boucle continue

    un service qui tourne en permanence

  2. reprise

    repart d'une session interrompue

18-19 mai · le self-healgarde-fous

  1. boucles bornées

    je l'empêche de s'emballer

  2. game days

    je simule des pannes pour vérifier qu'il s'en remet

27 mai · la maturitémachine à états

  1. classement en amont

    l'état de chaque ticket, calculé une fois et transporté

Personne n'a décidé de construire un daemon. Chaque étape n'a fait qu'enlever un humain de la boucle, parce que rester dans la boucle devenait pénible.

Sauf que Forge n’est pas né daemon. Il a commencé tout petit, et il a dérivé vers l’autonomie en six semaines. C’est une mouture, autant le dire tout de suite. Ça tourne, oui, mais ça coince encore beaucoup, et je serais bien en peine de le jurer fiable.

Et tout ça reste de la R&D, sur mon propre outil. Pas une chaîne que je brancherais sur la prod d’un client : là, justement, je ne dormirais pas. Ce que je raconte, c’est un chantier en cours, pas une recette éprouvée.

Une commande, sous surveillance

La première version est une simple commande que je tape dans le chat : /dispatch-tickets. Je lui passe quelques numéros de tickets, elle me propose un plan, je dis « go », et elle lance un agent par ticket. Rien d’autonome là-dedans : je suis aux commandes, je valide chaque fournée, et le mode d’emploi de l’agent est écrit noir sur blanc dans la commande, cent quatre-vingts lignes de consignes.

La première tuile n’a pas tardé. Deux tickets peuvent toucher les mêmes fichiers, et quand deux agents y travaillent en même temps, ils se télescopent. Je me suis retrouvé avec des conflits de merge plus entremêlés que des spaghettis bien mariés. La commande s’est donc mise à repérer ces couplages et à refuser de lancer en parallèle des tickets qui se chevauchent. Un garde-fou que je n’avais pas anticipé, et que la pratique a imposé d’elle-même.

Sortir de la conversation

Le 11 mai, je sors du chat tout ce que la commande y faisait, pour le confier à un programme déterministe.

D’abord, les agents passent en headless. Ils tournent sans interface, sans supervision, là où chaque action passait avant sous mes yeux.

Ensuite arrive un mode « auto ». Au lieu de lui dicter les numéros, je laisse le programme lire la backlog lui-même et choisir quoi prendre (il pioche les tickets marqués « prêts à développer »).

Et la commande, elle, fond. De cent quatre-vingts lignes de consignes, elle se réduit à un aiguillage vers le code. Le mode d’emploi a déménagé du chat vers le programme.

La commande ne pilote plus rien. Elle lance le cycle, et le programme l’enchaîne seul.

Ce que la commande déclenche, désormais

Plan

  • lire la backlogles tickets prêts à développer
  • classerl'état de chaque ticket

Exécution

  • un worktree par ticketun atelier git isolé
  • agent headless2 ou 3 en parallèle

Review

  • le relecteursurveille la CI, fusionne si vert
La commande ne fait plus que donner le départ. Ensuite, le programme enchaîne seul, plan après exécution après review.

Ça tourne sans moi

Vous voyez le moment où un outil cesse d’être un outil, pour devenir quelque chose qui œuvre pendant que vous dormez ? On y est.

Restait à enlever le dernier humain de la boucle : moi. Le lendemain, Forge devient un daemon, un programme qui tourne en fond en permanence. Je l’installe comme un service système : il démarre tout seul, se relance s’il plante, et boucle sans fin, vidant la backlog jusqu’à épuisement avant d’attendre cinq minutes et de recommencer. Il sait reprendre un agent interrompu là où il s’était arrêté, et relancer une proposition de modification (une merge request) recalée en relecture.

Pour la première fois, je ferme mon laptop, et le travail continue.

L’empêcher de s’emballer

Un programme autonome qui se trompe, c’est un programme qui se trompe en boucle, vite et fort. L’apprenti sorcier et ses balais, sauf que les balais, c’est moi qui les ai lâchés.

Ça n’a pas raté.

Le 18 mai, je lance une commande à la main pendant que le daemon tourne : les deux écrivent dans le même fichier d’état et se marchent dessus. Effet boule de neige. Une autre fois, une proposition au test rouge repasse en relecture à chaque tour, et le relecteur automatique recommente le même verdict, encore et encore, quatre-vingt-dix-neuf fois sur la même MR avant que je ne tombe dessus. Quatre-vingt-dix-neuf notifications rigoureusement identiques : sur le coup ça m’a fait rire jaune, puis j’ai compris que c’était la signature d’une boucle que plus rien n’arrêtait.

La parade n’a pas été d’empiler des exceptions dans le code, mais de lui poser des limites simples. Un ticket qui échoue deux fois d’affilée bascule en bloqué plutôt que de boucler. Passé un certain nombre de relances sur une même proposition, je considère son budget épuisé et j’arrête de m’acharner dessus. Les ateliers des propositions déjà fusionnées ou fermées, eux, sont nettoyés au fil de l’eau. J’ai même écrit des scénarios de panne (couper le daemon en plein travail, l’inonder de fausses alertes) pour vérifier qu’il s’en remettait.

Un programme sans garde-fou s’emballe à fond, et toute la nuit.

Pourquoi ça tient

Si l’envie vous prend de monter le même genre de bestiole, voici ce que j’aurais aimé qu’on me dise. L’autonomie tient à une poignée de contraintes très terre à terre. Le daemon, lui, n’y est presque pour rien. Ces contraintes, je les ai apprises pour la plupart en me cognant dedans.

Les contraintes qui font tenir l'autonomie8 gates

Des tickets atomiquesle prérequis
  • Chaque ticket mergeable seul, sans toucher au même endroit qu'un autre
  • Pas de base + API + auth dans un même ticketsinon deux agents se télescopent
Doser les agents2 ou 3, pas plus
  • 14 agents en parallèle : ils se gênent, l'API sature
  • 2 à 3 agents, périmètre serré, minuteurle régime qui sort ~4 propositions propres par heure
Démarrer en secondesworktrees
  • Un git worktree par ticket
  • node_modules et binaires compilés partagés par lien symboliqueni installation ni recompilation au démarrage
Le contrat de l'agentMR ou BLOCKED
  • Il finit en livrant une proposition, ou en disant pourquoi il est bloquéune sortie, deux formes, jamais d'ambiguïté
  • Interdit d'attendre la CI : il pousse, ouvre la MR, terminéun autre programme surveille la pipeline

Presque rien de tout cela n'est dans le daemon. Ce sont des disciplines en amont, apprises pour la plupart en me cognant dedans.

Le découpage

Vous avez déjà vu deux personnes réécrire le même fichier chacune dans son coin, puis y passer l’après-midi à recoller les morceaux ? Avec des agents lâchés en parallèle, c’est le piège numéro un. Tout le jeu consiste à éviter qu’ils se marchent dessus dans les mêmes fichiers. Je m’appuie pour ça sur une carte des domaines du code (une domain map, qui range chaque zone du dépôt sous une étiquette) et je taille les tickets le long de ces frontières. Deux tickets posés sur deux domaines disjoints ne se télescopent presque jamais à la fusion, et les conflits de merge s’effondrent.

Par-dessus, un détecteur signale les tickets qui se chevauchent avant de les lancer ensemble. Il m’est devenu indispensable : sur un paquet de tickets envoyés d’un coup, c’est lui qui m’épargne les fusions où trois branches se disputent le même fichier. Je l’ai quand même laissé réglable, parce qu’il déclenche parfois pour rien (deux tickets qui touchent le même package.json ne sont pas vraiment en conflit), et je le serre ou le desserre selon le lot.

Le lancement

C’est la question qu’on me pose le plus. Un agent n’est rien d’autre qu’un claude -p démarré sans interface, à qui je passe un prompt et les pleins pouvoirs sur son atelier. Et cet atelier, justement, n’est pas qu’une image. C’est, très concrètement, un worktree git, une copie isolée du dépôt posée sur sa propre branche, où l’agent peut tout chambouler sans gêner les voisins. Chacun le sien. C’est précisément ce qui me laisse en lancer deux ou trois en parallèle sans qu’ils ne se pilent dessus dans les fichiers de travail.

Son contrat de sortie est volontairement rigide : sa dernière ligne est soit MR: <url>, soit BLOCKED: <raison>, jamais autre chose. Et défense d’attendre les tests : dès la proposition poussée, il s’arrête.

Un second programme, le relecteur, prend alors le relais. Il surveille la chaîne d’intégration (la CI, qui rejoue les tests à chaque envoi) et fusionne. Automatiquement si je l’y autorise, sinon en me laissant appuyer sur le bouton. Un agent qui poireauterait devant cette CI, ce serait un atelier monopolisé pour rien.

Le lancement et le contrat tiennent en quelques lignes :

// un agent = un claude -p headless, lancé dans son atelier
const args = ['-p', prompt, '--dangerously-skip-permissions', '--model', model];
spawn('claude', args, { cwd: worktreePath, env: { ...process.env, CI: 'true' } });

// sa sortie n'a que deux formes valides, reconnues par une seule règle
const TERMINAL_RE = /^(MR:\s*https?:\/\/\S+|BLOCKED:.+)/m;

La tuyauterie

L’astuce qui fait gagner le plus de temps : chaque atelier réutilise, via un raccourci partagé (un lien symbolique), le node_modules et les binaires déjà compilés du dépôt principal. Pas de réinstallation ni de recompilation au démarrage, un agent est opérationnel en quelques secondes au lieu de plusieurs minutes.

Tout cela borne le travail en amont. La prochaine marche, je la vois sur les agents eux-mêmes. Pour l’instant je leur laisse les pleins pouvoirs sur leur atelier (les clés de la boutique confiées à un stagiaire doué mais un peu pressé) et je trie à l’arrivée. J’aimerais resserrer : leur tailler des responsabilités plus étroites, avec un cadre d’exécution sur-mesure et des règles différentes selon qu’ils planifient, codent ou relisent. Moins de liberté à chaque maillon. Je tiens la qualité au fil de l’eau, plutôt que de la rattraper en bout de course.

Ce qui cloche encore

L’approche a ses angles morts. Le plus connu : tout repose sur des tickets bien découpés, et ça, aucune machine ne le fait à ma place. Plus gênant, Forge ne juge pas la qualité du code, il s’en remet aux tests. Une proposition subtilement fausse mais qui passe la CI peut filer jusqu’à la fusion, et le tri humain reste un filet dont je ne peux pas me passer. Et le coût, enfin, est ce qui m’inquiète le plus pour la suite.

Côté suites, Forge est aujourd’hui soudé à granit-golem. La prochaine étape consiste à l’extraire en plugin réutilisable, pour le lâcher tel quel sur mes autres projets. Ça suppose de découpler le cœur de l’API GitLab derrière une interface, de sorte qu’un futur portage vers GitHub ne soit plus qu’une implémentation à écrire.

Ce que je garde pour d’autres carnets

Reste un pan entier dont je n’ai presque rien dit. Comment garder l’œil sur une bestiole qui œuvre la nuit, pendant que je dors ? Un système autonome qu’on ne surveille pas tourne vite à la boîte noire. J’ai donc monté tout un dispositif de surveillance des tâches, pour voir d’un coup quel agent a bloqué, quelle étape a échoué, où le cycle s’est grippé (au réveil, c’est la première chose que je consulte, café à la main).

À côté, deux familles de mécanismes que je n’ai fait qu’effleurer ici. La première tient à la résilience. Quand un agent meurt en plein vol, je veux rejouer le chemin qu’il avait entamé plutôt que tout relancer (reprendre au dernier point de sauvegarde, pas recommencer le niveau). Quand un cycle plante, je veux le reprendre là où il s’est arrêté. Et le daemon, lui, doit se relever seul de ses pannes, borner ses boucles, rouvrir ce qui doit l’être, sans m’attendre. La seconde famille, c’est l’analyse rétrospective. Je fais relire mes sessions passées pour en tirer des correctifs, chaque tracas qui revient finissant en règle ou en garde-fou de plus.

Tout ça mérite ses propres carnets. Je les garde pour les prochains.

Ce que j’en retiens

La dernière étape, le 27 mai, a consisté à donner à Forge une mémoire propre de l’état de chaque ticket, au lieu de le laisser le redeviner partout. C’est une histoire en soi, que je raconte dans le carnet frère sur la machine à états.

Près de trois cents tickets ont fini fusionnés de cette manière, alors oui, ça marche. Tout n’est pas bon à prendre : certaines propositions repartent en correction, d’autres s’arrêtent en BLOCKED et m’attendent au réveil. Mais relire et trier le matin reste sans commune mesure avec coder ces trois cents tickets un par un.

Je n’ai jamais décidé « je vais construire un daemon autonome ». J’ai écrit une petite commande pratique, et chaque douleur concrète m’a poussé d’un cran. Valider à la main devenait lourd, alors j’ai automatisé. Surveiller chaque action sous mes yeux aussi : je suis passé en headless. Puis rester planté devant pour dire « go ». J’en ai fait un daemon. Et le jour où ça a déraillé, j’ai posé des garde-fous. Personne n’a tracé ce chemin, je l’ai pris un ras-le-bol après l’autre.

Le jour où je l’ai extrait en outil réutilisable pour mes autres projets, ce daemon a pris son nom : Ardente Forge (la petite cuisine derrière mes noms de code, ce sera pour un autre carnet).