Eu tenho 51 ferramentas de linha de comando nesta máquina. Eu não planejei esse número. Isso aconteceu porque um CLI é a menor distância entre uma ideia e algo que eu realmente posso executar, e porque nos últimos anos eu tive ajuda para escrevê-los mais rápido do que eu poderia sozinho.
Essa ajuda veio com um hábito que notei cedo, quando o GPT-3.5 e os primeiros modelos Claude eram os que eu trabalhava. Pergunte a 3 modelos diferentes para criar um CLI e você obtém 3 opiniões diferentes sobre o que é um CLI. Um busca por Commander. Um busca por Inquirer para a solicitação. Um busca por Chalk porque a saída deve ser colorida. Cada resposta é defensável. Juntos eles são um imposto, porque agora eu possuo 3 bases de código que discordam sobre análise de argumentos, sobre como uma falha parece, e sobre qual dessas bibliotecas eu agora sou responsável por monitorar.

Cada modelo buscou um conjunto diferente de bibliotecas, e eu fui quem teve que viver com todas elas

O problema não são as bibliotecas. É que as melhorias param de viajar.
Quando 3 CLIs discordam sobre como um comando relata falha, uma correção em um é uma correção em um. Não há nada para enviar para cima. O trabalho não se acumula, e depois da décima ferramenta você não está construindo alavancagem, você está mantendo um portfólio de quase falhas.
Eu já havia escrito sobre querer o oposto disso. Todo o argumento em How to Turn AI Gains Into Compounding Infrastructure é que um ganho se torna durável quando todo projeto dependente o herda. Uma superfície de capacidade compartilhada. Uma regra de promoção. Um lugar onde uma melhoria cai e se espalha.
Eu construí essa camada para capacidade de IA, para fluxo de trabalho, para operações. Eu não a construí para a coisa que eu realmente faço mais frequentemente.

Então eu criei uma base e fiz cada CLI projeto enviar suas melhorias para ela

A regra era simples e era minha impor: quando um CLI na minha propriedade precisava de algo melhor — uma forma mais limpa de registrar serviços, um caminho de erro melhor, um auxiliar de teste que tornasse um conjunto legível — essa melhoria não ficava no projeto. Ela entrou na base, e a base saiu para os outros.
Esse é todo o design. A base é pequena por intenção. Ela não tem opinião sobre o que sua ferramenta faz. Ela tem uma opinião forte sobre o que um comando é: algo que recebe argumentos, faz trabalho, relata o que aconteceu, e deixa.
A pasta foi criada em 6 de julho de 2025, e 2 das minhas ferramentas dependiam da versão 1.0.0 dela no mesmo dia. Isso é o sinal: não foi construído especulativamente e depois adotado. Foi extraído de trabalho que já existia, no ponto em que copiar a mesma estrutura entre projetos deixou de ser razoável.
Se espalhou rapidamente, porque espalhar era toda a ideia. 8 repositórios estavam sobre isso em 25 dias. 10 em 11 semanas.
Repositories Adopting the Base in 2025
Chart data
repositories
Jul 62
Jul 84
Jul 175
Jul 237
Jul 308
Sep 2010
Git veio depois de tudo isso. O repositório foi inicializado em 12 de novembro de 2025, 4 meses depois, e publicado no dia seguinte — por isso o histórico de versões e o histórico real discordam, e por isso eu verifiquei o sistema de arquivos em vez de confiar no log de commits quando me sentei para escrever isso.
Esses 10 ferramentas fazem administração do Cloudflare. Local DNS e gerenciamento do nginx. Implantação contra Coolify. Automação de navegador. Relatório de custos entre provedores de modelo. A maioria são privadas, por isso as descrevo pelo que fazem em vez de pelo nome. As públicas são aia, que consultam vários modelos em paralelo, e a base em si. vssh, minha ferramenta de execução remota protegida, também é pública e surgiu do mesmo instinto — construir a superfície do operador uma vez, corretamente, e parar de reconstruí-la.
Os dividendos foram reais e foram chatos, que é a forma correta para dividendos de infraestrutura. Um reforço em uma ferramenta apareceu em todas elas. Quando descobri que um comando poderia imprimir uma mensagem de erro vermelha e ainda sair 0 — informando ao humano que falhou e ao shell que funcionou — a correção não entrou nos 16 lugares em uma ferramenta onde aconteceu. Ela entrou na base, e todas as ferramentas a herdaram.

Depois de 13 meses eu queria que fosse reconstruída, não apenas consertada

Em agosto de 2026 a base estava funcionando e eu ainda queria que fosse removida.
Não porque estava quebrada. Porque havia acumulado. Porque a regra de exit-code da qual eu mais me orgulhei havia sido retrofitted em vez de projetada desde o início. Porque o mundo para o qual ela foi escrita mudou debaixo dela: a maioria das invocações dos meus CLIs não são mais digitadas por mim. Elas são emitidas por agentes, lendo stdout, stderr, e $? como seus únicos sentidos.
Então, em vez de consertar, eu defini os termos de forma diferente. Eu dei ao Claude Fable uma única instrução, e a tornei deliberadamente grande:
Se este fosse o último framework CLI que a humanidade construiu — o que ainda está em serviço em uma década — você agora tem a chance de torná-lo assim.
Projete-o a partir daí. Eu não esperava um documento de volta. Eu esperava um plano.

Fable voltou com um tratado, e a restrição era que as promessas tivessem que ser poucas

O que chegou não foi uma lista de recursos. Foi estruturado como um tratado, dividido ao meio por uma parede dura.
Uma metade era um contrato: o que todo CLI construído sobre esta base garante a todo observador, escrito como cláusulas numeradas na linguagem RFC-2119 — DEVE, NÃO DEVE, DEVERIA, PODE. Doze famílias deles. Códigos de saída. Disciplina de fluxo. Saída da máquina. Auto-descrição. Gramática. Ambiente. Cancelamento. Determinismo. Orçamentos de desempenho. Compatibilidade.
A outra metade era a superfície de autoria, que podia crescer, e que existia apenas para tornar o cumprimento do contrato o caminho de menor resistência.
O raciocínio abaixo dele foi a parte que achei convincente. Um design pensado para durar uma década não pode apostar na moda, porque a moda é o que expira. Ele não pode apostar na astúcia, porque a astúcia é o que você não pode prever no ano 8. Ele só pode apostar nas interfaces que não mudaram desde os 1970s: vetores de argumentos, fluxos 3, um código de saída de 8 bits, variáveis de ambiente. E ele observou o fato genuinamente novo — que o leitor maioritário dessas interfaces agora é uma máquina que não pode fazer uma pergunta de acompanhamento.
A cláusula que acabou organizando tudo o mais foi a que ele abriu com:
Um resultado, muitas renderizações. Um comando calcula um único resultado. O código de saída código, o texto humano, o documento JSON e as linhas de streaming são todas projeções desse único valor. Eles não podem se contradizer, porque há apenas uma fonte.
Essa é a frase que toda a reconstrução gira em torno.
Diagram source
graph LR
    A["execute() retorna  
um valor"] --> B["código de saída"]
    A --> C["texto renderizado  
stdout"]
    A --> D["envelope JSON  
--json"]
    A --> E["fluxo NDJSON  
--ndjson"]
    F["logger.error()  
ctx.emit()"] -.-> B
    F -.-> G["eventos  
stderr"]

Opus 5 e eu encontrei a especificação correta sobre a tese e errada sobre 3 coisas

É aqui que o trabalho se tornou nosso em vez de meu.
Levei a especificação para Opus 5 e construímos em um dia. Não foi um dia limpo. As partes úteis são os lugares onde o documento encontrou a propriedade e perdeu.
A especificação queria que ctx.args se tornasse um registro de argumentos nomeados. É o melhor design em isolamento. Também teria quebrado todo comando em cada um dos 10 ferramentas, porque todos lêem ctx.args como um array. Mantivemos o array e colocamos os argumentos tipados em ctx.namedArgs ao lado dele. A regra que decidiu já estava escrito no contrato, uma cláusula acima: nunca quebre um consumidor outranqueia todo outro valor no repositório, incluindo a própria completude do contrato.
A especificação queria um grupo de comandos sem verbo ser um erro de uso. Executar um comando pai sem subcomando sairia 2. Defensável, e teria alterado o comportamento de todo script que executa um comando de agrupamento puro para ver seu help. Continuamos imprimindo help e saindo 0.
A especificação assumiu streaming e o único JSON documento como a mesma funcionalidade. Eles não são. Streamar um milhão de itens em memória constante é o ponto de um e impossível no outro, porque um chamador que pediu um único documento pediu que fosse único. Separamos o comportamento e anotamos qual cláusula governa qual.
Também encontramos coisas que a especificação não poderia saber, porque eram visíveis apenas a partir do artefato. Um arquivo de teste que executou testes 0 e relatou sucesso, tendo matado o runner parcialmente. Tratamento de sinal que saiu 0 ao Ctrl-C — um comando interrompido relatando que havia sido bem-sucedido. Dois auxiliares de saída que juntaram suas linhas com uma barra invertida-n literal, então toda tabela retornou em uma linha. Um auxiliar de cor que, uma vez que substituímos a dependência que ele havia embrulhado, reduziu silenciosamente sua própria assinatura de tipo e quebrou código que não havia mudado um caractere.
Esse último vale a pena ficar sentado com. Foi capturado por nenhum teste que nenhum de nós escreveu. Surgiu em uma verificação de tipo de consumer durante a migração, que é o único lugar onde poderia ter.

O contrato só conta porque a construção falha quando uma cláusula não tem teste

Uma promessa que nada verifica é um comentário.
Então o conjunto de conformidade analisa o arquivo de contrato, encontra cada cláusula contendo a palavra MUST, e falha a construção se uma delas não tiver teste registrado. Você não pode adicionar uma promessa a este projeto sem adicionar a coisa que a prova, no mesmo 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 cláusulas normativas. 92 testes mapeados a elas. 184 testes no total.
E nenhum desses testes de conformidade roda contra a fonte. Eles constroem o pacote com seu próprio script de construção, executam npm pack, descompactam o tarball, escrevem CLIs de fixture que importam o ponto de entrada descompactado, e os iniciam sob Node, Bun e Deno — afirmando sobre o status de saída e os bytes exatamente como um shell os veria.
Essa forma não foi uma escolha estética. Este pacote, uma vez enviado, tinha um stub de KB 65. Um único sinalizador "sideEffects": false deixou o bundler tree-shake o roteador e o módulo de código de saída do artefato enquanto seus nomes permaneciam na lista de exportação. A construção saiu 0. O conjunto de origem permaneceu verde o tempo todo. Somente o artefato era evidência, e ninguém estava olhando para o artefato.

Migrando 7 ferramentas encontrou 3 portões que ninguém sabia que existiam

Migramos 7 de 10 CLIs no mesmo dia, e a migração é onde o design obteve sua nota real.
O dividendo caiu imediatamente e não custou nada: porque os comandos na versão antiga já retornavam valores — o framework os usava apenas para derivar um código de saída, depois descartava — cada um desses valores de retorno tornou-se um JSON payload no dia da atualização. 7 ferramentas ganharam saída legível por máquina sem que nenhum comando fosse reescrito.
O que não esperávamos era o mesmo defeito em 3 ferramentas diferentes, nenhuma das quais sabia da outra. Cada uma tinha um portão na frente do roteador: uma lista mantida à mão de nomes de comandos válidos, ou uma etapa de inicialização que exigia credenciais antes que qualquer coisa rodasse. Em todos os casos o novo comando manifest — o que descreve toda a superfície da ferramenta em uma única chamada, para que um agente possa aprender sem ler o código fonte — respondia com "comando desconhecido" ou "token ausente."
Um deles mantinha uma segunda cópia de sua lista de comandos e uma tela de ajuda escrita à mão, ambas haviam divergido do que a ferramenta realmente fazia. Excluir ambos tirou sua suíte de 52 passando com 3 falhando para 57 passando com 0. A maior ferramenta do conjunto tem 364 testes, e eles passaram antes e depois da atualização sem alteração de código fonte.
O padrão se generalizou bem o suficiente para se tornar um procedimento escrito, enviado dentro do próprio pacote. É 9 passos, e os 2 passos que consomem o tempo são os 2 que ninguém antecipa.

Zero dependências é o único número que não precisa de monitoramento

A base tinha 2 dependências de tempo de execução. Agora não tem nenhuma.
Isso foi parcialmente estético e principalmente aritmético. Em 8 de setembro de 2025, um atacante phishingou a conta npm de Josh Junon, mantenedor de alguns dos pacotes mais dependentes em JavaScript, usando um domínio falso e um código de uso único ao vivo. 18 pacotes foram publicados com versões maliciosas, incluindo chalk e debug — pacotes que juntos têm algo em torno de 2,6 bilhões de downloads por semana. O payload era um crypto-clipper. Os mantenedores perceberam e revertiram em aproximadamente 2 horas, e as versões comprometidas ainda foram baixadas cerca de 2.6 milhões de vezes nesse intervalo.
Chalk é uma das 3 bibliotecas que os modelos continuaram a buscar quando eu lhes pedi um CLI.
A base não foi afetada — nunca dependia de chalk — e quero ser preciso em vez de dramático sobre isso, porque foi criada 2 meses após o incidente. A relevância não é que evitamos algo. É que o incidente descreve exatamente a classe de risco: cada dependência é uma década de decisões de lançamento de outra pessoa, e você está confiando em uma conta que não controla. O tratamento de cores que substituiu uma dependência é sobre 60 linhas. O prompting que substituiu a outra é sobre 120. Zero é o único número que não precisa de monitoramento.

O que o base devolve agora

A versão que foi lançada tem 85 KB, não minificada, sem dependências de tempo de execução, rodando em Node, Bun e Deno. Todo comando construído sobre ele obtém, sem código por comando:
GarantiaO que isso significa na prática
Códigos de saída honestosUm erro reportado a um humano é reportado ao shell
--json e --ndjsonO valor que seu comando retorna, em uma forma que a máquina pode analisar
manifestA ferramenta inteira descrita em 1 chamada determinística, carregando nada
Disciplina de fluxostdout é carga útil; cada linha de log está em stderr
Erros de usoSaia com 2 para "você me chamou errado", distinto de 1 para "eu tentei e falhei"
Segurança de promptUm prompt sem terminal falha em milissegundos em vez de ficar preso para sempre
CancelamentoCtrl-C interrompe o sinal do comando, então sai 130
A coisa que continuo voltando é não qualquer item único nessa lista. É que a lista agora é verificável. O próprio exemplo do README roda como um teste contra o tarball publicado, e os números citados em sua prosa são mantidos pelos números que o conjunto produz — uma regra que pegou seu primeiro erro dentro de um minuto de ser escrito, onde a página dizia 87 KB e o artefato era 85.
A base é de código aberto em github.com/light-merlin-dark/merlin-cli, e o contrato é um arquivo no repositório em vez de uma reivindicação em um site.
Quatro anos atrás o problema era que cada modelo tinha uma opinião diferente sobre o que um CLI deveria ser. A resposta nunca foi argumentar com as opiniões. Era possuir a base que todos construíam, e escrever as promessas em algum lugar onde uma construção possa falhar.