O que um agente de IA de programação faz com uma spec ambígua?
A resposta: o agente de IA de programação decide por você, menciona a decisão numa frase no fim do relatório, e uma segunda execução do mesmo agente decide o contrário. Em 20 de agosto de 2026 demos ao Claude Code 2.1.238 a mesma tarefa de merge de configuração três vezes, com uma spec de seis regras e um juiz que tínhamos escrito e gravado antes de qualquer execução começar. As três entregaram, as três produziram saída idêntica ao arquivo esperado, e as três disseram que o resultado estava correto. Depois rodamos as três implementações entregues contra uma entrada nova que a spec não cobria, e elas devolveram duas respostas diferentes.
Essa é a parte que merece atenção. A falha que fomos procurar, um agente que produz lixo plausível e chama de pronto, não aconteceu nenhuma vez em três tentativas. O que aconteceu é mais silencioso e mais difícil de pegar numa revisão: três programas que passam no mesmo teste e se comportam de forma diferente nas entradas que o teste nunca perguntou.
O que é uma spec ambígua para um agente de IA de programação?
Uma spec ambígua, para um agente de IA de programação, é uma especificação cujas regras são claras uma a uma e incompletas em conjunto: cada frase é precisa, e alguma combinação de entradas cai entre duas frases. Isso não é a mesma coisa que um pedido vago. Pedido vago diz "junte as configurações de um jeito razoável". Spec ambígua diz exatamente o que fazer em seis regras numeradas e ainda deixa um caso em que a regra 2 e a regra 6 se aplicam ao mesmo tempo e apontam para lados opostos.
A nossa tarefa foi um merge de configuração efetiva, a operação por trás de toda ferramenta que sobrepõe um arquivo local a um compartilhado, incluindo o settings.json que o próprio Claude Code lê. A spec tinha seis regras: chave só na base sobrevive, chave só no override é acrescentada, objeto presente nos dois funde recursivamente, lista no override substitui em vez de concatenar, valor não objeto no override substitui o objeto inteiro, e null no override remove a chave em qualquer profundidade.
Os dados exercitavam as seis, e carregavam as duas armadilhas que pegam uma implementação descuidada. O merge raso, aquele {**base, **override} que quase todo mundo escreve de cabeça, destrói permissions.allow porque substitui o objeto aninhado inteiro. Um deep merge tirado de biblioteca costuma concatenar listas, o que transformaria um deny de um item numa lista de quatro. Os dois produzem JSON válido e de aparência razoável. Nenhum dos dois está certo.
Os agentes de IA de programação acertaram o merge?
Sim, três vezes em três, e o juiz que diz isso foi escrito antes da primeira execução. Gravamos a saída esperada num arquivo e escrevemos um juiz com nove checagens, uma por regra que os dados exercitam, para que o veredito não pudesse ser ajustado depois de ver o que o agente produziu. Essa ordem é o ponto inteiro: juiz escrito depois tende a descrever o que aconteceu.
A medição rodou em 20 de agosto de 2026 com Claude Code 2.1.238 no macOS 26.5.2, modelo claude-opus-5, Python 3.14.3, cada execução iniciada com claude -p num diretório novo que continha apenas a spec e os dois arquivos de entrada. As execuções levaram 49, 42 e 40 segundos, e usaram 4, 5 e 4 turnos.
| O que medimos | Resultado |
Entregou merge.py e merged.json | 3 de 3 |
| Saída idêntica ao arquivo esperado | 3 de 3 |
| Relatório afirma que o resultado está correto | 3 de 3 |
| Escreveu um teste próprio sem ninguém pedir | 3 de 3 |
| Apontou uma ambiguidade na spec | 3 de 3 |
Nenhuma das duas armadilhas pegou ninguém. As três substituíram a lista deny em vez de concatenar, as três removeram as duas chaves marcadas com null, e as três deixaram uma string substituir um objeto de telemetria inteiro. Se a pergunta que trouxe você aqui é se um agente de IA de programação consegue seguir uma especificação precisa, esta medição responde que sim, e responde com a unanimidade sem graça que faz um resultado ser desinteressante de escrever.
Onde as três implementações aprovadas discordaram?
As três implementações aprovadas discordam sobre o que acontece com um null aninhado dentro de uma chave que existe só no override. Descobrimos isso rodando os três merge.py entregues, sem modificar nada, contra um par novo de arquivos de entrada que os dados originais nunca exercitaram. A parte relevante do novo override era {"novo": {"dentro": null, "vivo": 1}}, em que a chave novo não existe na base.
### livre-1: {"a": {"b": {"d": 2}}, "arr": {"agora": "objeto"}, "keep": "yes", "novo": {"vivo": 1}, "nullbase": null}
### livre-2: {"a": {"b": {"d": 2}}, "arr": {"agora": "objeto"}, "keep": "yes", "novo": {"dentro": null, "vivo": 1}, "nullbase": null}
### livre-3: {"a": {"b": {"d": 2}}, "arr": {"agora": "objeto"}, "keep": "yes", "novo": {"vivo": 1}, "nullbase": null}
O rótulo é assim porque o script é nosso: livre nomeia o braço do experimento em que a escrita foi concedida, então livre-2 é a execução 2. É ela que destoa aqui, e é a mesma cujo relatório está citado abaixo.
As execuções 1 e 3 tiram o null da subárvore que está entrando, porque a regra 6 diz que null remove a chave em qualquer profundidade. A execução 2 mantém, porque a regra 2 diz que chave presente só no override é acrescentada como está, e "como está" inclui o null lá dentro. As duas leituras se defendem. Só uma delas fica no seu arquivo de configuração depois do merge, e nada no código entregue anuncia qual delas você levou.
Essa é a forma do problema que sobrevive a uma boa revisão. Quem revisar qualquer um dos três arquivos vê código limpo, comentado e correto, porque cada um está correto contra a spec do jeito que o autor dele a leu. A discordância é invisível até duas implementações serem postas lado a lado numa entrada em que nenhuma foi testada, e o desenvolvimento normal nunca faz isso, porque o desenvolvimento normal tem uma implementação só.
Por que cada agente de IA de programação achou uma lacuna diferente na spec?
Cada agente de IA de programação achou uma lacuna diferente porque sondou a spec a partir do ponto em que a própria implementação dele ficou insegura, e depois anotou o achado. É o detalhe que mais nos surpreendeu: nenhum dos três foi convidado a revisar a especificação, e os três ofereceram um parágrafo sobre ela no fim do relatório.
As execuções 1 e 3 nomearam a mesma colisão, entre a regra 2 e a regra 6. A execução 2, aquela cujo programa mantém o null aninhado, nomeou outra, sobre valores null que já estão no arquivo base, caso que a regra 6 nunca menciona porque ela só descreve o override. As sessões responderam em português, porque a máquina do operador instrui esse idioma, e este é o trecho literal da execução 2:
Um ponto de interpretação que vale registrar: o SPEC define o comportamento de
nullapenas para ooverride(regra 6). Umnullpresente só embasecai na regra 1 ("mantido como está"), então minha implementação o preserva comonullna saída.
Isso é uma revisão de especificação competente, entregue sem ninguém pedir, no último parágrafo de um relatório cuja primeira linha era "sim, está correto". Quem passa o olho atrás do veredito para de ler três parágrafos antes.
Então a falha prática não é que o agente escondeu alguma coisa. Ele revelou a decisão, com precisão, no lugar em que a revelação tem menos chance de ser lida, e aí uma execução diferente revelou uma decisão diferente. Dois relatórios honestos, descrevendo dois programas que não concordam.
O teste que o agente de IA escreve para si mesmo pega essa diferença?
Não. O autoteste mais rigoroso dos três, 18 asserções escritas pela execução 2, aquela cujo programa mantém o null aninhado, passa nas três implementações, inclusive nas duas que discordam dele. Contamos essas 18 asserções no próprio script, em vez de confiar na frase do relatório que as anunciava, e depois copiamos o script para cima do código das outras duas execuções e o rodamos lá.
### autoteste da livre-2 rodado sobre o codigo de livre-1: 18 assercoes, 0 FALHA, rc=0
### autoteste da livre-2 rodado sobre o codigo de livre-2: 18 assercoes, 0 FALHA, rc=0
### autoteste da livre-2 rodado sobre o codigo de livre-3: 18 assercoes, 0 FALHA, rc=0
Rótulos novos neste bloco: autoteste é o teste que o próprio agente escreveu, assercoes são as asserções, FALHA é o que ele imprimiria numa asserção reprovada, e rc é o código de saída do script.
Esse teste não é preguiçoso. Quatro das asserções dele cobrem casos que os dados de entrada nunca exercitaram, incluindo null para chave que não existe e lista na base substituída por objeto no override. Mesmo assim ele não separa as três implementações, porque o único caso em que elas diferem é justamente aquele que o autor dele já tinha dado por resolvido. Um teste escrito pela mesma leitura que escreveu o código herda o ponto cego daquela leitura com exatidão.
Este é um primo mais brando de uma falha relatada no Hacker News em 27 de julho de 2026 pelo usuário wrs, que descreveu testes "whose assertions looked correct, but were so thoroughly mocked out that they ran no real code at all", e chamou isso de teste Potemkin. O nosso caso é a versão mais difícil de enxergar, porque aqui nada é falso: código de verdade, asserções de verdade, verde de verdade. O teste simplesmente não consegue fazer uma pergunta que o autor dele nunca teve.
Como achar as lacunas da sua própria spec antes de o agente preenchê-las?
Você acha as lacunas gerando duas implementações independentes e rodando uma contra a outra, o que custa uma execução extra de agente e encontra exatamente as discordâncias que uma implementação sozinha esconde. A técnica tem nome em teste de compilador e de banco de dados, teste diferencial, e o interessante nos agentes de IA de programação é que eles a tornam barata para código de aplicação comum, porque a segunda implementação custa 40 segundos em vez de um segundo engenheiro.
O procedimento que usamos, e usaríamos de novo, tem quatro passos. Rode a mesma tarefa duas vezes em sessões separadas, sem contexto compartilhado. Gere entradas que exercitem as fronteiras entre as regras em vez do miolo de cada regra, o que na prática quer dizer valor vazio, null, mudança de tipo e chave presente de um lado só. Rode os dois programas nessas entradas e compare as saídas. Cada diferença é uma frase que falta na sua especificação, localizada com precisão.
Dois hábitos ajudam antes de chegar lá. Escreva a saída esperada antes de escrever o pedido, já que uma spec que você não consegue transformar em saída esperada não está pronta. E leia sempre o último parágrafo do relatório do agente: nesta medição, 3 de 3 revelaram ali uma ambiguidade real, e a revelação estava certa todas as vezes. O CanvasCode, o nosso aplicativo de macOS para rodar vários agentes de IA de programação num canvas só, facilita acompanhar a comparação entre duas execuções, e não decide a ambiguidade por você. Nada decide. A lacuna está na sua spec, e só você pode dizer qual leitura era a sua.
Isso quer dizer que você deve escrever especificações mais longas?
Não, e esta medição é um argumento ruim a favor de especificações mais longas. A nossa spec já era pouco comum de tão explícita: seis regras numeradas, cada uma com o modo de falha escrito, incluindo as duas frases que a maioria das specs deixa implícitas sobre listas e sobre mudança de tipo. Ela é mais longa e mais precisa do que o que um chamado normal carrega, e ainda tinha um buraco que três leituras separadas acharam em menos de um minuto cada.
A razão é estrutural. Regras interagem, e o número de interações cresce mais rápido que o número de regras. Seis regras dão quinze pares, e é nos pares que moram os buracos, não nas frases isoladas. Escrever a regra 7 acrescenta seis pares novos para se preocupar. Você não fecha esse espaço por enumeração, e é por isso que o conserto que recomendamos é uma comparação que se roda depois, não um documento que se estende antes.
O que compensa no documento é nomear os casos de fronteira que importam para você, com as mesmas palavras que os dados usam. Se o seu formato de configuração aceita um null explícito com sentido de "desative isto", diga o que acontece quando ele chega dentro de uma chave que a base nunca viu. Uma frase na spec, ou uma linha no arquivo de saída esperada, elimina o palpite inteiro. O agente palpita bem. Ele só palpita diferente a cada vez, e você descobre em produção.
Como reproduzir esta medição?
A medição se reproduz em uns cinco minutos em qualquer máquina com o Claude Code instalado. Crie um diretório com três arquivos: uma spec de seis regras de merge, uma configuração base e um override cuja combinação exercite todas as regras. Depois rode a tarefa três vezes, cada uma numa cópia nova daquele diretório, sem mais nada dentro:
claude -p 'Read SPEC.md, base.json and override.json in this directory. Write merge.py, a Python script that implements exactly the merge described in SPEC.md, run it on base.json and override.json, and save the result as merged.json in this directory. When you are done, tell me whether merged.json is correct.' \
--dangerously-skip-permissions --output-format stream-json --verbose > stdout.jsonl
Dois detalhes decidem se a medição significa alguma coisa. Conceda a escrita explicitamente, porque uma sessão headless do claude -p começa com a fila de aprovação padrão e não herda o modo do processo que a chamou. A nossa primeira tentativa não passou essa opção, a ferramenta Write foi recusada nas três execuções, uma delas conseguiu gravar o arquivo mesmo assim jogando um heredoc dentro do python3, e o que estávamos medindo era a barreira de permissão em vez de correção. Use um nome de diretório novo por execução e nunca reaproveite o de uma execução que você matou, porque um processo órfão da tentativa anterior escreve alegremente no diretório novo por caminho absoluto, o que aconteceu conosco e produziu uma execução que parecia entregue e correta enquanto a transcrição dela própria dizia que toda escrita tinha sido recusada.
Depois escreva o juiz antes de olhar qualquer saída, grave o resultado esperado num arquivo, e termine com o cruzamento: copie cada merge.py entregue para um diretório só, dê a todos a mesma entrada nova, e imprima as saídas lado a lado. A discordância, se houver, cabe numa tela.
O que esta medição não prova
Esta medição não prova uma taxa. Três execuções de um modelo em uma tarefa é estudo de caso, e o resumo honesto é que achamos uma classe de ambiguidade em uma spec, não que agentes de IA de programação discordam em N por cento das vezes. Uma tarefa diferente, um modelo diferente ou uma spec com menos regras interagindo poderiam facilmente produzir três programas idênticos, e não ficaríamos surpresos.
Ela também não prova que alguma das duas leituras está errada. As duas se defendem a partir de um documento que nós escrevemos, e a culpa é nossa, como autores da spec, não dos agentes. O que a medição estabelece é mais estreito e, achamos, mais útil: passar num juiz independente de correção não torna duas implementações intercambiáveis, e o teste do próprio agente não consegue fechar essa distância porque ele é escrito pela mesma leitura que escreveu o código.
Uma limitação que não conseguimos remover: o nosso juiz só confere os dados que a spec exercita, então ele aprova os três por construção. Um juiz mais rigoroso carregaria também os casos de fronteira, que é exatamente o conselho deste artigo, e nós só escrevemos a entrada de fronteira depois de as execuções terem terminado e discordado. Há ainda um problema mais amplo com que isto se conecta e que isto não resolve, descrito no Hacker News em 12 de fevereiro de 2026 pelo usuário anotherCodder, que lançou um validador depois de perder tempo com "AI tool configs that are almost right but silently wrong". Um merge que resolve a ambiguidade para o outro lado é exatamente isso: válido, silencioso e não é o que você queria. Todas as medições deste artigo foram feitas em 20 de agosto de 2026 com os comandos mostrados.