Un espace de travail par agent : pourquoi les environnements de développement ne devraient rien partager
Lorsque plusieurs agents de code travaillent en même temps sur une même stack, la diaphonie entre eux est l'erreur la plus coûteuse : une migration dans la mauvaise base de données, un job dans la mauvaise stack, un build qui n'arrive jamais. À quoi ressemblait notre quotidien avant devstack, et pourquoi chaque worktree reçoit aujourd'hui un espace de travail fermé sur lui-même.
Une matinée ordinaire chez nous : un ticket est ouvert dans l'éditeur, et dans trois terminaux des agents de code travaillent sur trois autres, chacun sur sa propre branche. Ce n'est plus une expérience, c'est le cas normal. La question qui se pose concerne moins les modèles que l'environnement : où tourne réellement le code qu'un agent vient de modifier, et qu'en voit-il ?
Cet article décrit comment nous n'avons longtemps pu répondre à cette question qu'avec des contournements, et pourquoi devstack, notre outil open source pour les stacks de développement locales, impose aujourd'hui une règle claire : chaque agent travaille dans un espace de travail fermé sur lui-même. Ce qui circule entre deux espaces de travail n'est pas une fonctionnalité, c'est une erreur.
Avant : une stack, de nombreux auteurs
Notre point de départ était une stack qui avait grandi avec le temps : une API .NET avec un migrateur de base de données, un frontend Angular, quelques petits services auxiliaires et une vingtaine de conteneurs, de SQL Server et Elasticsearch jusqu'à un planificateur de jobs et une file de webhooks. Le développement se fait sous Windows 11. L'ensemble était orchestré par un AppHost lancé sous un observateur de fichiers. Pour une personne avec un IDE, une telle organisation fonctionne bien. Dans notre quotidien, il n'y avait pourtant jamais une seule personne : l'IDE, des terminaux et plusieurs agents écrivaient en même temps dans le même arbre de travail.
Il en est résulté une liste de maux de tête, dont chacun a coûté du temps réel :
- « Mais le endpoint existe. » Un nouveau endpoint répond 404 alors que le code est là et que le projet compile. Le processus en cours garde ses propres DLL verrouillées, le nouveau build ne les écrase qu'en partie, et rien ne le signale. L'agent cherche alors l'erreur dans un code qui ne tourne même pas.
- Le même service trois fois en une seconde. Plusieurs agents enregistrent presque au même moment, et l'observateur redémarre à chaque modification. Les instances en double démarrent leurs workers en arrière-plan avant de perdre la course pour le port, et récupèrent au passage des jobs et des messages de file. Au final, l'application elle-même a eu besoin d'un verrou contre les démarrages multiples.
- Une modification inachevée bloque tout. Le redémarrage arrive avant que l'on sache si le nouvel état compile. Un agent au milieu d'un renommage retire à tous les autres la stack qui fonctionnait.
- La migration atterrit dans la mauvaise base de données. Une stack lancée depuis un second worktree trouvait les conteneurs du premier, parce qu'ils occupaient les mêmes ports fixes, et y appliquait ses migrations.
- Le job atterrit dans la mauvaise stack. Un planificateur ou une file pull partagés par deux stacks confient le travail à celle qui le demande en premier. Ce n'est pas une question de ressources, c'est une erreur de justesse.
En trois mois, l'AppHost, le script de démarrage et le runbook ont atteint environ 3 300 lignes, dont une bonne part de contournements. Chacun avait du sens pris isolément. Ensemble, ils décrivaient un seul problème de fond : les espaces de travail n'étaient pas séparés.
La diaphonie est le véritable anti-pattern
Quand on travaille avec plusieurs agents, on pense d'abord aux conflits dans le code. Les branches et les pull requests les maîtrisent bien. Plus dangereuse est la diaphonie à l'exécution : un agent modifie quelque chose, et un autre en subit les effets sans le savoir.
Pour un agent, c'est particulièrement coûteux. Une personne finit par remarquer qu'un comportement est étrange et pose la question. Un agent prend le comportement de son environnement pour la vérité. Si l'API répond avec l'ancien état, il construit des explications pour un comportement que son code n'a jamais eu. Si un autre agent modifie la base de données partagée, le test qu'il vient de voir passer échoue à l'exécution suivante. Le débogage vise le mauvais adversaire, et à la fin des commits arrivent en revue pour résoudre un problème qui n'a jamais existé.
Nous en tirons une règle simple : un espace de travail doit être complet pour l'agent qui y travaille, et rien en lui ne doit dépendre d'un autre espace de travail.
Ce dont a besoin un espace de travail fermé sur lui-même
Dans devstack, un tel espace de travail s'appelle une lane, et chaque git worktree en est une. git worktree add, puis devstack up dans le nouveau répertoire, et la même stack tourne une seconde fois à côté de la première :
- Ses propres ports. Les services d'une lane et les ports publiés de ses conteneurs sont décalés d'un écart fixe. Deux lanes ne se gênent jamais, et chacune sait où trouver ses propres services.
- Ses propres conteneurs et ses propres données. Chaque lane démarre son propre projet Compose à partir des fichiers Compose de son propre worktree, avec ses propres volumes et réseaux. Une branche qui modifie un fichier Compose tourne exactement avec cette modification, et non avec l'infrastructure d'un autre.
- Pas de travail venu d'ailleurs. Par défaut, les workers en arrière-plan ne tournent pas dans une lane, car ils peuvent aussi parler à des systèmes extérieurs à la stack. Qui en a besoin les active pour cette lane, et alors son planificateur et sa file n'appartiennent qu'à elle.
- Un build qui ne touche à rien de ce qui tourne. Une modification est compilée comme candidate à côté de l'instance en cours et ne prend le relais qu'une fois saine. Une erreur de compilation laisse l'ancien build répondre, et une modification plus récente annule le build d'une plus ancienne.
Nous avons longtemps admis une exception : les conteneurs sans état propre, par exemple un tableau de bord de télémétrie, pouvaient être partagés entre les lanes et l'arbre principal. En pratique, cela aussi a provoqué de la diaphonie. Les conteneurs d'une lane ne trouvaient pas les services partagés par leur nom, si bien que la télémétrie manquait sans le moindre avertissement. Toutes les lanes écrivaient dans la même télémétrie, et une lane sur une autre branche recevait la définition de l'arbre principal. Depuis la version 0.33, le partage est donc déprécié. Là où la mémoire manque, un profil laisse de côté de façon ciblée ce dont une lane n'a pas besoin : de manière visible, plutôt qu'à moitié séparé.
L'agent interroge l'environnement au lieu de deviner
Un espace de travail fermé n'est utile que si l'agent peut le lire. Pour cela, devstack fournit un serveur MCP et des skills générés. Un agent demande status et obtient pour sa lane une réponse sans ambiguïté : sa modification est-elle déjà active, ou affiche-t-on not yet active: building, failed ou held ? Il lit les logs de ses propres services, les redémarre et exécute les commandes que la stack déclare, par exemple une réinitialisation de la base de données. Tous les outils agissent sur la lane du worktree dans lequel l'agent travaille. Il ne redémarre pas par erreur la stack de son voisin.
Le débogage s'en trouve déplacé. À la question « mon code tourne-t-il seulement ? », c'est l'environnement qui répond, et non l'agent par des suppositions.
Quand la machine ne suffit plus
Un jeu de conteneurs séparé par lane coûte de la mémoire. C'est le prix de cette règle, et nous le payons sciemment. Avant de démarrer une lane, devstack vérifie si la mémoire suffit, et sinon indique le chiffre et les solutions. La solution la plus importante n'est pas une lane à moitié séparée, mais une lane sur une autre machine : devstack up --on <node> la démarre sur un serveur du pool de l'équipe, et ses ports sont redirigés en local comme si elle tournait ici. L'espace de travail reste complet, il se trouve simplement ailleurs.
Ce qui a changé au quotidien
La matinée ressemble aujourd'hui à celle d'avant : un ticket dans l'éditeur, plusieurs agents sur leurs propres branches. La différence, c'est que chacun d'eux dispose de sa propre stack complète. Une migration dans une lane ne touche que la base de données de cette lane. Une modification inachevée produit au pire une candidate en échec, et l'ancien build de la lane continue de répondre. Les scripts de démarrage, les verrous et les pages de runbook d'autrefois ne nous sont plus nécessaires.
Si vous travaillez vous aussi avec plusieurs agents sur une même stack, il vaut la peine d'examiner vos propres hypothèses : où vos espaces de travail partagent-ils aujourd'hui quelque chose, une base de données, un port, une file, un répertoire de build ? Chacun de ces points est un chemin possible pour la diaphonie. devstack est open source, encore avant la version 1.0 et utilisé chez nous au quotidien. Il tourne nativement sous Windows et Linux, macOS est en aperçu. Vous le trouverez sur GitHub.