Réécrire 535 000 lignes avec des agents IA : la méthode Bun, et ce qu'elle ne prouve pas
Bun est passé de Zig à Rust en 11 jours avec 64 agents Claude, pour 165 000 dollars. La méthode est copiable sur un projet solo. Les quatre chiffres qui circulent, beaucoup moins.
Le 20 août, Bun 1.4 est sorti. C'est la première version du runtime écrite en Rust, après quatre ans en Zig. La phrase qui circule depuis résume mal ce qui s'est passé : « un type a réécrit un million de lignes en 11 jours avec 64 agents Claude pour 165 000 dollars ».
Chaque chiffre de cette phrase est sourcé et exact. La conclusion que la plupart des gens en tirent est fausse.
Jarred Sumner a publié un post-mortem de son portage le 8 juillet, et c'est le document le plus détaillé qui existe aujourd'hui sur l'orchestration d'agents à cette échelle. Il y a deux choses à en faire : voler la méthode, qui est solide et transposable à un projet solo, et arrêter de citer les chiffres de travers, parce qu'ils ne disent pas ce qu'on leur fait dire.
Ce qui s'est vraiment passé, dans l'ordre
| Date | Événement |
|---|---|
| 2 décembre 2025 | Anthropic rachète Bun |
| 3 mai 2026 | Début du portage Zig vers Rust |
| 13 mai 2026 | Bun 1.3.14, dernière version en Zig |
| 14 mai 2026 | Merge de la PR #30412, 11 jours après le début |
| 11 juin 2026 | Sumner présente le portage à Code w/ Claude Tokyo |
| 17 juin 2026 | Claude Code 2.1.181 embarque le Bun en Rust en production |
| 8 juillet 2026 | Publication du post-mortem |
| 9 juillet 2026 | Réponse d'Andrew Kelley, créateur de Zig |
| 20 août 2026 | Bun 1.4.0, première version publique en Rust |
Un point à poser tout de suite, parce qu'il change la lecture de tout le reste : Bun appartient à Anthropic depuis décembre 2025, et Jarred Sumner y travaille. Le portage a été fait avec une préversion de Claude Fable 5, sortie publiquement un mois plus tard. Claude Code tourne sur Bun. On n'a donc pas un retour d'expérience client neutre, on a Anthropic qui teste son modèle non sorti sur sa propre infrastructure, et qui en publie le récit. Ça n'invalide rien, tout est vérifiable. Mais ça veut dire lire les chiffres qui suivent en sachant qui les a produits et pourquoi.
La méthode, étape par étape
Deux décisions avant la première ligne de code
Sumner dit n'avoir tranché que deux questions en amont, le reste étant de la tactique.
Tout d'un coup ou par morceaux :
everything all at once is better. An incremental rewrite adds temporary code that you hope gets deleted eventually, and would be painful in the short-medium term.
Il tranche sur la base d'une expérience antérieure, le portage du transpileur d'esbuild vers Zig, fait sans LLM. Pas de couche de compatibilité, pas de FFI Zig vers Rust à maintenir pendant six mois.
Traduction fidèle ou Rust idiomatique :
Do the rewrite that looks like we transpiled our Zig code to Rust. We can gradually refactor it to reduce unsafe usage and look more like idiomatic Rust after Bun v1.4 ships.
C'est le choix le plus important du projet. En gardant l'architecture Zig intacte, l'équipe peut encore lire son propre code après le portage, et chaque fichier Rust reste comparable ligne à ligne à son original. Le prix à payer : environ 13 000 occurrences du mot-clé unsafe dans la base finale, soit à peu près 4 % du code, dont 78 % sur une seule ligne.
Trois heures de conversation, un fichier
Avant tout code, trois heures de discussion avec Claude sur les correspondances de patterns Zig vers Rust, sérialisées dans un PORTING.md. Ce n'est pas un prompt, c'est une spécification de traduction que tous les agents recevront ensuite. Le document a ensuite été publié sur Hacker News et sert de référence depuis.
Le fichier que personne ne cite : LIFETIMES.tsv
C'est la vraie préparation, et la partie la plus intéressante techniquement. Un workflow dédié a lu chaque champ de chaque struct de la base, tracé le flux de contrôle sur les cas complexes, proposé une durée de vie pour chaque champ problématique, fait passer chaque proposition devant deux relecteurs adverses, puis sérialisé le tout dans un LIFETIMES.tsv. Une dernière passe de relecture a cherché les contradictions entre PORTING.md et LIFETIMES.tsv.
Traduction pour ton projet : avant de lancer des agents sur une migration, tu produis d'abord une carte de la contrainte qui va casser partout. En Zig vers Rust, c'est la durée de vie des références. Chez toi, ce sera peut-être le modèle de transaction, la gestion d'erreur ou le format de date. Cette carte est du contenu, pas du prompt, et elle se relit.
Le pilote : trois fichiers
Avant de lancer les 1 448 fichiers .zig, essai sur trois. Pour chacun : un implémenteur écrit le .rs, deux relecteurs adverses vérifient la fidélité comportementale et le respect des consignes, un correcteur applique. On verra plus bas pourquoi ce pilote ne pouvait structurellement pas détecter ce qui a réellement cassé ensuite.
La boucle : 1 implémenteur, 2 relecteurs adverses, 1 correcteur
C'est le cœur transposable de tout le cas, et ça n'a rien à voir avec l'échelle.
L'implémenteur voit le fichier Zig d'origine, le plan de portage, et son propre raisonnement. Les relecteurs, eux, ne voient que le diff. Pas le raisonnement de l'implémenteur, pas ses justifications. Consigne unique : pars du principe que ce code est faux, trouve pourquoi. Un relecteur ne code jamais, un implémenteur ne relit jamais.
L'asymétrie de contexte est tout le truc. Un modèle qui vient d'écrire du code et à qui tu demandes de le relire va défendre son raisonnement, parce que ce raisonnement est encore dans sa fenêtre de contexte. Le même modèle, dans une fenêtre vierge, avec le diff seul et l'instruction de le démolir, trouve des choses.
Trois bugs réels attrapés comme ça, tous cités dans le post-mortem :
- un use-after-free sur la fermeture asynchrone d'un pipe libuv, corrigé avec un
Box::leak; - une troncature de timestamp négatif,
trunc()arrondissant vers zéro là où il fallaitfloor()pour garder les nanosecondes dans l'intervalle valide ; - un
unwrap_or()dont l'argument était évalué de façon empressée, donc paniquait même quand la valeur par défaut n'était pas nécessaire, remplacé parunwrap_or_else().
Ce sont exactement les bugs qu'une relecture humaine fatiguée laisse passer au fichier numéro 400.
Le mur des 16 000 erreurs de compilation
Une fois tout traduit, environ 16 000 erreurs de compilation. Traitées comme une file de travail plutôt que comme une catastrophe : regroupement par crate, résolution préalable des dépendances cycliques, puis pour chaque crate un cargo check, les erreurs écrites dans un fichier, corrigées, relues par deux agents adverses, appliquées.
La règle anti triche
Deux dérives sont apparues à ce moment. Claude a d'abord interprété « faisons compiler toutes les crates » comme « remplace par des stubs les fonctions qui ne compilent pas ». Puis les agents se sont mis à écrire de longs commentaires explicatifs pour justifier leurs contournements. Une phrase ajoutée aux relecteurs a suffi :
If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong. Fix the code.
Note-la quelque part. C'est le meilleur détecteur de code faux que j'aie vu formulé en une ligne, et il marche aussi sur les humains.
L'ordre des tests
Smoke tests d'abord (bun --version, erreurs de linker), puis les sous-commandes, puis la suite locale sur 100 fichiers de test tirés au hasard et répartis par dossier, puis la CI complète par plateforme jusqu'au vert. Aucun test supprimé, aucun test passé en skip.
Le vrai garde-fou n'est pas l'IA, c'est la suite de tests
Voilà ce que le récit « 64 agents » masque. Bun disposait avant le projet d'une suite de tests d'environ un million d'appels expect() répartis sur plus de 57 000 fichiers, écrite en TypeScript. Donc agnostique au langage d'implémentation : elle teste le comportement du runtime, pas le code Zig.
C'est ce qui rend le portage vérifiable. Sans cette suite, la relecture adverse par IA n'a aucun point d'ancrage et devient du théâtre. Gergely Orosz, qui a rencontré Sumner à San Francisco, résume les trois préconditions : un ingénieur très motivé qui connaît la base par cœur, une suite de tests extrêmement robuste (« so when the test suite passes, you know it works »), et la volonté d'investir beaucoup en tokens sans savoir si ça marchera.
Si tu retires n'importe laquelle des trois, le résultat n'est pas « un peu moins bon », il est inexploitable.
Et ce filet a des angles morts, que les 19 régressions dessinent précisément. Elles viennent presque toutes de code syntaxiquement identique dans les deux langages mais sémantiquement différent. Le meilleur exemple : assert en Zig est une fonction, son argument s'exécute dans tous les builds. debug_assert! en Rust est une macro, l'expression entière disparaît en release. Un appel à insert_stale qui alimentait le graphe de rechargement à chaud a donc cessé de tourner en production et cassé le HMR sur certains projets React, pendant que les builds de debug passaient. Aucune suite de tests exécutée en debug ne voit ça, quel que soit son nombre d'assertions.
Les quatre chiffres qu'on lit de travers
1. « 11 jours » est la durée de la traduction, pas du projet
Les 11 jours vont du 3 au 14 mai, c'est-à-dire jusqu'au merge. Les utilisateurs de Bun, eux, ont attendu le 20 août.
Le détail vérifiable est dans la liste des releases GitHub : Bun sortait une version toutes les deux à quatre semaines (1.3.11 le 18 mars, 1.3.12 le 10 avril, 1.3.13 le 20 avril, 1.3.14 le 13 mai). Puis plus rien pendant 99 jours, jusqu'à la 1.4.0. Zéro release stable entre les deux.
Deux nuances honnêtes. D'abord, la 1.4 embarque bien plus que le portage : Bun.WebView, Bun.Image, Bun.markdown, JSON5, JSONL, les API Terminal et cron, la compatibilité Node 26.3, Windows ARM64. Une partie du délai est du travail de fonctionnalités. Ensuite, le Rust tournait en production chez un utilisateur dès le 17 juin, 34 jours après le merge : Claude Code. Anthropic a testé sur elle-même avant de servir les autres.
Il reste que l'argument « sans l'IA on aurait dû geler le projet pendant un an » s'accommode mal d'un train de release à l'arrêt pendant trois mois. Le chiffre honnête n'est pas 11 jours contre 365, c'est environ 110 jours du premier commit à la version publique, contre une estimation d'un an. Toujours impressionnant. Facteur 3, pas facteur 33.
2. « 165 000 dollars contre trois ingénieurs pendant un an »
La comparaison vient de Sumner lui-même :
By hand, I think this would've taken 3 engineers with full context on the codebase about a year, during which time we wouldn't be able to improve Node.js compatibility, fix bugs, fix security issues or implement new features.
C'est une estimation a posteriori, pas une mesure. Personne n'a lancé trois développeurs sur ce portage en parallèle pour comparer. Il n'existe aucun groupe de contrôle, donc aucune économie démontrée, seulement une économie plausible.
Sumner est d'ailleurs plus honnête que ceux qui le citent. Il enchaîne immédiatement :
We never would've done that. The realistic alternative was to do nothing and keep fixing the bugs at the top of this post forever.
La vraie alternative n'était pas trois ingénieurs pendant un an, c'était ne rien faire. L'économie que certains calculent à partir de sa phrase compare donc l'IA à un projet qui n'aurait jamais existé.
Et les 165 000 dollars ne sont que la facture API, au tarif public, d'Anthropic à Anthropic, pour un modèle que personne d'autre ne pouvait acheter à ce moment-là. Ils excluent le temps de Sumner, les trois mois de stabilisation, et le coût des 19 régressions connues arrivées en production, toutes corrigées depuis. Le chiffre est réel, il n'est pas complet.
Ce qui reste solide malgré tout : 5,9 milliards de tokens d'entrée non cachés, 690 millions en sortie, 72 milliards de lectures en cache. Le rapport entre cache et non-cache est la vraie leçon économique du run. Sans le cache, l'addition n'était pas de 165 000 dollars. Si tu orchestres des agents en série sur une même base de code, ta facture se joue là.
3. « 5 fois moins de CPU, 35 % de mémoire en moins »
Ce sont les chiffres de la 1.4, et ils sont exacts. Ils ne viennent pas de Rust.
Le billet de release attribue explicitement la baisse de CPU au repos à l'optimisation des timers du ramasse-miettes, au changement de structure de données pour les Strong roots de JavaScriptCore (liste chaînée vers liste chaînée de tableaux segmentés), à la réduction des appels futex, et aux modifications de mimalloc. La baisse de mémoire vient de JavaScriptCore qui utilise désormais mimalloc, étendu avec le nettoyage partiel de pages, un thread de récupération qui libère pendant que JavaScript est au repos, et un zeroing paresseux amélioré.
Rien de tout ça ne dépend du langage du runtime : JavaScriptCore et l'allocateur sont du C++, et ce travail était faisable à l'identique sur la base Zig. C'est précisément le reproche de Kelley sur l'optimisation au moment de la liaison : le gain était disponible avant, il n'était simplement pas activé.
Les gains réellement attribuables au portage sont ceux que Sumner mesure lui-même dans le post-mortem, et ils sont modestes : 2,8 à 4,8 % de débit HTTP en plus, 2,2 à 4,7 % sur le build, et un démarrage de Claude Code 10 % plus rapide sous Linux (517 ms vers 464 ms). Le vrai gain structurel n'est pas une courbe de perf, c'est une classe de bugs devenue impossible : les use-after-free et double-free qui hantaient le runtime sont maintenant des erreurs de compilation, et une fuite du build en processus est passée de 3 506 Mo à 586 Mo après mille builds.
Sumner le résume mieux que ses relais :
Startup got 10% faster on Linux but otherwise, barely anyone noticed. Boring is good.
4. « Personne n'a relu »
Andrew Kelley, créateur de Zig, a qualifié l'opération d'« unreviewed slop ». C'est la critique la plus reprise du projet, et elle mérite d'être examinée sérieusement plutôt que balayée.
Au sens strict il a raison : personne n'a relu un million de lignes ligne par ligne. Mais ce n'était pas caché, c'était le protocole assumé et documenté. La confiance repose sur la suite de tests, pas sur l'œil humain.
« Unreviewed » reste un raccourci. Sumner écrit avoir relu la PR en vérifiant que les agents adverses attrapaient bien les écarts entre Zig et Rust, que le guide de portage et le guide de durées de vie étaient respectés, et en lisant lui-même beaucoup de code côte à côte, Zig contre Rust. C'est une relecture par échantillonnage et par le processus. Ce n'est pas une relecture exhaustive, ce n'est pas non plus rien.
Sa meilleure objection est ailleurs, et elle est difficile à écarter : si la suite de tests attrape tout, comment expliquer les bugs mémoire qui traînaient dans la version Zig ? La réponse honnête, c'est qu'elle n'attrape pas tout. Elle attrape les régressions de comportement. Les bugs mémoire, c'est le compilateur Rust qui les attrape désormais. Ce sont deux filets différents, et c'est leur superposition qui rend le projet défendable, pas la relecture par IA.
Et on a une mesure de ce trou, pas seulement une intuition. Le 14 mai, jour du merge, un contributeur extérieur ouvre l'issue #30719 : PathString::slice construit une tranche à partir d'un pointeur brut après libération de l'allocation. Du comportement indéfini atteignable depuis du Rust réputé sûr, sans unsafe visible côté appelant. C'est très exactement la faiblesse d'une traduction mécanique : le pattern était légal en Zig, son transport littéral en Rust ne l'est plus. Deux jours après, la PR #30876 ajoute le support de cargo-miri et corrige au passage un problème d'aliasing sur HiveArray. En août, Miri tourne dans la CI sur plusieurs crates (#37078, #39218), et Bun publie un audit public de son code unsafe.
Lis cette séquence dans les deux sens. Le projet a réagi en deux jours, c'est à son crédit. Mais ni le million d'assertions ni les relecteurs adverses n'avaient vu le problème : il a fallu un outil de vérification que le projet n'avait pas encore branché, et un humain extérieur pour le réclamer. Le filet avait un trou, exactement de la taille que Kelley annonçait.
Kelley pousse plus loin, en soutenant que les vrais problèmes de Bun étaient organisationnels et non linguistiques :
The main problem, however, was code quality.
Il décrit des hacks empilés sur des hacks, un abus d'assertions, et une fuite en avant fonctionnalité après fonctionnalité. Sur le fait qu'une partie des gains de la 1.4 étaient accessibles en Zig, il a factuellement raison. Sur le reste, il compare un projet qu'il aurait mené autrement à un projet mené par quelqu'un d'autre, ce qui n'est pas réfutable non plus.
Les deux fautes de méthode que tu peux éviter
Le pilote à trois fichiers testait la mauvaise variable
Trois échantillons, c'est trop peu pour conclure quoi que ce soit. Mais le problème n'est pas la taille.
Le pilote a validé la fidélité de traduction, fichier par fichier, en série, avec un implémenteur et deux relecteurs. Ce qui a réellement cassé ensuite, c'est le comportement de plusieurs agents tournant en parallèle sur le même dépôt git. Même avec cinquante fichiers en série, tu ne le voyais pas venir : ce n'est pas un problème de volume, c'est un problème de concurrence.
Passer de 3 fichiers en série à 1 448 fichiers sur 64 agents concurrents, c'est changer deux variables à la fois. Si tu pilotes une migration par agents, ton essai doit inclure le mode d'exécution cible, pas seulement un extrait du travail. Trois fichiers sur deux worktrees en parallèle valaient mieux que dix fichiers en série.
La collision entre agents était prévisible
Le post-mortem est franc là-dessus. Deux minutes après le lancement sur les 1 448 fichiers, un Claude lance un git stash avant de commiter, un autre un git stash pop, puis un git reset HEAD --hard. Ils se marchaient dessus. Les règles ajoutées : jamais de git stash, jamais de git reset, aucune commande git qui ne commite pas un fichier précis, pas de cargo, aucune commande lente. Puis découpage en 4 shards, 4 worktrees, 16 agents chacun, chacun ne commitant que ses fichiers.
À décharge, il explique pourquoi chaque agent n'a pas eu son propre worktree dès le départ : le dépôt Bun est trop gros pour 64 copies sur disque, et les changements doivent de toute façon compiler ensemble. La contrainte est réelle.
Ça n'excuse pas le reste. Plusieurs processus autonomes qui écrivent dans un état partagé sans verrou, c'est une situation de compétition classique. Ces règles avaient leur place dans le prompt système avant le premier run, pas deux minutes après. Le coût réel s'est limité à deux minutes et un peu de nettoyage, mais uniquement parce que quelqu'un regardait l'écran.
Bonne nouvelle pour toi : tu n'as plus à réinventer ce garde-fou. Claude Code sait faire tourner des agents dans des worktrees séparés nativement, et l'isolation a été rendue vraiment étanche début août, y compris contre les commandes git destructrices lancées depuis un agent isolé.
Ce que tu voles pour ton projet, même solo
Rien de ce qui suit ne demande 64 agents.
- Sépare strictement qui écrit et qui critique. Un sous-agent implémente, un autre relit dans une fenêtre vierge, avec le diff seul et la consigne de partir du principe que c'est faux. Ne lui passe jamais le raisonnement de l'implémenteur.
- Écris ta carte de contraintes avant de lancer quoi que ce soit. L'équivalent de
PORTING.mdetLIFETIMES.tsvchez toi. Du contenu versionné, relisible, pas un prompt. - Traduis mécaniquement, refactore après. Une migration qui change l'architecture en même temps que le langage n'est plus vérifiable par diff.
- Fais du filet de test un prérequis, pas un livrable. Si ta suite ne peut pas prononcer un verdict, tu n'as pas de projet, tu as un pari. Le sujet mérite son propre passage.
- Traite les erreurs de compilation comme une file de travail, groupées par module, pas comme un incident.
- Quand ça casse, corrige le processus qui génère le code, pas le code. C'est la formule de Sumner : « fixing the process that generates the code instead of hand-fixing the code ». Un correctif à la main sur un fichier ne protège pas les 1 447 autres.
- Colle la règle du commentaire dans ton
CLAUDE.md. S'il faut un paragraphe pour justifier un contournement, le code est faux. - Impose les règles git dès le premier run, ou utilise l'isolation par worktree native.
Ce qui manque à ce cas pour être une preuve
Le trou, et c'est celui qui me gêne le plus : il n'y a nulle part une comparaison entre le code produit par l'IA et le code qu'un humain aurait produit sur les mêmes fichiers. Pas un extrait, pas un fichier témoin, rien.
Ce qu'on a, ce sont des métriques Rust-nouveau contre Zig-ancien. Ce n'est pas Rust-par-IA contre Rust-à-la-main. Le portage manuel n'a jamais existé, donc ce cas ne dit strictement rien sur « l'IA fait mieux qu'un humain ». Il dit qu'un portage mécanique, adossé à une suite de tests exhaustive et indépendante du langage d'implémentation, est réalisable par des agents : 11 jours de traduction, environ 110 jours jusqu'à la production.
C'est déjà énorme. Ce n'est pas la même affirmation.
Et j'ai bien peur que ce trou devienne la norme, parce que produire le témoin coûte exactement ce qu'on cherchait à économiser. Trois fichiers portés à la main par un humain, publiés à côté de leur version IA, auraient réglé la question pour tout le monde. Personne ne le fera.
Pour aller plus loin
- Rewriting Bun in Rust, le post-mortem de Jarred Sumner, 8 juillet 2026. La source primaire, à lire en entier.
- My Thoughts on the Bun Rust Rewrite, la réponse d'Andrew Kelley, 9 juillet 2026.
- The Pulse: What can we learn from Bun's rapid Rust rewrite with AI?, Gergely Orosz, 16 juillet 2026, la meilleure analyse extérieure.
- La note de Simon Willison sur le post-mortem, courte et juste.
- La couverture de The Register, au merge le 14 mai puis sur la polémique le 14 juillet.
- Côté vidéo, Sumner a présenté le portage à Code w/ Claude Tokyo le 11 juin, sur la Founder stage. Aucun enregistrement public n'est référencé sur la page de session à ce jour.
Ce qu'il faut retenir
Le cas Bun est la meilleure documentation publique existante sur l'orchestration d'agents à grande échelle, et le pattern implémenteur plus relecteurs adverses en contexte séparé se transpose tel quel à un projet d'une personne.
Utilise-le pour parler de méthode. Ne l'utilise jamais comme preuve que les agents produisent du code de confiance : sans une suite de tests d'un million d'assertions écrite dans un langage tiers, ce projet n'existe pas.
Quant aux 11 jours et aux 165 000 dollars, ils tiennent en une phrase une fois remis d'aplomb : c'est le délai jusqu'au merge, la version publique est arrivée 99 jours plus tard, et la comparaison avec trois ingénieurs pendant un an est une estimation de l'auteur, pas une mesure.
Pierre Rondeau
Développeur et indie builder. Je construis des produits et automatisations avec l'IA. Créateur de Claude Hub.
LinkedIn