J'ai 51 outils en ligne de commande sur cette machine. Je n'avais pas prévu ce nombre. Cela s'est produit parce qu'un CLI est la distance la plus courte entre une idée et quelque chose que je peux réellement exécuter, et parce que ces dernières années j'ai eu de l'aide pour les écrire plus rapidement que je ne pourrais le faire seul.
Cette aide s'est accompagnée d'une habitude que j'ai remarquée tôt, à l'époque où GPT-3.5 et les premiers modèles Claude étaient ceux avec lesquels je travaillais. Demandez à 3 modèles différents de créer un CLI et vous obtenez 3 opinions différentes sur ce qu'est un CLI. Un choisit Commander. Un choisit Inquirer pour l'invite. Un choisit Chalk parce que la sortie doit être colorée. Chaque réponse est défendable. Ensemble, ils constituent une taxe, car maintenant je possède 3 bases de code qui divergent sur l'analyse des arguments, sur l'apparence d'une erreur, et sur celles des bibliothèques dont je suis désormais responsable de la surveillance.

Chaque modèle a choisi un ensemble différent de bibliothèques, et j’étais celui qui devait vivre avec toutes celles-ci

La taxe n’est pas les bibliothèques. C’est que les améliorations cessent de voyager.
Quand 3 CLIs ne sont pas d’accord sur la façon dont une commande signale une erreur, une correction dans l’un est une correction dans l’un. Il n’y a rien à mettre en amont. Le travail ne s’accumule pas, et après le dixième outil vous ne construisez pas de levier, vous maintenez un portefeuille de quasi‑échecs.
J’avais déjà écrit sur le désir d’obtenir l’opposé de cela. L’argument complet dans How to Turn AI Gains Into Compounding Infrastructure est que la gain devient durable lorsque chaque projet dépendant l’hérite. Une surface de capacité partagée. Une règle de promotion. Un endroit où une amélioration atterrit et se répand.
J’avais construit cette couche pour la capacité IA, pour le flux de travail, pour les opérations. Je ne l’avais pas construite pour la chose que je fais le plus souvent.

Alors j'ai construit une base et j'ai fait que chaque CLI projet mette à jour ses améliorations dans celle-ci

La règle était simple et c'était à moi de l'appliquer : quand un CLI dans mon patrimoine avait besoin de quelque chose de mieux — une façon plus propre d'enregistrer les services, un meilleur chemin d'erreur, un assistant de test qui rendait une suite lisible — cette amélioration ne restait pas dans le projet. Elle est entrée dans la base, et la base est sortie vers les autres.
C'est tout le design. La base est petite intentionnellement. Elle n'a pas d'opinion sur ce que votre outil fait. Il a une opinion forte sur ce qu'est une commande : quelque chose qui prend des arguments, fait du travail, rapporte ce qui s'est passé, et part.
Le dossier a été créé le 6 juillet 2025, et 2 de mes outils dépendaient de la version 1.0.0 de celle-ci le même jour. C'est le signal : elle n'a pas été construite de façon spéculative puis adoptée. Elle a été extraite d'un travail qui existait déjà, au point où copier la même structure entre projets n'était plus raisonnable.
Elle s'est rapidement répandue, parce que la diffusion était l'idée entière. 8 dépôts étaient sur elle dans 25 jours. 10 dans 11 semaines.
Repositories Adopting the Base in 2025
Chart data
repositories
Jul 62
Jul 84
Jul 175
Jul 237
Jul 308
Sep 2010
Git est arrivé plus tard que tout cela. Le dépôt a été initialisé le 12 novembre 2025, 4 mois plus tard, et publié le lendemain — c'est pourquoi l'historique des versions et l'historique réel ne concordent pas, et pourquoi j'ai vérifié le système de fichiers plutôt que de faire confiance au journal des commits quand je me suis assis à écrire ceci.
Ces 10 outils font l'administration Cloudflare. Local DNS et gestion nginx. Déploiement contre Coolify. Automatisation du navigateur. Rapport de coûts entre fournisseurs de modèles. La plupart sont privés, c’est pourquoi je les décris par ce qu’ils font plutôt que par leur nom. Les publics sont aia, qui consultent plusieurs modèles en parallèle, et la base elle‑même. vssh, mon outil d’exécution distante sécurisé, est aussi public et est né du même instinct — construire une surface opérateur une fois, correctement, et arrêter de la reconstruire.
Les dividendes étaient réels et ennuyeux, ce qui est la forme correcte pour les dividendes d’infrastructure. Un renforcement dans un outil est apparu dans tous ceux-ci. Quand j’ai découvert qu’une commande pouvait afficher un message d’erreur rouge et quand même sortir 0 — en informant l’humain qu’elle a échoué et le shell qu’elle a fonctionné — la correction n’est pas passée dans les 16 endroits d’un outil où cela s’était produit. Elle est entrée dans la base, et chaque outil l’a héritée.

Après 13 mois, je voulais qu’elle soit reconstruite, pas patchée

En août 2026, la base fonctionnait et je voulais toujours qu’elle disparaisse.
Pas parce qu’elle était cassée. Parce qu’elle avait accumulé. Parce que la règle de code de sortie dont je suis le plus fier avait été rétrofitée plutôt que conçue dès le départ. Parce que le monde pour lequel elle était écrite avait changé en dessous d’elle : la plupart des invocations de mes CLIs ne sont plus tapées par moi. Elles sont émises par des agents, lisant stdout, stderr, et $? comme leurs seuls sens.
Alors au lieu de patcher, j’ai changé les termes. J’ai donné à Claude Fable une seule instruction, et je l’ai rendue délibérément large :
Si c’était le dernier cadre CLI que l’humanité avait construit — celui qui est encore en service dans une décennie — vous avez maintenant la chance de le rendre ainsi.
Concevez-le à partir de là. Je ne m’attendais pas à un document en retour. J’attendais un plan.

Fable est revenu avec un traité, et la contrainte était que les promesses devaient être peu nombreuses

Ce qui est arrivé n'était pas une liste de fonctionnalités. Elle était structurée comme un traité, divisée en deux par un mur solide.
La moitié était un contrat : ce que chaque CLI construit sur cette base garantit à tout observateur, écrit sous forme de clauses numérotées en langage RFC-2119 — DOIT, DOIT PAS, DEVRAIT, PEUT. Douze familles d'entre elles. Codes de sortie. Discipline de flux. Sortie de machine. Auto-description. Grammaire. Environnement. Annulation. Déterminisme. Budgets de performance. Compatibilité.
L'autre moitié était la surface d'édition, qui était autorisée à croître, et qui existait uniquement pour rendre la satisfaction du contrat le chemin de moindre résistance.
Le raisonnement en dessous était la partie que je trouvais convaincante. Un design destiné à durer une décennie ne peut pas miser sur la mode, car la mode est ce qui expire. Il ne peut pas miser sur l'ingéniosité, car l'ingéniosité est ce que vous ne pouvez pas prévoir en année 8. Il ne peut miser que sur les interfaces qui n'ont pas changé depuis les 1970s : vecteurs d'arguments, flux 3, un code de sortie à 8 bits, variables d'environnement. Et il a noté le seul fait réellement nouveau — que le lecteur majoritaire de ces interfaces est maintenant une machine qui ne peut pas poser de question de suivi.
La clause qui a fini par organiser tout le reste était celle qu'il a ouverte avec :
Un résultat, de nombreuses représentations. Une commande calcule un seul résultat. Le code de sortie code, le texte humain, le document JSON et les lignes en streaming sont tous des projections de cette seule valeur. Ils ne peuvent pas se contredire, car il n'y a qu'une seule source.
C'est la phrase sur laquelle tout le rebuild repose.
Diagram source
graph LR
    A["execute() retourne  
une valeur"] --> B["code de sortie"]
    A --> C["texte rendu  
stdout"]
    A --> D["enveloppe JSON  
--json"]
    A --> E["flux NDJSON  
--ndjson"]
    F["logger.error()  
ctx.emit()"] -.-> B
    F -.-> G["événements  
stderr"]

Opus 5 et j’ai trouvé que la spécification était correcte sur la thèse et incorrecte sur 3 choses

C’est là que le travail est devenu le nôtre plutôt que le mien.
J’ai apporté la spécification à Opus 5 et nous l’avons construite en un jour. Pas une journée propre. Les parties utiles sont les endroits où le document a rencontré la propriété et a perdu.
La spécification voulait que ctx.args devienne un enregistrement d’arguments nommés. C’est le meilleur design isolé. Cela aurait également rompu chaque commande dans chacune des 10 outils, car ils lisent tous ctx.args comme un tableau. Nous avons gardé le tableau et avons placé les arguments typés sur ctx.namedArgs à côté. La règle qui a décidé il était déjà écrit dans le contrat, une clause au-dessus : ne jamais casser un consommateur prime sur toute autre valeur dans le dépôt, y compris la propre complétude.
La spécification voulait qu'un groupe de commandes sans verbe soit une erreur d'utilisation. Exécuter un commande parente sans sous-commande provoquerait 2. Défendable, et cela aurait changé le comportement de chaque script qui exécute une commande de groupe vierge pour voir son aide. Nous avons continué à afficher l'aide et à quitter 0.
La spécification supposait que le streaming et le document unique JSON étaient la même fonctionnalité. Ils ne le sont pas. Faire du streaming d'un million d'éléments en mémoire constante est le point d'un et impossible dans l'autre, car un appelant qui a demandé un document unique a demandé qu'il soit unique. Nous avons séparé le comportement et noté quelle clause gouverne laquelle.
Nous avons également trouvé des choses que la spécification ne pouvait pas connaître, car elles n'étaient visibles que depuis l'artifact. Un fichier de test qui a exécuté 0 tests et a signalé le succès, ayant tué le runner en cours de route. Gestion des signaux qui quittait 0 sur Ctrl-C — une commande interrompue signalant qu'elle avait réussi. Deux aides de sortie qui joignaient leurs lignes avec un backslash-n littéral, de sorte que chaque tableau revenait sur une seule ligne. Un aideur de couleur qui, une fois que nous avons remplacé la dépendance qu'il enveloppait, a discrètement réduit sa propre signature de type et a cassé le code qui n'avait pas changé un caractère.
Celui-là vaut la peine d'être examiné. Il n'a été capturé par aucun test que nous avons écrit. Il est apparu dans le typecheck d'un consommateur lors de la migration, ce qui est le seul endroit où il aurait pu apparaître.

Le contrat ne compte que parce que la construction échoue lorsqu'une clause n'a pas de test

Une promesse que rien ne vérifie est un commentaire.
Ainsi, la suite de conformité analyse le fichier de contrat, trouve chaque clause contenant le mot MUST, et échoue la construction si l'une d'entre elles n'a pas de test enregistré. Vous ne pouvez pas ajouter une promesse à ce projet sans ajouter la chose qui la prouve, dans le même commit.
Conformance Tests by Contract Family
Chart data
Value
Grammar20
Exit codes (truth)12
Machine output11
Self-description10
Environment8
Prompt safety6
Streams5
Cancellation5
Determinism5
46 clauses normatives. 92 tests associés à elles. 184 tests au total.
Et aucun de ces tests de conformité ne s'exécute contre le code source. Ils construisent le paquet avec son propre script de construction, exécutent npm pack, déballent le tarball, écrivent des CLIs de fixture qui importent le point d'entrée déballé, et les lancent sous Node, Bun et Deno — en vérifiant le statut de sortie et les octets exactement comme un shell les verrait.
Cette forme n'était pas un choix esthétique. Ce paquet, une fois livré, contenait un stub 65 KB. Un seul drapeau "sideEffects": false permettait au bundler de tree-shake le routeur et le module de code de sortie hors de l'artifact tout en gardant leurs noms dans la liste d'exportation. La construction s'est terminée avec 0. La suite source est restée verte tout le long. Seul l'artefact était une preuve, et personne ne regardait l'artefact.

La migration des 7 outils a trouvé 3 des portes que personne ne savait qu’elles existaient

Nous avons migré 7 des 10 CLIs le même jour, et la migration est là où la conception a obtenu sa note réelle.
Le dividende est arrivé immédiatement et n’a rien coûté : parce que les commandes dans l’ancienne version renvoyaient déjà des valeurs — le cadre les utilisait uniquement pour dériver un code de sortie, puis les abandonnait — chacune de ces valeurs de retour est devenue un JSON charge utile le jour de la mise à niveau. 7 outils ont gagné une sortie lisible par machine sans qu’aucune commande ne soit réécrite.
Ce que nous ne nous attendions pas était le même défaut dans 3 différents outils, aucun d’entre eux ne connaissant l’autre. Chacun avait une porte devant le routeur : une liste maintenue à la main des noms de commandes valides, ou une étape de démarrage qui exigeait des identifiants avant que quoi que ce soit ne s’exécute. Dans chaque cas, la nouvelle commande manifest — celle qui décrit toute la surface de l’outil en un seul appel, afin qu’un agent puisse l’apprendre sans lire le code source — répondait par « commande inconnue » ou « jeton manquant ».
L’un d’eux conservait une seconde copie de sa liste de commandes et un écran d’aide écrit à la main, tous deux avaient dérivé de ce que l’outil faisait réellement. Supprimer les deux a fait passer sa suite de 52 à l’échec de 3 à l’échec de 57 à l’échec de 0. Le plus grand outil de l’ensemble a 364 tests, et ils ont réussi avant et après la mise à niveau sans changement de source.
Le motif s’est généralisé suffisamment pour devenir une procédure écrite, expédiée à l’intérieur du paquet lui‑même. Il s’agit de 9 étapes, et les 2 étapes qui consomment le temps sont les 2 que personne n’anticipe.

Zero dépendances est le seul nombre qui ne nécessite pas de surveillance

La base avait 2 dépendances d'exécution. Elle n'en a maintenant aucune.
Cela était partiellement esthétique et surtout arithmétique. Le 8 septembre 2025, un attaquant a piraté le compte npm de Josh Junon, mainteneur de certains des paquets les plus dépendants dans JavaScript, en utilisant un domaine fictif et un code à usage unique en direct. 18 paquets ont été publiés avec des versions malveillantes, y compris chalk et debug — paquets portant quelque chose dans l'ordre de 2,6 milliards de téléchargements par semaine entre eux. La charge utile était un crypto-clipper. Les mainteneurs l'ont détecté et rétabli en environ 2 heures, et les versions compromises ont encore été téléchargées environ 2.6 millions de fois dans cette fenêtre.
Chalk est l'une des 3 bibliothèques que les modèles continuaient à solliciter lorsque je leur demandais un CLI.
La base n'a pas été affectée — elle ne dépendait jamais de chalk — et je veux être précis plutôt que dramatique à ce sujet, parce qu'elle a été créée 2 mois après l'incident. La pertinence n'est pas que nous ayons évité quelque chose. C'est que l'incident décrit exactement la classe de risque : chaque dépendance est une décennie de décisions de publication d'autrui, et vous faites confiance à un compte que vous ne contrôlez pas. La gestion des couleurs qui a remplacé une dépendance concerne environ 60 lignes. Le prompt qui a remplacé l'autre concerne environ 120. Zero est le seul nombre qui ne nécessite pas de surveillance.

Ce que la base renvoie maintenant

La version qui a été livrée est de 85 KB, non minifiée, sans dépendances d'exécution, fonctionnant sur Node, Bun et Deno. Chaque commande construite dessus obtient, sans code par commande :
GarantieCe que cela signifie en pratique
Codes de sortie honnêtesUne erreur signalée à un humain est signalée au shell
--json et --ndjsonLa valeur que votre commande renvoie, dans une forme que la machine peut analyser
manifestL'outil complet décrit dans 1 appel déterministe, ne charge rien
Discipline de fluxstdout est la charge utile ; chaque ligne de log est sur stderr
Erreurs d'utilisationSortie 2 pour "vous m'avez appelé incorrectement", distinct de 1 pour "j'ai essayé et échoué"
Sécurité du promptUn prompt sans terminal échoue en millisecondes au lieu de rester bloqué pour toujours
AnnulationCtrl-C interrompt le signal de la commande, puis quitte 130
La chose à laquelle je reviens constamment n’est pas un seul élément de cette liste. C’est que la liste est désormais consultable. L’exemple propre au README s’exécute comme un test contre le tarball publié, et les nombres cités dans son texte sont comparés aux nombres que produit la suite — une règle qui a détecté sa première erreur dans la minute qui suivit sa rédaction, où la page indiquait 87 KB et l’artifact était 85.
La base est open source sur github.com/light-merlin-dark/merlin-cli, et le contrat est un fichier dans le dépôt plutôt qu’une revendication sur un site web.
Il y a quatre ans, le problème était que chaque modèle avait une opinion différente sur ce qu’un CLI devrait être. La réponse n’a jamais été de débattre des opinions. Il fallait posséder la base sur laquelle ils construisent tous, et consigner les promesses quelque part où une construction peut échouer.