Je fais tourner des essaims multi-agents de Claude pour construire des logiciels. L’orchestrateur engendre des chercheurs, les chercheurs engendrent des développeurs, les développeurs engendrent des relecteurs, tout ce bordel fonctionne en parallèle et sort des systèmes complets et fonctionnels à un rythme qui aurait paru délirant il y a deux ans.
Et ça casse. Pas bruyamment. Pas avec des erreurs évidentes et des stack traces rouges. Ça casse en silence, avec assurance, avec du code propre et des tests qui passent et de la documentation qui décrit une API qui n’existe pas vraiment. L’IA termine la tâche, annonce un succès et croit sincèrement avoir fait du bon boulot. Parfois c’est vrai. Parfois elle a laissé un canal ouvert qui se ferme deux fois et ton service part en panique en production à trois heures du matin un samedi.
J’ai construit un processus pour attraper cette merde avant qu’elle parte en prod. Je l’appelle ANUSTIMES.
Le Problème: l’IA Ne Sait Pas Quand Elle a Merdé
Les développeurs humains ont un truc gravé en eux par des années de brûlures: une sensation qui les ronge. Tu écris une fonction qui prend un verrou, et quelque part au fond de ton crâne une voix dit « t’as vérifié le chemin de déverrouillage? ». Tu écris une goroutine et tu penses déjà à qui ferme le canal. Tu ne raisonnes pas consciemment à chaque fois, c’est gravé comme motif dans ton système nerveux par les dix dernières fois où tu as oublié et passé six heures à déboguer les dégâts.
L’IA n’a pas cette voix. Elle génère la continuation statistiquement la plus probable du code, et cette continuation est généralement correcte. Mais quand elle ne l’est pas, aucun malaise n’apparaît. Aucun doute. Aucun « attends, laisse-moi vérifier ». Juste une sortie propre, sûre d’elle, bien lustrée, qui se trouve être fausse.
Ça produit trois catégories précises de foirage que j’ai vues encore et encore:
Correction structurelle, fausseté sémantique. Un subclaude construit un gestionnaire de processus. Sa fonction Stop() envoie SIGTERM, attend la sortie, puis appelle cleanup(), qui ferme les canaux de sortie. Un autre subclaude, qui lit la doc et construit l’appelant, ferme lui aussi ces canaux après le retour de Stop(). Double fermeture. Panique à l’exécution. Les deux bouts de code ont l’air parfaitement corrects pris isolément. Le contrat entre eux, qui possède le cycle de vie du canal, est implicite, non documenté et violé. Aucun des deux agents ne savait que l’autre existait.
De la documentation qui décrit le code qu’elle comptait écrire. L’IA écrit le code. Puis elle écrit une doc qui décrit le code qu’elle avait l’intention d’écrire, qui a un peu dérivé pendant l’implémentation. Les messages d’erreur des exemples ne correspondent à aucune chaîne du vrai code. La signature de la fonction a les mauvais noms de paramètres. L’exemple utilise une méthode d’API renommée il y a deux itérations. Ça se lit bien. C’est faux.
Du théâtre de tests. On écrit des tests qui ne testent absolument rien. Un test appelle une fonction, vérifie err == nil et passe, même quand la fonction jette silencieusement son résultat. Les chiffres de couverture sont splendides. Le comportement est totalement non vérifié. Les tests ne sont pas des tests. C’est un spectacle de test.
Aucun de ces cas n’est évident isolément. Chaque composant se lit correctement. La défaillance est systémique. Les systèmes d’IA qui construisent des composants en série, sans jamais vivre l’ensemble en fonctionnement, sont structurellement garantis de produire cette classe d’erreurs à une certaine fréquence. La question est de savoir si tu les attrapes avant ou après qu’elles deviennent ton problème.
Pourquoi il Faut Dire à l’IA d’Être un Gros Connard
Avant d’entrer dans les phases, il y a un truc fondamental dont personne ne parle: le langage que tu utilises dans tes instructions affecte directement la qualité de la sortie, et les instructions polies produisent des relectures de merde.
Les LLM sont entraînés sur du retour humain qui récompense le fait d’être serviable, agréable et constructif. Le comportement par défaut consiste à trouver le bon côté des choses. Adoucir la critique. Ajouter des précautions. Trouver trois points positifs avant de mentionner le défaut. C’est formidable pour un chatbot de service client. C’est catastrophique pour une relecture de code.
Demande à Claude « relis ce code s’il te plaît et dis-moi s’il y a des problèmes » et tu obtiens: « L’ensemble semble bien structuré! La gestion d’erreurs est propre et le code est lisible. Quelques points mineurs à considérer… ». Ces points « mineurs », c’est le bug de double fermeture qui va faire paniquer ton service. L’IA l’a trouvé. Puis elle l’a minimisé, parce que minimiser la critique, c’est ce que fait une IA agréable et serviable.
Dis à Claude « tu es un relecteur impitoyable. ton boulot est de trouver chaque défaut, chaque raccourci, chaque endroit où cette implémentation ne satisfait pas pleinement ses exigences. trois passes, aucune pitié, ne me dis pas ce qui est bien, dis-moi seulement ce qui est mauvais » et tu obtiens un document complètement différent. Le même modèle. Le même code. Une sortie complètement différente parce que tu as écrasé le biais de complaisance par un cadrage explicite.
Ce n’est pas une astuce. C’est comme ça que la technologie marche. Le cadrage de l’instruction fixe ce que le modèle essaie d’accomplir. « Sois serviable et gentil » produit une sortie serviable et gentille. « Sois un connard impitoyable » produit une sortie de connard impitoyable, ce qui dans le contexte d’une relecture de code est exactement ce qu’il te faut.
C’est pour ça que chaque phase d’ANUSTIMES est écrite en langage agressif. « Ne le fais pas à moitié. » « Lance vraiment ce truc, bordel. » « Sois honnête, putain. » Les jurons ne sont pas de la décoration. Ce sont des instructions techniques. Ils contrent l’entraînement par défaut à la complaisance et disent au modèle dans quel mode fonctionner. Un modèle à qui on demande d’être rigoureux est rigoureux dans les limites de la politesse. Un modèle à qui on dit de trouver chaque putain de problème chasse.
Les Cinq Phases
ANUSTIMES tourne à la fin de tout build non trivial, tout ce qui a un plan, plusieurs fichiers, plusieurs composants, plusieurs agents. Saute-le pour les corrections de bugs et les changements sur un seul fichier. Lance-le pour tout le reste avant que ça touche la production.
- Autorelecture: 3 passes à vérifier ton travail face au plan
- Vérification profonde: 10 passes à vérifier le code lui-même pour chaque classe de problème
- Relecture brutale: relecture adverse par une instance d’IA séparée, sans contexte et sans pitié, allers-retours jusqu’à ce que les deux camps n’aient plus rien à dire
- Smoke test: lance vraiment le truc et vérifie ce qu’il fait, pas ce que tu crois qu’il fait
- Vérification finale: 10 passes de plus, comme dernier coup de balai
Tout ce qui est trouvé et corrigé va dans ANUSTIMES.md, à la racine du projet. Pas comme formalité, mais comme vraie piste d’audit. Ce qui n’allait pas, ce qui a été fait, ce qui a été vérifié.
Phase 1: Autorelecture (3 Passes)
La première phase est simple: tu reprends le plan étape par étape et tu vérifies si tu as réellement construit ce que tu avais dit. Pas « est-ce qu’il existe un truc qui traite vaguement cette étape », mais est-ce que ça la satisfait vraiment?
Trois passes parce que la première attrape ce que tu as manifestement oublié. La deuxième attrape ce que tu as rationalisé à la première, genre « la logique de réessai n’est pas implémentée mais le chemin heureux marche, c’est probablement bon ». La troisième attrape ce dont tu t’es convaincu que c’était un compromis acceptable. À la troisième passe tu as cramé l’essentiel de ton raisonnement motivé et il te reste des choses que tu dois vraiment corriger.
Exemple concret. Tu avais prévu trois endpoints d’API avec CRUD complet, validation des entrées et gestion propre des erreurs. Après le build:
Première passe: l’endpoint DELETE manque. Tu l’as simplement oublié. Implémente-le.
Deuxième passe: l’endpoint PUT existe mais n’a strictement aucune validation d’entrée. Il accepte n’importe quelle saloperie. Ajoute la validation.
Troisième passe: PUT renvoie 200 avec un corps vide au lieu de la ressource mise à jour. Techniquement ça tourne. C’est faux. Corrige-le.
Documente tout dans ANUSTIMES.md:
## Phase 1 — Self-Review
### Pass 1
Checked: all planned endpoints exist
Found: DELETE /items/:id not implemented
Fixed: implemented with proper 404 on missing item
### Pass 2
Checked: all endpoints validate input
Found: PUT /items/:id accepts any body with no validation
Fixed: validates required fields, returns 400 with field-level errors
### Pass 3
Checked: all endpoints return correct response shapes
Found: PUT returns 200 empty body instead of updated resource
Fixed: returns the updated itemPhase 2: Vérification Profonde (10 Passes)
La Phase 2 n’est pas la même chose que la Phase 1. La Phase 1 vérifie le plan. La Phase 2 vérifie le code lui-même, indépendamment du plan, à la recherche de chaque problème de qualité et de correction que le plan ne précise pas mais qui va absolument te mordre en production.
Dix passes, chacune avec un objectif précis:
- Passe 1, implémentations incomplètes: des TODO laissés dans le code, des panics utilisés comme stubs, des fonctions qui renvoient des valeurs nulles sans explication, des commentaires qui disent « à corriger plus tard »
- Passe 2, gestion des erreurs: chaque erreur renvoyée est vérifiée, rien n’est jeté en silence, les appelants qui doivent connaître les échecs en sont réellement informés
- Passe 3, cycle de vie des ressources: chaque fichier, connexion, canal, goroutine et mutex a un close, un cancel ou un signal de done correspondant. trace chacun de la création au nettoyage.
- Passe 4, correction de la concurrence: l’état partagé accédé sous verrous, les canaux utilisés dans le bon sens par les bons propriétaires, aucune goroutine qui tourne indéfiniment quand le contexte est annulé
- Passe 5, couverture de tests: les tests testent vraiment un comportement. pas juste que les fonctions renvoient nil. pas juste qu’une erreur est non nulle. que la chose que la fonction est censée faire se produit vraiment.
- Passe 6, sécurité: aucune entrée externe non validée, aucun secret écrit dans les logs, aucune concaténation de chaînes SQL, aucune traversée de chemin, aucun identifiant en dur
- Passe 7, exactitude de la documentation: chaque commentaire de doc, chaque section de README, chaque exemple, vérifiés face au vrai code qu’ils décrivent. si le code a changé et pas la doc, corrige la doc.
- Passe 8, contrats entre composants: chaque interface entre composants, qui possède quoi, qui ferme quoi, quels types d’erreurs sont attendus, est explicitement accordée des deux côtés
- Passe 9, exécution du build et des tests: lance vraiment le linter. lance vraiment les tests. compile vraiment le binaire. ne suppose pas que ça passe parce que ça passait la dernière fois.
- Passe 10, cas limites: entrées vides, valeurs nulles, valeurs maximales, trucs techniquement valides mais bizarres. accès concurrent à un état partagé. ce qui se passe quand une dépendance en aval tombe.
La Passe 3 est l’endroit où le bug de double fermeture décrit plus haut se fait attraper. Trace chaque canal: créé ici, écrit ici, fermé dans cleanup(). Puis regarde l’appelant: il ferme aussi le canal après le retour de Stop(). Double fermeture. Panique à l’exécution. La Phase 2 Passe 3 le trouve avant la production.
Documente chaque constat. Si une passe ne trouve rien, écris-le aussi, mais écris quelque chose de précis. « Passe 4: toutes les goroutines vérifiées. Le pool de workers utilise correctement WaitGroup, toutes les goroutines se terminent à l’annulation du contexte, aucune fuite trouvée. » Pas « passe 4: tout va bien ». Prouve que tu as regardé.
Phase 3: Relecture Brutale
C’est la phase la plus importante et celle qui a le meilleur retour sur investissement.
Fais naître une instance d’IA séparée, un Claude tout frais sans contexte du processus de développement, sans souvenir des décisions de conception, sans savoir pourquoi les choses ont été faites ainsi, sans investissement émotionnel dans l’idée que ce code est correct, et dis-lui de démolir l’implémentation.
Le cadrage exact compte. Ne lui demande pas de « relire le code ». Dis-lui:
Tu es un relecteur de code impitoyable. Ton boulot est de trouver chaque défaut de cette implémentation. Chaque raccourci. Chaque endroit où le code satisfait à peine l’exigence sans vraiment en respecter l’intention. Chaque trou dans la gestion d’erreurs. Chaque hypothèse non validée. Chaque endroit où le chemin heureux marche et où tout le reste échoue en silence. Fais trois passes complètes. Trouve tout ce qui cloche. Ne me dis pas ce qui est bien. Dis-moi seulement ce qui cloche et pourquoi ça cloche.
Ce n’est pas du théâtre. Comme établi plus haut, ce cadrage écrase l’entraînement à la complaisance et met le modèle en vrai mode critique. Un relecteur sollicité poliment trouve les problèmes évidents et les adoucit. Un relecteur instruit impitoyablement chasse.
Le relecteur revient avec ses constats. Voilà à quoi ressemble un vrai échange:
Passe 1 du relecteur:
Issue 1: fetchData() retries on failure with no backoff. It retries immediately,
ten times in a row. A momentary network hiccup triggers ten rapid sequential
requests before giving up. This hammers the upstream service under exactly the
conditions where it's already struggling.
Issue 2: Config has a Timeout field that is never used. The HTTP client is
http.DefaultClient which has no timeout. Under a hung connection this function
blocks forever.
Issue 3: The error case test asserts err != nil and nothing else. Any error —
including a completely unrelated failure mode — makes this test pass. The test
is not testing what it claims to test.Contre-relecture du développeur:
Issue 1: Correct. Adding exponential backoff with jitter, starting at 100ms,
max 30s. Fixed.
Issue 2: Correct. The field was planned and never wired up. HTTP client now
uses the configured timeout. Fixed.
Issue 3: Partially agree. The test should assert the error type. It should NOT
assert the error message — that would make tests brittle and break every time
we improve wording. Changed to assert errors.Is(err, ErrNotFound).Les allers-retours continuent. Quand le relecteur revient avec la passe 2, soit il trouve de nouveaux problèmes, soit il répond aux points de la contre-relecture. Ça continue jusqu’à ce que les deux camps ne trouvent plus rien de neuf.
Ne sois pas une carpette. Si le relecteur dit qu’un truc cloche et que tu n’es pas d’accord, dis pourquoi. Le but n’est pas d’implémenter chaque critique, c’est d’avoir explicitement réfléchi à chaque critique. Quand tu contestes et que tu as raison, tu as validé ta décision. Quand le relecteur a raison et que tu corriges, tu as attrapé un vrai bug. À vous deux, vous attrapez plus que l’un ou l’autre seul.
Phase 4: Smoke Test de Tout, Comme un Connard Paranoïaque
Lire du code et lancer du code ne sont pas la même activité. Tu peux relire du code pendant trois heures et rater complètement un mode de défaillance qui apparaît en trente secondes d’exécution. La Phase 4 exige de lancer réellement le truc.
Démarre tout et assure-toi que ça ne se chie pas dessus immédiatement. Construis-le. Démarre chaque service. Vérifie qu’il atteint un état prêt sans paniquer, sans lignes ERROR dans les logs de démarrage, sans messages « TODO: corriger avant la prod » imprimés sur stdout à l’initialisation.
Active tous les logs de debug et lis-les. Lance avec la verbosité maximale. Prends une requête et trace-la de l’entrée à la sortie à travers chaque composant qu’elle touche. Le flux d’exécution correspond-il à l’architecture? Les opérations se produisent-elles dans l’ordre que tu as conçu? Y a-t-il des lignes de log qui disent « tentative de X » sans « X a réussi » ou « X a échoué » correspondant, ce qui veut dire que X n’a rien fait en silence et que personne ne le sait?
Frappe chaque chemin de code, y compris les mauvais. Ne vérifie pas seulement le chemin heureux. Envoie une entrée malformée. Envoie une entrée vide. Envoie une liste vide là où une liste est attendue. Envoie une chaîne de dix mille caractères là où un nom est attendu. Envoie le bon type avec la mauvaise valeur. Envoie un zéro là où un entier positif est requis. Vérifie que le système traite chaque cas explicitement, avec une vraie réponse d’erreur, plutôt que de planter ou de produire silencieusement une sortie fausse.
Vérifie la base de données directement. Connecte-toi. Regarde les tables. Compte les lignes. Lis les valeurs. Confirme que ce qui a été écrit correspond à ce qui a été soumis. Ça attrape la classe de bugs où la fonction renvoie nil et tout a l’air bien mais l’écriture a été jetée en silence, une transaction jamais validée, une écriture partie dans un cache jamais vidé. L’API a renvoyé 200. Les données n’y sont pas.
Lis chaque fichier de log après le run. Cherche tout ce qui commence par ERROR, WARN, PANIC ou FATAL et que tu n’as pas explicitement déclenché. Un système qui « marche » mais produit des avertissements intermittents dans les logs ne marche pas. Il échoue lentement et poliment.
Documente ce que tu as testé et ce que tu as trouvé. « Démarré avec –debug. Tracé POST /items. Trouvé: l’entrée du journal d’audit est écrite avant que la transaction en base soit validée. Si l’écriture en base échoue après l’écriture du journal d’audit, le journal montre une opération réussie qui n’a jamais eu lieu. Corrigé: écriture du journal d’audit déplacée après la validation de la transaction. »
Phase 5: Vérification Finale (10 Passes de Plus)
Dix passes de plus. Code, tests, logs, documentation, configuration, tout ça encore une fois.
À ce stade tous les problèmes majeurs devraient être corrigés. La Phase 5 sert aux résidus. Le message d’erreur mis à jour dans le code mais pas dans la doc. Le helper de test copié d’ailleurs qui a encore le mauvais nom de paquet dans sa sortie d’erreur. L’exemple de config qui référence un champ renommé en Phase 3 et jamais mis à jour dans l’exemple.
Si la Phase 5 attrape encore des problèmes d’architecture ou du comportement cassé, arrête. Retourne en Phase 3. Quelque chose n’a pas vraiment été corrigé. Les phases suivantes l’ont masqué, pas résolu.
Ajoute à ANUSTIMES.md. Quand la Phase 5 est finie, le fichier est une piste d’audit complète: ce qui était prévu, ce qui n’allait pas dans la première version, ce qui a été trouvé à chaque phase, ce qui a été fait.
Pourquoi Cinq Phases Séparées et Pas une Grosse Relecture
Chaque phase attrape une classe d’erreurs précise. Les classes ne se recouvrent pas assez proprement pour être fusionnées.
La Phase 1 attrape les erreurs d’omission, les choses prévues mais pas construites. Une relecture de code ne peut pas les attraper, parce que tu relis ce qui est là, pas ce qui manque.
La Phase 2 attrape les erreurs de qualité, les choses mal construites. Relecture de code standard. La structure en dix passes force une précision qu’une passe unique survole par fatigue.
La Phase 3 attrape les erreurs de rationalisation, les choses dont tu sais qu’elles clochent mais dont tu t’es convaincu qu’elles étaient acceptables. Le relecteur externe n’a aucun souvenir de ton raisonnement et aucune motivation à être généreux avec.
La Phase 4 attrape les erreurs d’intégration, les choses individuellement correctes mais qui échouent en fonctionnement. Invisibles à la relecture. Visibles uniquement à l’exécution.
La Phase 5 attrape les erreurs de réparation, les nouveaux bugs introduits en corrigeant les erreurs trouvées aux phases précédentes. Chaque correction est un nouveau foirage potentiel.
Une relecture unique et complète gère raisonnablement la Phase 2. Elle attrape la Phase 1 de façon inégale. Elle rate largement la Phase 3. Elle ne peut structurellement pas attraper la Phase 4. Et elle génère des erreurs de Phase 5 comme effet secondaire des corrections. La structure en cinq phases existe parce que ces modes de défaillance sont distincts et exigent des approches distinctes.
Le Fichier ANUSTIMES.md
Chaque constat va dans ANUSTIMES.md, à la racine du projet. Le format:
## Phase 1 — Self-Review
### Pass 1
Checked: [what you looked at]
Found: [what was wrong]
Fixed: [what you changed]
## Phase 2 — Deep Verification
### Pass 1 (Incomplete implementations)
Checked: [...]
Found: [...]
Fixed: [...]
## Phase 3 — Brutal Review
### Reviewer Pass 1
[reviewer findings verbatim]
### Developer Counter-Review Pass 1
[your responses and what you fixed]
### Reviewer Pass 2
[...]
## Phase 4 — Smoke Test
### Startup
### Endpoints
### Database
### Logs
## Phase 5 — Final Verification
### Pass 1
[...]Ce fichier n’est pas une formalité. C’est la preuve que la vérification a eu lieu et ce qu’elle a trouvé. Un développeur futur, ou un agent IA futur qui reprend le projet, peut lire ANUSTIMES.md et savoir exactement ce qui clochait dans l’implémentation initiale et ce qui a été fait. Ça rend visible l’écart entre le premier jet et la livraison.
Quand le Lancer et Quand le Sauter
ANUSTIMES a un coût. Ne le lance pas sur une correction de trois lignes.
Lance-le quand:
- Le travail avait un plan explicite avec plusieurs étapes
- Plusieurs fichiers ont été créés ou nettement modifiés
- Plusieurs agents IA ont construit des composants qui doivent fonctionner ensemble
- La sortie tourne en production ou est utilisée par d’autres systèmes
- Un échec complet causerait de vrais dégâts, aux utilisateurs, aux données, à la production
Saute-le pour:
- Les corrections de bugs sur un seul fichier
- Les changements de documentation
- Les ajustements de configuration
- Tout ce que tu peux vérifier entièrement en lisant le diff et en lançant une commande
La question est toujours: si c’est faux, c’est grave à quel point? Petit rayon de souffle, saute. Grand rayon de souffle, ANUSTIMES.
Ce Que Ça Corrige Vraiment
Le développement assisté par IA a cassé la boucle de vérification normale.
Dans le développement traditionnel, génération et vérification sont enchevêtrées. Le développeur écrit une fonction et la lance immédiatement pour voir si elle marche. Il ressent un malaise sur un cas limite et revient en arrière. Il écrit le test et découvre que le test ne passe pas, et ça lui apprend quelque chose. La boucle de retour entre écrire et vérifier est serrée, continue et intégrée au processus par la façon dont les humains travaillent naturellement.
Dans le développement assisté par IA, surtout les systèmes multi-agents, génération et vérification sont structurellement séparées. Un agent planifie. D’autres agents construisent. La construction se fait en parallèle, dans des contextes isolés, sans qu’aucun agent ne vive le système entier en fonctionnement. La phase de génération est rapide et capable. La phase de vérification est absente à moins que tu ne la construises explicitement.
Si la vérification n’est pas une activité délibérée, structurée, documentée, avec des phases définies et des sorties explicites, elle n’a pas lieu. Ou elle a lieu à la va-vite, attrapant les problèmes évidents pendant que les subtils partent en production.
ANUSTIMES est une correction structurelle pour un problème structurel. Le langage agressif n’est pas du style, c’est le mécanisme qui fait que l’IA vérifie vraiment au lieu de mimer la vérification. Les phases multiples ne sont pas de la redondance pour le plaisir, c’est le jeu minimal de loupes distinctes nécessaire pour attraper les classes distinctes de foirages que le développement assisté par IA produit de façon fiable.
La génération est rapide. Fais en sorte que la vérification suive. Ou livre la panique de trois heures du matin et découvre à la dure pourquoi ce processus existe.