Pular para o conteúdo principal

A validação de que os três achados morreram

A colheita do primeiro dogfooding deixou três achados P1 de operabilidade. A Fase 12 os endereçou um a um; este documento é a prova de que os três morreram na mesma execução, e não só em testes unitários que cada fatia escreveu para si.

A validação é um script: pnpm --filter api validacao:fase-12. Ele sai com código diferente de zero quando o critério não fecha — é critério de aceite, não relatório — e ao final imprime a tabela de evidência lida do banco. Os ids abaixo não são transcritos à mão.

O que ela NÃO prova​

Isto vem primeiro, e não em rodapé, porque uma validação que esconde os próprios limites vale menos que nenhuma.

Não prova GitHub remoto. A adoção roda contra o LocalGitProvider. O motivo não é conveniência: o fork usado na Fase 10 nunca foi nomeado — a linha 135 da missão continua sendo um TODO(humano): qual owner/repo do fork? —, então não existe alvo para readotar. O caminho exercitado é o mesmo nos dois providers (getRepo → plano → origin: 'adopted'); o que muda é a rede, e essa diferença está coberta pelo smoke adopt-repository.smoke.spec.ts, que só roda com ADOPT_TEST_REPO e GITHUB_TEST_TOKEN reais.

Não prova o julgamento dos gates. QA e SecOps são agentes de LLM. Aqui o veredito entra pelo RecordGateVerdictUseCase — que é o funil real por onde o parecer deles passa, e onde nasce a linha de outbox task.gate_resolved. O que a Fase 12b precisa provar é a cadeia veredito → outbox → wake → claim, não se o modelo sabe ler uma suite. O julgamento continua coberto pelos aceites da Fase 4a, que o ADR 0020 declara explicitamente não determinísticos com modelo local.

Não faz merge. Merge em branch protegida é decisão do usuário, por desenho (RN-014). O passo 6 do roteiro mostra a trava recusando exatamente isso, com autonomia auto_approve e permissions.json liberando.

Não usa LLM em lugar nenhum. O dev é o NoopDevAgentServer. Ele não escreve código de verdade — mas desde a Fase 12d ele exercita a mesma máquina de estados do agente real (Engine.Dev.AgentIo), e é isso que está sob teste aqui. Antes disso o Noop processava uma task e parava: o achado #10 sobrevivia dentro do próprio instrumento de medida, o que teria reprovado o critério "zero restarts" por defeito da ferramenta, não do produto.

O roteiro​

#passoo que é afirmado
0criar projetonasce com story_promotion = manual sem ninguém configurar nada
1adotar um bare repo pré-existente, com main e develop, sem qa nem rcorigin = 'adopted'; o plano diagnostica branch faltante e branch fora do template; plan_decision fica nula; nenhuma linha inserida à mão
1bdecidir "adotar como está"o template não é forçado sobre o repositório do usuário (RN-045)
2o PO cria uma história completa, com 3 tarefasa história fica draft + proposed_ready; claimNext devolve null
3o usuário promovea história vira ready; a proposta sai da fila; o evento registra user, não agent/po
4ativar a execução e resolver 3 gates em sequência3 tarefas, 1 agente, 0 restarts do engine
5fila vaziadev.idle explícito, processo vivo
6propor merge em branch protegida com tudo liberadopending — continua sendo sua decisão

O passo 2 é o que mata o achado #13 de forma verificável: não basta a história ficar draft, é preciso que nada seja pegável. Por isso o script chama claimNext diretamente e exige null. O passo 4 é o que mata o #10: da segunda volta em diante ninguém dispara :work — o agente reivindica sozinho, acordado pelo task.gate_resolved da volta anterior.

A evidência​

Cole aqui a tabela que o script imprime. Cada linha é um session_events.id (ULID) que existe no banco e pode ser consultado depois.

Executada em 2026-08-07, saída 0, com a stack de dev de pé e o script rodando de dentro do container da api. Os ids abaixo saíram do banco na própria corrida — projeto f84f7226, sessão de backlog 680ab9e9, sessão de execução 91f384fa.

etapaeventoidseq
1. adoçãobootstrap.repository_adopted01KZCW6SBGZZ5J2DTNKR35EQPC1
1. adoção (como está)bootstrap.adopted_as_is01KZCW6SETRNVJEX5V018GY5Q02
2. o PO propõebacklog.story_promotion_proposed01KZCW6SGJJQX623FW4TQ28ZV33
3. você promovebacklog.story_transitioned01KZCW6SH7F14Y1QCJQHNJNF504
4. dev reivindicadev.working01KZCW6SWFY33TF8DDX9MYHG405
4. PR aberta, esperando o gatedev.awaiting_gate01KZCW6TB712EMZCC29WDK7YJC14
4. dev reivindicadev.working01KZCW6VAYQD1R0XH6J3VKKKJ918
4. PR aberta, esperando o gatedev.awaiting_gate01KZCW6VQ3TZSH72CHAECBHZSX27
4. dev reivindicadev.working01KZCW6XA40AVK9N71PPRWKG4N31
4. PR aberta, esperando o gatedev.awaiting_gate01KZCW6XKXYTNW6ARDWF4VRW4240
5. fila vazia, agente ociosodev.idle01KZCW6Y812HAPVG3ZYS2CM6WA43

Os três pares dev.working → dev.awaiting_gate são as três tasks, com um agente só e sem restart do engine entre elas — o achado #10, provado por execução em vez de por teste unitário. O dev.idle final é o passo 5: fila vazia com o processo vivo, não morto.

O que a primeira execução custou​

A tabela acima levou quatro correções para existir, e vale registrar quais, porque três delas eram do INSTRUMENTO e uma era do PRODUTO — a distinção é o ponto.

No instrumento (o script e o NoopDevAgentServer):

  1. A cobaia nascia em os.tmpdir(), local ao container. Quem adota é a api; quem clona para montar o worktree é o dev agent, que roda no engine. O bare ficava em /tmp da api e o engine não o enxergava. Passou a nascer em GIT_LOCAL_REPOS_ROOT, que é o volume compartilhado — e que o cabeçalho do próprio script já declarava como pré-requisito.
  2. O Noop marcava a task in_review e parava aí, sem chamar open_gate: tasks.gate_status ficava NULL. awaiting_gate sem gate aberto, e nada para julgar.
  3. {:pr_settled, %{opened: true}} chegando com o agente já em :awaiting_gate não tinha cláusula no Noop. FunctionClauseError, e como o server é restart: :temporary, o processo morria de vez.

As três são a mesma história: o Noop foi alinhado ao agente real na Fase 12d, mas não acompanhou o que veio depois. É exatamente o risco que este documento já nomeava — "o achado #10 sobrevivia dentro do próprio instrumento de medida" — agora em três instâncias novas.

No produto, e este é o achado que só execução real encontra:

  1. Com a fila vazia, POST /internal/sessions/:id/tasks/claim responde 201 com content-length: 0. O caso de uso devolve null, mas o NestJS serializa isso como corpo VAZIO; o Req entrega "", que não é nil, e o AgentIo.try_claim/2 casava com a cláusula de task encontrada — chamando run_task("") e estourando BadMapError.

    try_claim/2 mora no AgentIo, compartilhado com o DevAgentServer real: todo dev agent morria no momento em que a fila do módulo esvaziava, que é o desfecho mais comum que existe. E morria de vez — server restart: :temporary, com o Monitor apagando a linha de estado atrás. O oposto exato do que a Fase 12b entregou: em vez de dev.idle supervisionado e acordável por evento, processo morto.

    A suite nunca pegou porque o fake devolve nil corretamente. Está corrigido na fronteira (claim_task/4) e guardado no contrato (try_claim/2), sem mexer no status HTTP da rota.

O achado 4 é a resposta empírica à pergunta que esta fase existe para fazer: o que a execução real prova que o teste não prova.

O script se recusa a terminar com sucesso se alguma etapa que ele afirmou ter exercitado não deixar evidência no event log — sem essa checagem, uma consulta errada produziria uma tabela curta e a validação passaria mesmo assim, que é o modo de falha clássico de relatório gerado.

Antes × agora​

A coluna Fase 10 cita apenas o que é derivável do que ficou escrito. Tudo que dependeria de uma contagem ao vivo aparece como não medido, pelo motivo explicado na colheita.

Fase 10Agora
apontar o projeto para um repositório existenteseed manual em duas tabelas, antes da primeira sessão (dogfooding-mission.md:104-134)rota de adoção; origin = 'adopted'; zero escrita à mão
política divergente do repositórionão havia diagnóstico — o bootstrap era a única via, e ela impunha o templateplano em dry-run que descreve a divergência e não aplica nada sem aprovação
restarts do engine por task entregue1, por construção — propriedade do achado #10 (:666), não estimativa. Total real: não medido0
tandasa fase inteira rodou em tandas (:393-416)não existem: o agente atravessa a fila do módulo sozinho
agente sem taskprocesso morto (restart: :temporary)idle explícito, supervisionado, acordável por evento
sequência de falhasqueimava orçamento em sériecircuit breaker para em idle_tripped (RN-047)
história → readyautomática na criação, sem passo humano (achado #13, :669)decisão do usuário, com o ator gravado no event log (RN-048)
recusar uma histórianão existia estado, evento nem botão (achado #14)devolução ao PO com motivo fixado na sessão dele
intervenções manuais totaisnão medido — a tabela de observação ficou em branco (:488-490)as do pipeline de aprovação, que a fase não mudou
merge em branch protegidamanual, por desenhomanual, por desenho — inalterado, e o passo 6 prova

A última linha importa tanto quanto as outras. A Fase 12 é sobre o agente não morrer entre tarefas e sobre a decisão voltar para o usuário; ela não amplia autonomia nenhuma. O pipeline de aprovações está exatamente como estava.

Como rodar​

# a stack de dev de pé (api e engine compartilhando /data)
pnpm dev

# de dentro do container da api
docker compose -f docker/docker-compose.yml exec api \
pnpm --filter api validacao:fase-12

Se algum critério não fechar, o script diz qual — a mensagem começa com CRITÉRIO NÃO FECHOU: e nomeia a afirmação que falhou.