RETOUR D'EXPÉRIENCE
Remédiation Autonome
Un audit nocturne avec Kimi, un epic dérivé avec Opus, et un workflow qui a mené vingt-deux constats de l'issue à la PR fusionnée en une seule après-midi — sans aucune intervention humaine entre un constat et la branche. Voici comment le run s'est réellement déroulé.
Deux runs, polarité inversée
Le 2 juin 2026, la factory d'agents de Roxabi a retourné sa propre machinerie multi-agents sur sa propre base de code. Un audit qualité avait déjà balayé le dépôt — 69 agents sur 20 vagues, huit domaines, 3 291 fichiers — et distillé 1 258 issues brutes en 22 constats actionnables. Voici le compte rendu de la suite : solder ces 22 constats, de bout en bout, sans aucune barrière humaine entre une issue et sa fusion.
L'audit et la remédiation sont la même forme, parcourue en sens inverse. L'audit était un fan-out en lecture : beaucoup d'agents qui observent, rien qui change. La remédiation était un fan-out en écriture suivi d'une fusion sérialisée : beaucoup d'agents qui modifient, une seule branche qui absorbe. Même machinerie, polarité inversée.
| Phase | Temps réel | Échelle | Résultat |
|---|---|---|---|
| Audit (précurseur) | 2 h 20 m | 69 agents · 20 vagues · 8 domaines | 1 258 issues → 22 actionnables |
| Remédiation (ce run) | 4 h 32 m | 19 feuilles · 4 vagues · auto-merge | 22 constats livrés · 4 gates de dette CI |
De l'audit à l'epic
La remédiation n'est pas partie d'une intuition — elle est partie d'un plan, et ce plan a été construit par la machine la nuit précédente. Le mot « audit » recouvre ici deux instruments distincts, à ne pas confondre.
| Scanner déterministe | Audit multi-agents profond | |
|---|---|---|
| Quoi | une métrique de dette par marqueurs de suppression | un fan-out de lecture qualitatif |
| Comment | audit_quality_debt.py · make quality-debt-report | 69 agents · 20 vagues · 8 domaines, piloté par Kimi |
| Sortie | quality-debt-report.json (des comptes) | un manifest + 8 rapports de domaine → 1 258 issues |
| Cadence | chaque PR — CI informatif, ne bloque jamais | ponctuel, à la demande (de nuit) |
Le scanner est le pouls permanent — peu coûteux, déterministe, toujours actif. L'audit profond, lui, est le diagnostic. Il a tourné de nuit, en fan-out de lecture seule — beaucoup d'agents qui lisent, aucun qui écrit — partitionnant 3 291 fichiers sur huit domaines d'analyse en vingt vagues, piloté par Kimi : un modèle peu coûteux à long contexte routé via le proxy LiteLLM, ce qui rend un balayage de 69 agents abordable à lancer pendant que l'on dort. Il a fait remonter 1 258 issues brutes, les a classées P0–P3, et les a synthétisées en un manifest committé et huit rapports de domaine. Cette moitié lecture a son propre compte rendu — le doc compagnon Audit Multi-Agents — et sa méthode est un artefact réutilisable, le playbook d'audit.
Un tas de 1 258 issues n'est pas un plan. Opus a donc pris les constats de plus haute sévérité — les issues P0 et P1 — et a fait le travail qui transforme des symptômes en plan : il a dérivé les causes racines qui les sous-tendent. Non pas « corriger ces deux cents lignes », mais « voici la poignée de causes structurelles que ces lignes partagent ».
Ces causes racines sont devenues l'epic #1662 — 22 constats actionnables. L'epic a été découpé en tâches feuilles, et les feuilles ont été reliées par des arêtes GitHub natives blocked_by en un graphe de dépendances : là où deux correctifs touchaient le même fichier, l'un était marqué bloqué par l'autre ; là où les empreintes étaient disjointes, aucune arête. Ce graphe est l'ordonnancement.
blocked_by encodent l'ordre — et le workflow ne fait que parcourir le graphe.Le graphe de dépendances est le contrat entre planification et exécution. Les arêtes blocked_by de l'epic sont l'ordonnancement par vagues — le harnais les lit, il ne les invente pas.
Le harnais
L'ordonnancement est de la donnée ; le harnais est le moteur qui l'exécute. epic-1662-wave.mjs est un script Workflow — un contrôle de flux JavaScript déterministe (boucles, fan-out, conditions) enveloppant des appels agent() pilotés par le modèle. Chaque invocation exécute exactement une vague de dépendances de feuilles et, délibérément, ne fusionne pas — la fusion est le travail de la boucle principale. C'est cette séparation qui permet aux dépendants de chaque vague de brancher depuis une base déjà fusionnée.
À l'intérieur, chaque feuille suit le même pipeline, adapté à sa taille.
- Implémenter. Une petite feuille reçoit un agent d'implémentation dans un worktree git isolé. Une grosse est d'abord découpée par un agent
PLANen groupes de fichiers disjoints, implémentés par des sous-agents parallèles (aucun ne touche le même fichier, donc la fusion est sans conflit par construction), puis recousue par un agentINTEGRATE. Dans les deux cas, ça se termine par une PR portant une trace de décision. - Revue. Cinq lentilles relisent le diff en parallèle — exactitude, architecture axiale, sécurité, tests et conformité aux règles — chacune un agent spécialiste. Seuls les constats bloquants et majeurs avancent.
- Validation. Chaque constat qui avance affronte deux sceptiques, chacun invité à le réfuter par défaut. Un constat ne survit que s'il est confirmé — le bruit de revue plausible-mais-faux meurt ici.
- Correctif & report. Un fixer applique les constats confirmés, relance les gates et pousse. La boucle tourne deux fois au plus ; tout ce qui reste non résolu est déposé en issue sœur reportée, bloquée par l'originale — le lot ne cale jamais sur un constat tenace.
La forme tient en un écran — un panel de revue parallèle, puis un vote sceptique par constat :
// 5 lentilles relisent le diff en parallèle — chacune un agent spécialiste
const reviews = await parallel(LENSES.map((L) => () =>
agent(reviewPrompt(n, L, diff), { agentType: L.agent, schema: FINDINGS_SCHEMA })))
// garder bloquant/majeur, puis réfuter chaque constat avec 2 sceptiques
const verdicts = await parallel(findings.map((f) => () =>
parallel([1, 2].map((k) => () =>
agent(validatePrompt(n, f, diff, k), { schema: VERDICT_SCHEMA })))
.then((votes) => ({ f, real: votes.some((v) => v.real) }))))
const confirmed = verdicts.filter((v) => v.real).map((v) => v.f)
Quatre propriétés tiennent pour chaque agent du run, par construction :
- Sortie typée, jamais en prose. Chaque agent est forcé d'émettre un résultat structuré contre un schéma JSON — le mode d'échec « l'agent a fini en paragraphe au lieu d'un résultat » est éliminé par conception.
- Un seul contrat, injecté verbatim. Les mêmes règles RCA (§6) entrent dans chaque prompt, pour que dix-neuf agents non coordonnés raisonnent de la même façon.
- Réessai au niveau de l'inférence. Un wrapper
tryAgentréessaie trois fois avec back-off croissant — le correctif de la tempête de surcharge du soir d'ouverture, désormais un défaut. - Isoler, différer, continuer. Une feuille qui lève une erreur est capturée, enregistrée et ignorée ; la vague n'avorte jamais sur un échec isolé. La concurrence est plafonnée à trois issues en vol, pour la cadence de fusion.
De la PR relue à la fusion
Le workflow rend une PR relue ; la boucle principale prend le relais — et c'est là que tout reste autonome. La séparation compte : le workflow développe, la boucle principale fusionne, pour que les dépendants branchent toujours depuis une base fraîchement fusionnée.
ci et le scan de secrets doivent être au vert — les mêmes checks que l'agent a déjà exécutés en local.reviewed, activer gh pr merge --auto. Aucune approbation humaine dans le chemin.staging exige des branches à jour ; une boucle de poll lance update-branch à chaque BEHIND et abandonne sur DIRTY.blocked_by elle, ouvrant la vague suivante.La barrière de fusion était CI au vert + un label reviewed + la revue croisée entre agents en cours de run — et rien d'autre. Aucune approbation humaine ne se tenait entre une PR au vert et la branche staging.
Ordonnancer par empreinte
Dix-neuf feuilles ne peuvent pas tourner toutes en même temps — certaines touchent les mêmes fichiers, et deux agents qui éditent un seul fichier produisent un conflit de fusion, pas une fonctionnalité. L'ordonnanceur a suivi un unique prédicat, lu directement sur le graphe blocked_by de l'epic : empreinte disjointe ⇒ parallèle ; fichier partagé ⇒ série. Le parallélisme était plafonné à trois agents concurrents, et autorisé uniquement là où les empreintes étaient prouvablement disjointes.
| Vague | Feuilles | Stratégie | Pourquoi |
|---|---|---|---|
| V1 | 9 | fan-out parallèle | empreintes disjointes — core, infra, config, adapters |
| V2 | 3 | départ conditionné | chacune dépend d'une fusion de V1 |
| V3 | 5 | strictement en série | partagent toutes ci.yml · pre-commit · stack.yml |
| V4 | 2 | en série | partagent un CLAUDE.md |
Sérialiser V3 fut la décision d'ordonnancement la plus déterminante. Cinq feuilles ajoutaient toutes à la même config CI ; en parallèle, elles se seraient écrasées les unes les autres. En série, chacune voyait le changement de la précédente.
Un seul contrat, dix-neuf agents
Dix-neuf agents ont travaillé en isolation, aucun conscient des autres. Ce qui a gardé leur production cohérente — et relisible — fut un unique contrat de raisonnement, injecté verbatim dans chaque agent d'implémentation et répercuté dans chaque corps de PR.
obs(constat) → cause_racine # pas le symptôme
→ corrections ∈ {Patch ⊻ Archi} # choisir, explicitement
→ corriger au niveau de la cause
→ trust(input)? · testé ∧ correct # vert ≠ correct
→ tracer la décision dans le corps de PRLa règle qui a le plus servi fut vert ≠ correct : un run CI au vert est nécessaire, jamais suffisant. Et forcer chaque agent à choisir entre un patch (traiter le symptôme) et un correctif architectural (déplacer la cause) — à voix haute, dans la PR — a transformé dix-neuf runs non coordonnés en un dossier relisible. Ce document est rédigeable parce que ce dossier existe.
Ce qui a cassé, et pourquoi
La valeur d'un retour d'expérience est dans les échecs. Sept blocages ont enrayé le run ; chacun a été ramené à une cause racine et corrigé à ce niveau, pas rustiné. Les coûteux étaient systémiques — et vivent désormais comme défauts durcis dans le harnais d'orchestration.
| Ce qui a cassé | Cause racine | Correctif |
|---|---|---|
| 9 agents sur 9 ont échoué au premier essai | Une surcharge transitoire API 529 à la première inférence, masquée en « terminé » | Réessai + back-off au niveau de l'inférence |
| Les agents lourds finissaient en prose, sans résultat | Les gros diffs faisaient exploser le budget de sortie structurée | Rendre le diff optionnel, le récupérer hors bande, scinder les feuilles surdimensionnées |
| Les PR ne fusionnaient jamais | Un blocage par retard en strict-merge — aucune PR n'est à jour, donc l'auto-update ne se déclenche jamais | Une boucle watcher update-branch manuelle par PR |
Les refactos parallèles passaient DIRTY | Des éditions interdépendantes sur les mêmes fichiers CI | Sérialiser les vagues par empreinte de fichier partagé |
| Le check CI requis disparaissait en silence | Un deux-points non quoté dans un name: d'étape YAML invalidait tout le workflow | Quoter tout name: contenant un deux-points + un gate de validation YAML |
| Un gate de dette criait au loup | Le formateur recomposait un appel multi-ligne hors de sa ligne de commentaire | Réécriture du gate pour parcourir les parenthèses équilibrées, multi-ligne |
| Un agent a livré un câblage cassé | Un worktree branché depuis un staging local périmé n'a jamais vu les nouveaux hooks | Fast-forward et épinglage de la base sur origin/staging avant le spawn |
Seul le dernier a exigé un sauvetage humain. Tout le reste a été capté, ramené à sa cause, et transformé en défaut — la différence entre un correctif ponctuel et un pipeline qui tourne plus propre la fois suivante.
Le coût, sans fard
Le temps est une donnée dure, lue directement sur les horodatages des PR. Les tokens, non — le harnais n'a jamais capté l'usage par agent, donc le total du run est une estimation ascendante : compter les agents que le pipeline lance réellement, multiplier par ce que coûte un agent. C'est étiqueté comme une estimation, mais l'ordre de grandeur ne fait pas de doute.
Combien d'agents par issue ?
Le run n'a jamais été trente agents — c'était le compte des invocations /dev de premier niveau. Le vrai fan-out, c'est le pipeline par issue (§4) qui lance un sous-agent neuf à chaque étape, et les étapes lourdes qui re-fan-out à leur tour. Lu directement sur epic-1662-wave.mjs :
| Étape (§4) | Agents, un tour | Note |
|---|---|---|
| Implémenter — petite feuille | 2 | 1 impl (worktree isolé) + 1 récupération de diff hors bande |
| Implémenter — grosse feuille | 5–6 | 1 PLAN + 2–3 sous-impl en parallèle + 1 INTEGRATE + 1 diff |
| Revue (panel) | 5 | un agent spécialiste par lentille, en parallèle |
| Validation | 2 × chaque constat | deux sceptiques indépendants par constat (~3 constats → ~6 agents) |
| Correctif | 1 / tour | la boucle revue→validation→correctif tourne deux fois au plus |
| Report | 0–1 | une fois, seulement si un résiduel survit à 2 tours |
Une petite feuille représentative, un seul tour de revue, ~3 constats : 2 + 5 + 6 + 1 ≈ 14 agents. Un second tour double presque le bloc revue/validation/correctif, poussant une feuille contestée vers ~25. Moyenné sur les dix-neuf feuilles fusionnées (majoritairement petites, quatre grosses), comptez ~18 agents par issue → ~340 invocations d'agents sur le run.
Plus le debug. La tempête de surcharge d'ouverture (§7) a lancé une vague entière morte à la première inférence — 9 agents, ≈ 0 token chacun (l'API 529 a frappé avant tout travail réel) : ça a coûté du temps mural et un re-run, pas des tokens. Ajoutez une poignée de re-devs et la récupération à la main du #1697 (base périmée) par-dessus.
Les tokens, estimés
Un agent vivant d'impl, de revue ou de correctif lit sa tranche du dépôt, le diff de la PR, et exécute toute la suite de gates — ce qui place un seul agent à 100–200k tokens, parfois plus. L'unique donnée dure se situe au bas de cette fourchette, exactement comme attendu : les deux agents instrumentés étaient des récupérations post-compaction, reprises depuis un contexte rétréci. Donc ~340 agents × ~150k ≈ ~50 M tokens pour le run — une fourchette de ~35–70 M aux bornes 100–200k — et ~2,5–3 M tokens par issue. L'estimation antérieure de « ~1,5–2,5 M pour tout le run » était fausse d'un facteur ~20 : c'était à peu près le coût d'une seule issue.
La preuve que c'est passé
Dix-neuf PR fusionnées prouvent que le processus est allé au bout. Elles ne prouvent pas que le code est juste — un pipeline vert et un epic clos peuvent tous deux reposer sur un correctif qui a discrètement dérivé de son intention. Une fois le run terminé, un audit de second niveau a donc relu directement la source fusionnée sur staging, constat par constat, avec une seule question : chaque correctif a-t-il vraiment atterri comme sa PR le prétendait ?
Trois agents spécialistes se sont partagé la surface — refactors et architecture, les chemins sensibles à la sécurité, et la dérive inter-couches — et ont lu les fichiers, pas les diffs.
| Auditeur | Périmètre | Verdict |
|---|---|---|
backend-dev | refactors DRY, axe d'étage, configs, protocoles | 8 / 8 ✅ |
security-auditor | process_one, journalisation des tours, catch large, sleep() | 4 / 4 ✅ |
axial-adr-review | pollution inter-couches, dérive hors de l'axe primaire | 2 fuites mineures |
Les deux fuites architecturales étaient réelles mais minces — du code de cœur de hub discriminant encore deux types de message propres à une plateforme là où un trait générique a sa place, et un classificateur d'erreurs s'appuyant sur les noms de modules d'adaptateur plutôt que sur une taxonomie que les adaptateurs déclarent. Ajoutez une limite codée en dur qui réclame un nom, et c'était tout le résidu : versé en suivis non bloquants, au dossier plutôt qu'enfoui.
La machinerie qui a trouvé la dette a été retournée sur le code qui l'a résorbée. 19 correctifs sur 19 confirmés dans la source, zéro régression — et les trois points qu'elle a soulevés sont versés au dossier, pas balayés. La validation, c'est le fan-out de lecture, rejoué une fois de plus.
Ce qui se capitalise
Les constats étaient propres à une base de code. Les patterns, non — voici les parties qui se reportent au run suivant.
- Les feuilles plutôt que les parapluies. Ordonnancer sur la feuille actionnable ; laisser les issues parentes se fermer d'elles-mêmes quand leurs enfants fusionnent. Aucune dépendance fantôme.
- Parallélisme par empreinte. « Empreinte disjointe ⇒ parallèle ; fichier partagé ⇒ série » est un prédicat plus propre, pour éviter les conflits, que la taille ou la priorité d'une issue.
- Corps de PR à trace de décision. Cause racine + Patch ⊻ Archi + niveau, dans chaque PR. Cela rend des agents indépendants relisibles — et un retour d'expérience rédigeable.
- Isoler, différer, continuer. Une feuille en échec est isolée et différée ; la vague avance. Aucun échec isolé n'enraye le lot.
- Antériorité de référence pour les gates de dette. Un nouveau gate atterrit en bloquant les nouvelles violations tout en tolérant l'existant par antériorité, avec un plan de résorption à suivre — pour que le gate atterrisse aujourd'hui au lieu d'attendre une refonte de 90 items.
Un seul opérateur a mené vingt-deux constats jusqu'à la fusion en une seule après-midi — et chaque décision est au dossier. C'est toute la thèse en un run : le rendement d'une équipe, entièrement auditable, sans verrouillage.
Sources & code
Chaque artefact de ce compte rendu est public. Le dépôt est Roxabi/roxabi-factory (AGPL-3.0).
| Artefact | Ce que c'est |
|---|---|
| Epic #1662 | l'epic aux 22 constats (clos) |
epic-1662-wave.mjs | le harnais workflow (§3) |
epic-1662-ledger.md | journal d'exécution (source de vérité) |
AUDIT-SUMMARY.md | le rapport final de l'audit profond |
REMEDIATION-RECAP.md | le retour d'expérience que ce doc distille |
| playbook d'audit multi-agents | la méthodologie d'audit réutilisable |
| playbook pipeline qualité-dette | le mode d'emploi du scanner de dette permanent |