Exit Zero
Passei onze semanas construindo guardrails determinísticos em volta de agentes de código e o resto do tempo tentando provar que eles eram falsos. O teste de mutação encontrou um gate que imprimia FALHOU na tela e saía com exit 0, e ele era um dos dois gates que eu estava prestes a ligar num cliente.
Trabalho com desenvolvimento de software há quase trinta anos e acompanhei o que a disciplina construiu nesse tempo em método, processo e teste. No fim de 2024 comecei a desenvolver com assistência de IA, e a escala do trabalho mudou. Em 11 de setembro, em um dos produtos que esta plataforma governa, fechei 59 commits e um diff de 207.859 linhas adicionadas.
Vale decompor esse número, porque este relato trata justamente do hábito de decompor. Dessas linhas, 184.770 são 22 snapshots de migração de ORM gerados por ferramenta. O que escrevi de fato naquele dia foram 16.783 linhas de TypeScript (7.762 de código de produção e 9.021 de teste) e cerca de 3.400 de documentação. Artefatos que nenhum humano lê inflaram a manchete em doze vezes.
Volume não indica qualidade, aderência à necessidade do negócio ou software funcionando. Os dados publicados dizem o mesmo, com mais precisão do que os slogans construídos em cima deles.
A METR fez um ensaio controlado randomizado com 16 desenvolvedores open-source experientes, em 246 tarefas reais nos repositórios maduros dos próprios participantes. Antes de começar, eles previram um ganho de 24% de velocidade; ao terminar, estimaram que tinham sido acelerados em 20%. A medição mostrou que ficaram 19% mais lentos. São cerca de 39 pontos entre o percebido e o real, na direção que deveria preocupar quem põe fluxo agêntico em produção: as pessoas mais bem posicionadas para notar o problema não o notaram.
A GitClear analisou mais de 600 milhões de commits e encontrou, de 2023 para 2026, um aumento de 81% nos blocos de código duplicados, o maior nível já registrado (73,0 por milhão de linhas alteradas). No mesmo período, o copiar-e-colar dentro do mesmo commit subiu 41%, as construções que mascaram erro subiram 47% e o churn de duas semanas subiu 15%. Os marcadores de consolidação caíram na mesma janela: chamadas de função entre arquivos −35% e movimentos de linha por refatoração −70%.
Em dois relatórios seguidos, o DORA descreve a IA como amplificador: ela aumenta as forças das organizações de alto desempenho e as disfunções das que já vão mal. Pela leitura do DORA, o retorno depende da qualidade da plataforma interna em volta da ferramenta, e a ferramenta sozinha não o explica.
Somados, esses números dizem algo mais estreito do que "IA para código é ruim": dizem que o modelo não é a variável que decide. Uma pesquisa publicada em julho de 2026 testou isso diretamente. Ben Sghaier e colegas fixaram o modelo e variaram só o harness ao longo de 35 releases sequenciais de uma CLI, contra 50 tarefas estratificadas do SWE-bench Verified, e rastrearam as oscilações de qualidade até mudanças de harness. O título do artigo resume o argumento: Don't Blame the Large Language Model.
Quem desenvolve software se preocupa o tempo todo com processo, arquitetura, design, regra de negócio e contexto, e com a forma de manter tudo isso coerente do primeiro commit até o deploy e a sustentação. Construí um SDLC de IA aos poucos, tentando seguir as práticas clássicas de Engenharia de Software: spec, design, TDD e revisão.
Hoje a plataforma cobra algumas dessas práticas. Entrega sem os canônicos obrigatórios não passa, símbolo exportado sem consumidor é reportado e SQL destrutivo nunca chega ao shell. Outras ela só ensina. TDD existe como uma skill que o agente lê, sem gate que dispare. Revisão de código é um template de CODEOWNERS que entrego a cada projeto, e o meu harness não tem como garantir que ela aconteça. O hook que restringiria push está registrado e testado, mas não está ligado a nada.
Essa lacuna é o assunto deste relato: como eu sei que os guardrails, controles, regras e hooks que estou entregando estão eles mesmos seguindo as regras que defini?
Em resumo: se o harness é o que está sob engenharia, quem testa o harness?
O que a taxonomia de Böckeler deixa em aberto
Em abril de 2026, Birgitta Böckeler publicou Harness engineering for coding agent users, que deu ao campo seu vocabulário de trabalho: Agente = Modelo + Harness, em que o harness é tudo o que há num agente além do modelo. Ela divide os controles em dois eixos. Pela direção, os guias orientam antes de o agente agir (feedforward) e os sensores observam depois que ele age, para que se corrija (feedback). Pela execução, os controles computacionais são determinísticos e rápidos (linters, type checkers, hooks de pre-commit), e os inferenciais rodam num modelo e são mais lentos (agentes de revisão, juízes customizados).
Esse 2×2 é um bom mapa e não vou redesenhá-lo. Aqui interessa a lista de pontos que Böckeler deixa explicitamente em aberto no fim do artigo. Quatro deles:
- não existe equivalente a cobertura de código para avaliar o próprio harness;
- a coerência continua sem solução à medida que o harness cresce: como impedir que os controles se contradigam;
- falta ferramenta para configurar e sincronizar controles ao longo do pipeline de entrega;
- templates de harness se comportam mal em bases legadas e endividadas.
Ao ler o artigo, percebi que talvez eu tenha respondido algumas dessas perguntas. O que tenho são quatro respostas parciais, tiradas de um sistema que está rodando, e o §07 mostra até onde elas vão.
Medindo a lacuna no GitHub
Antes de escrever, varri pela API do GitHub os repositórios mais estrelados da área com uma única pergunta: o harness testa a si mesmo? Montei a amostra em 19 de agosto de 2026 com o cânone visível de harness engineering e refiz a contagem em 24 de setembro, quando os mesmos nove repositórios somavam cerca de 600 mil estrelas.
| Repositório | Estrelas | Arquivos | Testes junto a hooks | Mutação do harness |
|---|---|---|---|---|
| obra/superpowers | 291.202 | 255 | 2 | nenhuma |
| github/spec-kit | 138.776 | 686 | 7 | nenhuma |
| ruvnet/ruflo | 73.218 | 7.117 | 34 | nenhuma |
| bmad-code-org/BMAD-METHOD | 53.425 | 785 | 0 | nenhuma |
| SuperClaude-Org/SuperClaude | 23.906 | 475 | 0 | nenhuma |
| diet103/…-infrastructure-showcase | 10.029 | 161 | 0 | nenhuma |
| buildermethods/agent-os | 5.444 | 31 | 0 | nenhuma |
| disler/claude-code-hooks-mastery | 3.926 | 153 | 1 | nenhuma |
| karanb192/claude-code-hooks | 524 | 236 | 21 | nenhuma |
O padrão aparece nas duas pontas da distribuição. O repositório mais estrelado entre os que se apresentam explicitamente como vitrine de infraestrutura (10.029 estrelas, 161 arquivos de hooks, agentes e skills) não tem nenhum teste dos próprios hooks. O mais bem testado da amostra é também o menor, por duas ordens de grandeza: o karanb192/claude-code-hooks entrega um teste unitário por plugin e é o vizinho mais próximo de tudo o que descrevo aqui. Ele tem plugins chamados config-guard ("quem guarda os guardas"), protect-tests (bloqueia o agente de desabilitar teste para ficar verde) e dead-rules-audit (pontua quais das suas regras escritas o modelo de fato segue e sinaliza as cronicamente ignoradas para promoção a hook). São os instintos certos, e o autor chegou a eles de forma independente.
Nenhum repositório da amostra quebra um controle de propósito para conferir se alguma coisa percebe, e também não encontrei isso na literatura do arXiv.
Gate que ninguém chama é só um documento
A plataforma é uma camada de SDLC interna distribuída como plugin: 22 agentes, 54 skills, 19 hooks, 33 gates, 467 arquivos markdown e cerca de 7.600 linhas de shell, construída em 140 commits entre 29 de junho e 16 de setembro de 2026. A parte que interessa aqui são os gates, que interceptam chamadas de ferramenta e fronteiras de sessão: guarda de SQL destrutivo, varredura de segredo, restrição de push, checagem de plano-primeiro, runner de lint e cobertura, checagem de canônicos, entre outros.
O primeiro problema que encontrei veio da simples existência dos gates, antes que qualquer um deles falhasse.
A documentação dizia quais gates eram obrigatórios, um catálogo à parte dizia o que cada um checava e um terceiro arquivo dizia em que nível cada um era cobrado. Os três eram prosa mantida à mão, e os três se desalinharam com o tempo, porque nada no sistema ligava a frase "este gate é obrigatório" ao fato de um processo ser invocado.
A correção foi um registry, um único arquivo legível por máquina que é a autoridade sobre quais gates existem. Toda entrada nele precisa responder a três perguntas.
// uma entrada, abreviada { "runner": "hooks/anti-destrutivo.sh", "nivel": "🔴", // obrigatório "inviolavel": true, "gatilho": { "tipo": "EXEC", // EXEC | PROSA-CMD | orfao "onde": ["hooks.json:PreToolUse(Bash)"] }, "corpus": "stdin JSON tool_input.command | arg $1", "harness_cases": [ "anti-destrutivo/bloqueia-drop-de-tabela", "anti-destrutivo/bloqueia-delete-sem-where", … mais 10 ] }
O que ele roda (runner), onde está armado (gatilho.onde, um evento real num manifesto real) e o que prova que ele morde (harness_cases, casos executáveis e nomeados, ver §03). A cada commit, um checador confere o registry contra a realidade: se um runner está listado e não está armado, está armado e não está listado, ou cita casos que não existem, o commit falha.
O terceiro tipo de gatilho é o que eu defenderia com mais força. orfao quer dizer que o controle existe, está testado e hoje não está ligado a nada. Três das 33 entradas são órfãs. Elas não são bugs e ninguém as escondeu: são dormência declarada, com a razão registrada. Sem esse campo, sobraria um hook parado numa pasta, com cara de cobrado e sem nunca disparar, que é justamente o modo de falha que este arquivo existe para impedir.
Esta é a minha resposta direta à falta de "ferramenta para configurar e sincronizar controles" que Böckeler aponta. A solução é simples: um JSON e um script de shell que se recusa a deixar o JSON mentir, e todo o valor está nessa recusa.
237 casos, uma pasta para cada
Um gate é um script de shell que lê alguma coisa e decide, o que o torna testável como qualquer outra coisa. O formato que sobreviveu ao uso é burro de propósito: uma pasta por caso e um arquivo por asserção.
casos/anti-destrutivo/bloqueia-drop-de-tabela/
runner → hooks/anti-destrutivo.sh
arg → psql -c '<DDL destrutivo em uma tabela>'
exit → 2
espera_stderr → DDL destrutivo
motivo → guard-rail #1: derrubar tabela nunca passa.
Não há framework de teste, DSL de asserção nem biblioteca de mock. Um runner lê a pasta, executa o script indicado com a entrada dada e compara o código de saída e o stderr. Casos que precisam de sistema de arquivos ganham um bloco {SETUP}, e casos que precisam de stdin usam stdin.json no lugar de arg. São 237 casos em 244 pastas (sete delas são fixtures compartilhadas), e o registry cita 227 deles pelo nome a partir das entradas de gate.
Duas escolhas de projeto importam mais que o formato.
Todo caso carrega um motivo. Esse arquivo guarda o enunciado da regra que está sendo defendida, no vocabulário da própria regra, em vez de uma descrição da asserção. Quando um caso fica vermelho dois meses depois, quem o lê precisa saber se a regra mudou ou se o código quebrou, e a asserção sozinha não informa isso.
O stderr é asserido junto com o código de saída. Tem cara de preciosismo, e eu a considero a restrição mais valiosa do formato, por um motivo simples: um script que quebra num argumento inesperado também sai com código diferente de zero. Se você aferir só o código de saída, "o gate bloqueou o comando perigoso" e "o gate está quebrado" dão o mesmo verde. Sei disso porque cometi exatamente esse erro ao medir, e ele é um dos três erros descritos no §06.
A suíte completa roda no caminho de pre-commit. O motor de mutação (§04) fica fora dele porque leva minutos; é um comando de ponto limpo, que rodo depois de mexer num runner e antes de fechar release. O gate mais caro que medi roda em 1,14 s, contra um teto declarado de 60 s. Essa folga existe para o caso de travamento, e não para o caso comum.
Quem testa os testes
237 casos verdes provam que os gates se comportam corretamente em 237 entradas, mas não provam que algum caso perceberia se um gate parasse de funcionar. Essa segunda afirmação precisa de outro mecanismo: quebrar o runner de propósito, de forma mecânica e com uma mudança pequena por vez, e exigir que algum caso fique vermelho. Um mutante que sobrevive indica uma forma real de quebrar aquele gate que nenhum caso detecta.
A lista de operadores é curta de propósito, porque operador demais gera mutante equivalente e o ruído acaba com o mecanismo:
| Operador | O que simula |
|---|---|
| exit 2 → exit 0 | o gate para de bloquear |
| exit $FALHA → exit 0 | a postura para de bloquear |
| fail=1 → fail=0 | a falha deixa de contar |
| if ! … → if … | a condição perde a negação |
| -eq→-ne · -le→-gt · -lt→-ge | a comparação inverte |
| grep -q → grep -qv | o casamento inverte |
| block "…" → : "…" | a chamada de bloqueio é neutralizada |
O operador que joguei fora
O candidato óbvio a décimo primeiro operador era && → ||, e ele foi medido e reprovado. Gerou 35 dos 51 sobreviventes da primeira rodada completa, uma proporção de ruído de 2:1, e quase todos caíram em linhas de guarda de dependência opcional, do tipo [ -f prisma/schema.prisma ] && roda …. Trocar o conectivo nessas linhas só muda o comportamento quando a dependência opcional está no estado oposto, e esse é justamente o estado que nenhum fixture tem. Um relatório que ninguém lê pertence à mesma família de falha de um gate permanentemente amarelo.
O que os mutantes acharam
Foram três classes de problema, e a primeira é a razão de este artigo existir.
Verde falso. O problema estava em duas linhas do orquestrador principal de gates: as atribuições fail=1 de dois gates específicos em postura de bloqueio. Com elas zeradas, o runner imprime ⛔ FALHOU na tela e no stderr e sai com 0. É uma mensagem de reprovação impressa em cima de uma saída de sucesso, dentro do orquestrador que todo o resto chama.
Esses dois gates eram justamente os dois que estavam na fila para subir de warn para block num cliente, ou seja, o ramo que seria ligado em produção não tinha prova nenhuma por trás. Todos os casos que cobriam aqueles gates estavam verdes porque os exercitavam em postura de aviso, onde o fail=1 nunca é lido.
Um ledger que podia emudecer. Uma comparação invertida no hook de rastreio de commit faz uma condição de deduplicação ficar sempre verdadeira assim que o ledger tem mais de uma entrada. A partir daí, o hook para de registrar commit novo sem avisar, o gate de fechamento de sessão deixa de ver qualquer commit na sessão e a sessão fecha limpa. Esse caso só ficou testável depois que acrescentei uma costura de depuração, pela mesma razão do caso irmão: sair com 0 em silêncio é o que o hook faz quando funciona, e exit 0 sobrevive a qualquer mutação.
Um ramo sem cobertura que era obrigatório no papel. O ramo de checagem de heads de migração de banco estava marcado como obrigatório no catálogo, mas não tinha caso nem entrada na lista de sobreviventes aceitos. Era um sobrevivente silencioso, morando na fresta entre dois ledgers.
O ledger de sobreviventes aceitos e o bug dentro dele
Alguns mutantes sobrevivem legitimamente: equivalentes que mudam uma string em vez de uma comparação, ramos inalcançáveis sem ferramental específico, caminhos que dependem do ambiente. Esses vão para um arquivo de sobreviventes declarados, cada um com classe, razão e data, e o motor imprime a lista inteira a cada execução. Qualquer sobrevivente fora dessa lista reprova a execução. A dívida é permitida desde que listada; o que fica proibido é a dívida silenciosa.
Esse arquivo tinha um defeito próprio, e ele é a coisa mais instrutiva desta seção. As entradas eram chaveadas por runner + número da linha, e números de linha mudam. Medi duas vezes: em 5 de agosto, 2 de 14 entradas já estavam desancoradas, e em 15 de agosto eram 3 de 12. Uma delas apontava para um grep -q de um ramo Java que já não existia no arquivo, ou seja, era uma entrada morta guardando um número que qualquer mutante futuro podia herdar.
O modo de falha é assimétrico, e por isso precisei de duas medições para levá-lo a sério. Uma entrada deslocada reaparece como sobrevivente novo, o que gera ruído visível. Já um sobrevivente real que cai num número herdado é aceito sem aviso. A chave passou a ser runner + operador + trecho de código, o número de linha virou só documentação e o motor avisa quando reancora uma entrada. Errar para o lado do ruído é o único erro barato disponível aqui.
Rodei o motor de novo para esta versão do texto, em 24 de setembro de 2026, sobre a revisão 1.27.2 e numa cópia do repositório, porque os hooks da sessão que dispara o motor rodam da própria árvore e um hook mutado passaria a valer na sessão durante a rodada. Deu 102 mortos, 11 declarados e dois sobreviventes não declarados, e os dois estão em gates que nasceram depois da primeira versão deste artigo. O gate de observabilidade, de 2 de setembro, continua verde quando o exit 2 vira exit 0. O gate de merge de PR, de 27 de agosto, continua verde quando a comparação que reconhece o timeout (-eq 124) é invertida.
Em 20 de agosto, a mesma rodada tinha dado 98 mortos e zero sobreviventes não declarados, e a árvore que o motor deixou era idêntica byte a byte à que encontrou. O zero não se manteve sozinho: os gates escritos depois dele entraram com casos que passam, e só a mutação mostrou que esses casos não percebem o gate parar de bloquear. Chegar àquele zero de agosto levou duas rodadas e acrescentou quatro casos. A primeira rodada registrou seis mutantes vivos como "pré-existentes e não relacionados" e seguiu em frente, que é o que um time faz quando o mecanismo é novo e a fila é longa. Quando olhei os seis um a um, nenhum era ruído: três eram verdes falsos e três eram âncoras mortas.
A regra que teve de merecer a entrada
A régua da casa diz que regra sem sinal medido não vira gate, e que reprovar é um resultado legítimo. O candidato aqui era o export-sem-import, que acusa símbolo exportado no diff da tarefa sem nenhum consumidor. Ele torna executável uma regra que até então era só uma frase num documento de definition of done: implementado, testado e importado por ninguém é dead code, e dead code não foi entregue.
Antes de escrever o gate, escrevi uma sonda descartável e a rodei sobre o histórico de três repositórios. Registrei o limiar (precisão ≥ 70%) antes de olhar qualquer resultado.
| Repo | Commits | Avaliações | Limpo | Incerto | Acusa | Taxa sobre decidíveis |
|---|---|---|---|---|---|---|
| A (workspace de analytics) | 400 | 471 | 325 | 117 (25%) | 27 | 7,7% |
| B (gateway em produção) | 69 | 132 | 113 | 0 | 17 | 13,1% |
| C (site de marketing) | 16 | 0 | — | — | — | n/a |
Depois triei à mão, uma a uma, as 17 acusações. Treze eram verdadeiras: o símbolo continuava parado ali, sem consumidor. Quatro eram prematuras, porque o símbolo ganhou consumidor depois, e em três dessas quatro o consumidor chegou no mesmo dia, num commit seguinte da mesma sessão. Depois de uma correção descrita abaixo, zero eram falso positivo.
O número depende do que você chama de unidade
A precisão por commit é 13/17 = 76%. Por tarefa, que é como o gate roda de verdade (aferindo o diff acumulado da sessão em vez de ir commit a commit), ela é 13/14 = 93%, porque as três acusações do mesmo dia nunca disparam. A mesma sonda sobre os mesmos dados dá duas respostas, e elas caem em lados opostos da linha que a indústria usa.
Essa linha vem do Google. O Tricorder, plataforma de análise estática da empresa, impõe um teto: um analisador exibido ao desenvolvedor tem de ficar abaixo de aproximadamente 10% de falso positivo efetivo, porque acima disso os desenvolvedores o descartam e o desligam. A taxa geral do próprio Tricorder fica pouco abaixo de 5%. Contra esse teto, este gate reprova como checagem por commit e passa como checagem por tarefa.
Taxa de falso positivo contra o teto de confiança
A mesma regra, medida sobre as mesmas 17 acusações, em duas unidades de trabalho diferentes.
O número do Google é a taxa efetiva de falso positivo publicada para os analisadores do Tricorder; os dois números do gate são o complemento de 93% e 76% de precisão sobre 17 acusações triadas à mão em dois repositórios. Com n pequeno, isto serve de apoio a decisão e não de benchmark.
Quero ser cuidadoso com a conclusão, porque dá para tirar dela uma versão barata e uma verdadeira. A barata seria "escolha a unidade que favorece". A que considero verdadeira é que a unidade de medição é um parâmetro de projeto do gate, e escolhê-la equivale a escolher o limiar. Aqui, agregar por tarefa descreve o que o gate faz de fato, e por isso não conta como truque de relatório: um símbolo que ganha consumidor no commit seguinte da mesma sessão nunca foi defeito.
Três consequências de projeto que saíram da medição
Uma classe de falso positivo que a sonda achou para mim. A versão 2 acusou três símbolos num arquivo de schema, e os três eram falsos: o consumidor faz import * as schema from "./schema" e o ORM lê o módulo inteiro, então o nome do símbolo não aparece em lugar nenhum. É a mesma cegueira do barril export *, com outra sintaxe. A versão 3 trata os dois casos do mesmo jeito: arquivo consumido por namespace vira incerto, nunca acusação. O efeito medido foi de 30 para 27 acusações, de 105 para 117 incertos e de 3 para 0 falsos positivos.
Incerto é veredito de primeira classe, e corresponde a 25% dos casos no repo grande. Ele é reportado como incompleto, nunca como passou. É a regra skip ≠ pass outra vez, e é por isso que o gate nasce em postura de aviso em vez de bloqueio.
Gate novo nasce em warn, sempre. Um gate que vai para bloqueio no primeiro dia fica vermelho o tempo todo, e em uma semana alguém negocia a retirada dele. O gate só se forma quando uma leva de trabalho real passa verde.
Quatro dias depois, o gate foi auditado contra a própria regra
A auditoria achou duas cegueiras, as duas da família que esta plataforma existe para combater: casos que o gate não sabia decidir e em que também não avisava que não soube. O parser de símbolos reconhecia function, class e const/let/var, mas export type e export interface caíam num ramo padrão e sumiam, sem virar achado nem incerto. Além disso, o corpus era git diff HEAD, que não enxerga arquivo untracked, então antes do git add a resposta era "nada a checar". As duas foram corrigidas: a primeira virou incerto declarado e a segunda ampliou o corpus.
/metrics que nenhum arquivo importava e que, portanto, nunca era executado. Um arquivo de protocolo cujos type-guards só eram consumidos pelo próprio arquivo de teste, testados e cobertos, mas sem nenhuma chamada em código de produção; foram oito na primeira medição e nove, separados em três classes distintas, numa medição posterior e mais afiada. E dois casos em que o arquivo é importado, mas o símbolo acusado não, algo que uma checagem por arquivo perderia por construção.
Duas configurações que nenhum código lia
Em agosto rodei a plataforma contra um levantamento de 97 achados feito por outra pessoa, que fazia dogfood de um pipeline agêntico diferente. A regra era que diferença entre repositórios não conta como defeito: cada item tinha de ser reproduzido rodando o código deste repositório, com comando e saída, ou não contava. Quatro itens passaram por esse filtro. A auditoria refutou, recaracterizou ou confirmou cada um deles e gerou seis achados novos. Por causa de dois desses seis, recomendo a quem for construir algo parecido que comece pelo registry e deixe os gates para depois.
Os níveis de enforcement eram decorativos. Um arquivo de configuração declarava, para cada gate, se ele estava em off, warn ou block. O arquivo era calibrado com clientes e estava documentado, mas até a versão 1.9.0 nenhum código o lia. A postura era editada direto no runner, e o próprio runner admitia isso num comentário. Quem pusesse um gate em block naquele arquivo acreditava ter endurecido o pipeline e não tinha mudado nada.
O arquivo de limiares mentia sobre si mesmo. Um comentário no topo do arquivo de limiares de qualidade afirmava que o gate lia dali os comandos de lint, teste e cobertura. Isso era falso, porque o runner trazia hardcoded os equivalentes do ecossistema npm. A consequência medida foi um repositório de produção 100% Bun (765 testes passando, 49 pulados, nenhum falhando, 95,51% de cobertura de linha) que ficava permanentemente vermelho e não conseguia fechar sessão. O lint falhava por falta de um arquivo de configuração do ESLint, e a análise de dependências falhava por falta de um package-lock.json. Nenhuma das duas falhas diz algo sobre a qualidade do código; o gate estava medindo o ecossistema no lugar do repositório.
Hoje os dois arquivos são lidos por um leitor único, uma função consultada por todo hook e runner, para que a postura seja resolvida num só lugar. O leitor carrega uma allowlist e ignora qualquer chave que não esteja nela, o que impede a configuração local de um cliente de afrouxar um gate inviolável inventando uma entrada. Um mecanismo relacionado trata o problema de legado que Böckeler aponta. O repositório registra em que versão da plataforma passou pelo onboarding, e qualquer gate introduzido depois dessa versão reporta dívida datada em amarelo em vez de bloquear. Assim, um repositório já entregue não fica vermelho só porque a plataforma continuou evoluindo.
A maior parte do que medi, eu matei
Eu não esperava por isso quando comecei, e considero este o argumento mais forte a favor da disciplina: a maioria dos candidatos a regra que passaram por medição neste período não sobreviveu a ela.
| Candidato | Corpus | Veredito |
|---|---|---|
| Dependência fora do lockfile | 2 acusações, ambas verdadeiras | reprovado: as acusações são verdadeiras, mas apontam drift de lock, que o npm ci já faz falhar. Pegar pacote de fato alucinado exigiria uma chamada de rede por gate. |
| Guarda de repetição de ferramenta | 1.985 transcripts · 49.712 chamadas | reprovado: a repetição exata dentro da mesma sessão é 1,31% das chamadas, e assinaturas que se repetem ≥3× aparecem em 3,5% das sessões. A triagem manual de 15 casos matou a regra. |
| Gate anti-adulteração do harness | 123 commits × 6 superfícies | reprovado: um afrouxamento real em todo o histórico, zero acompanhados de código de produto e zero sem justificativa escrita. |
| Décimo primeiro operador de mutação | 51 sobreviventes no 1º run | reprovado: 35 deles vinham desse único operador. |
| Plano de poda de carga de contexto | 53 skills · 6.047 chars | reprovado: sub-skills aninhadas custam zero token, e as candidatas à poda eram justamente as mais referenciadas. |
| Gate de evidência visual | 1.683 commits · 7 repos · 180 dias | aprovado: 405 commits (24%) tocam UI, e 5 de 7 repos têm frente de UI, de 22% a 67%. A mesma tabela levou à revisão do desenho. |
O caso do anti-adulteração merece atenção à parte, porque é a refutação mais limpa que tenho. A hipótese era que um agente, ou um humano com prazo apertado, afrouxaria o harness em silêncio, baixando um limiar, apagando um caso ou enfraquecendo uma asserção. A varredura de seis superfícies em 123 commits achou exatamente um afrouxamento. Ele vinha com uma decisão humana escrita, com razão, custo aceito e data de revisão, e o mesmo commit plantou um caso de harness feito para ficar vermelho quando a regra endurecer de novo. O sinal foi zero: o único evento ligado à hipótese é exatamente o tipo de afrouxamento auditável e declarado que um gate bem desenhado teria de deixar passar.
O gate aprovado ensina na direção oposta. A mesma tabela que justificou construí-lo também destruiu o desenho original: só um dos sete repositórios declara como dirigir um navegador, então o gate, do jeito que estava especificado, responderia "não pude conferir" em toda sessão de quatro repositórios. Um aviso que se repete sem saída vira ruído que as pessoas aprendem a pular, e isso equivale a desligar o gate pela porta dos fundos. Foi essa medição que produziu a válvula de escape: um arquivo de isenção declarada, com causa, dono e prazo, que falha fechado (fail-closed) se a declaração vier pela metade.
Três vezes em que a medição errou
Num único dia de agosto, três medições produziram números confiantes e errados.
- Um laço de shell que iterava sobre uma lista concatenada quebrou num nome de diretório com espaço e produziu 594 hits, com a conclusão "esta regra é inviável, vai afogar o time em ruído". A contagem verdadeira era zero, e a regra estava certa.
- Contei
exit != 0como "bloqueou", sem considerar que um script invocado fora do manifesto sai com 1 diante de um argumento inesperado. A conclusão foi "as três formas de curinga estão bloqueadas", quando na verdade as três passavam direto. - Um caminho de raiz que não foi resolvido para absoluto fez um gate de portabilidade acusar o próprio nome da plataforma, como se ele não se reconhecesse.
As três sobreviveram porque o resultado era plausível. Os 594 hits confirmavam que "essa regra vai dar ruído", e o "BLOQUEADO" confirmava que "os hooks funcionam". A medição que concorda com o que você já acreditava é a que você menos confere, e foi exatamente ali que as três estavam.
Ficou uma regra prática: antes de concluir algo a partir de um número, rode a contraprova, um caso que tem de dar o resultado oposto. E um gate que bloqueia precisa imprimir o motivo, porque um código de saída diferente de zero, sozinho, pode significar apenas que o script quebrou.
O que isto não demonstra
Pela regra da própria plataforma, dívida é permitida desde que listada, e só a dívida silenciosa é proibida. Aplicando essa regra a este relato:
Não faço afirmação de eficácia aqui, e não teria base para fazer. Não medi se times que usam esta plataforma entregam mais rápido, entregam melhor ou sequer entregam, e não há grupo de controle. Como a METR mostrou que desenvolvedores experientes erraram a própria vazão por 39 pontos, minha impressão subjetiva sobre o valor da plataforma não vale nada, e não vou oferecê-la.
O harness declara os próprios buracos, e eles existem. A cada execução, a suíte reporta três gates sem nenhum caso (sem prova de que mordem) e outros cinco com cobertura parcial, com ramo declarado e não provado. Isso sai impresso no placar em vez de ficar guardado, e a ideia é essa, mas o leitor não deve tomar 237 verdes como 33 gates plenamente provados, ainda mais com dois sobreviventes não declarados na rodada de 24 de setembro.
Todo n deste relato é pequeno: dezessete acusações triadas à mão, dois repositórios com módulos ES e cinco amostras no baseline de skill. Doze projetos já seriam um estudo pequeno, e este é menor. Os números sustentam a afirmação "este mecanismo produziu este sinal neste contexto", o que basta para justificar uma decisão de projeto e não basta para generalizar.
A taxa de morte de mutantes não mede cobertura. Dez operadores formam um conjunto pequeno de propósito, escolhido pela relação sinal-ruído. Mesmo um placar com zero sobrevivente não declarado não diria nada sobre modos de falha que esses dez operadores não conseguem expressar. O verde falso do §04 foi achado pelo fail=1 → fail=0, ou seja, só apareceu porque alguém por acaso escreveu o operador que o expressa.
Tudo aqui é a plataforma de uma pessoa, em dogfood: onze semanas, 140 commits e dois repositórios de cliente nas bordas. Os gates que mais importam, os que estão na fila para sair de warn e virar block, ainda não rodaram em postura de bloqueio para valer. Esse é o maior buraco isolado, e o verde falso do §04 mostra como ele aparece quando você o encontra antes que custe caro.
Parte disto não é inédito, e prefiro dizer isso. Ablação de skill para conferir se ela muda comportamento já é prática padrão, com ferramental público; política fora do modelo é literatura estabelecida; teste de mutação tem cinquenta anos. O que não encontrei publicado foi apontar o motor de mutação para o harness e amarrar cada controle aos casos que provam que ele dispara. Não achar nada em algumas dezenas de buscas não prova que não exista, e se alguém já fez isso, eu gostaria de ter a referência.
O que vale roubar
- Escreva quais dos seus controles não estão ligados a nada. Faça isso num campo do próprio arquivo que afirma que seus controles existem, e não numa lista de bugs. Três dos meus 33 estão dormentes, e saber disso vale mais que os três controles.
- Afira o stderr além do código de saída. Script quebrado e gate bloqueando produzem o mesmo código diferente de zero, e se você só testa o código, nem a sua suíte nem você conseguem distinguir os dois.
- Quebre um controle de propósito esta semana. Troque um
fail=1porfail=0, ou apague uma chamada deblock, e veja se alguma coisa fica vermelha. Se nada ficar, você aprendeu a coisa mais útil deste artigo pelo preço de dez minutos. - Declare o limiar antes de olhar o resultado. O meu foi precisão ≥ 70%, escrito antes de rodar a sonda. Sem isso, todo número é post hoc e toda regra sobrevive.
- Deixe a medição matar coisas. Cinco dos meus seis candidatos morreram. Se suas medições só aprovam o que você já planejava construir, elas viraram cerimônia, e as três medições erradas do §06 mostram como isso fica visto por dentro.
- Gate novo entra em postura de aviso. Um gate que bloqueia desde o primeiro dia fica vermelho o tempo todo, e até sexta alguém negocia a retirada dele.
No fim, nada disto trata de IA. É a lição mais velha da disciplina chegando a um lugar novo: uma asserção que ninguém viu falhar não merece esse nome. Passamos trinta anos aprendendo a desconfiar de código não testado e depois construímos um plano de controle para agentes autônomos com scripts de shell e JSON que ninguém testa. Eu mesmo fiz isso nas primeiras seis semanas.
O harness é código de produção: roda a cada commit, barra cada entrega e, quando falha, falha em silêncio e em verde. Trate-o como tal.
A plataforma, as medições, a triagem manual e todos os achados acima são meus. Redigi o texto com apoio de IA e depois o editei. Um artigo que defende declarar o que você fez de fato, e não o que gostaria de ter feito, não pode abrir exceção para si mesmo.
- Birgitta Böckeler, Harness engineering for coding agent users, martinfowler.com, 2 de abril de 2026. A taxonomia guias/sensores × computacional/inferencial e as perguntas em aberto a que este relato responde.
- Thoughtworks, Technology Radar vol. 34, 15 de abril de 2026: spec-driven development, harness engineering, cognitive debt.
- METR, Measuring the Impact of Early-2025 AI on Experienced Open-Source Developer Productivity: os 19% de lentidão contra 20% de aceleração percebida.
- GitClear, The Maintainability Gap: 2026 AI Code Quality Research: duplicação, churn e reúso em mais de 600 milhões de commits.
- DORA, State of AI-assisted Software Development (2025) e o relatório de ROI de 2026: IA como amplificador; o retorno vem da plataforma.
- Ben Sghaier, Li, Adams, Hassan, Don't Blame the Large Language Model: How Agent Harness Evolution Shapes Coding Agent Quality, julho de 2026: modelo fixo, harness variado ao longo de 35 releases.
- Lin et al., Agentic Harness Engineering: Observability-Driven Automatic Evolution of Coding-Agent Harnesses, abril de 2026: previsão falsificável por edição de harness.
- Moreira, IACDM: Interactive Adversarial Convergence Development Methodology, 2026: avanço condicionado a máquina de estados externa ao modelo; 19 lentes de crítica.
- Farrag, The Productivity-Reliability Paradox: Specification-Driven Governance for AI-Augmented Software Development, maio de 2026.
- Sadowski et al., Lessons from Building Static Analysis Tools at Google, CACM 2018: o teto de ~10% de falso positivo e a taxa abaixo de 5% do Tricorder.
- karanb192/claude-code-hooks: vizinho mais próximo na prática, com testes por plugin,
config-guard,protect-tests,dead-rules-audit. - Test Double, Keep your coding agent on task with mutation testing: mutação aplicada aos testes de produto, e não ao harness.
- Levantamento de repositórios feito em 19 de agosto de 2026 pela API REST do GitHub (árvores de arquivo daquela data); contagens de estrela refeitas em 24 de setembro de 2026.