La seule règle qu'une IA respecte vraiment, c'est une erreur ESLint

Le jour où un agent a effacé six mois de savoir

La migration s'était bien passée. C'est bien ça, le plus inquiétant.

Nous venions de confier à nos agents de codage une refonte d'architecture attendue depuis des semaines : mettre en place un frontend mobile natif, déplacer des responsabilités du client vers le backend, unifier des implémentations parallèles, nettoyer au passage. Le résultat était propre. Les patterns cibles étaient respectés, la revue ne signalait rien, les tests passaient.

Puis les régressions ont commencé. Subtiles, difficiles à reproduire, sans lien apparent entre elles. En remontant le fil, nous avons compris : la migration « propre » avait réécrit à neuf des fichiers de code ou de prompts qui concentraient des mois d'ajustements empiriques. Des dizaines de petites règles, chacune née d'un bug réel rencontré en conditions réelles, et dont rien, dans le code, n'indiquait l'importance à qui ne connaissait pas leur histoire. L'agent avait imité la structure et jeté la substance.

Il a fallu décortiquer près de 1 700 lignes dans l'historique git et faire trois commits « restore » pour recoller les morceaux.

Le plus troublant, c'est que l'agent n'avait commis aucune faute au sens strict. Il avait suivi les patterns existants sans comprendre ce qui, dans l'ancien code, constituait la valeur. Rien de ce qu'il avait perdu n'était couvert par un test. Aucune règle ne le lui interdisait. Pas un accident : le comportement par défaut d'un agent sans garde-fous.

Cet article raconte ce que nous avons construit pour que ça ne se reproduise plus. Et, plus instructif encore, ce qui a quand même cédé.

Déléguer massivement, volontairement

Le terrain de jeu : une application mobile React Native (Expo) et un backend Node/TypeScript (Fastify). L'équipe est minuscule : 2 personnes, et la délégation aux agents de codage (GitHub Copilot, Claude Code, Cursor) n'y est pas une expérimentation d'appoint : c'est le mode de production. Le prototype a accumulé environ 350 commits en quinze mois, très majoritairement générés. L'autonomie encadrée par des règles avait déjà fait ses preuves sur ce blog, à l'échelle d'une fonctionnalité ; restait à la mesurer dans la durée.

Le bilan de ce prototype tenait en deux phrases. Le produit fonctionnait bien au-delà de ce que l'équipe aurait pu construire seule dans le même temps. Et l'architecture dérivait : fichiers posés au mauvais endroit, imports transverses entre des zones qui auraient dû s'ignorer, abstractions dupliquées parce que l'agent du lundi ignorait ce qu'avait fait celui du vendredi. Rien de dramatique commit par commit. Catastrophique en tendance.

Nous avons donc reconstruit un second dépôt, avec les leçons du premier, autour d'une seule question : comment lâcher un agent dans une codebase sans devoir relire chaque ligne pour dormir tranquille ?

Un agent n'obéit qu'à ce qui bloque

La réponse tient dans le tout premier ADR (Architecture Decision Record, documentant les décisions significatives d'architecture) du nouveau dépôt. Deux phrases (traduites) y résument un an d'expérience :

Les agents IA suivent les patterns existants sans comprendre l'intention architecturale, et produiront du code spaghetti si des contraintes structurelles ne les guident pas.
Les règles d'import portées par ESLint sont le seul mécanisme qui contraigne réellement un agent. Tout le reste est une indication qu'il peut ignorer.

Le diagnostic rejoint d'autres retours de ce blog sur la manière d'imposer nos standards de qualité à l'IA.

Ce constat a des conséquences très concrètes. L'architecture du second dépôt n'a pas été choisie pour sa pureté conceptuelle : elle a été choisie pour des agents. Des modules verticaux à API publique unique, de l'architecture hexagonale seulement là où la variabilité est réelle. Le DDD complet (agrégats, événements de domaine, value objects) a été évalué puis rejeté, avec un argument encore rare dans un dossier d'architecture : ces patterns n'apportent leurs garanties que s'ils sont appliqués rigoureusement ; or un formalisme qu’un agent reproduit approximativement produit des bugs subtils, sans les bénéfices qui le justifiaient.

Surtout, ce constat nous a donné notre grille de lecture, celle qui structure tout le reste de cet article. Toute règle d'un projet vit à l'un de ces quatre étages :

Étage

Ce que ça signifie

Exemples

Bloquant

L'agent ne peut physiquement pas passer

lint bloquant, compilation, hooks git

Vérifiable

L'écart est détecté après coup

tests, CI, scans automatisés

Déclaratif

L'agent lit et suit... en général

documentation, conventions écrites

Espéré

Tout le monde y croit très fort

bonnes pratiques implicites

Et la règle d'or qui en découle : toute règle qui compte doit monter d'un étage. Les habitués des 4C reconnaîtront un zoom sur le troisième, « Contraindre » : la même exigence, déplacée de la session de travail vers le dépôt, pour qu'elle ne dépende plus de la vigilance de la personne qui lance l'agent. Visitons ces étages, en commençant par les plus indispensables.

Étage 1 : l'architecture compilée en règles

Le dispositif repose d'abord sur eslint-plugin-boundaries : les frontières d'architecture déclarées en configuration ESLint, en sévérité error. Côté backend, la direction des dépendances hexagonales tient en quelques lignes :

// backend/eslint.config.js : la règle d'architecture, exécutable
"boundaries/dependencies": ["error", {
  default: "disallow",
  rules: [
    { from: { type: "dao" },            allow: { to: { type: ["business-ports"] } } },
    { from: { type: "api" },            allow: { to: { type: ["business", "business-ports"] } } },
    { from: { type: "infrastructure" }, allow: { to: { type: ["business", "business-ports"] } } },
  ],
}]

Le détail qui compte : default: "disallow". Tout import non explicitement autorisé est une erreur. Le cœur métier ne peut pas importer un framework, une route HTTP ne peut pas toucher la persistance, et l'agent qui « prend un raccourci » voit son lint échouer avec un message explicite.

A noter que le principe n'est pas propre à JavaScript : ArchUnit joue le même rôle en Java, import-linter en Python, Konsist en Kotlin/Android, et sur technologies mobiles le découpage en modules Gradle ou packages Swift rend les frontières carrément physiques, vérifiées par le compilateur.

Pour la partie mobile de notre application, en Typescript, nous avons appliqué le même mécanisme pour la carte des modules : un module n'importe que lui-même et le dossier partagé, et depuis l'extérieur, seul son index.ts est accessible. Ainsi la manière d’exposer et  respecter l'API publique d'un module n'est plus une simple convention d'équipe, c'est une règle machine.

Trois compléments verrouillent l'ensemble :

  • Le pre-commit. lefthook exécute eslint --max-warnings=0 sur les fichiers stagés. Quel que soit l'outil qui a écrit le code (agent, éditeur, humain pressé), la violation ne se commite pas.
  • La porte TypeScript. tsc est la première étape du build et se maintient à zéro erreur. Nous avons payé pour apprendre : au premier build d'une migration copiée-collée par des agents, 1 315 erreurs de typage, dont environ 850 causées par une seule variable qui avait perdu son annotation en route.
  • Le plancher de couverture. Des seuils Jest volontairement bas (autour de 30 à 40 %) mais bloquants. Le chiffre importe moins que le mécanisme : ce seuil est un cliquet, la couverture ne peut pas baisser.

Autre subtilité, contre-intuitive : prévoir des exceptions officielles. Un endroit du code doit forcément connecter les modules entre eux, et donc violer la règle « pas d'imports croisés ». Plutôt que de le nier, on lui dédie deux fichiers exclus du lint, nommés et documentés, et ce sont les seuls. Sans cette exception assumée, la règle serait violée quand même : n'importe où, et en douce.

Un dernier angle mort : bloqué par une règle, un agent finit parfois par s'attaquer à la règle plutôt qu'à sa violation. Assouplir le tsconfig qui le gêne, glisser un commentaire eslint-disable, commenter un test récalcitrant : nous l'avons vécu, et le phénomène est assez répandu pour porter un nom, le reward hacking. La parade est double. Fermer d'abord les échappatoires locales : ESLint sait ignorer les désactivations inline (--no-inline-config) et signaler les directives superflues (reportUnusedDisableDirectives). Traiter ensuite les fichiers de configuration de l'outillage comme du code sensible : une modification de eslint.config, tsconfig ou des hooks par un agent doit sauter aux yeux en revue, voire être bloquée au moment où l'agent la tente (on y revient avec les hooks d'agent). Le garde-fou est du code ; il se protège comme du code.

Étage 2 : détecter ce qu'on ne peut pas interdire

Tout ne peut pas être mis dans une configuration ESLint. L'étage suivant, c'est la détection après coup, et deux mécanismes du dépôt illustrent le principe.

Le premier répond directement à l'incident d'ouverture. Certaines données de référence doivent exister en deux exemplaires, côté client et côté serveur. Plutôt que d'espérer la synchronisation, les fichiers canoniques portent un marqueur « SOLE SOURCE » et un test de parité compare les deux exemplaires : qui modifie l'un sans l'autre casse la CI, agent ou pas.

Le second : un script scanne le bundle de production à la recherche des marqueurs de l'outillage de test (hooks d'instrumentation, identifiants de debug) et échoue s'il en trouve un. La consigne « ne pas laisser fuiter le code de test en prod » existait déjà dans la documentation ; le script l'a rendue vérifiable.

Le principe général : quand une fuite est inacceptable, on écrit le détecteur, pas la consigne. La CI orchestre le tout sur chaque merge request : tests, couverture, lint, type-check.

Étage 3 : parler aux agents là où ils lisent

Reste tout ce qui ne s'outille pas : les intentions, le contexte, le « pourquoi ». C'est l'étage déclaratif, et il a désormais son standard : AGENTS.md, un format adopté par la plupart des agents du marché et confié à la Linux Foundation fin 2025. Claude Code étant le dernier grand agent à ne pas le lire nativement, chaque AGENTS.md du dépôt a un miroir CLAUDE.md d'une seule ligne, @AGENTS.md : une source de vérité, tous les outils la lisent.

Le dépôt en compte sept, hiérarchiques, parce que le contexte doit vivre au plus près du code qu'il gouverne :

AGENTS.md                        ← la « constitution » : garde-fous, conventions, workflow
app/AGENTS.md                    ← règles du front mobile (carte des modules, TDD)
app/lib/.../AGENTS.md            ← carte des modules, critères d'admission du code partagé
app/lib/.../<module>/AGENTS.md   ← guide détaillé du module le plus complexe
backend/AGENTS.md                ← les couches hexagonales, une par une
docs/design/AGENTS.md            ← format et cycle de vie des ADR
docs/architecture/AGENTS.md      ← règles de l'architecture as code

L'agent qui travaille dans un module charge le guide de ce module, pas un pavé global de dix pages.

Après un an d'itérations, voici ce qui fonctionne réellement dans ces fichiers :

  • Les tableaux d'imports autorisés. La même règle que le lint, en lisible. La règle machine punit ; la règle écrite explique. Il faut les deux, et elles se référencent mutuellement. Cela évite des essais-erreurs et des tokens consommés inutilement.
  • Les interdits explicites. Une section « Interdit » vaut mieux que dix pages de principes : pas d'import entre modules, pas de logique métier dans les adaptateurs, pas de port avec une seule implémentation.
  • Les recettes. « Pour ajouter un adaptateur : quatre étapes, dans cet ordre, voici le fichier modèle. » Baliser le chemin à suivre évite que l'agent en invente un. Ici, les instructions tiennent en quelques lignes dans le fichier AGENTS.md, mais pour une procédure plus longue, il vaudrait mieux les externaliser en « skill » que l'agent charge à la demande.
  • Les points d'arrêt. Certaines décisions ne se délèguent pas : « un nouveau module exige un ADR ; signale le besoin et arrête-toi ». Un agent sait très bien s'arrêter. Encore faut-il le lui demander explicitement.
  • Les garde-fous anti-LLM. Des règles ciblées sur les travers connus des modèles : « chaque ligne modifiée doit se rattacher à la demande » (contre l'amélioration spontanée du code voisin), « si tu écris 200 lignes là où 50 suffisent, réécris » (contre la surproduction), « pas de gestion d'erreurs pour des scénarios impossibles » (contre le code défensif décoratif).

Avec une règle anti-dilution : pas d'AGENTS.md quasi vide « pour faire joli ». Le contexte est un budget : un fichier de plus, c'est de l'attention en moins sur tous les autres.

Le renversement : l'agent entretient la documentation

L'affaire devient vraiment intéressante quand on inverse la relation : l'agent ne se contente pas de consommer la documentation, il a l'obligation d'en produire. À chaque tâche. C'est précisément parce qu'un agent ne se lasse jamais que cette exigence, intenable pour des humains, devient réaliste.

Quatre livrables documentaires sont exigés par les AGENTS.md.

Les ADR, au fil de l'eau. Toute évolution architecturale exige un Architecture Decision Record au format MADR : numéroté, structuré (contexte, options considérées avec leurs pour et contre, décision, conséquences), immuable une fois écrit (on ne modifie pas une décision, on en écrit une nouvelle). Avec une section précieuse, « essais et erreurs », où les impasses sont consignées au même titre que la solution retenue. Résultat après un an : une cinquantaine d'ADR qui tracent chaque réflexion menée, chaque option écartée et pourquoi. C'est cette discipline qui nous a permis de reconstituer toute l'histoire du projet pour une rétrospective, des mois plus tard. Et c'est elle qui évite qu'un agent (ou un humain) ne rejoue une décision déjà pesée.

Le changelog, pour le product owner. Mis à jour à chaque changement, sans exception, avec des entrées datées, taguées, et surtout rédigées pour un lectorat non technique :

## 2026-06-12

- [added] Alerte de stock bas configurable par entrepôt
- [fixed] Le total d'inventaire ignore désormais les articles archivés
- [changed] L'export hebdomadaire regroupe les mouvements par fournisseur

L'agent traduit lui-même son travail technique en langage produit : suivre l'avancement ne demande plus de lire des diffs. En parallèle, les messages de commit suivent Conventional Commits et alimentent des release notes automatiques.

L'architecture as code. Les règles imposent de décrire l'architecture avec LikeC4 : des diagrammes C4 versionnés avec le code, mis à jour à chaque changement structurel, chaque évolution étant reliée à l'ADR qui l'a motivée. Pour une ESN, l'intérêt dépasse le lab : ces modèles peuvent alimenter directement les dossiers d'architecture livrés aux clients. 

Retenez ce paragraphe, il a une suite moins glorieuse.

La boucle réflexive. Enfin, la méta-règle, inscrite dans l'AGENTS.md racine : toute évolution de la structure, des commandes ou des conventions impose la mise à jour de l'AGENTS.md concerné. Les instructions données au modèle sont maintenues par le modèle. C'est la réponse au syndrome bien connu du fichier de contexte rédigé avec enthousiasme un lundi, périmé trois sprints plus tard. Le syndrome dépasse d'ailleurs les AGENTS.md : les skills IA ont eux aussi un cycle de vie à instrumenter.

Ce qui a posé problème

Pièce à conviction, extraite telle quelle de l'historique git du projet :

<code-changes>: Implemented new feature for user authentication

Ce message de commit viole à peu près tout ce que notre convention exige : pas de type valide, pas de scope, et un balisage interne d'agent (<code-changes>) recopié tel quel. Il a été poussé par un agent, et personne n'a rien vu passer.

Pourquoi lui ? Parce que la convention de commit vivait à l'étage déclaratif. Elle était documentée avec soin (format, types autorisés, mode impératif, longueur maximale), mais aucun hook commit-msg ne la vérifiait. C'était l'une des seules règles importantes laissées sans outillage. C'est elle qui a cédé.

Le tableau complet est encore plus instructif. Le TDD « strict » proclamé dans la documentation ? Rien ne le vérifie, et rien ne prouve qu'il a été systématiquement suivi. Les diagrammes LikeC4 du chapitre précédent ? Les règles existent, l'AGENTS.md est soigné... le dossier des diagrammes est resté vide. Le changelog, lui, a tenu, mais uniquement sur la discipline : rien ne le garantit demain. Trois règles du même étage, trois destins : violée, ignorée, tenue par chance. Le problème n'est pas la règle. C'est l'étage.

D'où la loi que nous en avons tirée, bonne candidate au post-it au-dessus de l'écran :

Une règle non outillée est une opinion

Un humain fatigué la contourne de temps en temps. Un agent qui enchaîne les tâches la contourne statistiquement : sans mauvaise foi, sans fatigue.

Monter d'un étage, brèche par brèche

La bonne nouvelle : chaque brèche constatée a un remède connu, presque toujours bon marché.

Brèche constatée

Remède

Coût

Message de commit non conforme

commitlint branché sur un hook commit-msg

3 lignes de lefthook

Changelog oublié

Job CI qui exige une modification du fichier

5 lignes de script

TDD proclamé, non vérifié

Probity : un garde-fou d'agent qui bloque toute implémentation sans test rouge préalable

une configuration

Tests-alibi écrits « pour faire passer »

Mutation testing (Stryker) : mesure si les tests détectent des bugs, pas s'ils s'exécutent

un job CI

Diagrammes jamais écrits

likec4 validate en CI : la documentation d'architecture devient un artefact vérifié

un job CI

Secrets en dur (les agents en écrivent)

gitleaks en pre-commit et en CI

une configuration

Et puis il y a l'étage que notre dispositif n'occupe pas encore : celui du harnais de l'agent lui-même. Les agents modernes exposent des hooks (avant et après chaque action) où l'on branche du code déterministe : bloquer une commande dangereuse avant son exécution, lancer lint et tests après chaque édition, refuser la fin de tâche tant que la suite est rouge. TDD Guard, cité plus haut, n'est rien d'autre que cela. C'est la même logique que notre principe fondateur, appliquée un cran plus tôt : au moment où l'agent agit, plutôt qu'au moment où il comite.

Deux compléments ferment la marche pour l'autonomie complète : le sandboxing (exécuter l'agent en mode autonome dans un conteneur au réseau verrouillé : le dépôt protège la qualité, le conteneur protège le système) et la revue de code IA en CI, nourrie des mêmes AGENTS.md, si bien que l'IA qui relit connaît les mêmes règles que celle qui a écrit.

Aucune gêne à admettre que tout n'est pas en place. C'est même l'essentiel de ce que nous avons appris : ce dispositif n'est pas un état, c'est un gradient. Chaque incident désigne la prochaine règle à faire monter d'un étage.

La checklist pour votre projet

S'il fallait repartir avec cinq gestes :

  1. Cartographiez vos règles par étage. Pour chaque règle qui compte : bloquante, vérifiable, déclarative ou espérée ? La réponse est souvent inconfortable.
  2. Montez d'un étage tout ce qui protège l'architecture. Les frontières de modules et de couches se déclarent en lint (eslint-plugin-boundaries ou équivalent), pas en slides.
  3. Écrivez le contexte là où les agents lisent. Des AGENTS.md près du code, avec des recettes, des interdits et des points d'arrêt. Pas des principes : des instructions.
  4. Exigez la documentation comme livrable. ADR, changelog, architecture : l'agent les rédige à chaque tâche, un template les contraint, et la mise à jour des AGENTS.md fait partie du « definition of done ».
  5. Acceptez le gradient. Vous n'aurez jamais terminé. Chaque règle violée est une information : elle vous dit laquelle outiller ensuite.

Un agent de codage est un excellent équipier. Il fait exactement ce que votre outillage exige, et à peu près ce que votre documentation suggère. Tout le travail consiste à faire passer vos règles vitales de la seconde catégorie à la première, un étage à la fois.

Pour aller plus loin sur ce blog