Un harnais de production pour Claude Code : six couches allant de Slack aux MR fusionnés.
Comment un harnais à six couches transforme les tickets Slack en code fusionné : traitement des événements, boucles d’agents contrôlés, état durable, auto-réparation, capacité d’observation et contrôle par l’opérateur.
Sources: Building a Production Agent Harness: Turning Claude Code Into a Multi-Agent Engineering Pipeline
Placer un modèle de codage isolément revient à obtenir un cerveau dans un bocal. Il écrit du code et appelle des fonctions sans problème, mais il ne verra pas vos messages privés sur Slack à 3 heures du matin, ne relancera pas une tâche CI qui est passée en état d’erreur, ne résoudra pas le conflit de fusion qui vient d’apparaître sur sa demande de fusion ou ne se souviendra pas qu’un réviseur a posé une question hier et que personne n’a répondu.
Ce qui transforme le modèle en quelque chose d’utile, c’est le cadre d’exécution : la structure de base qui fournit des « sens », des moyens d’action et de la mémoire. Ses composants sont la réception des événements, l’orchestration des agents, un état persistant, des boucles capables de se réparer elles-mêmes, l’observabilité, ainsi qu’un moyen pour l’opérateur humain de diriger le système. Le système décrit ci-dessous est un système en production, et non une démonstration de recherche. Il surveille un canal Slack, ouvre des demandes de fusion sur cinq dépôts internes, répond aux commentaires des réviseurs pendant la nuit et répare automatiquement les exécutions CI en erreur sans qu’on le lui demande.
C’est un pipeline interne à plusieurs agents, construit sur Claude Code. Il fonctionne sans interruption depuis plusieurs mois, en prenant des tâches depuis Slack et en les acheminant vers des travaux d’ingénierie réels. Ce qui suit offre une vue d’ensemble : ce qu’il fait, comment les différentes composantes sont connectées, ainsi que les contraintes rencontrées lors du développement qui ont influencé sa conception.
Les sections sont présentées dans cet ordre : pourquoi avoir recours à un tel système, ce que fait ce système précisément, les trois lacunes qu’il comble, les six niveaux de fonctionnement (ingestion des événements, orchestration des agents avec une analyse approfondie du cycle autonome et de la couche d’amélioration continue, état persistant, capacité d’autoguérison, observabilité et contrôle humain), quelques éléments supplémentaires en dehors de ces niveaux, une présentation étape par étape et une comparaison avec d’autres systèmes similaires.
Pourquoi une fenêtre de chat et quelques outils ne suffisent pas
Lorsque des plugins pour assistants de chat sont apparus en 2023, de nombreuses équipes ont tenté la solution évidente : placer une interface de chat devant un LLM, lui fournir quelques outils de appel de fonction et qualifier le résultat de « l’agent d’ingénierie ». Cette configuration fonctionne à peu près bien en démonstration, mais ne tient jamais en environnement de production. En moins d’une semaine, trois modes de défaillance se manifestent.
- Le contexte disparaît lorsque l’opérateur ferme la fenêtre. La mémoire doit persister au-delà d’une seule session de chat. Une véritable tâche d’ingénierie dure des jours : ouvrir un ticket, créer une branche, rédiger un projet, le soumettre à une revue, répondre aux commentaires, attendre que le test continue soit positif, puis fusionner les modifications. Aucun appel de modèle ne peut contenir tout cela.
- Rien ne réagit aux changements extérieurs. Un réviseur commente à 16 heures. Le test continue échoue avec le troisième commit. Un collègue répond dans le fil de discussion. L’agent doit être alerté par ces événements, plutôt que de faire des requêtes en boucle ou d’attendre que quelqu’un le sollicite à nouveau.
Résoudre ces problèmes relève de la fonction du « harness ». C’est aussi ce qui distingue une simple fenêtre de chat de Claude Code lui-même, des agents en arrière-plan de Cursor, de Devin ou du système décrit ici. Le modèle sous-jacent est presque interchangeable entre tous ces outils ; c’est le « harness » qui l’entoure qui diffère.
Le modèle n’a accès au monde qu’au travers du « harness », et c’est ce dernier qui effectue presque toute l’ingénierie nécessaire pour faire en sorte qu’un LLM se comporte comme un système.
La tâche pour laquelle le outil a été conçu
Le problème de départ était concrèt. Un canal Slack, appelons-le #support, reçoit environ 30 demandes par semaine. Chacune nécessite une enquête, suivie généralement d’une modification de code dans l’un des cinq dépôts. Une demande peut durer plusieurs jours. Les reviewers commentent les propositions de modification, les tests CI échouent fréquemment, et certaines enquêtes se révèlent être des reproductions de celles du mois précédent. L’équipe chargée de gérer cette charge était constamment sollicitée le soir et les week-ends, sans que rien ne s’améliore, car les mêmes types de demandes réapparaissaient à chaque cycle.
Cet outil a trois responsabilités, classées selon le degré d’intervention requis :
- Enquêter. Lorsqu’une nouvelle demande arrive dans
#support, suivre le fil de discussion, rassembler des informations provenant de différents dépôts et publier une analyse structurée dans ce même fil.
Ce troisième rôle est ce qui distingue cet outil d’un simple bot de gestion de tickets. La matière première du système est son propre code source, et le LLM peut y apporter des modifications via des demandes de pull qui passent par une révision humaine.
Le système est composé de six couches logiques, chacune ayant sa propre section ci-dessous.
Les trois lacunes que un modèle simple ne peut pas combler
Une idée sous-tend toute cette architecture : un LLM sans état, purement réactif, ne constitue pas un système de production. Ces six couches existent pour combler trois lacunes que le modèle ne peut résoudre seul.
Le premier défaut est la réactivité. Un modèle ne peut pas s’activer lui-même. La couche 1 (ingestion des événements) gère cela, car Slack, les résultats de GitLab CI et les pages de PagerDuty affluent tous dans une même file d’envoi. Le LLM ne fait jamais de requêtes ; il est simplement appelé.
La persistance constitue le deuxième défaut. Les modèles oublient tout entre les sessions et perdent leur contexte lorsque le travail est transféré d’une machine à une autre ; de plus, ils ne peuvent pas distinguer le fait d’avoir traité un problème d’un commit qui a réellement atteint le dépôt source. La couche 3 (état persistant) remédie à cela : des structures en mémoire éliminent les doublons au sein d’un même processus, des fichiers JSON locaux stockent les cartes opérationnelles, et des espaces de travail synchronisés via Git conservent un état durable des cas. La continuité des sessions entre machines, décrite plus loin, repose sur la même idée : l’état doit être stocké dans Git et jamais à l’intérieur d’un processus en cours d’exécution.
Le troisième défaut est la qualité. Lorsqu’il n’est pas structuré, un cycle de raisonnement d’un LLM tire des conclusions affirmées à partir de preuves insuffisantes, déclare les investigations terminées avant d’avoir vérifié toutes les sources accessibles, et produit des descriptions de MR qui semblent avoir été écrites par une machine dès la première phrase. La couche 2 (orchestration d’agents) et le cycle d’enquête y remédient grâce à des contrats de sortie structurés, un contrôle de qualité à chaque itération, une revue adverse une fois le cycle terminé, ainsi que des cycles d’auto-réparation autour du cycle externe de modification de code, de tests continus et de fusion.
L’effet cumulatif est ce qui relie les différentes couches entre elles, car chaque affaire résolue accélère la suivante. Les investigations approuvées deviennent des entrées dans la base de connaissances. Les MR approuvés se transforment en modèles auxquels l’analyseur de défauts peut se référer. Une affaire finalisée donne naissance à un MR d’amélioration visant le système lui-même. À chaque cycle, moins de types de tickets sont traités de manière insuffisante.
Niveau 1 : comment les événements activent le système
Trois sources de signaux externes peuvent activer l’ensemble logiciel :
- Les messages Slack, c’est-à-dire les mentions dans des canaux, les réponses dans des threads et les commandes administratives envoyées en message privé à soi-même.
- L’activité GitLab, c’est-à-dire les commentaires de revue de demande de modification, les résultats des pipelines et leurs échecs.
- Les alertes PagerDuty, utilisées lorsque la page de surveillance mentionne un ticket déjà vu par l’ensemble logiciel, afin que le contexte antérieur puisse être affiché.
Ces trois sources diffèrent en termes de latence et de fiabilité, ce qui explique pourquoi chacune dispose de son propre chemin d’entrée.
Deux détails pratiques ne figurent jamais dans la version théorique de ce diagramme.
Consistance éventuelle. L’endpoint conversations.history de Slack peut présenter un retard allant de 1 à 30 secondes. Un outil de surveillance qui constate un résultat vide et avance naïvement son curseur va manquer définitivement tout message dont la valeur ts se situe dans cette fenêtre de temps. Ainsi, le curseur reste bloqué lors des vérifications sans résultat, et la prochaine tentative débute à partir de la même date et heure les plus anciennes, ce processus se répétant jusqu’à ce qu’au moins un message soit récupéré. Un ensemble de suppression des doublons basé sur la valeur ts de chaque message filtre les doublons générés par ces tentatives répétées.
Socket Mode, accompagné d’un poller, c’est comme avoir une sécurité supplémentaire. Socket Mode permet de recevoir les réponses des threads avec une latence inférieure à une seconde, mais les connexions WebSocket se détachent. Le poller agit comme filet de sécurité, capturant tout ce que Socket Mode rate, et les deux mécanismes partagent un fichier socket-dedup afin qu’aucun message ne soit envoyé deux fois. Sans ce système, tout ce qui arrive pendant une connexion interrompue disparaît silencieusement, sans aucun signal indiquant à l’opérateur que le message a été manqué.
Le cycle cron de mr-monitor se comporte différemment. GitLab ne met pas les commentaires de revue dans le pipeline, ce qui fait que la consultation est la seule option, et l’adaptation de cette consultation en elle-même s’est avérée être la partie la plus difficile. Chaque MR à longue durée supplémentaire pris en charge par le système allonge le cycle de 10 minutes, ce qui accumule des curseurs pour des threads inactifs, et chacun d’eux entraîne une appel à l’API Slack par cycle. Laisser cela sans intervention provoquait des tempêtes de limitations de fréquence 429 continues, qui entravaient les consultations essentielles. La correction consiste à supprimer automatiquement un curseur lorsque l’API source répond par thread_not_found, ce qui le fait sortir du cycle suivant. Avant cette correction, un cycle effectuait 12 tentatives répétées pendant des heures d’affilée. Après, plus aucune.
Niveau 2 : manière dont les agents sont orchestrés
Le LLM s’exécute réellement ici. Le framework lance des processus en tant qu’unités temporaires systemd, et chacun reçoit un chargement JSON indiquant le flux de travail à exécuter, le répertoire case_dir à utiliser ainsi que le thread auquel il appartient.
Chaque pipeline correspond à une seule exécution prolongée de Claude Code (le « cerveau »), qui reçoit une instruction soigneusement formulée ainsi qu’un répertoire case_dir écrivable. Ce répertoire sert de espace de travail temporaire pour l’agent, contenant le fichier TASK.md du cas, une pile croissante de transcriptions followup_transcript_*.md, tous les rapports gap_report*.md générés par l’agent, ainsi que des artefacts JSON intermédiaires.
La manière dont les agents sont séquencés fait partie de la conception. Au lieu qu’un seul agent s’occupe de tout, plusieurs agents spécialisés sont connectés les uns aux autres, chacun transférant le travail à un moment précis :
oncall_runeffectue la première investigation, avec jusqu’à 20 itérations de raisonnement accompagnées d’un contrôle de qualité, pour se terminer par une auto-critique explicite. Son résultat est unANALYSIS.mddécrivant ce qui ne va pas et unTASK.mdindiquant les actions à entreprendre.case_followupest l’agent de chat à long terme. Un opérateur peut répondre par message privé dans le fil d’une affaire pendant des jours, et chaque réponse réactivecase_followupavec l’intégralité de la conversation jusqu’à ce moment. Il appelle égalementgap_analyzeren tant qu’agent secondaire tous les N réponses, et ce cycle génère les fichiersgap_report*.mdqui font référence au code même du système (le cycle d’amélioration continue est abordé plus loin).
finalize_case est l’élément de consolidation. Lorsqu’un dossier reste inactif pendant quatre heures, ou qu’un opérateur exécute admin: finalize <case>, il rassemble tous les rapports de lacunes, élimine les faux positifs et crée un seul MR contenant les corrections validées. Ce MR cible la base de code du harnais et non le répertoire d’origine du ticket, ce qui constitue en pratique un cycle d’amélioration continue.dev-agent est celui qui effectue le travail concret. Dans son propre arbre de travail git par dossier, il modifie le code, effectue des mises à jour, surveille les tests CI, répare les problèmes survenus lors des tests et répond aux reviewers. Il fonctionne en trois modes : address_review, fix_ci et task (l’implémentation initiale).Ce diagramme présente deux détails qui sont facilement négligés.
Tout d’abord, rien ne touche le processus de paiement principal, car tout le travail s’effectue dans des worktrees. Cela permet à plusieurs cas de fonctionner en parallèle sans entrer en conflit. Le poller et le dispatcheur dev-agent insèrent le chemin du worktree dans la charge utile, de sorte que l’LLM ne voit jamais le processus de paiement principal.
Deuxièmement, case_dir sert de point d’ancrage pour tout. Les fichiers d’état situés dans ~/.harness/ (voir la section sur la persistance) relient chaque thread Slack à un case_dir, et l’agent résout tous les chemins par rapport à celui-ci. Tout le reste devient alors plus simple : il suffit de déplacer le répertoire, de mettre à jour la carte, et chaque workflow suit automatiquement.
Le graphe montre ce qui est exécuté. La section suivante explique comment cela fonctionne, c’est-à-dire les mécanismes qui transforment une commande Slack en une enquête achevée.
Le cycle autonome, étape par étape
Si quelqu’un demande ce que fait réellement ce système, la réponse la plus courte et honnête est : un opérateur tape une commande Slack, et à partir de là, un cycle fermé de raisonnement répétitif, d’autocritique et d’utilisation d’outils s’exécute sans surveillance jusqu’à ce que le travail soit terminé ou que le système admette qu’il ne peut pas continuer. Presque chaque décision de conception intéressante se cache dans cette phrase, et le reste de cette section l’explique en détail.
Dans ce système se trouvent quatre boucles d’agents distinctes, chacune conçue pour résoudre un type de problème spécifique :
Cette illustration montre leurs formes. Viennent ensuite les détails, avec chaque mécanisme présenté dans l’ordre où il s’exécute. Le diagramme correspond à tous ces mécanismes, et ses nœuds deviennent compréhensibles une fois que les sections suivantes ont été lues. Les codes utilisés sont G1 à G4, N1 à N3, N5, A1 à A3 et D1 à D15.
Au préalable du cycle : le verrou de pré-engagement
Deux mesures de protection sont mises en place avant la première appel à Claude, et la première est structurelle. Avant que ne commence aucune itération d’enquête, le processus en cours de travail oblige Claude à s’engager vis-à-vis d’un hypothesis_slate. Ce document liste au moins six explications possibles ainsi que quatre ou cinq critères pour choisir parmi elles, et il est enregistré dans kickoff_precommitment.json, dont le contenu ne peut plus être modifié par la suite.
De tout ce qui existe dans le système, cette mécanisme de verrouillage constitue la mesure de protection ayant le poids méthodologique le plus important tout en étant la moins visible. Un cycle de raisonnement d’un LLM non contraint a tendance à retenir ce que ses premières preuves favorisent, puis à interpréter toutes les preuves ultérieures comme une confirmation. Ce verrouillage ne peut pas empêcher cette tendance. Ce qu’il fait, c’est obliger toute investigation à partir de l’ensemble des hypothèses possibles et à enregistrer comment elle en est arrivée à sa conclusion. Le réviseur adverse (Phase B) pose alors une question plus difficile que celle de la cohérence interne : est-ce bien la conclusion que le système initial aurait prédite, compte tenu des preuves ?
La deuxième mesure de sécurité est l’ancre empirique (Phase C). Ce n’est qu’à la première itération que quelques commandes au niveau du système d’exploitation sont exécutées sans l’intervention d’un LLM : les derniers enregistrements d’erreurs de journalctl, des captures d’écran du disque et de la mémoire, ainsi qu’une recherche de fichiers journaux. Les résultats sont intégrés dans la demande d’enquête initiale en tant que preuves brutes, fournissant à Claude une image factuelle de la machine avant même qu’aucune hypothèse ne soit émise. Cette étape ne bloque jamais, gère silencieusement les erreurs et ne dure pas plus d’une seconde. Elle est essentielle car elle permet à l’étape de raisonnement suivante de partir des faits réels plutôt que uniquement de la description du problème.
Résultat structuré : le rapport de complétion
Chaque itération se termine par un contrat JSON lisible, le completion_report, que l’outil lit pour déterminer ce qui doit se faire ensuite. Sa structure est fixe :
{
"status": "IN_PROGRESS" | "BLOCKED" | "COMPLETE",
"confidence": 78,
"open_questions": ["..."],
"unchecked_sources": [{"name": "...", "access_status": "..."}],
"contradiction_register": ["..."],
"assumption_register": ["..."],
"adjacent_problems": [{"summary": "...", "status": "...", "blocker": "..."}],
"draft_response": "the user-facing Slack message"
}
La prose libre peut entourer le bloc, mais les filtres évaluent le rapport analysé et les fichiers d’état le conservent. Sacrifier une partie de la liberté stylistique pour obtenir un contrat analysable s’est avéré être le principal levier permettant de garder les sorties des LLM utilisables au sein d’un pipeline. En omettant le schéma, chaque étape suivante doit extraire les faits à partir de la prose. En le conservant, les filtres deviennent de simples scripts Python déterministes.
Filtres de qualité qui donnent un sens à la confiance
Un score de confiance ne serait qu’un bruit marketing si Claude pouvait simplement taper confidence: 95 et passer à autre chose. C’est pourquoi la sortie de chaque itération passe par des filtres de qualité qui refusent de laisser l’enquête progresser tant que les preuves et le niveau de confiance indiqué ne coïncident pas.
Trois familles de filtres s’exécutent après chaque tour de Claude :
- G-gates vérifient la cohérence logique, afin que le raisonnement ne contredise pas lui-même d’une manche à l’autre. Le cas le plus évident est G1, qui s’active lorsque
open_questionsaugmente au cours de la manche tandis queconfidenceaugmente également, car une valeur dénominateur croissante annule toute hausse. Cette famille de contrôles s’assure également que les expressions indiquant une tension dans le texte sont reconnues comme des contradictions, et que les problèmes voisins sont traités comme des objets à statut valide plutôt que comme des chaînes de remplacement. Il y en a quatre. - N-gates vérifient l’intégrité structurelle, s’assurant que les éléments créés soient complètement formés et non incomplets. N1 exige que chaque action de correction exécutable soit associée à une
verify_actionfournissant une description, un résultat attendu et des délais. Une correction proposée sans moyen de la vérifier est considérée comme un travail inachevé. Il y en a trois.
COMPLETE tant que les problèmes adjacents restent ouverts, exigent un registre d’hypothèses non vide et insistent pour que au moins une action proposée corresponde directement à la cause racine. Il y en a sept.Deux contrôles supplémentaires s’ajoutent par-dessus. EQ1 oblige les nœuds de cause racine à citer des preuves concrètes (tout sauf INFERRED) ou à accepter une limite de 60 %, tandis que P7 abaisse ce plafond de 7 % pour chaque dimension adverse marquée FAIL, jusqu’à un minimum de 40 %.
Le cadre comprend au total 19 fonctions. Une façon pratique de s’en souvenir : les G-gates réagissent à ce que Claude écrit, les N-gates à ce qu’il omet, et les A-gates à ce qu’il affirme.
Une violation devient une entrée pending_guard_notes, qui est placée en tête de la demande de l’itération suivante sous le titre ## NOTES DE SURVEILLANCE DE LA QUALITÉ DU SYSTÈME (de l’itération précédente), accompagnée d’une instruction explicite de traiter chaque note en premier. C’est ce mécanisme de contrainte qui est utilisé. Les filtres ne bloquent pas le modèle ; ils font en sorte que ses violations antérieures soient ce qu’il lit en premier lors de la prochaine itération. La politesse de la part du modèle n’a aucune importance, c’est l’application stricte des règles qui fait avancer l’enquête.
Une étape de fixation dans le contexte (A7) s’exécute également à la fin de chaque générateur de prompts, que des violations aient été constatées ou non. Elle réattache mot pour mot les contradictions non résolues et les hypothèses non vérifiées du rapport actuel à la fin du prompt suivant. Cela compense le phénomène bien connu de déclin de l’attention, où un modèle note une complication à la troisième itération mais l’oublie complètement d’ici la septième.
La confiance sous forme de nombre concret
Le choix de conception le plus important est que l’agent s’attribue une valeur confidence: sur une échelle de 0 à 100 dans la sortie structurée de chaque itération. Ce chiffre n’est pas purement décoratif, car il contrôle le comportement réel :
- Lorsque la valeur est inférieure à 70, le cycle ne peut pas être interrompu. Le processus force l’exécution de trois tours supplémentaires et insère une note d’avertissement indiquant que l’agent a prétendu avoir terminé alors que sa valeur était en dessous de la barre cible.
status: COMPLETE et aucune open_questions, la phase d’analyse par oppositions (équipe rouge contre équipe bleue) peut commencer.degrading : l’agent régresse, il s’arrête alors et en informe l’opérateur.La trajectoire complète de la confiance pour chaque cas est enregistrée dans ANALYSIS.md sous forme de graphique simplifié, par exemple 45% → 62% → 78% → 80%. Les opérateurs y jettent un coup d’œil pour déterminer si le résultat mérite foi, et le cycle d’amélioration continue du système l’analyse afin de choisir quels cas antérieurs relancer avec une instruction différente.
Trois manières dont la boucle refuse de s’arrêter prématurément
Dans le diagramme, trois branches atteignent ready_for_adversarial ou blocked sans que la boucle ne se termine. Chacune représente une règle de continuation forcée : le système ignore le signal d’arrêt et insère à la place une note de protection.
- CONFIDENCE_FORCE s’applique lorsque
status=COMPLETEetopen_questions=[], mais que la valeur deconfidenceest inférieure à 70. Le modèle déclare la victoire tout en signalant des doutes par ce chiffre. Jusqu’à 3 itérations supplémentaires sont imposées, et elles comptent dans le budget de 20 itérations au lieu d’être ajoutées à ce dernier.
status=COMPLETE et que la valeur de confidence est d’au moins 70, mais que causal_chain_complete n’est pas vrai. Il s’agit d’une détection au niveau des symptômes sans cause racine identifiée. Cela force jusqu’à 3 itérations supplémentaires, toujours dans le cadre des 20 itérations prévues.status=BLOCKED alors que des sources techniques accessibles ont été ignorées ou déclarées indisponibles par inférence et non en raison d’une erreur réelle. Une enquête ne peut pas se terminer en état bloqué sans avoir véritablement exploré toutes les possibilités. Cela autorise jusqu’à 3 poursuites, comptées dans le même budget.Ces mécanismes permettent d’éviter les trois sorties prématurées les plus fréquentes : une incertitude présentée comme certaine, une complétion au niveau des symptômes et une indisponibilité imaginaire.
L’examinateur adverse
Lorsque le pipeline de vérification déclare avoir terminé son travail avec un niveau de confiance de 70 ou plus, le contrôle est transféré à une invocation distincte de Claude qui joue le rôle d’un examinateur adverse. Cet examinateur ne voit pas la demande de l’enquêteur ; il reçoit le texte d’analyse ainsi qu’une liste de contrôle comprenant 15 dimensions d’évaluation, de D1 à D15, couvrant la validité des hypothèses, la chaîne des preuves, le raisonnement causal, les hypothèses alternatives et bien d’autres aspects. Il renvoie alors un verdict structuré :
Il existe trois issues possibles pour cette étape d’examen :
- Les problèmes critiques sont renvoyés à l’enquêteur. Ses notes sont intégrées dans la itération suivante sous le nom de
pending_guard_notes, et le cycle reprend, l’enquêteur sachant désormais explicitement que l’examineur pense qu’un élément a été manqué. - S’il n’y a que de légères anomalies lors de la première passe, le cycle est exécuté une fois de plus avec ces critiques mineures ajoutées.
complete, et l’enquête est publiée.Le fait de faire de l’étape d’adversarialité une invocation distincte avec un prompt différent est intentionnel. Le même modèle dans un rôle différent parvient à détecter une part significative de l’autosatisfaction que l’itération avec un seul prompt laisse passer. Dans un cas, la prétention de confiance est passée de 90 à 65 entre l’enquête et son examen, le réviseur soulignant précisément l’hypothèse que l’enquêteur avait négligée.
Une deuxième revue se déroule en parallèle, construite différemment : la revue par l’équipe rouge (G5). Le revueur adversaire lit le récit complet de l’enquête, tandis que le revueur de l’équipe rouge n’a accès qu’à l’énoncé initial du problème et à la conclusion finale. Il ne reçoit ni raisonnement intermédiaire, ni trace d’éléments de preuve, ni informations issues de tours précédents. À partir de ces deux éléments, il formule une hypothèse indépendante et met en évidence les cas où les conclusions de l’enquête diffèrent de ce qu’un tiers aurait pu deviner.
L’objectif est l’isolement cognitif, et non une couverture supplémentaire. L’examen adverse vérifie si l’enquête est structurellement complète (D1 à D15). L’équipe rouge s’assure que la conclusion aurait pu être atteinte sans le cadre de l’enquête : le raisonnement est-il véritablement fondé sur le problème, ou bien l’enquête s’est-elle appuyée sur une première découverte, laissant la conclusion suivre cet élément plutôt que les preuves ? Ces deux processus s’exécutent en appels Claude distincts, et tous deux doivent réussir avant qu’un dossier ne soit marqué COMPLETE.
Cinq manières honnêtes de terminer
L’itération d’enquête peut se terminer pour cinq raisons. La raison qui se produit est enregistrée et affichée à l’opérateur, comme dans des lignes telles que celles-ci :
exit=complete confidence=89% iterations=12 elapsed=13min
exit=blocked confidence=62% iterations=4 elapsed=4min (cannot proceed without info)
exit=stalled confidence=58% iterations=7 elapsed=22min (two rounds, zero new facts)
exit=degrading confidence=42% iterations=5 elapsed=8min (dropped >15% twice)
exit=timeout confidence=73% iterations=18 elapsed=50min
Parmi ces cinq, blocked est celui sur lequel un opérateur peut agir de la manière la plus directe. L’agent a enregistré des bloqueurs explicites dans unchecked_sources ou adjacent_problems, et il faut qu’un humain effectue un appel pour les supprimer. Les quatre autres correspondent à des modes de défaillance techniques que le système détecte sans tenter de les résoudre automatiquement.
Ce à quoi Claude peut accéder depuis l’intérieur
Ici, les limites des outils sont importantes, car les seuils de confiance dépendent en partie de ce à quoi Claude peut réellement accéder. Un ordre demandant à l’agent de vérifier ses sources non contrôlées n’a aucun sens si les outils nécessaires à cette vérification n’existent pas. Chaque pipeline lance un sous-processus de Claude Code avec l’option --permission-mode bypassPermissions. Aucune liste d’outils autorisés n’apparaît dans la demande. La limite est définie par ce qui est intégré au catalogue du Model Context Protocol (MCP), qui comprend cinq systèmes externes :
Quelques contraintes méritent d’être mentionnées, car elles influencent le comportement plus que tout autre élément indiqué sur la fiche du modèle :
- Le pipeline d’enquête interdit explicitement
Bash(--disallowedTools Bash), afin d’éviter les conditions de course avec git, car deux processus Claude modifiant en même temps un arbre de travail corrompraient l’état. Le pipeline dev-agent autorise bien Bash, mais il s’exécute dans un arbre de travail isolé. - Les outils d’écriture dans Slack sont bloqués lors de chaque invocation du pipeline.
send_message_to_selfetedit_message_to_selfsont interdits, de sorte que Claude ne peut pas publier directement sur Slack. Toute communication sortante passe par le champdraft_responsedu rapport de complétion structuré, que l’outil ne publie qu’après avoir vérifié les conditions de sortie.
bypassPermissions ne signifie pas en pratique une liberté totale. Le prompt de cadre de chaque pipeline encode des règles strictes. ANALYSIS.md peut être modifié, mais message.json ne le peut pas. Les écritures en dehors de case_dir sont automatiquement réversées via git diff après l’exécution. Les écritures dans la base de données de production nécessitent une confirmation explicite de l’opérateur, conformément à la commande. La sécurité réside dans la couche de cadre ainsi que dans une vérification des limites après exécution, et non à la couche d’appel de l’outil.search_datadog_services renvoie une erreur de permission, le prompt oriente l’agent pour qu’il passe à mcp__glean_default__search, en utilisant des mots-clés de Jira comme substitut. La chaîne de solutions de secours fait partie du mécanisme d’application des procédures standard et n’a pas sa place pendant l’exécution.tool_use au fur et à mesure de leur arrivée et envoie jusqu’à 10 opérations de lecture simultanées (requêtes Datadog, recherches Glean, lectures de fichiers). Tout ce qui écrit, que ce soit Bash avec des effets secondaires, des modifications, des commentaires MR ou des écritures dans une base de données, s’effectue un par un. Ainsi, une seule itération d’enquête peut lancer six requêtes de journalisation en parallèle tout en maintenant un ordre strict pour les écritures.Un squelette, quatre tailles
Tout pipeline du système exécute une variante du même squelette :
Le pipeline d’enquête en mode veille effectue jusqu’à 20 itérations. La phase d’analyse des tâches en compte jusqu’à 8. La phase de mise en œuvre exécute le squelette une fois mais itère trois fois sur l’étape de construction. Le dev-agent ne possède pas de boucle interne et s’exécute une fois par démarrage, mais mr-monitor déplace en fait l’itération à l’extérieur : il crée un nouveau dev-agent chaque fois que le CI échoue, jusqu’à trois fois consécutives, avant de transférer l’affaire à un humain.
Cette uniformité est intentionnelle. Ces quatre boucles existent parce que les problèmes diffèrent, mais ils utilisent tous le même langage : la confiance est un nombre, une issue a un nom, l’auto-critique correspond à une appel distinct de Claude, et l’escalade signifie un message privé sur Slack dans le fil de discussion ayant donné naissance à l’affaire. Un opérateur apprend cette structure une fois et la reconnaît partout par la suite.
La couche d’amélioration continue
Cela clôt le cycle esquissé précédemment, dans lequel l’outil modifie son propre substrat sous supervision humaine. Deux cycles assurent ce travail, à des échelles temporelles différentes.
Le cycle 1 répare la structure cas par cas, de la faille à la PR. Une fois une enquête terminée, gap_analyzer recherche les endroits où l’outil a sous-performé : sources de données manquantes, choix d’outils incorrects, erreurs dans le pipeline, tests absents ou descriptions de MR mal formatées. Les filtres de catégorie et de confiance éliminent les failles avant que finalize_case ne les reçoive. Lorsqu’un cas reste inactif pendant quatre heures, finalize_case lance patch_suggester dans un arbre de travail dédié et génère une PR relative au codebase de l’outil, en modifiant des fichiers de code, des modèles d’instructions ou des sections de procédures opérationnelles. La fusion de cette PR nécessite une intervention humaine, tandis que son rédaction ne l’exige pas.
Le cycle 2 renforce le comportement dans différents cas, allant du mode de défaillance à la proposition de procédure opérationnelle standard. À la fin de chaque cas, l’étape auto_retrospective enregistre quels modes de défaillance se sont produits, classés en trois catégories : les défaillances de collecte de données (des sources accessibles ont-elles été ignorées ?), les violations des critères de qualité (une affirmation de fiabilité contredit-elle les preuves ?) et les défaillances liées à l’exhaustivité de l’analyse (la recherche de la cause racine s’est-elle arrêtée prématurément ?). Les enregistrements s’accumulent par projet dans le registre des cas. Chaque semaine, une tâche cron nommée knowledge-updater compte les dix derniers cas et, lorsque un mode dépasse un seuil donné, rédige une proposition de procédure opérationnelle standard : 3 cas sur 10 entraînent une proposition, tandis que 6 cas sur 10 déclenchent un avertissement. Les propositions sont concrètes. Par exemple, l’indication « requête directe sur la base de données ignorée » est apparue 3 fois ce mois-ci, si bien que la procédure opérationnelle standard exige désormais une requête directe sur le tableau avant toute inférence de schéma. Les humains examinent ces propositions et les appliquent.
La personnalisation en découle. La procédure opérationnelle standard encode la vision du groupe concernant la manière d’enquêter, et elle commence par être générique. Avec le temps, l’outil kb-updater crée un profil des schémas d’échec d’un projet à partir des cas rencontrés et répétés. Après 20 à 30 cas, la procédure opérationnelle standard de ce projet ne ressemble plus au modèle générique : elle indique quels sources de données sont constamment ignorées, quels outils sont mal utilisés et quelles approches d’enquête manquent régulièrement dans ce codebase spécifique. Chaque projet conserve des procédures opérationnelles standard distinctes, de sorte que les erreurs de l’équipe A ne influencent que ses propres règles et n’affectent jamais l’équipe B.
La durabilité constitue un autre avantage. Une fenêtre de contexte dure uniquement pendant une session, tandis qu’un changement de code fusionné est versionné, examiné, testé et devient permanent. Une mise à jour des procédures opérationnelles, une fois approuvée par un opérateur, apparaît dans toutes les investigations ultérieures. Le substrat continue d’évoluer, de sorte que chaque nouveau cas s’exécute avec un outil amélioré par rapport à son prédécesseur.
Niveau 3 : état survivant aux redémarrages
L’orchestration est ce qui s’exécute pour chaque événement. L’état persistant, quant à lui, reste inchangé lorsque le processus est redémarré, que la machine est réinitialisée ou que l’opérateur passe d’un ordinateur portable à une VM cloud. L’outil conserve son état en trois niveaux présentant des propriétés nettement différentes :
Les caches en mémoire sont de petite taille et peuvent être reconstruits. Ils se trouvent dans le processus de surveillance et de maintien en ligne, et sont reconstruits à partir de l’état local après un redémarrage.
État local dans ~/.harness/*.json représente la carte opérationnelle. Les fichiers clés sont :
task-thread-map.jsonassocie un Slack thread_ts à un ID JIRA ainsi qu’à un chemin absolutask_dir.oncall_thread_map.jsona la même structure pour les cas d’enquête en mode on-call.monitored-mrs.jsonliste les MR ouverts par harness ainsi que leur statut CI.patch-pr-state.jsonenregistre le statut final par cas (merged,no_gaps,failedetc.).oncall-state.jsoncontient le flag de coupe-circuit, abordé dans la Couche 6.socket-dedupconserve les timestamps des messages récents déjà traités en mode Socket.
Cette couche appartient à une seule machine et n’est pas partagée. Elle survit à un redémarrage, mais disparaît si la machine tombe en panne.
État synchronisé avec Git est la couche fiable. Les espaces de travail par affaire situés sous case-workspaces/<ticket-id>/ sont enregistrés et envoyés dans un répertoire Git privé à chaque changement d’état significatif. Une deuxième machine qui clone ce répertoire peut reprendre le même contexte de conversation, ce qui assure la continuité des sessions entre machines. Un opérateur peut discuter avec l’outil depuis un ordinateur portable ou une machine virtuelle de développement à distance sans rien perdre.
La consolidation de la mémoire est ensuite devenue une troisième fonction pour l’état synchronisé par git-synced. Une longue conversation génère des dizaines de fichiers followup_transcript_*.md. Lorsque suffisamment d’activité s’accumule, un sous-agent lit tous ces fichiers, réécrit TASK.md pour y consigner l’état condensé, et le flux de travail cesse d’alimenter en transcriptions brutes les étapes suivantes. Omettre la consolidation peut faire dépasser la taille du contexte du modèle par les requêtes. La procéder présente le risque de compresser trop fortement et de perdre des informations, ce qui s’est effectivement produit : un document de 14 KB a été réduit à 587 octets. Trois mesures de protection ont été mises en place suite à cet incident : un modèle de requête indiquant « UPDATE, pas REWRITE », une protection relative à la taille du résultat qui interdit les écritures inférieures à 50 % de la taille de l’entrée, ainsi qu’un fichier de verrouillage par cas afin d’éviter que deux opérations de consolidation ne se chevauchent.
Le chemin menant d’un seul fil Slack au fichier TASK.md correspondant est court, mais il vaut la peine d’être illustré :
Les deux fichiers d’état contiennent le même type de valeur, à savoir un chemin absolu vers l’espace de travail du cas. Tout flux de travail aboutit tôt ou tard à un case_dir, puis fonctionne en se référant à ce dernier. Lorsque la racine de l’espace de travail a été renommée au cours du projet, un script de migration s’en est chargé en réécrivant les chemins absolus dans les deux tableaux tout en déplaçant le répertoire.
L’état durable constitue la base. La question suivante est de savoir ce qui se passe lorsque les opérations basées sur cet état échouent ; la réponse est que le système de gestion se remet de ces échecs sans les transmettre à l’opérateur.
Niveau 4 : boucles qui se réparent elles-mêmes
C’est au niveau où le système de gestion cesse d’être un simple notificateur pour devenir autonome. Il dispose de trois boucles, chacune se fermant sans intervention de l’opérateur.
Les deux premiers sont entièrement autonomes. Le troisième ne l’est pas délibérément. Pour renouveler la session du fournisseur d’identité, une demande de MFA liée au matériel est nécessaire ; l’agent s’arrête donc et demande une action humaine, ce qui entraîne un délai de 30 secondes. Il s’agit d’une intégration humaine conçue dans le système, sans lacune de capacités.
Ces boucles partagent une caractéristique subtile : chacune dispose d’un budget de tentatives et peut reconnaître ce qu’elle a déjà fait. La correction automatique dans CI est limitée par demande de modification, de sorte que trois échecs consécutifs entraînent une intervention de l’opérateur. La réponse automatique des reviewers élimine les doublons par auteur, ce qui signifie que lorsque la dernière note du bot dans une discussion est plus récente que celle du reviewer, la boucle reste silencieuse car la réponse existe déjà. L’outil de fournisseur d’identité permet une fenêtre d’approbation de 15 minutes. Si ces limites sont supprimées, les boucles peuvent parfois déraper. Au début, une demande de modification contenant un test d’intégration instable a donné naissance à 80 commits inutiles de type « corriger CI » en une seule après-midi, sans que personne ne s’en aperçoive.
Récupération après des opérations isolées
Les boucles de correction automatique du CI et de réponse des reviewers décrites ci-dessus permettent de restaurer l’intégralité des workflows du début à la fin. En dessous d’elles, chaque opération primitive pouvant échouer (un push, un rebase, un commentaire dans une MR, un reset de branche) repose sur un modèle à trois couches distinct et plus petit destiné à assurer la récupération automatique pour chaque opération. Celui-ci diffère du modèle de sécurité à trois couches présenté à la Couche 6, qui concerne les restrictions, c’est-à-dire ce que l’agent peut faire. Celui-ci, quant à lui, vise la récupération, c’est-à-dire ce qui se passe lorsque une opération autorisée échoue. Il est facile de les confondre, il faut donc les garder séparés.
L1 est un mécanisme de protection déterministe. Basé sur des règles et actif en permanence, il répond en moins d’une seconde. Pour les mises à jour, c’est _pre_push_rebase(repo_path, branch) : juste avant la mise à jour, le système exécute git fetch origin et git rebase origin/. Un rebase réussi permet la mise à jour. En cas de conflit ou d’échec du fetch, L1 interrompt la opération en toute sécurité, liste les fichiers en conflit et transmet l’affaire à L2. L1 ne peut pas être créatif, intentionnellement. Les règles restent suffisamment simples pour être comprises et suffisamment peu coûteuses, sans consommation de tokens, pour être appliquées à chaque opération.
L2 est un cycle d’auto-réparation automatisé. Lorsque L1 échoue à résoudre la situation, le système lance une session Claude restreinte (_self_heal_push_loop) et lui fournit toutes les informations relatives à l’échec : l’état du git, les fichiers en conflit, la tâche qui a motivé l’envoi ainsi que quelques limites strictes. Il ne faut jamais effectuer de force-push sur une branche protégée, jamais jeter des commits non envoyés et jamais choisir aveuglément un côté dans un conflit. Trois contraintes garantissent cela :
- Essai limité. Chaque incident bénéficie d’au plus 3 tentatives, et chaque tour de Claude est limité à 4 minutes. Après chaque action de Claude, l’outil exécute à nouveau le véritable
git push. L’opinion de Claude quant à l’élimination du conflit n’a aucune valeur ; seul un code de sortie 0 degit pushest considéré comme fiable. Si le prochain essai renvoie un code différent de 0, la boucle enregistre une tentative échouée, quel que soit le degré de confiance affiché par Claude. - Seuil de confiance à 70 ou plus. Ce critère est repris tel quel de la configuration
oncall_rundu pipeline d’enquête en temps réel. Claude évalue sa propre performance sur une échelle de 0 à 100 à chaque tour ; si son score est inférieur à 70, elle doit abandonner plutôt que d’agir. Un score en deçà de ce seuil ne lui permet pas d’accéder à l’arbre de travail.
git push et jamais aux informations fournies par Claude à son sujet. L2 ne peut pas contourner ce mécanisme ; elle ne peut lui fournir que des données issues d’un véritable push.L3 correspond à l’escalade vers un opérateur. Si L2 abandonne en raison d’un faible niveau de confiance ou épuise ses tentatives, le système envoie à l’opérateur un message privé contenant l’intégralité des données de L2, c’est-à-dire chacune des tentatives effectuées, chaque action réalisée par Claude, chaque niveau de confiance indiqué ainsi que le résultat final du git status. L’opérateur part d’un état entièrement documenté et non de l’hypothèse qu’« il y a eu un problème ». L’échelle temporelle est de plusieurs heures, et non de quelques secondes.
Deux principes ressortent clairement :
- L2 est activé uniquement en cas d’incident et pas en permanence. L1 fonctionne à chaque déploiement car il est peu coûteux, tandis que L2 consomme des tokens et n’intervient qu’après échec de L1 ; ainsi, le coût dépend des cas rares et non des cas courants.
- La vérité absolue provient du système réel. Peu importe ce que Claude pense de son propre travail, la validation reproduit l’opération réelle : le code de sortie de
git pushpour les déploiements, un exécution réelle des tests pour les corrections, ainsi que la vérification par le mécanisme de réponse du code de sortie du déploiement pour les réponses aux propositions de modification. Le rapport auto-rapporté par l’agent n’est jamais considéré comme la vérité absolue, car le système de test le vérifie à nouveau par rapport au système réel.
Cette même enveloppe convient bien plus que pour les simples poussées. Une opération est considérée comme qualifiée lorsqu’elle dispose de trois éléments : une correction basée sur des règles simples (L1), une tentative de réessai automatisée strictement limitée (L2) et un moyen efficace pour transférer la tâche à une personne (L3). Cette structure en trois parties permet de distinguer un cycle autonome capable de se remettre de pannes qu’il connaît déjà d’un autre qui a besoin d’un humain dès que la situation change.
Niveau 5 : quatre endroits pour voir ce qui s’est passé
L’outil d’intégration écrit dans quatre surfaces d’observabilité, chacune étant adaptée à un type de lecteur différent :
- Mises à jour en direct sur Slack. Tant qu’un agent est en cours d’exécution, un message indicateur dans le fil de discussion passe par différents états (
Reading TASK.md, puisSearching codebase, puisDrafting patch) avant de se finaliser parDoneouFailed: <reason>. Ce rapporteur en flux modifie directement ce message, ce qui évite que les fils de discussion ne soient envahis de messages inutiles. - Journalisation structurée à
~/.harness/logs/agent.loget~/.harness/logs/error.log. Chaque démarrage, chaque envoi dans une chaîne et chaque tentative de réessai deviennent un événement structuré, et ce sont ces fichiers qui sont consultés lorsqu’il est nécessaire de diagnostiquer un problème.
~/.harness/logs/system.log pour les événements transversaux tels que la fin des déploiements, les actions de surveillance et l’état de synchronisation.Un tableau de bord a été délibérément omis. Slack et grep servent de consoles. Un tableau de bord constituerait une troisième source d’information, et c’est la première que les ingénieurs cessent de consulter dès qu’elle devient obsolète. Le modèle de messages en flux fournit suffisamment d’informations opérationnelles pour que personne ne souhaite y ajouter une interface utilisateur.
Surveillance des connexions MCP
Cinq serveurs MCP externes (Slack, Datadog, Jira et Confluence, Glean, GitLab) constituent la base de ce système. Chacun dispose de son propre jeton OAuth, d’un rythme de renouvellement spécifique ainsi que de mécanismes de défaillance opaques. Lorsqu’un jeton expire sans être détecté, une session Claude commence à répondre par « Je n’ai pas accès à cet outil », sans que l’opérateur ne perçoive aucune erreur.
Un petit service nommé mcp-watchdog comble cette faille. Tous les 10 minutes, il vérifie l’état de chaque point d’entrée santé des MCP et tente une appel à un outil sans effet concret. En cas d’échec, il procède comme suit :
- Il tente un renouvellement silencieux du jeton.
- Si le renouvellement échoue, il envoie un message privé sur Slack à l’opérateur, du type « Le MCP d’Atlassian nécessite une réauthentification, cliquez ici. »
- Après deux déconnexions consécutives (
DISCONNECT_CONFIRM_THRESHOLD), il envoie une réaction:warning:sur le ticket le plus récent pour alerter.
En l’absence de ce mécanisme, un jeton expiré est interprété comme signe que « l’agent est stupide aujourd’hui », un type de panne difficile à diagnostiquer car agent.log indique jamais « jeton expiré », mais plutôt « outil a renvoyé un résultat vide ». Considérer la santé de l’authentification comme une dimension d’observabilité de premier plan est quelque chose qui aurait dû être mis en place plus tôt.
Couche 6 : l’interface de contrôle de l’opérateur
C’est l’interface la plus utilisée, mais aussi celle qui a été sous-développée au début. Chaque interaction avec l’opérateur passe par un seul canal, le self-DM, et les commandes se répartissent en six familles :
- Tâche :
admin: task <jira>exécute l’ensemble du processus d’analyse, de mise en œuvre et de soumission de demande, et l’ajout de--autole rend automatisé.
admin: oncall-run <url> déclenche une enquête à partir d’un fil de discussion Slack, tandis que oncall-toggle active ou désactive la surveillance automatique.admin: mr rescan rescanne à nouveau un MR spécifique, et mr pause/resume active ou désactive le cycle de réponse automatique pour ce MR.admin: oncall pause [durée] met en silence l’envoi automatique pendant un certain temps, et oncall resume/status termine la pause ou indique l’état actuel.admin: finalize <case> rassemble les rapports de lacunes et soumet une proposition de correction visant à améliorer le système lui-même.admin: register-thread relie manuellement un fil Slack à un dossier, tandis que close-case marque celui-ci comme terminé.Le mécanisme d’arrêt temporaire a nécessité le plus de temps pour être mis au point correctement. La première version mettait en pause tout le système, y compris les commandes administratives, ce qui empêchait un opérateur ayant mis le système en pause de le reprendre. La deuxième version mettait en pause de manière trop restreinte, si bien que la demande « arrêter le bruit » n’était pas prise en compte. La conception actuelle repose sur une distinction précise : la mise en pause silence le déploiement automatique déclenché par les activités d’autres personnes, mais jamais les commandes administratives de l’opérateur ou ses actions au sein du fil. Dans le système de surveillance, les sites de déploiement sont divisés en deux groupes. Un groupe correspond aux activités automatiques générées par d’autres utilisateurs, et il est bloqué lorsqu’une mise en pause est active. L’autre groupe correspond aux intentions de l’opérateur lui-même, et il reste actif en permanence.
La calibration a été obtenue de manière difficile. Peu après l’envoi de la première version de pause, une réponse automatique aux commentaires des reviewers s’est déclenchée en silence dans un autre cas sans être remarquée, car il semblait que la pause était bien en vigueur. La conception actuelle considère la pause comme un silence contrôlé : elle met le système en veille tout en préservant l’autonomie de l’opérateur.
Une illustration montre cette séparation :
La règle est que les actions de l’opérateur lui-même ne sont jamais mises en silence, tandis que les activités des autres personnes sont bloquées pendant la pause. C’est tout ce que contrôle admin: oncall pause.
Deux autres niveaux de sécurité
L’interrupteur de pause est le mécanisme de sécurité le plus visible, mais il n’est pas le seul. Deux autres fonctionnent sans aucune configuration en temps de exécution, l’un dans le code et l’autre grâce à des conventions de déploiement :
L’liste des canaux autorisés dans Slack mérite plus d’attention qu’il n’y paraît. Un incident évité de justesse au début du projet a été causé par un jeton Slack mal configuré, permettant d’écrire dans des canaux internes de l’équipe. L’agent aurait pu publier directement auprès des collègues, en contournant l’opérateur, et personne n’aurait pu distinguer ces publications de celles d’un humain. Désormais, chaque appel sortant chat.postMessage vérifie sa destination contre une liste d’autorisation avant d’être envoyé. La seule destination autorisée est le message privé de l’opérateur, ainsi qu’un ID de canal codé en dur pour la conversation entre l’opérateur et le bot. Si l’agent souhaite contacter un collègue, il renvoie le contenu à l’opérateur, qui décide s’il le transmet ou non. Cette règle architecturale se révèle toujours efficace : l’agent ne communique à l’extérieur que par le biais de la boîte de réception de l’opérateur.
Au-delà des six couches : ce qui en fait une solution de production
Ces six couches constituent la structure porteuse. Plusieurs mécanismes échappent à cette taxonomie mais revêtent une importance équivalente. Ils permettent de distinguer un harnais qui fonctionne bien d’un autre qui fonctionne sans interruption.
Routage de plusieurs projets à partir d’un seul fichier YAML
Aujourd’hui, deux projets tournent sur cet harnais, chacun avec ses propres règles de fonctionnement, et l’ajout d’un troisième ne nécessite aucune modification de code, seulement une nouvelle entrée dans agent-config/projects.yaml:
projects:
team-a:
sop: "team-a/SOP.md"
kb: "team-a/KNOWLEDGE-BASE.md"
dfr_threads_dir: "team-a/threads"
task_dir: "task-workspaces"
slack_channels: ["support-channel-a", "support-channel-b"]
jira_prefixes: ["AAA", "BBB", "CCC"]
team-b:
sop: "team-b/SOP.md"
kb: "team-b/knowledge-base/kb.md"
dfr_threads_dir: "team-b/threads"
task_dir: "task-workspaces"
slack_channels: []
jira_prefixes: ["XX", "YY"]
La fonction resolve_project() dirige chaque signal reçu en fonction de sa priorité : d’abord le préfixe Jira, puis le canal Slack, et enfin la valeur par défaut. Chaque projet dispose de son propre fichier SOP, de son propre fichier KB ainsi que de sa propre structure de dossiers. L’assemblage automatique des prompts pour les enquêtes sélectionne le bon fichier SOP et le bon fichier KB en fonction du projet auquel se réfère le signal. C’est ce qui permet à une instance de harness de servir deux équipes avec deux ensembles de règles distincts.
Cette approche est généralisable. Toute organisation disposant de plusieurs équipes de produits au sein d’un même harness peut utiliser la même méthode : configuration déclarative sans branches de code par projet. Elle impose également une discipline stricte, car tout ce qui est spécifique à un projet doit figurer dans son fichier SOP et jamais dans le code, celui-ci étant partagé entre tous les projets.
Reprendre une session sur n’importe quel ordinateur
L’opérateur travaille sur deux machines : un ordinateur portable et une VM cloud pérenne dans le centre de données, et une session Claude doit pouvoir être reprise sur les deux. C’est difficile car Claude Code stocke par défaut l’état de la session sur le disque local, ce qui fait que l’on ne peut pas y accéder après un redémarrage ailleurs.
Cet outil résout ce problème grâce à un stockage de sessions basé sur git :
Le mécanisme se compose de trois parties :
- Les sessions sont suivies via git. Les fichiers
.jsonlet.metade chaque session sont commités et poussés vers un dépôt privé à chaque changement d’état significatif, et ce dépôt constitue la source de vérité. - Un téléchargeur en arrière-plan s’exécute toutes les 5 secondes sur chaque machine pour maintenir la copie locale à jour. Il télécharge à l’avance le contexte nécessaire, de sorte que la reprise d’une session Claude n’entraîne pas de latence réseau à chaque frappe.
session-start.sh parcourt les fichiers locaux .meta pour trouver l’entrée correspondant à ce répertoire de travail et récupérer son UUID. Ainsi, un seul UUID suit l’opérateur d’une machine à l’autre.Le fichier de liaison de synchronisation, user-prompt-submit.sh, s’exécute après chaque demande de Claude et effectue trois opérations dans un ordre strict, sérialisées à l’aide de flock :
- Récupération et réinitialisation. L’exécution de
git fetch origin main && git reset --hardfait en sorte que le fichier localactive.jsonlconserve la transcription de l’autre machine. - Extraire le delta avant d’écraser les données. Il lit l’empreinte numérique indiquant le nombre de lignes déjà injectées depuis l’autre machine, extrait uniquement les lignes plus récentes et les enregistre dans un fichier temporaire. Cela doit se faire avant l’étape 3. Un bug précédent avait inversé ces deux étapes, ce qui faisait que l’injection continuait à lire le propre transcript de la machine.
- Envoyer son propre transcript. Il copie le transcript de la session locale de Claude dans
active.jsonl, effectue un commit et une mise à jour, puis définit l’empreinte numérique en fonction du nombre de lignes local.
Lorsque le groupe cesse son travail, le delta enregistré à l’étape 2 parvient à Claude sous forme de message système indiquant que l’autre machine a ajouté N messages depuis la dernière synchronisation. La prochaine demande prend alors en compte le contexte des deux machines.
Un fichier séparé .linecount contient le vrai nombre total de lignes, car wc -l donne des résultats erronés lorsque le transcript a été tronqué en raison de contraintes de taille. Les transcripts sont coupés à 1 000 lignes avant d’être envoyés afin de rester en deçà de la limite de 25 Mo du git. En l’absence de .linecount, chaque troncature réinitialiserait le compteur, ce qui entraînerait la rediffusion de milliers de lignes déjà traitées.
L’opération git sérialisée via flock empêche la corruption de index.lock due à des envois simultanés, ce qui s’est déjà produit une fois lorsque le démarrage d’une session bloquée coïncidait avec l’exécution d’un outil de enrichissement métier. Un mécanisme de limitation, configurable avec une valeur par défaut de 30 secondes, permet d’éviter la synchronisation complète en cas de frappes rapides, ce qui évite toute latence git par caractère.
L’idée plus générale qui mérite d’être retenue est celle de git en tant que stockage d’état. Git agit ici comme bien plus qu’un simple répertoire de code : il fonctionne comme un journal durable similaire aux CRDT pour enregistrer l’état de l’agent, les deux machines de l’opérateur servant de lecteurs finalement cohérents.
La consolidation de la mémoire est étroitement liée à la continuité entre les machines. L’étape memory_consolidate, qui réécrit les transcriptions en fichiers TASK.md et ANALYSIS.md propres, n’est pas déclenchée par un minuteur. Un service distinct, consolidate_trigger, vérifie à chaque tour quatre signaux : un SHA de commit ou un verbe indiquant une décision dans la transcription, un fil de discussion qui vient de se fermer, un espace de travail de cas contenant plus de 5 KB de transcriptions accumulées, ou au moins une heure s’étant écoulée depuis la dernière consolidation. N’importe quel de ces signaux suffit à la déclencher. Le principe est que les informations cessent de s’accumuler après une décision, et non après un certain délai en heures ; ensemble, ces quatre signaux permettent d’évaluer si la conversation a atteint une pause naturelle, de sorte que l’opérateur n’a jamais besoin de le préciser.
Les worktrees ne sont pas seulement un détail d’implémentation
Auparavant, on disait que tout se passait dans les worktrees. Cela mérite d’être développé, car le choix qui sous-tend cela a une réelle importance.
Dans la conception initiale, l’agent devait générer un diff et le outil harness devait exécuter git apply dans le répertoire principal. Cela entraînait deux modes de défaillance qui se reproduisaient constamment :
- Des en-têtes de diff fragiles. Claude générait un diff dont les numéros de ligne étaient corrects au moment de la génération, mais erronés après l’application d’un commit concurrent, ce qui faisait que
git applyrejetait l’intégralité du patch. - Un arbre principal bloqué. Si
git apply --checkéchouait en cours de route, le répertoire principal restait partiellement appliqué, et le cas suivant traité sur ce même répertoire héritait de cet état corrompu.
worktree_manager est la solution. Avant tout agent qui doit modifier du code, il crée un arbre de travail isolé sous ~/.worktrees/case-<case_id>-<stamp>/, extrait à partir d’une branche propre. Claude y travaille avec ses outils natifs d’édition et d’écriture, ce qui élimine l’utilisation de git apply ainsi que les opérations complexes via du texte diff. Il fonctionne comme un ingénieur humain : nouvelle branche, nouvel extrait, puis édition, commit et push.
Lorsque la demande de fusion est intégrée ou fermée, l’arbre de travail est supprimé. Une échec partiel n’affecte que cet arbre de travail, laissant intactes la branche principale et les autres cas en cours.
C’est ce qui permet aux agents concurrents d’être sécurisés sur une même machine. Sans arbres de travail par cas, deux cas dans le même répertoire se disputeraient l’accès à l’état du système de fichiers. Avec eux, les cas sont isolés par conception.
The harness corrige ses propres demandes de fusion
Le code source du harnais se trouve dans un dépôt GitHub privé. Lorsque le processus de rapport des écarts, de finalisation et de boucle MR génère une proposition d’amélioration pour le harnais lui-même, cette proposition est soumise à son propre pipeline CI comprenant pytest, des outils de linting et une vérification syntaxique en Python. Un service distinct, pr-ci-fixer, gère les échecs survenant dans ce pipeline de la même manière que mr-monitor gère les échecs sur les propositions d’amélioration liées aux tickets.
Le résultat est une capacité d’auto-réparation tant pour le code écrit pour les tickets que pour le code relatif au harnais. Supposons qu’une proposition d’amélioration ajoute un nouveau budget de tentatives et brise un test existant, provoquant ainsi un échec du CI. pr-ci-fixer le détecte, lance une session Claude dans un worktree, corrige le problème et effectue à nouveau un push, de sorte que l’examinateur humain trouve une proposition d’amélioration validée prête à être fusionnée. Ce cycle s’est déjà appliqué à plusieurs reprises pour des modifications réelles du harnais.
Diriger le cycle vers la source même du harnais constitue le meilleur moyen de vérifier s’il mérite bien le nom de production. Un agent capable de maintenir ses propres demandes de mise à jour en état « vert » est probablement digne de confiance pour gérer celles des autres.
Cinq niveaux de tests pour cinq catégories d’échecs
Le harnais compte 4 656 fonctions de test réparties sur 126 fichiers. Ce chiffre n’a peu de sens sans un cadre définissant ce que chaque test détecte, car les tests ne sont pas tous équivalents.
Le cadre de L0 à L4 assigne chaque test à l’un des cinq niveaux en fonction de ce qu’il isole :
- L0, frontière pure. Aucun élément n’est simulé, il n’y a ni système de fichiers ni opérations d’entrée/sortie. Il couvre la logique pure telle que les fonctions de contrôle, l’extraction de JSON et les calculs liés aux plafonds de confiance ; tout ce qui passe au niveau L0 fonctionne dans n’importe quel environnement de déploiement.
Les tests L4g coûtent le plus cher à écrire mais sont les plus utiles à conserver. Ils constituent une mémoire institutionnelle sur les modes de défaillance qui restent invisibles tant qu’ils ne se produisent pas à nouveau.
Un guide de style pour éviter que le texte généré ne paraisse écrit par une machine
Chaque proposition de modification qui passe par l’outil d’ouverture des harnais est soumise à un filtre supplémentaire avant d’être envoyée, selon une procédure opérationnelle standard spécifiquement conçue pour que le résultat ne paraisse pas généré par une intelligence artificielle. Les règles sont claires :
- Aucune mention de l’intelligence artificielle. Aucun
Co-Authored-By: Claude, aucune mention de "Generated with Claude Code"> et aucun suffixe[bot]dans les messages de mise à jour. La proposition de modification appartient à l’équipe et non au modèle. - Aucun titre à compléter. Les modèles du type "Ce que fait cette proposition / pourquoi nous en avons besoin"> sont interdits. La description doit être un texte continu accompagné d’une liste à puces concrète
Changes:, rédigée comme un ingénieur le ferait à la main. - Aucune mise en gras en ligne. Une étiquette
Root cause:suivie d’un paragraphe est interdite, tout commeFix:etRisk:. Le gras utilisé sous forme de balises est le signe le plus fiable d’un texte généré par machine.
pytest tests/test_foo.py::test_bar -v avec la commande réelle et les 3 lignes de sortie correspondantes, si.Lorsque la description du MR est rédigée, le SOP est injecté dans l’invite de génération de correctifs. Il existe en dehors du modèle d’invite car les opérateurs le gèrent : vous pouvez le modifier à tout moment, et le prochain MR suivra les nouvelles règles, sans redémarrage de daemon ni modification de code. Même le cycle de vérification peut proposer des modifications au SOP, de sorte que les règles métier concernant la manière dont l’agent rédige les MRs circulent sous forme de MR eux-mêmes, contre le fichier qui les contient.
Cela est plus important qu’il n’y paraît. Rien ne détruit plus rapidement la confiance dans un processus automatisé de MR que le fait que l’équipe remarque que toutes les descriptions ont l’air générées par une machine. Une fois cette impression ancrée, le niveau de complexité augmente, les signaux authentiques des agents sont noyés dans le bruit, et les opérateurs commencent à vouloir réécrire manuellement les descriptions, ce qui va à l’encontre de l’objectif. Les procédures standard de style garantissent que les descriptions se fondent parmi celles écrites par des humains, ce qui permet au cycle de fonctionner correctement.
Un ticket, une MR, du début à la fin
C’est ce qui se passe lorsque un vrai ticket arrive. Les temps indiqués sont approximatifs.
D’un bout à l’autre, ce cycle prend généralement entre 30 et 90 minutes en temps réel pour un ticket de petite ou moyenne taille. L’implication active totale de l’opérateur dure généralement moins de 5 minutes : confirmer l’analyse, orienter éventuellement les décisions ambiguës et cliquer éventuellement sur « fusionner ».
Les investigations s’accumulent. Après 30 cas, on compte 30 fichiers gap_report*.md, chacun indiquant un aspect que le LLM jugeait pouvant être amélioré. Une exécution de admin: gap-patterns tous les deux semaines regroupe ces éléments en modes de défaillance récurrents, et la phase finale transforme les groupes les plus importants en demandes de correction destinées au dépôt du harness, ce qui permet à celui-ci de s’améliorer lui-même. C’est ainsi que le cycle d’autosurveillance devient concret.
Comparaison avec d’autres harnesses
Divers autres systèmes adoptent une structure similaire à celle du harness. Le tableau montre les points de convergence et de divergence avec eux :
Les domaines où ce harness va particulièrement en profondeur :
- Blocage des hypothèses avant engagement. Claude s’engage en faveur d’au moins six hypothèses candidates avant que ne commence une seule itération d’enquête, ce qui empêche toute rationalisation a posteriori. Aucun outil commercial comparé ne propose un mécanisme équivalent.
- Framework de contrôle de qualité composé de 19 fonctions avec des plafonds de confiance mécaniques. La confiance est limitée par une formule fixe qui part de 1,0 et diminue de 0,08 pour chaque question en suspens ainsi que de 0,05 pour chaque source non vérifiée, ce qui en fait plus qu’une simple valeur auto-déclarée. Des pénalités adversariales par dimension s’ajoutent à trois familles de contrôles couvrant la cohérence logique, la complétude structurelle et les plafonds d’affirmation. Aucun des outils commerciaux comparés ne dispose de ce système.
oncall_run à case_followup, gap_analyzer, finalize_case et dev-agent. Chaque maillon dispose d’un schéma de transfert défini ainsi que d’un arbre de travail isolé, ce qui permet de répartir les tâches entre les agents au lieu de les concentrer sur un seul.Points où la conception est délibérément simplifiée :
- Il n’existe pas d’outil d’évaluation formel. Le LLM n’est pas évalué à l’aide de benchmarks, et les retours en production servent d’évaluation.
- Il n’y a pas de tableau de bord, seulement Slack et des journaux d’activité.
- Il n’y a pas de compteur de coûts. L’utilisation des tokens est suivie de manière approximative et n’apparaît pas sur un tableau de bord ; le coût est plutôt limité par les étapes de confirmation effectuées par un humain.
Ce qui reste, et le méta-boucle
L’outil est encore en évolution, avec des points à résoudre concernant la couverture du boucle autonome, le suivi des coûts et un outil d’évaluation formel, mais aucun de ces éléments n’est un obstacle majeur. Il remplit déjà la fonction pour laquelle il a été conçu : transformer les signaux reçus via Slack en code intégré, répondre aux retours des reviewers en une nuit, assurer un automatisation continue de la validation et intégrer ses propres améliorations dans sa prochaine version.
La véritable leçon réside dans la boucle métacyclique. Chaque cicatrice présente dans l’ensemble de gestion existe parce qu’un ticket réel a exercé une pression sur cette partie. Le développement et le fonctionnement forment une seule boucle qui évolue à deux vitesses différentes. Le signal de production revient à la source même de l’ensemble de gestion, l’agent rédige une correction, un humain la examine et l’intègre, ce qui permet d’éliminer progressivement une autre catégorie de bugs.
Le système n’est pas encore terminé. Un ensemble de gestion en production qui croit l’être a cessé d’accumuler des cicatrices, ce qui signifie en d’autres termes qu’il ne fonctionne plus sous charge réelle.
Construisez d’abord la boucle, puis faites-en un système autonome. Un système autonome sans voie de retour vers son propre substrat ne fait que propager les mêmes erreurs plus rapidement et en plus grand nombre, c’est pourquoi l’ordre des étapes est important.