Engine
PR Gates
RN-016 — O veredito do gate prevalece sobre o enunciado da task
Se a descrição da task diz uma coisa e o veredito do gate diz outra, vale o veredito.
Desde a Fase 8b, "o veredito" pode vir de duas especializações — a regra
não muda: cada uma prevalece sobre a task DENTRO do que avalia
(Automação sobre a cobertura de testes; Performance/Segurança sobre os RNFs
de performance e os achados de código). QaLead.consolidar/1 não arbitra
entre elas e a task: qualquer changes_requested de qualquer uma já reprova
o todo (ver RN-036).
- Onde: prompt de cada especialização em
apps/engine/lib/engine/gates/qa_automacao_agent.exeqa_performance_seguranca_agent.ex - Origem: ADR 0020 §9
Psicólogo e Anamnese
RN-023 — A causa de término é classificação determinística
Vem do motivo registrado, nunca de julgamento do LLM e nunca por
eliminação. Toda falha registra a origem: infra | modelo | código | política.
- Onde:
apps/engine/lib/engine/psychologist/termination_classifier.ex - Origem: ADR 0022 §5 e ADR 0020 §6
PR Gates
RN-036 — QA vira área: o Lead consolida sem mudar o contrato do gate
A especialização de Automação (o QAAgent da Fase 4a) sempre é delegada;
Performance/Segurança só quando a história tem RNF de performance pertinente
(Engine.Gates.QaLead.rnf_de_performance?/1 — heurística de palavra-chave, não
NLP). A decisão de NÃO delegar é sempre registrada — uma delegação
dispensed com justificativa, nunca silêncio.
Consolidação: approved só se TODAS as delegações que rodaram aprovaram; qualquer
changes_requested reprova o todo, com os itens da(s)
especialização(ões) que pediram mudança, cada item prefixado com o
rótulo de quem o levantou ("[QA de Automação] ...") — é assim que a
origem é rastreada SEM mudar itens de string[] para outro
formato. Falha de delegação (qualquer origem — infra, modelo, codigo,
politica) NUNCA vira changes_requested: o Lead bloqueia a task
com a origem real (RN-015).
O qa_verdict que chega à api é o MESMO artefato e passa
pela MESMA rota de sempre (RecordGateVerdictUseCase,
nextGateStatus) — nenhum dos dois mudou. O que a api sabe da
área mora só nos eventos delegation.completed/delegation.failed/
delegation.dispensed, que a especialização e o Lead
registram à parte.
- Onde:
apps/engine/lib/engine/gates/qa_lead.ex(consolidar/1,rnf_de_performance?/1),qa_lead_server.ex(a fiação);apps/api/src/application/use-cases/execution/record-delegation.use-case.ts;apps/api/src/db/schema/agents.ts(tabeladelegations, enumfailure_origin) - Teste:
apps/engine/test/engine/gates/qa_lead_test.exs(a árvore de decisão pura),qa_lead_server_test.exs(a fiação: decisão → delegação → registro → consolidação → a MESMA chamada de sempre) e — a prova de que o contrato não mudou —record-gate-verdict.use-case.spec.ts,pr-gate-state-machine.spec.tserecord-infra-gate-verdict.use-case.spec.tspassam sem mudança nenhuma - Origem: ADR 0038
Do lado do apps/web (Fase 8d): o painel do time agrupa qa/
qa-automacao/qa-performance-seguranca como lead + especializações
recolhíveis, a linha do tempo da PR abre o veredito consolidado nas
suas partes (ProjectApprovalsTab.tsx, PrGateTimeline.tsx), e o
feed narra delegation.* — ver apps/web/src/lib/agents.ts
(AREAS/areaFor).
RN-047 — Circuit breaker do dev agent: N bloqueios consecutivos o param, sem queimar orçamento em laço
Cada dev agent mantém um contador (dev_agent_states.consecutive_blocked)
de quantas tarefas TERMINARAM blocked em sequência — localmente (no
ToolLoop) ou remotamente (o teto de correções do gate esgotado). Ao atingir o
teto por projeto (max_consecutive_blocked, default 3), o agente para em
idle_tripped sem tentar pegar a próxima tarefa. Um desfecho terminal
aprovado zera o contador; uma tarefa bloqueada isolada segue o fluxo normal
(devolvida com diagnóstico, disponível para um humano desbloquear) — o
breaker trata da SEQUÊNCIA, não da tarefa.
A única saída de idle_tripped é um rearme explícito
(POST .../agents/:agentId/rearm, papel developer): ele zera o contador e
o agente volta a tentar pegar tarefa. Não há destravamento automático — o
mesmo princípio do MarkTaskBlockedUseCase/unblock, aplicado à sequência
em vez da tarefa. Rearmar um agente que não está travado é 409, não
sucesso silencioso: o evento dev.rearmed é imutável, e registrá-lo para um
rearme que não aconteceu seria uma mentira no event log.
Um bloqueio que vem de FORA do agente (o QaLeadServer falhando
internamente, por exemplo) também precisa acordá-lo — é por isso que a
emissão de task.gate_resolved mora no MarkTaskBlockedUseCase, o funil por
onde todo bloqueio passa, e não no RecordGateVerdictUseCase, que só vê
parte deles. Sem isso o agente ficaria em awaiting_gate para sempre, com a
tarefa morta e o contador do breaker nunca incrementando.
Reiniciar o engine com um agente em working não conta para o contador:
a tarefa retida é bloqueada com um diagnóstico de reinício, mas esse
bloqueio não é o agente "queimando o teto" — é a infraestrutura caindo. O
contador só sobe quando o próprio ciclo dev↔gate produz um blocked real.
- Onde:
apps/engine/lib/engine/dev/dev_agent_server.ex(finish_task/2,resume_state/2),apps/api/src/application/use-cases/execution/rearm-dev-agent.use-case.ts,apps/api/src/db/schema/iam.ts(projects.max_consecutive_blocked) - Teste:
apps/engine/test/engine/dev/dev_agent_server_test.exs(describecircuit breaker),apps/engine/test/engine/dev/dev_rehydrator_test.exs(describethe four rehydrated states),test/application/use-cases/execution/rearm-dev-agent.use-case.spec.ts - Origem: ADR 0045
RN-050 — Sem PR aberta, nenhum gate abre
O dev agent propõe commit, push e PR e lê o desfecho de cada um. Ele só
abre o gate se os três executaram. Se um ficou pending — a autonomia do
agente em require_approval — ele entra em awaiting_approval, segurando o
worktree, e não abre gate nenhum.
Sem isso o gate abria de qualquer jeito, e o dano era silencioso: o QA varre
o worktree, não a PR; achou os arquivos, aprovou; o SecOps aprovou; a
tarefa fechou como concluída — sem uma linha commitada e sem PR nenhuma.
Só depois, ao aprovar o commit, o usuário veria a ação falhar (com
diagnóstico vazio, porque System.cmd num diretório apagado devolve
{"", 2}).
O que libera o agente é task.pr_settled, emitido pela api quando pr_open
chega a um desfecho terminal: opened: true abre o gate; opened: false
(negada ou falha) devolve a tarefa com diagnóstico, em vez de deixar o
agente esperando para sempre por um gate que ninguém vai abrir.
Uma PR negada não conta para o circuit breaker da RN-047: a decisão foi do usuário, não do agente queimando o teto — o mesmo princípio da recuperação de reinício.
Essa regra também elimina o D5 (worktree reciclado sob aprovação pendente)
como consequência: o worktree só é liberado em gate_resolved, o gate só
abre depois da PR, e a PR só abre depois do commit e do push.
- Onde:
apps/engine/lib/engine/dev/agent_io.ex(propose/3),apps/engine/lib/engine/dev/dev_agent_server.ex(abrir_gate/1,aguardar_aprovacao/2),apps/api/src/application/use-cases/actions/execute-git-action.use-case.ts(settlePrOpen) - Teste:
apps/engine/test/engine/dev/dev_agent_server_test.exs(describepending approval doesn't open a gate) - Origem: ADR 0048
RN-059 — Falha de turno é evento durável com origem, e o agente fala
Quando um turno de LLM falha, o agente grava agent.error no event log
com três campos: origem (vocabulário do ADR 0020), mensagem em português e
o reason bruto. E a mensagem aparece no fio da conversa, não só no log.
Era o contrário, e o desfecho era o pior possível: os quatro agentes
conversacionais gravavam agent.response com conteúdo vazio —
indistinguível de sucesso no log imutável — e mandavam o motivo por
broadcast, que é efêmero. Quem não estivesse com a aba aberta naquele
segundo nunca saberia que houve erro; quem estivesse, via um balão em branco.
Havia um segundo caminho, pior ainda: quando a api narrava a falha no PRÓPRIO frame final (budget, credencial ausente, binding faltando), o turno não caía no ramo de erro e não emitia evento nenhum — silêncio absoluto.
Esse mesmo ramo, uma vez corrigido, custou uma segunda rodada: no PO, no
Arquiteto e no Dev Lead ele devolvia {state, ""} — uma TUPLA onde todos os
outros ramos de run_turn devolvem o state (um mapa). O Map.put/3 de
TurnoAssincrono.tratar_resultado/2 levantava BadMapError dentro do
handle_info, e como os quatro conversacionais são restart: :temporary, o
agente MORRIA e não voltava. A falha deixava de ser silenciosa e virava uma
queda — com os gatilhos mais corriqueiros que existem. A regra vale inteira: o
agente narra a falha e continua de pé. Por isso tratar_resultado/2 tem
uma segunda barreira, no ponto compartilhado pelos quatro: resultado de turno
que não é mapa vira agent.error com origem codigo, nunca um processo morto.
A origem NUNCA é adivinhada: cada padrão em FalhaDeTurno.origem/1 tem um
motivo escrito, e o que não casa com nenhum sai como codigo — a lacuna é
do nosso classificador, e essa é a origem que aponta a ação certa (ADR 0020).
Os eventos já gravados não se apagam — a tela os NOMEIA como resposta vazia anterior a esta regra, em vez de mostrar branco.
- Onde:
apps/engine/lib/engine/agents/falha_de_turno.ex,criativo_server.ex,po_server.ex,arquiteto_server.ex,dev_lead_server.ex,infra_lead_server.ex(emit_falha/2),turno_assincrono.ex(tratar_resultado/2, a segunda barreira),apps/web/src/lib/session-falha.ts - Teste:
apps/engine/test/engine/agents/criativo_server_test.exs(evento durável com origem; nunca grava resposta vazia; erro narrado no frame final também vira evento);po_server_test.exs(o frame final com erro não derruba o agente — GenServer de VERDADE, comProcess.alive?/1);arquiteto_server_test.exsedev_lead_server_test.exs(mesmo caminho, ciclo completo);turno_assincrono_test.exs(a segunda barreira narra em vez de derrubar);apps/web/src/lib/session-falha.test.ts - Origem: execução real da FASE 13b
RN-061 — Uma falha de FERRAMENTA também é um evento, e volta para o modelo
O resultado de uma chamada de ferramenta nunca é descartado. Ele vira
tool.result no event log (ok e, quando falha, erro), o agente diz o
que aconteceu no fio, e o motivo volta para o modelo no papel tool —
para ele corrigir e reemitir no turno seguinte. Um erro de ferramenta é
entrada do laço, não fim de linha.
O agente Criativo era o único que o descartava
(_ = EmitArtifact.run(args, state)) — o PO e o Arquiteto já o realimentavam.
Numa execução real o modelo emitiu titulo/descricao contra um schema que
exige title/description/origin: as quatro regras de negócio da
conversa foram recusadas, nenhum evento foi gravado, e ele continuava
dizendo "registrei as regras" com o painel vazio.
A descrição da ferramenta agora NOMEIA os campos obrigatórios de cada tipo,
em inglês e com um exemplo preenchido — inclusive que business_rule.origin
é uma lista não vazia de seq das mensagens que originaram a regra, não
texto livre. Sem isso o modelo adivinha, e adivinha no idioma da conversa.
É a mesma regra da RN-059 aplicada ao outro caminho de falha: duas políticas para o mesmo problema seriam duas chances de engolir o erro.
- Onde:
apps/engine/lib/engine/agents/criativo_server.ex(dispatch_tool,realimentar),apps/engine/lib/engine/harness/tools/emit_artifact.ex(descricao/0),apps/engine/lib/engine/harness/artifact_schemas.ex(required/1) - Teste:
apps/engine/test/engine/agents/criativo_server_test.exs(a refused tool becomes a tool.result with an error, and the agent speaks) - Origem: execução real da FASE 13b
RN-062 — Mensagem a agente conversacional REIDRATA o processo
Uma mensagem endereçada a Criativo, PO ou Arquiteto sobe o processo se ele não
estiver de pé, antes de entregar. O init de cada servidor já reconstrói o
histórico do event log; faltava quem o chamasse.
Antes, um restart do engine matava a conversa em silêncio: a sessão sobrevivia
como active, o processo do agente não, e a próxima mensagem morria com
GenServer.call ... exited — sem evento, sem erro na tela, sem nada. O usuário
via a própria mensagem aparecer e nenhuma resposta chegar, para sempre.
O comentário de revise/2 dizia que agente morto nesta rota "é um bug". É — e
basta o engine reiniciar para acontecer. É a mesma garantia que a Fase 12b deu
aos dev agents, aplicada aos conversacionais.
- Onde:
apps/engine/lib/engine_web/controllers/agent_command_controller.ex(message/2) - Teste: coberto pela suite de agentes; a prova de execução está em docs/explanation/validacao-real.md
- Origem: execução real da FASE 13b
RN-063 — Encerrar sem produzir é desfecho, não falha
A Anamnese tem uma ferramenta para dizer "não há nada a emitir, e este é o
motivo" (skip_proficiency). A rodada encerra com anamnese.run_skipped e o
motivo no payload — nunca com anamnese.run_failed.
Antes ela não tinha esse verbo. A única ferramenta era emit_proficiency, que
recusa lista vazia (com razão: perfil vazio não é perfil). Numa janela sem
membro elegível a Anamnese descobria isso na PRIMEIRA iteração, escrevia em
prosa "não há membros elegíveis", chamava emit_proficiency com profiles: [],
era recusada — e repetia até o teto de iterações. Cada volta reenvia o
histórico, que cresce a cada volta.
Numa execução real isso custou 145 mil tokens de entrada e 4× o gasto do Criativo e do PO somados, sem produzir nada. E voltava a cada tick do agendador, a cada 15 minutos, para sempre.
O teto de iterações funcionava — não era laço infinito. O desperdício era por rodada, repetido indefinidamente, que é pior: um laço trava e alguém percebe; este sangrava devagar.
Narrar run_failed para uma rodada que fez a coisa certa também é defeito: quem
lê o log aprende a ignorar o evento de falha.
- Onde:
apps/engine/lib/engine/anamnese/tools/skip_proficiency.ex,apps/engine/lib/engine/anamnese/hooks/termination.ex,apps/engine/lib/engine/workers/anamnese_worker.ex(handle_outcome) - Teste:
apps/engine/test/engine/workers/anamnese_worker_test.exs(encerrar sem perfis é DESFECHO: narra run_skipped com o motivo, não falha) - Origem: execução real da FASE 13b
RN-069 — Repetir uma tarefa recria a branch, não falha
WorktreeManager.add_worktree/3 usa git worktree add -B (cria ou
reseta), não -b. Ele já removia o diretório do worktree anterior, mas
deixava a branch para trás — e como o nome dela vem do slug da tarefa, a
segunda tentativa na MESMA tarefa sempre caía em fatal: a branch named 'feature/<slug>' already exists.
O efeito era permanente: desbloquear a tarefa não ajudava, reativar a
execução não ajudava, e o circuit breaker desarmava sem saída. Numa execução
real a única saída era um git worktree prune manual no workspace do
projeto.
Resetar é a escolha certa: o worktree anterior já foi removido, o trabalho
daquela tentativa não vale nada (a tarefa voltou para a fila), e a branch
renasce do ponto atual de work_dir.
- Onde:
apps/engine/lib/engine/dev/worktree_manager.ex - Teste:
apps/engine/test/engine/dev/worktree_manager_test.exs(retrying the SAME task recreates the worktree instead of failing) - Origem: execução real da FASE 13b
RN-073 — Uma aprovação pendente SUSPENDE o laço, não o queima
Quando uma ferramenta de pipeline devolve pending, o ToolLoop para e o
dev agent entra em :awaiting_approval, segurando a tarefa, o worktree e o
histórico do laço. A decisão do usuário emite task.action_settled, que o
acorda: o resultado real ocupa o lugar onde a palavra "pending" estaria, e o
laço retoma de onde parou.
Duas propriedades que o teste trava:
- Nada é gravado durante a espera. O espaço da mensagem da ferramenta fica vazio. Gravar "pending" ali diria ao modelo que foi isso que o comando respondeu — que era exatamente o defeito.
- Uma recusa é uma resposta. O motivo ocupa o lugar do resultado e o
agente aprende que aquele caminho fechou, em vez de esperar para sempre por
algo que ninguém vai aprovar. É o mesmo princípio de
pr_settledcomopened: false(RN-047), um nível abaixo. - O wake tem de CHEGAR.
task.action_settlednasce no agregadotask, não naproposed_actionque o nome da tabela sugere: o drain do engine lê uma lista fechada de agregados (sessionetask). Emitido fora dessa lista, o evento é gravado com sucesso, fica comprocessed_atnulo e nem chega a ser lido — sem job, sem erro, sem log, e o agente espera para sempre. O contrato atravessa duas linguagens, e é por isso que está fixado nos dois lados.
Se o engine reiniciar durante a espera, a regra deixa de valer para essa
tarefa: o laço suspenso só existe em memória, então ela volta para a fila
bloqueada com origem infra, e a decisão tomada depois não tem onde ser
aplicada. Bloquear com diagnóstico é deliberado — a alternativa era a espera
eterna e silenciosa, que é exatamente o que esta regra existe para acabar.
O que isto conserta: pending costumava voltar como o RESULTADO da
ferramenta e o laço seguia em frente. O modelo lia aquilo como a resposta do
comando, não aprendia nada, tentava outra coisa — e cada tentativa queimava
uma iteração até toolloop.limit_reached {iteration: 8, max_iterations: 8},
com a tarefa bloqueada por "limite de iterações atingido" sem uma linha
escrita. As aprovações concedidas chegavam depois de o laço esgotado e não
serviam para nada.
A allowlist de terminal (RN-068) continua valendo, mas deixa de ser a única defesa: é uma lista de comandos previstos, e o modelo inventa comandos.
- Onde:
apps/engine/lib/engine/harness/hooks/action_pipeline.ex,harness/tool_loop.ex,dev/dev_agent_server.ex,workers/dev_agent_wake_worker.ex; emissão emapps/api/src/application/use-cases/actions/{approve,deny}-action.use-case.ts - Teste:
apps/engine/test/engine/dev/dev_agent_awaiting_approval_test.exs(pending action STOPS the agent;approved: resumes the loop with the REAL output;restart during the wait BLOCKS the task) eapps/engine/test/engine/dev/wake_do_outbox_ao_agente_test.exs, que percorre a cadeia inteira — outbox, drain, fila e processo — porque os testes por elo estavam todos verdes com a entrega quebrada; o agregado está fixado no lado da api emapps/api/test/application/use-cases/actions/approve-deny-action.use-case.spec.ts - Origem: ADR 0052, fase A da triagem
RN-074 — A saída de terminal tem teto de bytes
A saída de um comando é cortada em TERMINAL_OUTPUT_MAX_BYTES (default 32 KiB)
antes de virar resultado da ferramenta, e o corte deixa uma marca dizendo os
dois tamanhos e o que fazer:
[saída truncada: 32768 de 1048576 bytes. Refine o comando (head, grep,
-maxdepth) para ver o que falta.]
Três propriedades que o teste fixa:
- O teto é
>, não>=. Saída que cabe exatamente no limite passa intacta — marcá-la faria o modelo refinar um comando que já deu tudo. - O corte não parte caractere multibyte.
binary_part/3corta por byte; cair no meio de uméproduz binário inválido que quebra a serialização JSON antes de o resultado chegar ao modelo. raw_bytescontinua sendo o tamanho REAL produzido, não o truncado. É medição, e mentir nela esconderia justamente o comportamento que motivou o teto. Quem quiser detectar truncagem comparabyte_size(stdout)comraw_bytes.
O que isso conserta: a saída de cada comando fica no histórico do laço e viaja
em todo turno seguinte. Sem teto, um find numa árvore grande basta — a
execução do hello-limpo morreu com {413, "request entity too large"} no
turno 18, sem uma linha escrita. O estouro é de bytes da requisição, não de
janela de contexto: a maior chamada bem-sucedida tinha 28.993 tokens de entrada.
A marca é endereçada ao modelo, não ao humano — sem dizer o que fazer, ele tende a repetir o mesmo comando.
- Onde:
apps/engine/lib/engine/actions/terminal_executor.ex(truncate/2), teto emapps/engine/config/runtime.exs - Teste:
apps/engine/test/engine/actions/terminal_executor_test.exs(describeteto de bytes da saída) - Origem: achado S de achados-execucao-real.md, Fase F do backlog
RN-076 — A credencial de git nunca é escrita em arquivo
O engine trabalha em repositório remoto pedindo o remoto de trabalho à api
(GET /internal/projects/:projectId/git-remote), que devolve a origem limpa
e o token do owner à parte. O token entra na invocação do git pelo ambiente do
processo filho e em nenhum outro lugar:
- não no
origin— é a URL limpa que fica gravada no.git/config; - não em argv —
psmostra a linha de comando de qualquer processo; - não em arquivo — nem helper persistido, nem
~/.git-credentials.
O helper de credencial é passado por -c, vale só para aquele processo, e vem
depois de um credential.helper= vazio: helpers são acumulativos e o primeiro a
responder ganha, então sem zerar antes um helper do host responderia no lugar.
Por que isso é regra e não preferência. Escrever
https://x-access-token:TOKEN@github.com/… no origin — o que quase todo
tutorial ensina — grava a credencial em texto puro dentro da pasta do
projeto, exatamente onde a RN-075 dá ao dev agent leitura
auto-aprovada. Um cat .git/config devolveria o token sem passar por
aprovação nenhuma, e ele viajaria ao provider de LLM no histórico do laço. O
escopo de caminho protege contra o agente ler para FORA do projeto; não tem como
proteger contra um segredo que o próprio produto colocou DENTRO.
A credencial é a do owner do workspace, pelo mesmo resolvedor da
RN-058 — duas regras de "de quem é a credencial" divergiriam.
Provider local não tem token nem consulta a api: é resolvido direto do banco,
e é o caminho que o pnpm dev e a suite inteira exercitam.
- Onde:
apps/engine/lib/engine/actions/git_auth.ex,engine/projects/project_repository.ex(remoto_de_trabalho/1),apps/api/src/application/use-cases/git/get-project-git-remote.use-case.ts - Teste:
apps/engine/test/engine/actions/git_auth_test.exs(o token não aparece em argv nem no helper) eapps/api/test/application/use-cases/git/get-project-git-remote.use-case.spec.ts(a origem devolvida não contém o token nem@) - Origem: ADR 0056, achado N, Fase B do backlog
RN-077 — A origem da falha é sempre uma das quatro
Todo desfecho de falha nomeia a ORIGEM no vocabulário fechado do
ADR 0020 —
infra | modelo | codigo | politica. Não há quinto valor: null e
"indeterminada" deixaram de ser possíveis.
Duas garantias estruturais, e nenhuma depende de alguém lembrar:
AgentIo.block_task/4não tem default para a origem. Ela era "obrigatória em espírito", com"indeterminada"de default — e o desfecho mais caro da execução real saiu exatamente assim, porque o call site não passou nada. Sem default, esquecer vira erro de compilação.FalhaDeTurno.origem/1sempre devolve uma das quatro, e há teste de tabela que falha se alguma entrada — inclusive uma forma nunca vista — produzir outra coisa.
Por que indeterminada saiu. Ela existiu com um argumento razoável: não
chutar seria mais honesto que escolher no escuro. O efeito real foi o oposto —
indeterminada não aponta ação nenhuma, e quem triava a rodada seguinte
recomeçava a investigação do zero. O que ela significava de fato era o
classificador não reconheceu esta forma, que é lacuna do nosso código: codigo
é a origem que aponta a ação certa (acrescentar a cláusula que falta). O
diagnóstico continua indo verbatim, então nada se perde.
As origens não são chute. Cada desfecho do ToolLoop diz quem decidiu parar:
report_blocked e teto de iterações são do modelo (ele decidiu, ou gastou o
que tinha); orçamento e PR não aprovada são política (foi uma decisão, nada
quebrou); restart e falha ao montar contexto são infra; e quando há
last_error, a origem sai do MESMO erro que o diagnóstico narra — era esse par
que se contradizia, com diagnosis dizendo {413, …} e origem dizendo
"indeterminada" na mesma linha.
- Onde:
apps/engine/lib/engine/agents/falha_de_turno.ex,engine/dev/agent_io.ex(block_task/4, sem default),engine/dev/dev_agent_server.ex(origem_da_parada/1) - Teste:
apps/engine/test/engine/agents/falha_de_turno_test.exs(o vocabulário é fechado) eapps/engine/test/engine/dev/dev_agent_server_test.exs, que afirma a origem no evento emitido - Origem: achados P, Q e T de achados-execucao-real.md, Fase G do backlog
RN-079 — O Psicólogo não analisa sessão sem evento analisável
Antes de gastar um turno de modelo, a análise pergunta se há o que analisar. Não
havendo, ela não roda e o desfecho vira psychologist.analysis_skipped.
Analisável exclui duas coisas, por motivos diferentes:
- o rastro dos próprios analistas. O Psicólogo grava o turno dele no log da
sessão que está analisando (
agent.response,tool.call,tool.result, a hipótese). Contar isso faria uma sessão vazia parecer povoada a partir da primeira análise, e cada retentativa a encheria mais — o critério nunca mais reprovaria. Vale igual para a Anamnese; bootstrap.*, que é provisionamento de repositório rodando sozinho: nove passos de máquina não dizem nada sobre a pessoa.
Tudo o mais conta, inclusive proposed_action.* — o usuário aprovando e negando
sem escrever mensagem nenhuma é comportamento, lição que a Anamnese já tinha
aprendido (RN-063).
São duas contagens, e elas não se substituem. A crua dimensiona o trabalho
(quanto log ler, logo qual tier de triagem, leve ou pesado); a analisável decide
se há trabalho. Confundi-las é o defeito: uma sessão só de bootstrap passava por "20
eventos" sem ter nenhum, ganhava a análise, e o modelo — sem nada para citar —
inventava seq inexistentes até a validação de evidência rejeitar e ele
desistir, com o orçamento já gasto.
Pular vale também para reprocessamento manual. Reprocessar não fabrica material: quem clicou recebe o motivo no log em vez de uma hipótese inventada sobre um log que não existe.
O skip vira evento, ao contrário do da Anamnese, que é só log — aquele roda a cada 15 min e viraria ruído, este roda uma vez por fechamento de sessão, e uma análise ausente sem nada narrado é indiagnosticável.
- Onde:
apps/engine/lib/engine/session_events/event.ex(count_analisaveis/1),apps/engine/lib/engine/psychologist/triage.ex(should_run?/1),apps/engine/lib/engine/workers/psychologist_worker.ex - Teste:
apps/engine/test/engine/session_events/event_analisaveis_test.exs(inclui a reprodução da sessão do achado: 14 eventos, nenhum analisável),apps/engine/test/engine/workers/psychologist_worker_test.exs - Origem: achado J, Fase E do backlog
RN-080 — Regra de negócio duplicada é recusada na entrada
business_rule cujo título já existe no projeto não é gravada. A recusa
volta ao modelo pelo mesmo caminho de um payload inválido, e ele segue para a
próxima regra em vez de parar.
Na entrada porque não há outro lugar. Não existe tabela de regras: o
artefato É o evento artifact.business_rule, e evento de domínio não é apagado
nem editado. Deixar entrar significa conviver com a duplicata para sempre.
Escopo de projeto, não de sessão — é entre sessões que a duplicata nasce. Rodar o Criativo de novo abre sessão nova, e uma checagem por sessão não veria a rodada anterior, que é exatamente o caso do achado.
A comparação normaliza caixa, acento e espaço redundante; pontuação fica.
Duplicata semântica continua passando, e isso é declarado, não esquecido:
"Saudação com nome" e "Quem chama pode se identificar" seguem sendo duas regras,
porque separá-las é julgamento e não cabe num if.
Desde a RN-681 a duplicata semântica de TÍTULO é avisada por
embedding depois de gravar — continua passando, nunca é recusada aqui.
- Onde:
apps/engine/lib/engine/harness/artifact_dedupe.ex,apps/engine/lib/engine/harness/tools/emit_artifact.ex,apps/engine/lib/engine/session_events/event.ex(titulos_de_regras/1) - Teste:
apps/engine/test/engine/harness/artifact_dedupe_test.exs,apps/engine/test/engine/harness/emit_artifact_dedupe_test.exs - Origem: achado K, Fase E do backlog
RN-085 — O teto de iterações é por TIPO de agente
Quantas voltas um agente pode dar no laço de ferramenta depende do trabalho que ele faz: 8 para quem conversa, 60 para o dev agent e para os subagentes de QA. Não há mais um número único.
O teto de 8 nasceu de agente conversacional e foi herdado por quem trabalha.
Na validação real da 13b isso apareceu como bloqueio: o dev agent gastou as
oito voltas explorando um repositório recém-provisionado e nunca escreveu um
arquivo; com 25, escreveu três e rodou os testes. O desfecho registrado era
limite de iterações atingido com origem modelo — tecnicamente verdade e
praticamente inútil, porque o modelo nunca chegou a julgar nada.
Subir o default global seria a correção errada, e é isso que a regra protege: o Criativo não precisa de 60 voltas para conversar, e o teto também é a trava contra laço infinito.
Quem pode subir não é "quem trabalha muito", é quem tem trava de gasto por
baixo. O teto de iterações protege contra laço infinito; quem protege o
BOLSO é o token_budget_micros. Dev agents e subagentes de QA rodam com o
task_budget_micros da task, então afrouxar as voltas não afrouxa a conta.
infra-workflows usa ferramenta pesada e mesmo assim fica em 8: ele roda
sem budget, e para ele o teto é a única trava que existe.
Duas bordas com teste próprio:
-
dev-leadé conversacional, apesar do prefixodev-que identifica os dev agents (dev-<modulo>,dev-<modulo>-2). O lead decide e delega, e sem a cláusula explícita nasceria com o teto do trabalho pesado por acidente de nomenclatura. -
Agente desconhecido cai no teto mais baixo. Errar para o lado barato: quem precisa de mais voltas aparece como
limite de iterações atingidoe é corrigido; quem ganha 60 por engano gasta calado. -
Onde:
apps/engine/lib/engine/harness/iteracoes.ex, aplicado emapps/engine/lib/engine/harness/tool_loop.ex(init/1) -
Teste:
apps/engine/test/engine/harness/iteracoes_test.exseapps/engine/test/engine/harness/tool_loop_test.exs(o teto vem do TIPO do agente quando o chamador não passa um,teto explícito do chamador VENCE o do tipo) -
Origem: achado X, validação real da 13b; ADR 0053, FASE 14d
RN-115 — A Anamnese pode ser pausada globalmente; a pausa é do PRODUTO, nunca apaga dado
ANAMNESE_ENABLED (env var do engine, boolean, default false a partir
desta regra) decide se uma rodada NOVA da Anamnese pode acontecer — periódica
(AnamneseSchedulerWorker) ou sob demanda (AnamneseCommandController).
Decisão de PRODUTO do usuário em 2026-08-10 ("hoje ele não está trazendo
dados de muito valor"), não bug — ver docs/explanation/backlog.md. Desativada,
NENHUM dado existente é tocado: hipóteses, perfis de proficiência e patches
de instrução já gravados continuam intactos e visíveis, e o opt-out POR
MEMBRO (RN-025) continua um conceito separado — a pausa é do SISTEMA, não do
perfilamento individual.
AnamneseSchedulerWorker.kickoff/0 (chamado uma vez no boot) NÃO agenda o
job periódico quando desativado, em vez de agendar e deixar perform/1
no-opar a cada tick — mais barato (a fila do Oban não recebe um job a cada
ANAMNESE_INTERVAL_SECONDS só para não fazer nada) e mais claro para quem
inspeciona a fila. Correção em 2026-08-10 (achado real em execução, não
hipótese): a versão original desta regra deixava perform/1 incondicional
de propósito, para a corrente entre rodadas não carregar a decisão de
ligar/desligar consigo — mas isso significava que uma corrente já agendada
ANTES de a flag existir (ou de alguém desativá-la) continuava se
reagendando pra sempre, rodando Anamnese de verdade com a flag dizendo
false. Foi exatamente o que aconteceu num Postgres de dev mais antigo que
o PR original, remediado manualmente cancelando os jobs agendados.
perform/1 agora confere enabled?/0 a cada tick, igual kickoff/0: se
desativado, nem enqueue_projects/0 nem o reagendamento acontecem, e a
corrente morre ali — o que também AUTO-CURA sozinho o cenário de job antigo
que ainda dispara uma vez, sem precisar de intervenção manual.
AnamneseCommandController.run/2 (rota sob demanda, "reanalisar agora" nas
Configurações) responde 503 com corpo {"error": "anamnese_desativada"}
quando desativado — distinto de propósito do 409 vazio que já existia para
"projeto sem sessão" (os dois eram fáceis de confundir num 409 puro, e são
causas bem diferentes). RunAnamneseUseCase, do lado api, converte o 503 do
engine em ServiceUnavailableException com reason: "anamnese_disabled" no
corpo — nunca um 500 genérico nem um 409 reaproveitado. A web
(ProjectSettingsTab.tsx) descobre o estado no primeiro clique de "Rodar
agora" (não há hoje uma leitura prévia do estado global) e, a partir daí,
desabilita o botão e mantém a explicação VISÍVEL na tela — não só um toast
que some (RN-088: nunca falha silenciosa ou confusa).
- Onde:
apps/engine/lib/engine/workers/anamnese_scheduler_worker.ex(enabled?/0,kickoff/0),apps/engine/lib/engine_web/controllers/anamnese_command_controller.ex,apps/engine/config/runtime.exs,apps/api/src/domain/anamnese/anamnese-disabled.error.ts,apps/api/src/infrastructure/http-clients/api-to-engine-client.ts(runAnamnese),apps/api/src/application/use-cases/anamnese/run-anamnese.use-case.ts,apps/web/src/routes/settings/ProficiencySection.tsx - Teste:
apps/engine/test/engine/workers/anamnese_scheduler_worker_test.exs(kickoff/0não agenda desativado, agenda ativado, default desligado;perform/1desativado no meio da corrente não faz fan-out nem reagenda),apps/engine/test/engine_web/controllers/anamnese_command_controller_test.exs(503 distinto de 409, com e sem sessão),apps/api/test/application/use-cases/anamnese/run-anamnese.use-case.spec.ts,apps/web/src/routes/ProjectSettingsTab.test.tsx(ProficiencySection) - Borda: a flag é GLOBAL (todos os projetos/workspaces), não por projeto
— ao contrário do teto de paralelismo (RN-083) ou do modelo herdável por
área (RN-102), que são decisões por escopo. Ligar de volta é
ANAMNESE_ENABLED=truee reiniciar o engine; não há botão na UI para isso (é operacional, não uma preferência de projeto). - Origem: sem ADR — decisão de produto reversível, não mudança estrutural de arquitetura. Ver docs/explanation/backlog.md.
RN-116 — Falha ao CRIAR um handoff não derruba o agente
confirm_readiness (Criativo → PO) e offer_infra_handoff (Arquiteto → Infra;
o offer_dev_handoff, Arquiteto → Dev Lead, saiu na
RN-672, e o handoff ao Dev Lead que a Infra
oferece segue a mesma régua) chamam a api pra criar o handoff DEPOIS de o
turno já ter rodado — no caso do Criativo, depois de o product_brief já
estar gravado no event log. Se essa chamada falhar (api fora, 5xx, etc.), o
handoff não existe, mas isso NUNCA derruba o GenServer do agente: a falha vira
agent.error durável, com origem (FalhaDeTurno.origem/1) e uma mensagem
que diz o que JÁ foi salvo (o product_brief, as regras) e o que não foi (o
handoff) — para o usuário saber que confirmar de novo é seguro, não repete
trabalho.
Era o oposto: as três chamadas usavam {:ok, _handoff} = EngineApiClient.create_handoff(...)
— um match rígido. {:error, _} virava MatchError, e como os três agentes
sobem com restart: :temporary num DynamicSupervisor :one_for_one, o
processo simplesmente SUMIA — sem agent.error, sem resposta no fio, só
silêncio. Do lado de quem observava: a informação (regras, product brief)
parecia ter "passado" (estava gravada), mas nada iniciava do lado do agente
seguinte, porque o handoff nunca chegou a existir. Reabrir a conversa não
resolvia sozinho — só uma NOVA mensagem reativa o processo (rehidratando do
event log), e só uma nova confirmação de prontidão tenta o handoff de novo.
A mensagem NÃO reusa FalhaDeTurno.mensagem/1 (a de RN-059, "não consegui
completar este turno... nada foi gasto"): nos três call sites o trabalho já
rodou (ou nem precisava rodar, no caso de offer_dev_handoff) — dizer "nada
foi gasto" seria falso quando tokens já tinham sido gastos no turno de
consolidação. Reusa só FalhaDeTurno.origem/1, que classifica pelo FORMATO
do motivo (status HTTP, exceção de transporte), não por ser turno de LLM.
O Engine.Harness.Tools.OfferHandoff (a ferramenta que o PO usa via tool
call, dentro do ToolLoop) já tratava {:error, reason} sem crashar — o
defeito era só nestes três handlers server-driven, que chamam
EngineApiClient.create_handoff/5 DIRETO em vez de passar pela ferramenta.
- Onde:
apps/engine/lib/engine/agents/criativo_server.ex(handle_call(:confirm_readiness, ...),emit_falha_handoff/3),apps/engine/lib/engine/agents/arquiteto_server.ex(handle_call(:offer_infra_handoff, ...),emit_falha_handoff/3);apps/engine/lib/engine/infra/infra_lead_server.ex(emit_falha_do_handoff_ao_dev_lead/2, RN-672) - Teste:
apps/engine/test/engine/agents/criativo_server_test.exs("prontidão: falha ao criar o handoff NÃO derruba o processo, e vira agent.error durável");apps/engine/test/engine/agents/arquiteto_server_test.exs(offer_infra_handoff, sucesso e falha);apps/engine/test/engine/infra/infra_lead_server_test.exs(o handoff ao Dev Lead que a Infra oferece, recusado com 500) - Origem: relato de uso real no projeto
exp-001(Criativo → PO); a mesma falha estrutural foi achada por leitura de código nos dois handoffs do Arquiteto, sem reprodução separada para eles
RN-117 — O Psicólogo pode ser pausado globalmente; a pausa é do PRODUTO, nunca apaga dado
PSYCHOLOGIST_ENABLED (env var do engine, boolean, default false) decide
se uma rodada NOVA do Psicólogo pode acontecer — automática (fechamento de
sessão, roteado pelo Engine.Outbox.Drain) ou sob demanda
(PsychologistCommandController.reanalyze/2). Mesma decisão de PRODUTO do
usuário em 2026-08-10 já aplicada à Anamnese (RN-115, "hoje ele não está
trazendo dados de muito valor") — não bug, ver docs/explanation/backlog.md.
Desativado, NENHUM dado existente é tocado: análises e hipóteses já
emitidas continuam intactas e visíveis.
Diferente da Anamnese (cujo gatilho automático é um TICK periódico que a
própria flag decide se reagenda), o gatilho automático do Psicólogo é o
fechamento de sessão — o Engine.Outbox.Drain roteia
session.closed/session.closed_abnormally pra PsychologistWorker só
quando PsychologistWorker.enabled?/0 é true (Drain.handlers_for/1);
desativado, só SessionLifecycleWorker roda, e o job do Psicólogo nem
nasce. PsychologistWorker.perform/1 continua incondicional de propósito —
mas NÃO é mais "o mesmo padrão" do AnamneseSchedulerWorker (ver a correção
de 2026-08-10 na RN-115 acima): lá perform/1 passou a conferir a flag
porque ele PRÓPRIO reagenda a corrente a cada tick, e um job antigo
disparando incondicionalmente reabria a Anamnese com a flag desligada. O
Psicólogo não tem corrente nenhuma que se reagende sozinha — cada job nasce
de UM evento (session.closed), e quem decide é o Drain no momento em que
RECEBE o evento, não o worker no momento em que RODA; um job de Psicólogo
já enfileirado antes de desligar a flag é, no máximo, a última rodada
pendente, nunca uma corrente infinita. Por isso a suite pré-existente de
PsychologistWorker (que chama perform/1 direto) não precisou mudar.
PsychologistCommandController.reanalyze/2 (rota sob demanda,
"Reanalisar" na aba Insights) responde 503 com corpo
{"error": "psicologo_desativado"} quando desativado, sem sequer criar o
job. ReanalyzeSessionUseCase, do lado api, converte o 503 do engine em
ServiceUnavailableException com reason: "psychologist_disabled" no
corpo — nunca um 500 genérico. Isto descobre a pausa quando já existe uma
análise para reprocessar; a tela SEM hipótese nenhuma tem uma leitura
prévia própria, que não existia aqui — ver RN-454.
- Onde:
apps/engine/lib/engine/workers/psychologist_worker.ex(enabled?/0),apps/engine/lib/engine/outbox/drain.ex(handlers_for/1),apps/engine/lib/engine_web/controllers/psychologist_command_controller.ex,apps/engine/config/runtime.exs,apps/api/src/domain/psychologist/psychologist-disabled.error.ts,apps/api/src/infrastructure/http-clients/api-to-engine-client.ts(reanalyzeSession),apps/api/src/application/use-cases/execution/reanalyze-session.use-case.ts,apps/web/src/routes/ProjectInsightsTab.tsx - Teste:
apps/engine/test/engine/outbox/drain_test.exs(session.closedsó enfileira o Psicólogo quando ativado),apps/engine/test/engine_web/controllers/psychologist_command_controller_test.exs(503 sem criar job, 202 com job enfileirado quando ativado),apps/api/test/application/use-cases/execution/reanalyze-session.use-case.spec.ts,apps/web/src/routes/ProjectInsightsTab.test.tsx - Borda: a flag é GLOBAL (todos os projetos/workspaces), como a da
Anamnese. Ligar de volta é
PSYCHOLOGIST_ENABLED=truee reiniciar o engine; não há botão na UI para isso. - Origem: sem ADR — decisão de produto reversível, não mudança estrutural de arquitetura. Ver docs/explanation/backlog.md.
Session
RN-122 — O botão "Parar" cancela o turno DE VERDADE, matando a task que segura a chamada de LLM
Até aqui não havia cancelamento em lugar nenhum. A raiz era estrutural:
agent_command_controller.ex tratava a mensagem do usuário com
GenServer.call(pid, {:user_message, texto}, ...) — SÍNCRONO — e o
turno inteiro (tool loop + chamada SSE ao LLM) rodava DENTRO do handle_call. O
processo do agente ficava bloqueado até o turno acabar e não tratava NENHUMA outra
mensagem nesse meio-tempo — nem um futuro comando de cancelar.
A correção: o handle_call/handle_cast de turno dos quatro agentes
conversacionais (Criativo, PO, Arquiteto, Dev Lead) dispara um
Task.Supervisor.async_nolink/2 (Engine.Agents.TurnoAssincrono, novo,
compartilhado pelos quatro) com o trabalho pesado e devolve {:noreply, state} —
a resposta ao GenServer.call original fica ADIADA até a task
terminar (GenServer.reply/2, disparado quando {ref, resultado} chega ao
handle_info). Como o processo do agente DEIXA de ficar bloqueado, um novo
handle_cast(:cancel, state) chega e é tratado enquanto o turno roda:
Task.shutdown/2 no modo :brutal_kill mata a task, o que derruba a
conexão HTTP (SSE) que ela mantém com a api — é isso que faz o cancelamento
economizar tokens de fato, e não só parar de renderizar no cliente. O
from original recebe {:error, :cancelado}, e um agent.error TERMINAL é
registrado com origem "politica" (a mesma origem de
orçamento/credencial/vínculo — cancelar é decisão do usuário de parar de gastar
tokens, não um quinto valor no vocabulário fechado do ADR 0020) — sem ele,
o GetSessionPendingWorkUseCase veria agent.activated sem
agent.response/agent.error depois, e a sessão ficaria pendurada
no sinal de trabalho pendente.
Duas mensagens concorrentes para o mesmo agente (o usuário manda de novo enquanto um turno
já roda) nunca disparam uma segunda task:
TurnoAssincrono.iniciar/3 responde {:error, :turno_em_andamento}
na hora.
Revisada pela RN-578 (ADR 0163): a resposta ao
GenServer.calloriginal não fica mais adiada até a task terminar — ela sai no ACEITE, assim que a task sobe, e cancelar não responde a ninguém (quem chamou já tem:ok).{:error, :turno_em_andamento}deixou de ser descartado pelo controller: é um 409 nomeado e umagent.errordurável.
- Onde:
apps/engine/lib/engine/agents/turno_assincrono.ex(o mecanismo),apps/engine/lib/engine/agents/{criativo,po,arquiteto,dev_lead}_server.ex(os quatrohandle_call/handle_castde turno),apps/engine/lib/engine_web/controllers/agent_command_controller.ex:373(cancel/2),apps/engine/lib/engine_web/router.ex(POST /internal/sessions/:sessionId/agent/cancel),apps/api/src/application/use-cases/agents/cancel-agent-turn.use-case.ts,apps/api/src/interfaces/http/agents/agents.controller.ts(POST /projects/:projectId/sessions/:sessionId/agents/:agent/cancel),apps/web/src/routes/SessionPage.tsx(handleCancel, botão "Parar" no composer) - Teste:
apps/engine/test/engine/agents/turno_assincrono_test.exs(a task morre DE VERDADE —Process.alive?/1antes e depois — e não só "deixou de importar o resultado"),apps/api/test/application/use-cases/agents/cancel-agent-turn.use-case.spec.ts - Caso de borda: cancelar sem turno em andamento é NO-OP
idempotente — não há task para matar nem
frompendente para responder. O cancelamento NÃO desfaz efeito colateral que já rodou ANTES de a task morrer (por exemplo, uma ferramenta que já tinha sido despachada e registrado o seu evento) — só interrompe o que ainda não aconteceu. - Origem: investigação desta sessão; sem ADR próprio (mudança de padrão de concorrência DENTRO do harness, não de fronteira de camada/banco).
RN-129 — O ToolLoop nunca grava agent.response vazio; falha de transporte vira agent.error durável
A RN-059 fechou o balão vazio para os quatro agentes
conversacionais, mas eles não passam pelo Engine.Harness.ToolLoop — cada um
chama EngineApiClient.llm_turn_stream/6 no próprio módulo. O ToolLoop
(usado por dev agents, QA Automação/Performance-Segurança, Infra-Workflows,
Anamnese e Psicólogo) tinha o MESMO defeito num caminho diferente: emitia
agent.response a cada iteração, mesmo quando o modelo só chamou ferramenta
sem texto, ou terminou o turno sem produzir nada — e a falha de transporte
(provider fora do ar, timeout) virava agent.response com content ausente,
igualmente indistinguível de sucesso.
Achado ao vivo numa sessão de execução real (dev agents): duas bolhas com o texto de compatibilidade da RN-059 ("resposta vazia — evento anterior...") apareceram numa sessão criada minutos antes — não eram eventos antigos, eram o mesmo defeito acontecendo de novo, só que na aba de execução.
Duas correções, no ponto ESTRUTURAL comum a todo consumidor do ToolLoop,
não módulo por módulo:
- Conteúdo vazio nunca vira
agent.response. Iteração que só chamou ferramenta já está narrada portool.call/tool.result; iteração que não produziu nada (nem texto, nem tool call) deixa o desfecho para quem chamou o loop decidir —ctx.last_error/{:ok, ctx}carregam a informação, e quem consome (ex.:DevAgentServer.handle_outcome/4) já grava o evento durável do PRÓPRIO domínio (dev.blocked, comorigem). - Falha de transporte vira
agent.errordurável, comorigem(Engine.Agents.FalhaDeTurno.origem/1— o MESMO helper que os quatro agentes conversacionais usam, sem duplicar classificação) emensagemem português — nunca maisagent.responsesemcontent.
- Onde:
apps/engine/lib/engine/harness/tool_loop.ex(loop/1,emit_falha/2) - Teste:
apps/engine/test/engine/harness/tool_loop_test.exs("iteração só com tool call (sem texto) não grava agent.response vazio", "modelo termina o turno sem texto e sem tool call...", "falha de transporte... grava agent.error durável com origem") - Origem: RN-059 (regra que esta estende) — achado ao vivo numa sessão de execução real com dev agents
PR Gates
RN-140 — Um ciclo de gate que morre no meio retoma sozinho, sem intervenção manual
QaLeadServer/SecOpsAgentServer são restart: :temporary, e as
transições intermediárias do gate (DevAgentServer.correct/3,
Dispatcher.run_secops/2) são chamadas diretas em memória — feitas DEPOIS
de o record_gate_verdict já ter avançado de forma durável o gate_status na
api. Uma queda entre as duas prendia a PR para sempre em
awaiting_qa/awaiting_secops: nada sabia que aquele passo tinha ficado
pendente, e nenhum restart do engine resolvia — o próprio ADR 0057
já declarava a suspensão em {:awaiting, ...} como limite
conhecido, e investigar de novo mostrou que a janela entre um veredito
registrado e uma chamada de despacho é, na prática, mais fácil de acertar.
gate_states (schema engine, chave {project_id, task_id, gate})
registra o ciclo em voo nos MESMOS pontos em que as transições
já aconteciam — "in_progress" antes de qualquer subagente/scanner rodar,
"dispatch_pending" logo depois de o record_gate_verdict devolver
correct/run_secops e antes da chamada em processo.
Engine.Gates.GateRescuer varre linhas paradas há mais de 15 minutos
(configurável, GATE_RESCUE_STALE_AFTER_SECONDS) — generoso de propósito,
porque o ToolLoop de um subagente de QA roda legitimamente até
TOOL_LOOP_MAX_ITERATIONS_GATE (60) iterações, e um limiar curto
resgataria (e duplicaria) um ciclo só lento — e retoma:
"in_progress" reinicia a área inteira (sem retomada cirúrgica do
ctx, que não sobrevive a um restart, a mesma escolha do dev
agent); "dispatch_pending" reenvia exatamente a chamada perdida. Chamado no
boot e por um tick do Oban que se reagenda a cada 5 minutos
(GATE_RESCUE_INTERVAL_SECONDS),
Engine.Workers.GateRescueSchedulerWorker — o mesmo idioma de
ModelSyncSchedulerWorker/AnamneseSchedulerWorker.
Duas guardas contra trabalho duplicado: processo vivo NO MESMO nó
(Registry.lookup) nunca é perturbado, e o limiar de staleness
cobre o que a guarda local não alcança (réplica remota — o Registry
é local ao nó, a mesma ressalva do Engine.Dev.Wake desde o
ADR 0045). O
pior desfecho de uma corrida residual é trabalho duplicado e barato — a api
rejeita um segundo record_gate_verdict para um gate que não é mais dono do
gate_status (nextGateStatus), e DevAgentServer.correct/3 já é
idempotente por guarda de estado desde o ADR 0052 — nunca
dado inconsistente.
- Onde:
apps/engine/lib/engine/gates/gate_state.ex,gate_rescuer.ex, os pontos de escrita emqa_lead_server.ex(run_area/3,apply_gate_result/6) esecops_agent_server.ex(run_secops/3,apply_verdict/6), eapps/engine/lib/engine/workers/gate_rescue_scheduler_worker.ex - Teste:
apps/engine/test/engine/gates/gate_rescuer_test.exs— mata umQaLeadServerreal com o ciclo em voo (DynamicSupervisor.terminate_child/2, a linha durável sobrevivendo ao processo) e prova queGateRescuer.run/0reconecta a área sozinho até um desfecho real; o cenário do enunciado (run_secopsperdido) e o retornocorrectperdido, ambos com processo e despacho REAIS, semFakeGateDispatcher; e as duas guardas (processo local vivo não é perturbado; linha recente não é tocada) - Origem: ADR 0067, que estende o ADR 0057
RN-141 — O conteúdo lido por read_file também tem teto de bytes
A RN-074 travou a saída do terminal contra
{413, "request entity too large"}, mas deixou aberta a mesma porta pelo
read_file: ele lia o arquivo INTEIRO, sem teto, e esse conteúdo entrava no
histórico do laço e viajava em todo turno seguinte. Um PR com arquivo grande
(lockfile, bundle, arquivo gerado) bastava pra travar dev agents E o QA de
Performance/Segurança — que só tem ReadFile/SearchWorkspace (sem
Terminal, de propósito) pra investigar uma PR, então não tinha rota de
escape nenhuma quando o arquivo era grande demais.
O conteúdo é cortado em READ_FILE_MAX_BYTES (default 32 KiB, mesmo valor da
RN-074 por coincidência de contexto, não por acoplamento — as duas variáveis
são independentes) antes de virar resultado da ferramenta, com marca dizendo
o arquivo e os dois tamanhos:
[arquivo package-lock.json truncado: mostrando 32768 de 1048576 bytes. Use
search_workspace para localizar um trecho específico em vez de reler o
arquivo inteiro.]
Mesmas três propriedades da RN-074 (teto é > não >=; corte não parte
caractere multibyte; a marca é endereçada ao modelo, dizendo o que fazer). A
truncagem mora na FERRAMENTA (Engine.Harness.Tools.ReadFile), não em
Engine.Harness.WorkspaceFiles.read_file/2 — essa é a base genérica de
acesso a arquivo, compartilhada por write_file/search_workspace, e
truncar ali cortaria conteúdo de quem não precisa desse teto.
search_workspace não teve o mesmo tratamento: ele devolve só os PATHS que
bateram (matched_content é booleano), nunca o conteúdo do arquivo — o vetor
de estouro que motivou esta RN não se aplica a ele.
- Onde:
apps/engine/lib/engine/harness/tools/read_file.ex(truncate/2), teto emapps/engine/config/runtime.exs(read_file_max_bytes) - Teste:
apps/engine/test/engine/harness/tools/read_file_test.exs(describeteto de bytes do conteúdo) - Origem: achado ao vivo no event log de uma execução real — os 4 dev
agents de um projeto e os QA de Automação/Performance-Segurança bloqueados
com
{413, "request entity too large"}, mesma causa raiz da RN-074, porta diferente
Session
RN-142 — Confirmar prontidão SEM nenhuma regra de negócio é recusado pelo engine, não só escondido na UI
Antes, clicar em "Estou pronto para produzir" — ou chamar a rota direto —
SEMPRE criava o product_brief e oferecia o handoff ao PO, mesmo
numa conversa em que zero regras de negócio tinham sido capturadas.
business_rule_refs(state) já existia em criativo_server.ex, mas
só para PREENCHER o campo "rules" do brief — nunca como condição
de bloqueio. Na UI, SessionPage.tsx desabilitava o botão só durante o
streaming, sem olhar a contagem de regras.
A garantia real tinha de morar no servidor, não na tela: quem
chama a rota direto (ou um cliente futuro que não seja este frontend)
não pode furar o guardrail só porque o botão está desenhado
desabilitado. CriativoServer.handle_call(:confirm_readiness, ...) agora confere
business_rule_refs(state) ANTES de disparar a Task do
Engine.Agents.TurnoAssincrono (RN-122) — vazio, ele
recusa ali mesmo, sem rodar o turno de consolidação, sem
product_brief, sem handoff.
A recusa não vira HTTP 4xx:
agent_command_controller.ex#readiness/2 IGNORA o retorno deste GenServer.call
e sempre responde 202, o mesmo padrão que o message/2 já usa
desde a RN-122 ("esta resposta é só o recebimento" — o desfecho
real mora no log de eventos). Por isso a recusa é narrada como um
agent.error DURÁVEL (RN-059), com origem "politica" (é uma
decisão de produto, não falha de infra/modelo/turno) — e uma mensagem
explicando por quê; o fio da sessão já sabe renderizar esse tipo de
evento (lerFalhaDeTurno, o mesmo balão vermelho de qualquer outra falha de turno).
A UX complementar, em SessionPage.tsx: o botão nasce disabled
(com um title explicando por quê) enquanto events não tem
artifact.business_rule — a MESMA fonte que já alimenta o
painel "Regras de negócio" (ContextAside), sem segunda leitura que pudesse
divergir da primeira.
- Onde:
apps/engine/lib/engine/agents/criativo_server.ex(handle_call(:confirm_readiness, ...),emit_falha_sem_regra/1),apps/web/src/routes/SessionPage.tsx(hasBusinessRule, o botão "Estou pronto para produzir") - Teste:
apps/engine/test/engine/agents/criativo_server_test.exs("readiness: refuses when NO business rule was captured" — sem turno, sem brief, sem handoff; a recusa narrada com origem"politica"),apps/web/src/routes/SessionPage.readiness-exige-regra.test.tsx(botão desabilitado sem regra, habilitado com 1+) - Caso de borda: a sessão já tinha
readiness.confirmedregistrado pela api ANTES de sinalizar o engine (ConfirmReadinessUseCase) — isso não muda: é o registro do CLIQUE do usuário, um fato que aconteceu independente de o engine aceitar ou recusar depois. - Origem: investigação de código confirmando um relato de usuário — sem ADR (guardrail novo num fluxo que já existia, sem mudança de fronteira de camada/banco).
RN-150 — search_workspace tem teto de QUANTIDADE de hits e de BYTES, cada um com sua marca
Achado numa revisão de PR: search_workspace (dev agents e os dois agentes
de QA/gate, qa_tools.ex e qa_performance_seguranca_agent.ex — este
último só tem read_file/search_workspace, sem Terminal, de propósito)
devolvia TODOS os resultados da busca, sem teto nenhum — mesma classe do
achado S (Engine.Actions.TerminalExecutor.truncate/2) e da correção de
read_file (Engine.Harness.Tools.ReadFile.truncate/2): o resultado fica
no histórico do laço e viaja em todo turno seguinte, e uma árvore grande
basta pra estourar {413, "request entity too large"} do provider.
Dois tetos independentes, porque a busca estoura de duas formas diferentes:
- Quantidade de hits — uma árvore com milhares de arquivos batendo o
termo produz milhares de linhas
- caminhomesmo que nenhum arquivo individual seja grande. Truncar só por BYTES no fim ainda pagaria o custo de escanear e ler o conteúdo de cada um desses arquivos antes de montar a string. Por isso o teto de quantidade (SEARCH_WORKSPACE_MAX_HITS, default 500) vive emWorkspaceFiles.search/3, que já PARA de consumir a busca assim que encontra hit suficiente — o pipeline roda sobre umStream, eEnum.take(stream, max_hits + 1)só lê da fonte o que precisa pra produzir osmax_hits + 1primeiros resultados. O "+1" é o que permite dizer que HAVIA mais sem continuar escaneando o resto pra contar o total exato — contar o total pagaria de novo o I/O que o teto existe pra evitar, então a marca diz "mostrando os N primeiros" e nunca inventa um total. - Bytes do texto final — mesmo com hits limitados, caminhos muito
longos podem produzir uma string grande. Teto de bytes
(
SEARCH_WORKSPACE_MAX_BYTES, default 32.768), mesmo padrão determinal_output_max_bytes/read_file_max_bytes— variável PRÓPRIA, não reaproveita as outras duas: mesma classe de estouro, divergir uma não deve exigir tocar as outras.
A marca de truncagem é dirigida ao MODELO, não ao humano: diz o que foi
cortado (hits e/ou bytes) e instrui a refinar o termo da busca — mesmo
espírito das marcas de TerminalExecutor/ReadFile.
- Onde:
apps/engine/lib/engine/harness/workspace_files.ex(search/3,take_capped/2),apps/engine/lib/engine/harness/tools/search_workspace.ex(truncate/3,marca_de_truncagem/5),apps/engine/config/runtime.exs(search_workspace_max_hits,search_workspace_max_bytes) - Teste:
apps/engine/test/engine/harness/workspace_files_test.exs(search/3commax_hitscorta a QUANTIDADE e marca truncagem só quando há mais que o teto),apps/engine/test/engine/harness/search_workspace_test.exs(busca com poucos resultados não é alterada; busca com mais hits que o teto é truncada com aviso claro; texto final maior que o teto de bytes também é cortado) - Origem: achado de revisão de PR — segunda causa real do 413 em
revisões, depois da correção de
read_file
RN-162 — Perguntas estruturadas do Criativo
O Criativo pode, quando faz VÁRIAS perguntas na mesma resposta, emitir a
lista em formato ESTRUTURADO em vez de deixar o usuário responder item por
item em texto livre — ferramenta nova ask_structured_questions
(apps/engine/lib/engine/harness/tools/ask_structured_questions.ex,
:direct), registrada ao lado de emit_artifact. Schema:
{ questions: [{ id, label, type?, options? }] } — id único e não-vazio,
label não-vazio, type ∈ text|textarea|select (default text),
options obrigatório e não-vazio quando type: select. Grava
chat.structured_question.
O frontend (StructuredQuestionCard, SessionPage.tsx) renderiza um
formulário com um campo por pergunta — Input/Textarea/Select do
design system, conforme type. POST .../agents/:agent/structured- question/:questionSetId/answer (AnswerStructuredQuestionUseCase) valida
que toda pergunta tem resposta não-vazia, grava
chat.structured_question_answered (referenciando questionSetId = id do
evento da pergunta) e REUSA SendAgentMessageUseCase — as respostas viram
uma mensagem concatenada ("1. {label}: {resposta}\n2. ..."), como se o
usuário tivesse digitado no fio; não há canal novo de "o agente lê a
resposta estruturada". Um conjunto de perguntas só pode ser respondido
UMA vez: reenvio é recusado com 409, e o formulário nem chega a
reaparecer — o card vira somente leitura assim que existe um
chat.structured_question_answered posterior com o mesmo questionSetId.
- Onde:
apps/engine/lib/engine/harness/tools/ask_structured_questions.ex,apps/engine/lib/engine/agents/criativo_server.ex,apps/api/src/application/use-cases/agents/answer-structured-question.use-case.ts,apps/api/src/interfaces/http/agents/agents.controller.ts,apps/web/src/routes/SessionPage.tsx(StructuredQuestionCard) - Teste:
apps/engine/test/engine/harness/tools/ask_structured_questions_test.exs,apps/engine/test/engine/agents/criativo_server_test.exs,apps/api/test/application/use-cases/agents/answer-structured-question.use-case.spec.ts,apps/web/src/routes/SessionPage.perguntas-estruturadas.test.tsx - Origem: pedido do usuário — sem precedente de input estruturado no chat
RN-163 — O Criativo cumpre a promessa de tentar de novo
Cada turno do Criativo roda um laço bounded de tool use, com teto de 12 idas ao modelo — o mesmo desenho que o PO e o Arquiteto já tinham. Antes o modelo era chamado UMA vez por turno: o resultado da ferramenta era anexado ao histórico em memória e ninguém mais o lia, então a frase "vou corrigir e tentar de novo" — literal no código — só se cumpria se o usuário mandasse outra mensagem. Para quem usava, o Criativo simplesmente parava de responder depois de dizer que ia corrigir.
Quatro consequências, e cada uma é uma regra:
- Erro de ferramenta é entrada, não fim de linha. O motivo volta ao modelo
como mensagem
toole o laço chama o modelo de novo, que reemite corrigido DENTRO do mesmo turno. - Nada se anuncia que o código não vá executar. A frase de retentativa é decidida depois de despachar as ferramentas e sabendo quantas voltas sobraram: com volta disponível, o agente diz que vai corrigir; sem volta, ele não promete.
- A falha de ferramenta virou
agent.errordurável, com origem (RN-059) — eraagent.response, indistinguível no event log de uma resposta normal.origem: infraquando a api recusou oappend_event;origem: modelopara o payload que o modelo escreveu (chave errada,originque não é lista, regra duplicada, tipo system-emitted). O payload carregatooleretentativa. - Teto esgotado não termina em silêncio. Vira
agent.errorcomreason: "limite_de_iteracoes"eorigem: modelo, a mesma leitura dotoolloop.limit_reacheddoToolLoop. O nome do evento NÃO é reusado: este agente não roda dentro doToolLoop, e o evento mentiria sobre quem o produziu.
Duas fronteiras do laço, ambas deliberadas: ask_structured_questions
bem-sucedida encerra o turno (a bola está com o usuário, e as respostas
voltam num turno futuro — RN-162); e um turno que teve falha em
alguma volta fecha com um desfecho CONSOLIDADO no fio, para a última palavra
não ser o erro de uma volta que já foi corrigida depois.
- Onde:
apps/engine/lib/engine/agents/criativo_server.ex(run_turn_capturing/3,continuar/4,emit_falha_de_ferramenta/4,emit_falha_limite/2) - Teste:
apps/engine/test/engine/agents/criativo_server_test.exs(laço que corrige de verdade, teto esgotado narrado, origeminfravsmodelo, pergunta estruturada que encerra o turno) - Origem: uso real no projeto
exp001— "o Criativo não respondeu depois de dizer que iria corrigir e tentar de novo"
RN-164 — O PO LÊ o que já existe, escopado ao projeto
O PO ganhou duas ferramentas de LEITURA — listar_regras_de_negocio e
listar_backlog (:direct, sem parâmetro nenhum) — servidas por duas rotas
internas escopadas ao projeto: GET /internal/projects/:projectId/business-rules
e GET /internal/projects/:projectId/backlog.
A primeira devolve todo artifact.business_rule das sessões do projeto com a
description inteira, quais histórias já citam cada regra e o
uncoveredCount; a segunda devolve a MESMA árvore épico → história → tarefa
da aba Backlog, pelo mesmo ListBacklogUseCase (três leituras por projeto,
nunca N+1). O texto que volta ao modelo põe as regras DESCOBERTAS primeiro e
os épicos ÓRFÃOS antes da árvore — é o que gera trabalho.
O que a regra corrige: até aqui o PO tinha quatro ferramentas e todas de escrita. O contexto dele era montado uma vez, no kickoff, a partir dos 200 últimos eventos da sessão — dali em diante ele não sabia quais regras existiam, quais já tinha coberto, nem o que já havia criado. O escopo é o projeto e não a sessão de propósito: regra capturada numa sessão anterior é exatamente o que a leitura por sessão escondia.
Ler não vira proposed_action (não é efeito externo), mas é CONTIDA no
sentido do ADR 0060: nenhuma das duas rotas aceita parâmetro além do id do
projeto — sem busca, sem paginação, sem filtro —, o custo por chamada é
constante, e o texto entregue ao modelo tem teto de linhas dizendo o total
real quando trunca.
- Onde:
apps/engine/lib/engine/harness/tools/listar_regras_de_negocio.ex,apps/engine/lib/engine/harness/tools/listar_backlog.ex,apps/engine/lib/engine/agents/po_server.ex,apps/engine/lib/engine/sessions/engine_api_client.ex,apps/api/src/application/use-cases/backlog/list-business-rules.use-case.ts,apps/api/src/interfaces/http/internal/internal-projects.controller.ts - Teste:
apps/engine/test/engine/harness/tools/listar_regras_de_negocio_test.exs,apps/engine/test/engine/harness/tools/listar_backlog_test.exs,apps/engine/test/engine/agents/po_server_test.exs,apps/api/test/application/use-cases/backlog/list-business-rules.use-case.spec.ts - Origem: uso real no projeto
exp001— "crie ferramenta para o PO conseguir listar as regras de negócio"
RN-165 — Épico sem história é cobrado, e o PO pergunta quando não sabe
Quando o PO encerra um turno tendo criado um épico e nenhuma história para
ele, isso vira desfecho EXPLÍCITO: evento durável
backlog.epic_without_story (com origem: "modelo", os ids e títulos dos
épicos e a mensagem) mais o broadcast agent.error — o padrão da
RN-059: o log é o que sobrevive, o broadcast é o agente dizendo no
fio. A cobrança é por OCORRÊNCIA: reportada uma vez, a lista de pendências é
esvaziada, e não vira alarme que repete a cada turno.
O que conta é a criação que deu certo: um create_story recusado pela api
(business_rule_id inexistente, por exemplo) não quita o épico. Tratá-lo como
se quitasse seria trocar um silêncio por outro. Os épicos pendentes vivem no
state do PoServer e NÃO são reidratados: a cobrança é sobre a obrigação
assumida no turno, e reconstruí-la do event log reabriria épico antigo que o
usuário já resolveu de outro jeito.
Junto vieram as duas peças que faltavam para o PO ter uma saída além de parar:
ask_structured_questions — a MESMA ferramenta do Criativo
(RN-162), só passada a advertisar no po_server — e a instrução de
kickoff dizendo, com todas as letras, que épico sem história trava a execução
e que faltando informação se PERGUNTA, nunca se para nem se inventa. A
instrução anterior não dizia uma palavra sobre o que fazer diante de uma
lacuna, e diante de uma lacuna sem instrução um modelo escolhe entre inventar
e parar.
- Onde:
apps/engine/lib/engine/agents/po_server.ex(anotar_obrigacao/4,encerrar_turno/1,obrigacoes/0),apps/engine/lib/engine/harness/tools/create_epic.ex(id_no_resultado/1),apps/web/src/lib/activity.ts - Teste:
apps/engine/test/engine/agents/po_server_test.exs(describe "RN-165"),apps/web/src/lib/activity.test.ts - Origem: uso real no projeto
exp001— backlog sem história, logo sem tarefa, logo execução travada sem erro visível
RN-166 — O teto de iterações do PO deixa rastro
Esgotado o teto de iterações do laço de ferramentas do PoServer (12), o
turno emite toolloop.limit_reached com {iteration, max_iterations} —
o MESMO tipo e o mesmo payload que o Engine.Harness.ToolLoop já emitia
desde a Fase 3. Antes, a cláusula devolvia o state e pronto: de fora, um laço
esgotado era indistinguível de um turno que simplesmente acabou.
Reusar o identificador em vez de criar um po.* é deliberado: é o mesmo fato
(o laço bateu no teto), e quem lê o event log não deve precisar aprender um
segundo nome por causa de o agente conversacional ter laço próprio em vez de
usar o ToolLoop.
- Onde:
apps/engine/lib/engine/agents/po_server.ex(run_turn/2, cláusularemaining <= 0) - Teste:
apps/engine/test/engine/agents/po_server_test.exs("teto de iterações emite toolloop.limit_reached") - Origem: investigação do travamento do
exp001— o laço terminava em silêncio
RN-171 — A pergunta de lista tem saída por texto livre, por default
Pergunta type: "select" de ask_structured_questions (RN-162) aceita
resposta fora da lista. O campo é allowOther, e o default é true:
quem não declara nada oferece a saída. Fechar a lista exige
allowOther: false explícito, e só faz sentido quando ela é genuinamente
fechada ("Sim"/"Não").
O default aberto não é preferência de estilo. Uma lista fechada por
ESQUECIMENTO do modelo trava a conversa inteira e o usuário não tem como
destravá-la de fora — foi exatamente o que o uso real encontrou: o modelo
ofereceu uma opção do tipo "Escreva você mesmo" e o formulário não tinha onde
escrever, porque o schema do tool não sabia expressar "além destas, o que
você quiser". Uma lista aberta por engano, no pior caso, oferece um campo a
mais. Os dois erros não custam a mesma coisa. Pelo mesmo motivo a descrição
do tool proíbe criar uma opção "Outro" dentro de options: o formulário
já a oferece sozinha, e duas escapatórias na mesma lista confundem.
allowOther só existe em select — em text/textarea o campo já é texto
livre, e o engine normaliza esses dois para false em vez de gravar estado
sem significado no event log. Na tela, escolher "Outra (escrever)" troca o
Select por um Input: o sentinela de interface (__outra__) nunca
viaja para o backend, e o que vai é o TEXTO digitado. O botão de envio
continua exigindo TODAS as perguntas preenchidas — AnswerStructuredQuestion UseCase recusa com 400 listando o que falta, então habilitar com campo vazio
só produziria um erro do servidor —, e estar em "Outra" com o texto ainda em
branco NÃO conta como preenchido.
O card também deixou de ser o único item do fio alinhado a nada: ele passa a
ser centralizado com o mesmo teto de 560px do ApprovalCard na variante
chat (RN-173) e ganha o avatar e a cor do agente, que é o que o
faz ler como FALA de alguém em vez de formulário órfão. Antes ele nascia
encostado à esquerda com teto de 480px, enquanto as bolhas começam 45px
adentro.
- Onde:
apps/engine/lib/engine/harness/tools/ask_structured_questions.ex(schema, validação enormalizar/1),apps/web/src/lib/api-types.ts(StructuredQuestion.allowOther),apps/web/src/routes/SessionPage.tsx(StructuredQuestionCard,OUTRA_RESPOSTA,permiteOutra),apps/web/src/routes/SessionPage.module.css(.structuredQuestionCard,.structuredQuestionCabecalho) - Teste:
apps/engine/test/engine/harness/tools/ask_structured_questions_test.exs(default aberto,falseexplícito,allowOthernão booleano recusado),apps/web/src/routes/SessionPage.perguntas-estruturadas.test.tsx(describe "saída por texto livre no select") - Origem: uso real no
exp001— "sempre dê a opção de input do usuário quando ele seleciona Escreva"
RN-175 — Toda resposta de agente diz com qual modelo foi gerada
agent.response carrega modelName nos três produtores do evento, e não
só nos quatro agentes conversacionais:
| produtor | quem passa por ali | antes |
|---|---|---|
os quatro conversacionais (criativo/po/arquiteto/dev_lead) |
chat | já gravava, desde a RN-146 |
Engine.Harness.ToolLoop |
TODO agente de execução e de gate — dev-*, QA, SecOps, Infra-Workflows, Psicólogo, Anamnese |
nunca gravou |
SendChatMessageUseCase (api) |
chat sem agente ativo, em que quem responde é o modelo | nunca gravou |
Nenhuma chamada nova: RunLlmTurnUseCase já devolve modelName no corpo e
StreamLlmTurnUseCase já o põe no frame final — o valor é o nome do modelo
do binding resolvido (model.name), não o eco do provider. Ele é null
quando o turno falhou antes de resolver o binding, e ausente em evento gravado
antes desta regra.
Na tela, o modelo deixou de ser a palavra solta modelo em mono 10px
--text-muted ao lado do nome do agente — que se lê como se o modelo se
CHAMASSE "modelo", e que reprova o contraste de texto de leitura. Virou um
chip com o ícone de modelo, --text-secondary sobre --surface-2, e o
rótulo de desconhecido passou a ser "modelo não registrado". A tela não
adivinha o modelo pelo binding ATUAL do agente quando o dado falta: atribuir a
uma resposta antiga um modelo que talvez nem existisse quando ela foi gerada
seria inventar procedência, e procedência inventada é pior que ausente — o
mesmo argumento do preço congelado em token_usage (RN-044).
Fora do escopo, declarado: agent.error continua sem o modelo nos quatro
servidores, mesmo quando a api o mandou no frame de erro (budget estourado, por
exemplo). É mudança de outro evento, com outra pergunta a responder.
- Onde:
apps/engine/lib/engine/harness/tool_loop.ex,apps/api/src/application/use-cases/llm/send-chat-message.use-case.ts,apps/web/src/routes/SessionPage.tsx(o chip emagent.response),apps/web/src/routes/SessionPage.module.css(.messageModelo) - Teste:
apps/engine/test/engine/harness/tool_loop_test.exs,apps/api/test/application/use-cases/llm/send-chat-message.use-case.spec.ts,apps/web/src/routes/SessionPage.arquiteto-modelo-icone.test.tsx,apps/web/src/design-contraste.test.ts(o par do chip) - Origem: uso real no
exp001— "PO não mostrou o modelo que estava utilizando; todos os agentes devem apresentar o seu modelo ao lado do nome"
Auditoria fluxo.yml × código — o plano do Dev Lead vira aprovação de verdade (RN-284, ADR 0086)
Não é fase planejada: é a correção de uma divergência que a auditoria de
packages/contracts/fluxo.yml × código encontrou (docs/explanation/auditoria-fluxo-vs-codigo.md,
achado A2) — o fluxo já declarava a saída plano-de-paralelismo do dev-lead
como via: proposed_action, e o código nunca foi ajustado para bater.
RN-284 — O turno do agente conversacional pode SUSPENDER esperando aprovação humana
Revisada pela RN-677 (ADR 0194):
propose_execution_planganhou pipeline de execução — aprovar o plano grava o módulo das tarefas (RN-678) e ATIVA a execução; a ação terminaexecutedoufailed, e é esse desfecho que o Dev Lead lê ao retomar. O item 1 abaixo ("não tem execute-* pipeline") deixou de valer para este tipo.
Revisada pela RN-578 (ADR 0163): o turno de TODO conversacional deixou de segurar o
GenServer.call— ofromé respondido no aceite, e a suspensão aqui descrita mudou só o FECHO do turno (awaiting_approvalem vez deagent.done), não mais o momento da resposta. Auser_messagerecusada durante a suspensão passa a ser 409 no clique.
Primeira vez que um agente conversacional (Criativo, PO, Arquiteto, Dev Lead —
todos rodam turno síncrono via GenServer.call de até 180s, mediado por
Engine.Agents.TurnoAssincrono) suspende esperando uma decisão humana. O
padrão já existia para o dev agent (RN-073, ADR 0052) e os gates de
QA/Infra (ADR 0057), mas os dois são disparados por cast e nunca esperavam
resposta síncrona — é exatamente esse ponto que este mecanismo resolve, e o
teto de paralelismo que fez o Dev Lead existir (RN-083) é a razão
de a primeira aprovação suspensa ser a dele.
O mecanismo, em quatro peças:
Engine.Agents.DevLeadTools.run/2(apps/engine/lib/engine/agents/dev_lead_tools.ex:81-107) chamaEngineApiClient.propose_action/5em vez deappend_event/3, e devolve{:ok, texto}(statusexecuted/auto_approved/approved— os três contam como sucesso, porquepropose_execution_plannão tem execute-* pipeline própria e a aprovação manual fica em"approved"para sempre),{:pending, action_id}ou{:error, texto}.Engine.Agents.DevLeadServer.run_turn/2(apps/engine/lib/engine/agents/dev_lead_server.ex, bloco de despacho detool_calls) usaEnum.reduce_whilepara PARAR no primeiro:pending, sem processar as chamadas seguintes nem recursar — e devolve ostatecom a chave:aguardando_aprovacaosetada (%{action_id:, tool_call_id:, tool_name:, remaining:}, oremainingjá descontando a iteração suspensa contra o teto). A mensagemrole: "tool"NÃO entra emstate.messagesnesse momento — gravar "pending" ali mentiria pro modelo que o comando já respondeu isso (mesmo raciocínio do dev agent,Engine.Harness.Hooks.ActionPipeline).Engine.Agents.TurnoAssincrono.tratar_resultado/2(apps/engine/lib/engine/agents/turno_assincrono.ex) responde aofromdo mesmo jeito e na mesma hora de sempre — é o que rompe o bloqueio síncrono no momento certo —, mas em vez definalizar/1(que emiteagent.doneeagent.status: idle) chamasuspender/1: sóagent.status: awaiting_approval, semagent.done, porque o turno não terminou. A checagem é pelo VALOR da chave (Map.get/2, truthy), nunca pela presença dela — o Dev Lead carregaaguardando_aprovacao: nildesde oinit/1, então a chave em si está sempre presente.Engine.Sessions.LiveBroadcast.agent_status/4(apps/engine/lib/engine/sessions/live_broadcast.ex:38-39) ampliou a guarda de["working", "idle"]para incluir"awaiting_approval"— sem isto oagent.statusdo passo 3 nem seria persistido.
A retomada. Engine.Agents.DevLeadServer assina
Engine.Dev.Wake.subscribe(project_id, "dev-lead") no init/1 — o MESMO
módulo que Engine.Gates.QaLeadServer já reusa para os subagentes de QA,
apesar do nome ser "dev": a entrega de {:action_settled, ...} é por AGENTE,
roteada pelo agentId do payload (apps/engine/lib/engine/workers/dev_agent_wake_worker.ex),
não por tipo de agente. Quando a decisão chega, um handle_info monta a
mensagem role: "tool" com o resultado REAL (texto_do_desfecho/1, mesmo
vocabulário do dev agent e do QaLeadServer), zera aguardando_aprovacao e
retoma com TurnoAssincrono.iniciar(state, nil, fn -> run_turn(state, pendente.remaining) end).
Enquanto suspenso, uma segunda user_message não inicia turno novo — um
guard em handle_call({:user_message, _text}, _from, %{aguardando_aprovacao: %{}}), testado ANTES da cláusula genérica, emite agent.error (origem
politica) explicando que há uma decisão pendente em Aprovações, sem subir
task nenhuma.
Lacuna aceita, declarada — restart durante a espera. Ao contrário do dev
agent (que reidrata laco_pendente via handle_continue no init/1, ADR
0052), o Dev Lead NÃO reidrata aguardando_aprovacao — é só em memória. Se o
engine reiniciar enquanto ele está suspenso, a decisão continua registrada e
visível em Aprovações (é durável na api), mas o Dev Lead não narra o desfecho
automaticamente: o processo que assinou o Wake morreu, e o próximo restart
sobe um Dev Lead novo, sem inscrição para aquela ação. Fechar isto exigiria o
mesmo mecanismo do ADR 0052 — fora do escopo desta mudança, que só faz o
comportamento bater com o que packages/contracts/fluxo.yml já declarava.
- Onde:
apps/engine/lib/engine/agents/dev_lead_tools.ex,dev_lead_server.ex;apps/engine/lib/engine/agents/turno_assincrono.ex(compartilhado pelos quatro conversacionais);apps/engine/lib/engine/sessions/live_broadcast.ex;apps/api/src/domain/actions/decide.ts(propose_execution_plan) - Teste:
apps/engine/test/engine/agents/dev_lead_tools_test.exs,dev_lead_server_test.exs(describe "suspensão em aprovação"),turno_assincrono_test.exs(describe "resultado com :aguardando_aprovacao"),live_broadcast_test.exs,wake_do_outbox_ao_dev_lead_test.exs(a corrente INTEIRA — outbox → drain → worker →Engine.Dev.Wake→ o processo do Dev Lead, mesmo padrão deEngine.Dev.WakeDoOutboxAoAgenteTest),apps/api/test/domain/actions/decide.spec.ts(describe "plano de execução do Dev Lead") - ADR: 0086 (revisa parte do ADR 0053; precedente direto: ADR 0052 e ADR 0057)
UX Designer — o quinto agente conversacional (RN-285..287, ADR 0087)
Não é gatilho de separação disparado (packages/contracts/fluxo.yml sempre declarou "quando
o projeto GERENCIADO tiver interface própria a desenhar" como critério) — é
decisão consciente do dono do produto de antecipar o papel. teste-de- usabilidade (exige usuário humano real) fica fora de alcance; metricas-de- uso segue lacuna, porque depende do papel analytics, que continua
proposto.
RN-285 — O UX Designer é conversacional SOLO, sem área
Engine.Agents.UxDesignerServer (apps/engine/lib/engine/agents/ux_designer_server.ex:40)
espelha Engine.Agents.ArquitetoServer/DevLeadServer: GenServer por sessão,
laço bounded de tool use próprio com teto 14 (mesmo calibre de Arquiteto/Dev
Lead — agente de raciocínio, não conversação leve como Criativo/PO). Ativado
por handoff accepted endereçado a "ux-designer" — mecanismo GENÉRICO já
existente (ActivateAgentUseCase/canActivateAgent na api não ganharam
linha nenhuma: qualquer agente com handoff aceito já é ativável).
O kickoff (build_kickoff/1, ux_designer_server.ex:222) lê a
artifact.product_brief mais recente da sessão — a MESMA "necessidade de
negócio" que o Criativo produz, sem artefato novo. O sistema de design
(design/tokens.css, design/COMPONENTS.md) é DESCRITO na identidade
(Engine.Harness.Agents, entrada "ux-designer"), texto estático, porque os
agentes conversacionais não têm ferramenta de leitura de arquivo do repo — é
a única camada do prompt presente em TODO turno, não só no kickoff.
Sem área, sem subagentes: packages/contracts/fluxo.yml já classificava o papel como
camada_produto, ao lado de Criativo e PO, nenhum dos quais tem área.
RN-286 — propose_prototype grava artefato sem tabela e sem caso de uso dedicado, e oferece DOIS handoffs sobre o MESMO artefato
artifact.prototipo_navegavel segue o desenho sem tabela de
artifact.project_image/artifact.c4_diagram (RN-149) — o event
log é o registro —, mas por um caminho DIFERENTE do que os dois usam.
choose_project_image/create_c4_diagram precisam de caso de uso NA API
(DecidirImagemDoProjetoUseCase/CreateC4DiagramUseCase) porque têm
conteúdo DERIVADO de outro artefato (o Container level vem do module_map
vigente) ou recusa de domínio compartilhada por mais de um consumidor (teto
de recursos da imagem). propose_prototype
(Engine.Agents.UxDesignerTools.run/2,
apps/engine/lib/engine/agents/ux_designer_tools.ex:122) não tem nenhum dos
dois motivos — é conteúdo AUTOCONTIDO que só o próprio UX Designer escreve e
só ele lê de volta —, então a validação de FORMA mora no engine
(Engine.Harness.ArtifactSchemas, tipo "prototipo_navegavel",
artifact_schemas.ex:49/135 — personas e jornadas não-vazias, ao menos uma
tela) e a gravação usa o caminho GENÉRICO que a api já expõe para qualquer
tipo de evento (EngineApiClient.append_event_returning/3), o mesmo
mecanismo do artifact.product_brief do Criativo. Nenhuma rota nova na api.
Depois de gravar, gravar_e_ofertar_handoffs/2 (ux_designer_tools.ex:149)
oferece DOIS handoffs sobre o MESMO artefato — create_handoff para "po" e
para "dev-lead", os dois com o artifactId do protótipo. Nunca um segundo
artefato para "spec-visual" (packages/contracts/fluxo.yml): o PO lê resumo/prototipo
para desenhar o backlog, o Dev Lead lê as MESMAS telas/anotacoes como
referência visual de implementação — duplicar o conteúdo arriscaria as duas
cópias divergirem na revisão seguinte, o mesmo argumento por trás do C4 não
redigitar o module_map.
Falha ao ofertar UM dos dois handoffs não desfaz o artefato já gravado nem o outro handoff (RN-116) — o motivo volta como texto do tool-result, entrada do laço e não fim de linha (RN-163): o modelo lê e pode reportar ao usuário na resposta seguinte.
Um propose_prototype BEM-SUCEDIDO encerra o turno
(ux_designer_server.ex:120, mesma guarda de propose_execution_plan no Dev
Lead) — sem isso o laço voltaria ao modelo, que poderia propor de novo e
produzir dois protótipos com o mesmo total. Só existe UMA ferramenta aqui
(não há um segundo tool call a encadear como no Arquiteto), então "para no
primeiro sucesso" não perde nada.
Staff: código pronto, dormente para disparo automático (RN-305/RN-306, ADR 0088)
Não é fase planejada: packages/contracts/fluxo.yml declara o Staff/Principal Engineer como
status: planned desde o ADR 0085 ("contrato pronto, ativação decidida,
aguarda gatilho"). O dono do produto decidiu antecipar o CÓDIGO mesmo sabendo
que o gatilho automático (a Anamnese notando um problema sistêmico
RECORRENTE) não vai disparar — a Anamnese está pausada
(ANAMNESE_ENABLED=false, decisão de produto de 2026-08-10). Ver a
pendência já documentada em RN-086: o mesmo sinal que faria a Anamnese
propor subir o teto de paralelismo é o que faria ela propor um handoff ao
Staff, e nenhum dos dois dispara enquanto ela estiver pausada.
RN-305 — O Staff ativa pelo caminho GENÉRICO de handoff, sem USER_STARTED_AGENTS, e sem kickoff/1
USER_STARTED_AGENTS (apps/api/src/domain/sessions/agent-activation.ts) é
a exceção do Criativo (inicia SEM handoff, por comando do usuário) — o Staff
NÃO entra nela. Investigação confirmou que canActivateAgent já ativa
qualquer agente com handoff accepted endereçado a ele, o mesmo caminho que
já vale para dev-lead/arquiteto/infra; e assertHandoffTargetAllowed
(apps/api/src/domain/agents/agent-areas.ts) só recusa handoff endereçado a
SUBAGENTE de área — o Staff não tem área, então nenhuma mudança de domínio
na api foi necessária. "Acionável manualmente" significa que a MECÂNICA de
domínio permite (qualquer agente pode chamar
EngineApiClient.create_handoff(..., "staff", ...), e um humano aceita pela
rota já existente), não que existe hoje uma tela dedicada para escolher
"endereçar handoff ao Staff" — a UI genérica de handoff a agente à escolha
segue no backlog (docs/explanation/backlog.md), como já estava antes desta
mudança.
Engine.Agents.StaffServer (apps/engine/lib/engine/agents/staff_server.ex)
é o quinto agente conversacional solo (junto de Criativo, PO, Arquiteto, Dev
Lead), espelhando o Arquiteto — GenServer por sessão, rehydration do event
log, laço bounded de tool use com teto 14 (staff_server.ex:49) — mas sem
kickoff/1: os outros leads sintetizam uma instrução de abertura a partir
de um artefato anterior no event log da sessão (product_brief, module_map,
backlog); o Staff não tem essa fonte, porque o problema sistêmico nasce de
fora da sessão. StaffSupervisor.start_agent/2 sobe o processo (rehidrata o
histórico) e ele fica ocioso até a primeira user_message
(staff_server.ex:60) — que é como quem endereçou o handoff explica o
problema. apps/engine/lib/engine_web/controllers/agent_command_controller.ex:60
(cláusula start/2 de "staff") nunca chama kickoff (a função nem
existe), ao contrário de po/arquiteto/dev-lead/infra.
- Onde:
apps/api/src/domain/sessions/agent-activation.ts(canActivateAgent,USER_STARTED_AGENTS— intocado),apps/api/src/domain/agents/agent-areas.ts(assertHandoffTargetAllowed— intocado),apps/engine/lib/engine/agents/staff_server.ex,staff_supervisor.ex,apps/engine/lib/engine_web/controllers/agent_command_controller.ex(cláusulasstart/2/message/2/via_for/2de"staff"),apps/engine/lib/engine/application.ex(Engine.Agents.StaffSupervisorna árvore de supervisão) - Teste:
apps/engine/test/engine/agents/staff_server_test.exs(rehydration, ausência de kickoff automático — o turno só roda poruser_message) - ADR: 0088
RN-306 — propose_rfc grava o artefato DIRETO e devolve o handoff no MESMO tool call, sem proposed_action
Engine.Agents.StaffTools.propose_rfc
(apps/engine/lib/engine/agents/staff_tools.ex:41) é a única ferramenta do
Staff: problema, opções com trade-offs, recomendação e o escopo de uma PoC
DESCARTÁVEL (descartavel: true é FIXO — nunca escrito pelo modelo). run/2
(staff_tools.ex:89) grava artifact.rfc_staff via
EngineApiClient.append_event_returning/3 — mesmo padrão SEM tabela e SEM
caso de uso dedicado de Engine.Harness.Tools.EmitInsight (o emit_insight
do Arquiteto), e não o de artifact.c4_diagram (ADR 0068), que tem caso de
uso próprio na api porque DERIVA o nível Container do module_map vigente —
o RFC não deriva nada do lado de lá, todo o payload vem do tool call.
Depois de gravar, o MESMO run/2 chama
EngineApiClient.create_handoff(..., "staff", "arquiteto", artifact_id) —
sem confirmação humana no meio, mesmo padrão de
CriativoServer.executar_confirm_readiness/1 emitindo o product_brief e
oferecendo o handoff ao PO na mesma resposta. propose_rfc NÃO é
proposed_action: registrar um documento de arquitetura não é efeito
externo (não é git, terminal, nem gasto de agente) — a decisão real
(adotar, adaptar, recusar a recomendação) é do Arquiteto, no handoff que a
ferramenta já devolve. Falha ao criar o handoff NÃO derruba o processo nem
perde o RFC já gravado (RN-116) — o motivo entra no tool-result, e o modelo
sabe que precisa tentar de novo só o handoff, não reescrever o RFC.
- Onde:
apps/engine/lib/engine/agents/staff_tools.ex - Teste:
apps/engine/test/engine/agents/staff_server_test.exs("propõe o RFC, grava o artefato e devolve o handoff ao arquiteto"; "propose_rfc com opções vazias vira tool-result de erro"; "falha ao devolver o handoff ao arquiteto NÃO derruba o processo") - ADR: 0088
Declarado, não escondido: a roster do painel do time
(apps/web/src/lib/agent-status.ts, staffActive) e o card do dashboard
(ProjectCardSummary.roster, apps/api/.../projects-summary.repository.ts)
mostram o Staff quando ativo, mesmo critério de infraActive — evitando a
divergência que o comentário de RosterFacts já alertava. SessionPage.tsx
NÃO foi tocado: staff fica fora de AGENTES_DE_CHAT, o mesmo padrão já
aceito para infra (um lead REAL e ATIVO também fora dessa lista — o
handoff para ele "nunca é aceito por AQUI", conforme o comentário do próprio
arquivo). O caminho ponta a ponta de uso hoje é a rota interna
(POST .../agent/message, agent: "staff"), não a tela de Sessão.
O appsec ganha o segundo momento do secops (RN-360/361, ADR 0090)
packages/contracts/fluxo.yml já declarava o id: appsec como proposto, com o critério
de separação escrito por antecipação: "mesmo padrão do QA: dois MOMENTOS, não
dois agentes por ora". Esta mudança constrói esse segundo momento — threat
model de DESIGN, ANTES de existir código/PR — decisão consciente do dono do
produto de antecipar a ativação, sem esperar o gate implementavel que o
próprio registro citava como gatilho.
RN-360 — O threat model de DESIGN roda sobre story + module_map, sem worktree/task_id, no MESMO processo do secops
Quem roda o appsec é o MESMO Engine.Gates.SecOpsAgentServer que já existe
para o veredito determinístico de PR — não um processo novo. run_design/2
(apps/engine/lib/engine/gates/secops_agent_server.ex:76) é um GenServer.cast
para a mesma chave de Registry ({project_id, "secops"}) que run/2 já
usa; o handle_cast({:run_design, story_id}, state)
(secops_agent_server.ex:93) busca o contexto por
Engine.Gates.AppSecContextBuilder.fetch/2
(apps/engine/lib/engine/gates/appsec_context_builder.ex:31) — a story no
backlog do projeto (EngineApiClient.list_backlog/1, a MESMA leitura que a
RN-164 deu ao PO) e o module_map vigente da sessão que criou a story
(EngineApiClient.get_infra_context/2, a MESMA leitura sem task/story que a
área de Infra já faz) — e então chama Engine.Gates.AppSecAgent.run/3
(apps/engine/lib/engine/gates/appsec_agent.ex:47).
AppSecAgent é módulo SEM ESTADO (não é GenServer, mesma forma de
QaPerformanceSegurancaAgent), com registro de ferramentas SEM Terminal —
[ReadFile, SearchWorkspace, EmitThreatModel] — rodando um checklist
STRIDE-lite (Spoofing/Tampering/Repudiation/Information disclosure/Denial of
service/Elevation of privilege) via Engine.Harness.ToolLoop.run/1. A
diferença estrutural do "segundo momento sem Terminal, sem task_id" para o
padrão de gate anterior: nenhum dev_state/worktree_path entra no ctx —
ReadFile/SearchWorkspace degradam para o fallback de
Engine.Actions.Workspace.workspace_dir/1 (o checkout COMPARTILHADO do
projeto, não um worktree de agente), e session_id vem de
story["sessionId"], nunca de um dev_state. EmitThreatModel
(apps/engine/lib/engine/gates/tools/emit_threat_model.ex) não tem veredito
approved/changes_requested — o appsec sempre TERMINA registrando o
threat model, nunca aprovando/reprovando nada —, e por isso não reaproveita
Engine.Gates.Hooks.Termination (a forma extraída é outra): tem hook
PRÓPRIO, Engine.Gates.Hooks.AppSecTermination.
Terminado com sucesso, run_appsec_design/3
(secops_agent_server.ex:234) emite artifact.threat_model
(storyId/threatModel/requisitosDeSeguranca/riscos, schema em
apps/engine/lib/engine/harness/artifact_schemas.ex:71 — riscos fica de
fora das chaves obrigatórias porque lista vazia é resposta válida). Falha
(teto de iterações, orçamento, ou o modelo parando sem chamar
emit_threat_model) vira agent.error durável com origem
(modelo/politica/infra), mesma régua da RN-059 — nunca resposta vazia
nem silêncio só em broadcast.
O gatilho automático existe desde a RN-539. Esta entrega foi
mantida autocontida de propósito (nenhum arquivo de outra frente tocado:
decide.ts, apps/api/config/gates.yml e dev_lead_tools.ex intocados), e declarava a
lacuna: run_design/2 era ACIONÁVEL e nada acionava sozinho. O ponto de
disparo que ela nomeava — assess_implementability do Dev Lead, frente
qa-estrategia, gate implementavel, mesmo ADR 0090 — passou a ser o
chamador de verdade, em paralelo e uma vez só por story.
- Onde:
apps/engine/lib/engine/gates/secops_agent_server.ex(run_design/2,handle_cast/2,run_appsec_design/3,emit_threat_model/3,emit_bloqueio_appsec/3);apps/engine/lib/engine/gates/appsec_agent.ex;apps/engine/lib/engine/gates/appsec_context_builder.ex;apps/engine/lib/engine/gates/tools/emit_threat_model.ex;apps/engine/lib/engine/gates/hooks/appsec_termination.ex;apps/engine/lib/engine/harness/artifact_schemas.ex(schemathreat_model) - Teste:
apps/engine/test/engine/gates/appsec_agent_test.exs,apps/engine/test/engine/gates/appsec_context_builder_test.exs,apps/engine/test/engine/gates/secops_agent_server_test.exs(os três testes derun_design) - Origem: ADR 0090
RN-361 — O threat model concluído cria TRÊS handoffs, sempre endereçando o LEAD
criar_handoffs_appsec/3 (secops_agent_server.ex:326) cria um handoff por
alvo declarado em packages/contracts/fluxo.yml (saidas do appsec): arquiteto,
dev-lead e infra — mesmo padrão de
OfferInfraHandoffUseCase/ArquitetoServer.executar_offer_infra_handoff/1
(chamadas SEPARADAS, uma por alvo, para uma falha de handoff não desfazer os
outros dois já criados). O id do fluxo é area-infra, mas o AGENTE
endereçável é "infra" (apps/api/src/domain/agents/agent-areas.ts —
lead: 'infra'), nunca area-infra: handoff externo endereça só o LEAD de
área (ADR 0038), e CreateHandoffUseCase.assertHandoffTargetAllowed
recusaria um toAgent que não resolve a um lead/agente-sem-área. Falha de UM
alvo vira agent.error narrado por alvo (RN-116) — os outros dois handoffs
já criados não são desfeitos.
Desde a RN-636 os três são PERGUNTADOS, não mais sempre criados: o destino que já tem oferta pendente ou já está ativo no projeto não recebe outra, e isso não é falha.
- Onde:
apps/engine/lib/engine/gates/secops_agent_server.ex:55(@appsec_handoff_targets),:377(criar_handoffs_appsec/3) - Teste:
apps/engine/test/engine/gates/secops_agent_server_test.exs("run_design: threat model concluído emite artifact.threat_model e cria os TRÊS handoffs") - Origem: ADR 0090
"N agentes online" no dashboard — status AO VIVO, nunca presença histórica (RN-409, ADR 0097)
RN-414 — rag_search: agentes ganham a ferramenta que o produto nunca tinha exposto
grep -rn "rag" apps/engine/lib dava ZERO ocorrências antes desta
entrega — o RAG completo (pgvector, busca híbrida) só era consumido pela
aba web "Chat RAG". A tool nova rag_search
(apps/engine/lib/engine/harness/tools/rag_search.ex, categoria
:direct, leitura não é efeito externo) chama POST /internal/rag/search
— rota nova que REUSA HybridSearchUseCase sem duplicar a lógica de
busca — e devolve hits formatados com citação (path + trecho), sempre
com degraded: true visível no INÍCIO do texto quando o embedding não
estava disponível (nunca escondido pelo corte de teto).
Tetos próprios, no espírito da RN-150: top_k clampado a 10 dentro da
própria tool (não confia no que a api aceitaria), teto de 16 KiB no texto
formatado (menor que os 32 KiB de search_workspace/read_file — cada
hit de RAG já é chunk+excerpt inteiro). Falha de rede vira erro legível
ao modelo, nunca crash do ToolLoop (RN-163).
Registrada em Engine.Harness.Tools (PO/Arquiteto/conversacionais) e
Engine.Dev.Tools (dev agent); estendida também aos gates de leitura que
já citam ADR/convenção indexada (QA-automação, QA-estratégia, AppSec,
QA-performance/segurança) — não a Infra.WorkflowsAgent nem
Psicólogo/Anamnese, que raciocinam sobre event log, não sobre docs/código
do projeto.
- Onde:
apps/engine/lib/engine/harness/tools/rag_search.ex;apps/engine/lib/engine/sessions/engine_api_client.ex(rag_search/4);apps/api/src/interfaces/http/internal/internal-rag.controller.ts - Teste:
rag_search_test.exs(hits formatados com citação; degradação visível; falha de rede sem crash; teto de bytes truncando com marca clara; clamp detop_k);engine_api_client_rag_test.exs - Origem: ver ADR 0100
RN-417 — Psicólogo e Anamnese consultam por relevância, com degradação declarada para recência
Psychologist.ContextBuilder/Anamnese.ContextBuilder continuam lendo o
que sempre leram (eventos recentes / janela temporal), e ganham uma
SEGUNDA fonte, EngineApiClient.rag_search/4, com uma query derivada do
GATILHO da análise — a causa de término já classificada, no Psicólogo;
competências do catálogo ainda sem current_profile, na Anamnese
(NUNCA texto livre de hipótese/racional, pela proibição já estabelecida
de a Anamnese jamais inferir saúde/personalidade/idade/gênero — a query
só contém nomes de competência/membro/projeto). Os hits entram no
orçamento EXISTENTE de Triage (max_prompt_events/max_payload_chars)
— descontam vagas da janela de recentes, nunca somam por fora dela.
A chamada ao RAG é estritamente ADITIVA: sem hit (RAG indisponível, erro,
ou simplesmente sem resultado), o comportamento é IDÊNTICO ao de antes
desta RN — nenhum teste pré-existente precisou mudar. degraded: true
(RAG caiu pra léxico-only por falta de embedding) aparece EXPLICITAMENTE
no contexto final, nos dois agentes — nunca escondido pelo corte de
teto.
Os kickoffs de Psicólogo (psychologist-kickoff) e Anamnese
(anamnese-kickoff) passam a resolver como TEMPLATE do grafo
(EngineApiClient.get_prompt_template/2) quando graph_templates_enabled?
está ligada (GRAPH_TEMPLATES_ENABLED, default false) — com fallback
obrigatório pro texto inline em qualquer falha (api fora, template não
semeado, flag desligada). :pinned => true continua igual nos dois
caminhos.
O TERCEIRO consumidor da mesma flag é o prompt de sumarização do
ContextManager (context-manager-summarize, o quarto e último template da
leva da RN-413 — era o único ainda sem consumidor, lacuna que a
ADR 0101
declarou aberta e que fecha aqui). Mesma régua, byte a byte: flag ligada
tenta o template e substitui {{turnos}} pelo MESMO corpo <role>: <content> que a trilha inline já montava; qualquer desfecho que não seja
{"body" => corpo} com corpo binário não-vazio cai no inline — SEM erro e
SEM log de erro, porque flag desligada, template ainda não semeado e api do
grafo fora do ar são o MESMO caminho de degradação. Com a flag desligada (o
default) a api nem é chamada. O render é função privada PRÓPRIA, não um
helper compartilhado: o conjunto de placeholders é do template, não do
mecanismo — cada um dos três consumidores tem o seu. E o fallback
determinístico de quando o MODELO falha ("(N turnos anteriores omitidos)") é outra coisa e NÃO passa por template nenhum: é comportamento
de código, não texto de prompt, e o próprio .md do template declara isso.
Desde a RN-621 o prompt de resumo, nas duas trilhas, pede que cada turno mantenha o idioma original e que citação e código não se traduzam; o template versionado subiu para a versão
"2".
Consumo do restante do grafo (query_user_context — hipóteses com
evidência e perfis lidos DIRETO do Neo4j) fica DECLARADO fora desta
entrega: ainda sem rota HTTP exposta do lado api; Psicólogo/Anamnese
hoje só consultam o RAG (pgvector) via rag_search, não o grafo de
relações em si.
- Onde:
apps/engine/lib/engine/psychologist/context_builder.ex,apps/engine/lib/engine/workers/psychologist_worker.ex;apps/engine/lib/engine/anamnese/context_builder.ex,apps/engine/lib/engine/workers/anamnese_worker.ex;apps/engine/lib/engine/harness/context_manager.ex(prompt/1,render_template/2,prompt_inline/1) - Teste:
context_builder_test.exsdos dois agentes (hits presentes; falha do RAG degrada sem erro;degraded: truevisível; clamp detop_k; query derivada do gatilho);psychologist_worker_test.exs/anamnese_worker_test.exs(template com sucesso e com fallback;:pinnedidêntico nos dois caminhos; flag desligada nunca chama a api);context_manager_test.exs(template com{{turnos}}substituído;{:error, :not_found}cai no inline e a compactação termina normal; flag desligada nunca chamaget_prompt_template) - Origem: ver ADR 0101
RN-419 — O runner local se autentica por ticket de uso único, escopado a projeto e papel
REVISADA pela RN-421 (ADR 0104): a condição de recusa do
ticket citada abaixo (workspaceMode !== 'local') virou executionMode !== 'runner' — mesma régua, nome novo. O resto desta entrada continua
valendo tal como está.
O canal terminal:<projectId> (socket Phoenix novo, /runner) recebe
dois papéis distintos — :runner (o CLI na máquina do usuário, no
máximo UM por projeto, exclusividade garantida por
:global.register_name/3) e :web (a aba Terminal, múltiplos
simultâneos) — e os dois entram autenticados por TICKET DE USO ÚNICO,
mesmo padrão de segurança da RN-108 (ticket de socket de sessão), com uma
inversão de propriedade: o ticket é EMITIDO PELO ENGINE
(runner_socket_tickets, schema "engine", migration Ecto própria — não
uma extensão da tabela de ticket de sessão, que é da api), e a API o pede
via rota HTTP interna nova (POST /internal/projects/:projectId/runner-tickets)
— o inverso do fluxo de ticket de sessão, onde a api grava direto na
própria tabela.
POST /projects/:projectId/runner-ticket (role developer+, recusa se
workspaceMode !== 'local') e POST /projects/:projectId/terminal-ticket
(role viewer+, qualquer modo) são as duas rotas públicas que emitem o
ticket pro cliente certo — o runner pede a primeira, a web pede a
segunda. Ticket consumido (validação + UPDATE condicional de uso
único) no join do canal; ticket de OUTRO projeto ou reusado nunca abre
o tópico.
- Onde:
apps/engine/lib/engine/runners/socket_ticket.ex,apps/engine/lib/engine/runners/registry.ex,apps/engine/lib/engine_web/channels/{runner_socket,terminal_channel}.ex;apps/api/src/interfaces/http/runner/runner-tickets.controller.ts,apps/api/src/application/use-cases/runner/request-runner-ticket.use-case.ts - Teste:
runner_socket_test.exs/terminal_channel_test.exs(join com ticket válido sucede; ticket reusado recusa; ticket de outro projeto não abre o tópico errado; segundo:runnerno mesmo projeto é recusado; um segundo runner consegue conectar depois que o primeiro cai);request-runner-ticket.use-case.spec.ts/runner-tickets.controller.spec.ts(recusa pra projeto não-localno ticket de runner; ticket de terminal funciona pra qualquer modo) - Origem: ver ADR 0103
RN-420 — Comando de agente roteado ao runner passa pelo MESMO pipeline de aprovação; PTY é ação do usuário, auditada
REVISADA pela RN-423 (ADR 0104): a condição pra rotear deixou
de ser binária. workspace_mode == "local" virou execution_mode == "runner" E workspace_verified_at não-nulo E runner conectado —
faltando qualquer uma das TRÊS, o comando é RECUSADO explicitamente, nunca
cai no System.cmd/bind-mount de mounted (que não existe pra um projeto
runner, sem bind-mount nenhum). O resto desta entrada — o pipeline de
aprovação de sempre, PTY como ação do usuário — continua valendo tal como
está.
Engine.Actions.TerminalExecutor decide rotear um comando pro runner (em
vez do System.cmd de sempre, dentro do container) DEPOIS que o
pipeline normal (decide()/proposed_action) já aprovou — o roteamento
é só uma escolha de DESTINO pro mesmo comando já autorizado, nunca um
segundo caminho de execução que escapa da política (que continua com os
tetos absolutos da RN-418 valendo igual, sudo/git incluídos). Condição
pra rotear (histórico, ver revisão acima): workspace_mode == "local" E
runner conectado (Engine.Runners.Registry.connected?/1) — sem qualquer
uma das duas, o comportamento de sempre continua (System.cmd no
container via bind-mount, ADR 0072 vira FALLBACK, nunca removido).
PTY interativo é DIFERENTE: é ação do USUÁRIO autenticado digitando no
terminal da própria máquina, não do agente — por isso NÃO passa por
proposed_action. Mas precisa deixar rastro: pty_open/pty_close
vindos da web emitem terminal.session.started/terminal.session.ended
no event log (endereçados à sessão mais recente do projeto,
ProjectSession.latest_id/1, mesmo mecanismo que a Anamnese já usa pra
narrar algo project-scoped). Achado real na consolidação: sem runner
conectado, a web precisa de um pty_error EXPLÍCITO de volta — descartar
o pedido só com um log no servidor deixaria a aba presa em "carregando"
pra sempre, nunca alcançando o estado "sem runner" que a tela já sabia
mostrar.
- Onde:
apps/engine/lib/engine/actions/terminal_executor.ex,apps/engine/lib/engine/runners/runner_router.ex,apps/engine/lib/engine_web/channels/terminal_channel.ex - Teste:
terminal_executor_test.exs(roteia pro runner com modolocal+ runner conectado;cwdpassa adiante; cai no caminho atual sem runner conectado; nunca roteia em modocontainer);terminal_channel_test.exs(pty_opensem runner conectado devolvepty_errorpra web, nunca fica sem resposta) - Origem: ver ADR 0103
O explorador de pasta do Runner vira três colunas, e a criação de um projeto runner deixa de esperar o passo final (RN-436/436)
RN-454 — A aba Insights sabe que o Psicólogo está pausado ANTES de o usuário esbarrar no 503
Achado por USO: a aba Insights, com zero hipóteses, mostrava "Sem hipóteses
ainda — o Psicólogo analisa cada sessão encerrada" mesmo com
PSYCHOLOGIST_ENABLED=false — a mesma frase que aparece quando o Psicólogo
está ATIVO e só ainda não rodou. As duas situações são indistinguíveis pelo
texto, o que é a mesma classe de defeito que a RN-088/RN-107 já fecharam
para outras telas: um estado que existe e o produto sabe, mas não mostra.
A RN-117 já cobria a descoberta da pausa, mas só no CLIQUE de
"Reanalisar" (503 → PsychologistDisabledError) — e esse botão só existe
na faixa de análises, que só aparece quando runs.length > 0. Uma sessão
sem hipótese nenhuma nunca chega perto dele, então a pausa era invisível
justamente na tela vazia.
GET /internal/psychologist/status (engine, PsychologistCommandController.status/2)
é leitura pura de PsychologistWorker.enabled?/0 — SEM efeito colateral,
diferente de /reanalyze, que cria um job quando ativado. A api expõe
GET /projects/:projectId/psychologist/status (GetPsychologistStatusUseCase,
role:viewer) por cima disso — projeto na URL só por consistência com as
rotas irmãs (hypotheses, psychologist/analyses); a flag em si é GLOBAL,
como a RN-117 já registra. ProjectInsightsTab.tsx consome essa leitura
(usePsychologistStatus) e escolhe a frase do estado vazio por ela: pausado
mostra "O Psicólogo está pausado — nenhuma sessão é analisada até ser
reativado" (insights.projectInsightsTab.emptyPaused); do contrário, mantém
a frase original, que É honesta quando a feature está de fato ativa. O
aviso persistente (pausedNotice) e os botões de "Reanalisar" também
passaram a refletir essa leitura proativa, e não só o useState descoberto
pelo 503 — que continua existindo, como reforço, para o caso raro de a
flag mudar EM VOO entre a leitura de status e o clique.
- Onde:
apps/engine/lib/engine_web/controllers/psychologist_command_controller.ex(status/2),apps/engine/lib/engine_web/router.ex(GET /internal/psychologist/status),apps/api/src/application/ports/api-to-engine-client.port.ts(getPsychologistStatus),apps/api/src/infrastructure/http-clients/api-to-engine-client.ts,apps/api/src/application/use-cases/execution/get-psychologist-status.use-case.ts,apps/api/src/interfaces/http/psychologist/psychologist.controller.ts(status),apps/web/src/lib/api-client.ts(getPsychologistStatus),apps/web/src/lib/hooks.ts(usePsychologistStatus),apps/web/src/routes/ProjectInsightsTab.tsx - Teste:
apps/engine/test/engine_web/controllers/psychologist_command_controller_test.exs(status/2,enabled: true/false),apps/api/test/infrastructure/http-clients/api-to-engine-client.spec.ts(getPsychologistStatus),apps/api/test/application/use-cases/execution/get-psychologist-status.use-case.spec.ts,apps/web/src/routes/ProjectInsightsTab.test.tsx - Borda: a flag continua GLOBAL (ver RN-117) — a rota da api aceita
projectIdna URL só para bater com o padrão das rotas irmãs do controller, e não porque a resposta varia por projeto. - Origem: achado por USO real navegando a aba Insights com
PSYCHOLOGIST_ENABLED=false, não roteiro. Sem ADR — extensão pontual do mecanismo de leitura já existente da RN-117, mesmo padrão da RN-088/ RN-107 para o resto do produto.
Pasta local anexada vira o quarto escopo do índice RAG, lido pelo NAVEGADOR (RN-455..457, ADR 0113)
RN-459 — Teto de iterações não termina mais calado em quatro dos seis agentes conversacionais
Achado durante a investigação da faixa de atividade do turno (RN-460):
arquiteto_server.ex, dev_lead_server.ex, ux_designer_server.ex e
staff_server.ex terminavam CALADOS quando o teto de iterações estourava
(defp run_turn(state, remaining) when remaining <= 0, do: state, sem
evento nenhum) — contradizendo a regra permanente de que laço de agente
não termina calado. Só o PO já emitia toolloop.limit_reached
(RN-166); o Criativo diverge deliberadamente com agent.error (reason: "limite_de_iteracoes", decisão documentada no próprio código — reusar o
nome do ToolLoop mentiria sobre quem produziu o evento) e não foi
tocado. Os quatro corrigidos passam a emitir toolloop.limit_reached
(mesmo evento do PO, mesmo payload iteration/max_iterations) antes de
retornar — nenhuma mudança na estrutura de dispatch_tool de cada um.
- Onde:
apps/engine/lib/engine/agents/arquiteto_server.ex:195,dev_lead_server.ex:244,ux_designer_server.ex:121,staff_server.ex:113 - Teste:
apps/engine/test/engine/agents/{arquiteto,dev_lead, ux_designer,staff}_server_test.exs("teto de iterações emite toolloop.limit_reached") - Origem: achado por leitura completa dos seis servers, durante a implementação da RN-460; escopo estendido por decisão do dono do produto
A busca do RAG deixa rastro, e o rastro congela os pesos (RN-479..481)
RN-492 — O comando de terminal do dev agent roda DENTRO do container real, quando há um
Engine.Actions.TerminalExecutor.decisao_de_execucao/1 ganha a QUINTA saída
(:executar_no_container): projeto em execution_mode: container com uma
linha REGISTRADA running em project_containers
(Engine.Containers.ProjectContainerLifecycle.running?/1, leitura read-only
direta da tabela, mesmo padrão de Engine.Projects.Project). Antes desta
regra, mesmo com um container de pé (RN-491), TODO comando de terminal
rodava via System.cmd no processo do engine, contra a pasta compartilhada
— o container ficava ocioso.
O comando atravessa engine → api → broker
(POST internal/projects/:projectId/container-exec,
ExecutarComandoNoContainerUseCase, ContainerBrokerPort.exec) e roda via
docker exec. cwd, quando presente, é TRADUZIDO do caminho de HOST (dentro
de PROJECT_WORKSPACES_ROOT) para dentro de /work — o único diretório que
o container enxerga — trocando o prefixo <project_workspaces_root>/ <workspace_dir_name> por /work e preservando o sufixo (o worktree
individual de um dev agent, .worktrees/<agent_id>, incluso). O worktree em
si não muda de lugar nem de mecanismo de criação: Workspace.ensure!/4
continua escrevendo no MESMO diretório físico que o broker monta em /work
(verificado em código: raizDoProjeto do broker e PROJECT_WORKSPACES_ROOT
do engine resolvem a mesma pasta, via workspace_dir_name, RN-109) — o
container é só mais um observador dela.
running REGISTRADO nunca confirma que o container está de pé DE VERDADE
agora (RN-486). Se ele morreu ou foi removido por fora entre o registro e
esta chamada, broker.exec falha e vira failed_result NORMAL — exit_code
nil, mensagem nomeando o motivo — nunca crash, nunca fallback silencioso de
volta pro System.cmd fora do container (isso reabriria o vetor de
isolamento que esta regra existe para fechar). mounted nunca cai nesta
saída: o broker recusa subir container pra esse modo
(ModoDeExecucaoNaoSuportadoError).
- Onde:
apps/engine/lib/engine/actions/terminal_executor.ex(decisao_de_execucao/1,run_no_container/4,cwd_para_container/2),apps/engine/lib/engine/containers/project_container_lifecycle.ex,apps/engine/lib/engine/sessions/engine_api_client.ex(executar_comando_no_container/4),apps/api/src/interfaces/http/internal/internal-projects.controller.ts(POST :projectId/container-exec),apps/api/src/application/use-cases/containers/executar-comando-no-container.use-case.ts,apps/api/src/application/ports/container-broker.port.ts(exec,timeoutMs) - Teste:
apps/engine/test/engine/actions/terminal_executor_test.exs(as cinco saídas dedecisao_de_execucao/1; tradução decwdna raiz e num worktree;sucesso: falsee falha de transporte viramfailed_result; container sem linharunning/emstoppedcai no caminho de sempre),apps/api/test/application/use-cases/containers/executar-comando-no-container.use-case.spec.ts(BrokerRecusouError/BrokerIndisponivelErrornunca propagam; outro erro propaga),apps/api/test/infrastructure/http-clients/container-broker.client.spec.ts - ADR: 0130, 0134
- Origem: plano do dono do produto, Parte 1 / PR 1.6
RN-502 — Dev agent só reivindica task com container running, e o terminal não cai mais fora dele
Duas metades da mesma regra: sem ambiente de execução, o dev agent não começa, e sem ambiente de execução, o comando não roda em lugar nenhum.
A guarda do claim. Engine.Dev.AgentIo.try_claim/2 — o ponto ÚNICO de
claim — consulta
Engine.Containers.ProjectContainerLifecycle.running?/1 ANTES de chamar
claim_task/1. Sem uma linha REGISTRADA running em project_containers
(RN-243/ADR 0081), o agente cai em :idle, persiste, emite
dev.blocked_by_container e não chama a api. A guarda vem antes do claim
de propósito: reivindicar para devolver logo em seguida deixaria a task
marcada e sem dono vivo, que é o estado que block_task/4 existe para nunca
produzir.
:idle, e não um status novo. É o único estado do qual um wake ainda
resgata — os guards de handle_info/2 são todos casados com ele
({:wake, :became_claimable} exige :idle, :rearm exige :idle_tripped,
gate_resolved exige task_id batendo,
RN-047). Um
:blocked_by_container inventado seria um estado do qual nada resgata.
No ENGINE e não só no activate-execution da api, porque o claim tem um
caminho que nenhuma rota cobre: a REIDRATAÇÃO. Engine.Dev.DevRehydrator não
faz cast :work; quem claima depois de um restart é DevAgentServer.init/1
→ finish_restart_recovery/1 → try_claim/2. Um gate só na fronteira HTTP
deixaria todo agente reidratado voltar a trabalhar sem container.
O wake. Leitura não avisa ninguém: um agente já parado continuaria parado
até um evento não relacionado passar por perto. Então a chegada em running
PUBLICA — RegistrarTransicaoDeContainerUseCase grava a linha e o evento na
MESMA transação, aggregateType: 'container' (o terceiro agregado que
Engine.Outbox.Drain passou a drenar, ao lado de session e task),
aggregateId = o PROJETO. Engine.Workers.DevAgentWakeWorker entrega
{:wake, :became_claimable} a TODOS os agentes do projeto
(DevAgentState.list_by_project/1 — o container é do projeto, não de um
módulo). É a MESMA mensagem que já existia, e não uma nova: a semântica dela
já é "pode haver trabalho agora", e uma mensagem própria exigiria cláusula
nova de handle_info/2 nos dois servers com guard idêntico ao que já existe.
Só running publica; provisioning/stopped/failed/removed não soltam
ninguém.
A segunda metade: o terminal. Engine.Actions.TerminalExecutor
degradava calado — container sem container running caía em
:caminho_de_sempre, isto é, System.cmd DENTRO do processo do engine, o
mesmo que fala com o banco, com a api e com todos os outros projetos. O ADR
0134 (RN-492) tinha fechado o isolamento só no caminho feliz.
Agora recusa (:recusar_container_ausente), espelhando o
:recusar_nao_verificado/:recusar_runner_desconectado que o modo runner
já tinha (RN-423), e como failed_result normal — nunca crash.
mounted entra no MESMO ramo: com container running atravessa pro broker
igual a container; sem ele, recusa. O catch-all :caminho_de_sempre
encolhe para o que sempre deveria ter sido sozinho — projeto inexistente ou
project_id malformado. Nenhum modo de execução cai nele.
Consequência declarada: projeto sem container de pé para de trabalhar, e
diz por quê. É deliberado — é o que a regra existe para fazer — e é por isso
que ela só pode entrar DEPOIS das mudanças que dão container ao modo
mounted.
- Onde:
apps/engine/lib/engine/dev/agent_io.ex(try_claim/2),apps/engine/lib/engine/actions/terminal_executor.ex(decisao_de_execucao/1),apps/engine/lib/engine/outbox/drain.ex,apps/engine/lib/engine/workers/dev_agent_wake_worker.ex,apps/api/src/application/use-cases/containers/registrar-transicao-de-container.use-case.ts - Teste:
apps/engine/test/engine/dev/claim_com_container_test.exs—:workinicial sem container cai em:idle, persiste, emite e não chama a api; o Noop passa pela mesma guarda; agente REIDRATADO para em:idlesem claimar;container.runningna outbox percorre drain → worker → wake → agente e ele re-claima; e o caminho de sempre segue intacto com containerrunning.apps/engine/test/engine/actions/terminal_executor_test.exs— recusa paracontainer(sem linha e comstopped) e paramounted;mountedcomrunningatravessa pro broker; só projeto inexistente cai em:caminho_de_sempre.apps/api/test/application/use-cases/containers/ciclo-de-vida-do-container.use-case.spec.ts—runningpublicacontainer.runningno agregadocontainer; os outros destinos e a criação da linha não publicam nada - ADR: 0143
- Origem: plano "Nome e local na mesma tela, e container antes dos dev
agents" (PR 7) — dez tasks do
exp001travaram de uma vez porque nada ordenava container antes de dev agent
O artefato de decisão dos conversacionais (RN-505)
RN-505 — decision_record reusa o padrão GENÉRICO de emit_artifact, não o dedicado
Um agente conversacional que toma uma decisão relevante numa conversa (por
que escolheu X e não Y, o que considerou, o que aceitou perder) registra isso
com emit_artifact e type: decision_record — payload com context,
options (lista), choice e consequences, as quatro chaves obrigatórias
em Engine.Harness.ArtifactSchemas (artifact_schemas.ex:82), tool-emittable
desde que entrou em @tool_emittable (artifact_schemas.ex:98). Sem
validação cruzada de propósito: choice não precisa bater com um item de
options — texto livre é mais robusto, e o schema genérico só valida
presença de chave (a mesma régua de note/business_rule).
Por que reusar o padrão GENÉRICO em vez do dedicado. artifact.project_image
e artifact.c4_diagram (RN vizinhas desta) são artefatos VERSIONADOS — cada
emissão substitui a anterior como "vigente", com rota HTTP própria de leitura
e schema TypeScript dedicado na api. Uma decisão não é assim: é um registro
append-only por natureza, não um "vigente" que se substitui — o mesmo
raciocínio que já vale para note/business_rule. Pagar o preço do padrão
dedicado (schema TS, versionamento, rota nova) para um log que só precisa
existir e ser pesquisável no event log da sessão custaria estrutura que a
decisão não pede.
Distinção declarada com open_adr_pr (só o Arquiteto tem). Os dois
COEXISTEM, com propósitos diferentes: decision_record é para TODA decisão
relevante de qualquer um dos seis conversacionais, sem fricção nenhuma —
nunca commita arquivo, nunca abre PR, nunca espera aprovação humana.
open_adr_pr é para quando a decisão é grande o bastante para virar
DOCUMENTO real em docs/adr/*.md, com PR de verdade e aprovação humana
obrigatória. Um projeto pode ter dez decision_record e zero ADR numa
sessão — o segundo não é a versão "séria" do primeiro, é outra ferramenta
para outra escala de decisão.
Wiring nos cinco conversacionais que ainda não tinham emit_artifact —
PO (po_server.ex:115), Arquiteto (arquiteto_server.ex:103), Dev Lead
(dev_lead_server.ex:153), UX Designer (ux_designer_server.ex:94) e Staff
(staff_server.ex:94); o Criativo já tinha a ferramenta desde a Fase 3b, só
ganhou o tipo novo (é o known/0 compartilhado quem decide, sem mudança no
CriativoServer). Em cada um, o run_tool/3 comum ganhou a cláusula
"emit_artifact" -> EmitArtifact.run/2 (ex.: po_server.ex:279) — sem
tratamento especial: diferente de propose_execution_plan/
assess_implementability no Dev Lead (que suspendem o turno esperando
aprovação, ADR 0086), emit_artifact nunca devolve {:pending, _}, então
entra no dispatch comum sem precisar do reduce_while que aquelas duas
ferramentas exigem. E os cinco ganharam a MESMA frase no system_prompt/1
(ex.: po_server.ex:463) explicando quando usar type: decision_record —
fora do texto de identidade (Engine.Harness.Agents) porque não é sobre
QUEM o agente é, é uma instrução operacional sobre uma ferramenta.
Fora de escopo, declarado. Agentes de execução (QA, SecOps, Dev Agent de
módulo) rodam sobre ToolLoop, sem harness comum aos seis conversacionais —
ficam de fora desta fatia, investigação própria por módulo antes de
estender. A amarração com a Frente 1 do mesmo plano (o dispatch do Infra
Lead emitindo um decision_record server-emitted quando propõe subir
container via runner) também fica de fora: depende de uma tool
(propose_container_start_via_runner) que outro PR está criando em
paralelo — ver TODO no PR desta mudança.
- Onde:
apps/engine/lib/engine/harness/artifact_schemas.ex(schema +@tool_emittable);apps/engine/lib/engine/agents/po_server.ex,arquiteto_server.ex,dev_lead_server.ex,ux_designer_server.ex,staff_server.ex(alias +tool_specs+run_tool/3+system_prompt/1) - Teste:
apps/engine/test/engine/harness/artifact_schemas_test.exs(describe("decision_record …"));apps/engine/test/engine/agents/po_server_test.exsearquiteto_server_test.exs(describe("Frente 3 do plano de decision_record — emit_artifact")) - Origem: plano do dono do produto, Frente 3 ("Artefato de decisão")
O join do agente local deixa de ser mudo (RN-514)
RN-514 — O runner DECLARA o que sabe fazer no join, o servidor CONCEDE a interseção em socket.assigns, e mensagem sem capacidade é RECUSADA em vez de sumir
Até aqui o join do canal terminal:<projectId> era mudo dos dois lados:
o runner mandava socket.channel(\terminal:${projectId}`, {}) e o servidor os ignorava literalmente (def join("terminal:" <> project_id, _params,
socket)). Toda a identidade vinha do ticket — o papel (:runner/:web) sai do kind` —, e o servidor não tinha como saber se o binário do outro lado
era de uma versão que entende uma mensagem nova.
O defeito que isso produz é o pior possível, e é o que esta regra fecha: a mensagem chega, o handler não existe do outro lado, e nada acontece — sem erro, sem log, sem ninguém saber que a função nunca rodou.
Três capacidades, e só o que o runner sabe fazer é declarado. O
vocabulário do servidor é exec (comando aprovado, ADR 0104), pty
(terminal interativo, ADR 0103) e espelho (copiar o trabalho para uma pasta
fora da base montada). O runner declara exec e pty, e espelho fica
de fora de propósito: declarar o que não se implementa é exatamente o
defeito que a negociação existe para impedir — o servidor concederia,
entregaria a mensagem, e ela sumiria do mesmo jeito. espelho nasce só como
nome do vocabulário, sem comportamento nenhum; quem o implementa é a
sessão 6 da FASE 28.
Ausência de params é o LEGADO, e isso é fato — não benevolência. Todo
binário anterior a esta mudança manda params vazios, e todo binário anterior
a esta mudança sabe fazer exec e pty: são as duas funções com que o
runner nasceu. Conceder {exec, pty} a quem não declara nada é ler
corretamente a única coisa que aquele binário poderia ter dito. O contrário
faria todo runner instalado parar de conectar, que é mudança exigindo
ação do operador antes do deploy — breaking/ e MAJOR, pela convenção do
repositório, e não é isto que esta entrega é. Params ausentes, capacidades
ausente, lista vazia e valor que não é lista caem todos no legado: são as
formas que "este binário não sabe declarar nada" assume no caminho de rede.
Desconhecido é IGNORADO, nunca motivo de recusa. O conjunto concedido é a interseção do declarado com o vocabulário que este servidor conhece — runner mais NOVO que o engine tem que conseguir conectar. Já declarar apenas nomes desconhecidos é declaração vazia, não legado: quem sabe declarar disse o que sabe fazer.
O que cada modo EXIGE. runner exige exec — é literalmente o que faz o
modo funcionar, e sem ele o TerminalExecutor rotearia comando aprovado para
um binário que não o executa, com a falha aparecendo como timeout em vez de
recusa. container e mounted não exigem nada (o runner nem é o caminho de
execução deles). Nada exige espelho ainda — o mecanismo de recusa nasce
implementado e testado, e não dispara na prática hoje. Isso é o desenho, não
pendência. Modo que não se conseguiu ler (projeto inexistente, id malformado,
consulta que falhou) exige nada: recusar por pré-condição não confirmada
seria colapsar "não sei" com "não tem" (RN-088).
O conjunto concedido vive em socket.assigns, nunca em tabela.
Capacidade é propriedade daquela conexão — uma tabela poderia afirmar que
um runner sabe algo que o processo conectado agora não sabe. É o mesmo
raciocínio que faz a entrada :global do Engine.Runners.Registry morrer
junto com o pid. Só o papel :runner recebe conjunto: o socket :web não
ganha :capacidades e nenhuma checagem de capacidade se aplica a ele — a
aba não executa nada, ela pede.
Recusa do join nomeia a que falta. {:error, %{reason: ...}} dizendo
qual capacidade o projeto exige e que o binário precisa ser atualizado. A
recusa é fatal do lado do runner, sem retry (JoinRecusadoError,
comportamento que já existia e não mudou) — por isso ela é a única chance de
explicar, e "recusado" sem o nome do que falta obrigaria a adivinhar. A
checagem acontece antes de Registry.register/2: registrar e recusar em
seguida deixaria o Registry afirmando um runner que não entrou.
E a metade que entrega valor hoje: mensagem sem a capacidade concedida é
RECUSADA com resposta nomeada, nunca engolida. exec para um runner sem
exec responde ao chamador no mesmo formato de exec_result que o
runner mandaria (exitCode 126 — "encontrado, mas não executável" —, a causa
no output), em vez de deixar o RunnerRouter bloqueado até o receive ... after e o usuário ver um timeout no lugar da causa. pty_* para um runner
sem pty não é relayado, e a :web que originou recebe pty_error nomeado,
pelo mesmo broadcast filtrado que o runner usaria para reportar erro de PTY —
sem isso a aba ficaria em "carregando" para sempre, o defeito que o whereis
de handle_in("pty_open", ...) já tinha fechado para o caso "sem runner".
Como o legado tem as duas capacidades, nada muda para ninguém hoje.
Isto não é fronteira de segurança. Capacidade é negociação de
protocolo; quem autoriza continua sendo o ticket de uso único
(RN-108) e o pipeline de
proposed_action. O que o runner ganha declarando é que mensagens daquele
tipo lhe sejam entregues, nunca permissão para nada.
- Onde:
apps/engine/lib/engine/runners/capacidades.ex(novo — o vocabulário, o legado, o que cada modo exige e as duas mensagens);apps/engine/lib/engine_web/channels/terminal_channel.ex:208(join/3passa a receberparams),:215(autorizar_por_papel/3),:251(entrar_como_runner/3, a concessão antes doRegistry),:305(exigencias_do_projeto/1, que eramodo_de_execucao/1até a RN-516 fazer a mesma consulta responder também o destino do espelho),:595(o gate deexecemhandle_info({:dispatch_exec, ...})) e:762(o deptyemhandle_info({:relay, "pty_" <> _, ...}));apps/runner/src/channel.ts(CAPACIDADES_DO_RUNNERe os params dosocket.channel/2) - Teste:
apps/engine/test/engine/runners/capacidades_test.exs(novo — o vocabulário puro: legado, lista vazia, desconhecido ignorado, o que cada modo exige, e que nada exigeespelho);apps/engine/test/engine_web/channels/terminal_channel_test.exs(describe "capacidades declaradas no join (ADR 0147 ponto 1, RN-514)"— concessão emassigns, join sem params, desconhecido ignorado,:websem conjunto, a recusa nomeandoexecnum projetorunner, e as duas mensagens recusadas com resposta em vez de silêncio);apps/runner/src/channel.spec.ts(o CONTEÚDO dos params do join,espelhofora da lista, e a recusa virandoJoinRecusadoErrorcom a mensagem do servidor legível) - ADR: 0147, ponto 1
- Origem: FASE 28, sessão 5
Os artefatos dos agentes viram arquivo, em docs/ (RN-523)
RN-523 — A pasta docs/ é PROJEÇÃO do event log: derivada, por agente, e nunca fonte de verdade
Todo artefato que os agentes produzem vive exclusivamente no event log —
apps/engine/lib/engine/harness/tools/emit_artifact.ex:3-5 diz literalmente
que "não há tabela de artefatos". Isso continua certo para a FONTE, e esta RN
não mexe nisso. O que ela fecha é outro buraco: essa memória não chegava a
lugar nenhum que uma pessoa abrisse. Quem abre a pasta do projeto no editor não
encontrava nada do raciocínio que produziu o código. (O provisionamento
adiado — criar projeto deixar de criar repositório, e o git nascer no aceite
do handoff ao Dev Lead — abre um intervalo inteiro em que o projeto não tem nem
repositório, e torna esta pasta ainda mais necessária; ele vem em PR próprio, e
esta RN não depende dele.)
Cada evento artifact.* de um tipo permitido vira um Markdown em
docs/<agente>/ dentro do workspace do projeto. É projeção derivada, no
mesmo sentido estrito que o ADR 0101 deu ao grafo Neo4j: a fonte é o evento, a
pasta pode ser apagada inteira e reconstruída — desde a RN-590 por
artefatos:reprojetar —, e nada no produto lê dela para decidir coisa
alguma.
O mecanismo é o do GraphProjector, copiado peça por peça — poller
próprio, drainOnce() público, flag contra ciclos sobrepostos, lote de 50, e
markProcessed só após sucesso. A linha de outbox é uma TERCEIRA, gravada na
mesma transação do evento, sob um aggregate_type PRÓPRIO
(artifact_projection): Engine.Outbox.Drain só drena session, task e
container, e disputar a linha 'session' seria perder a corrida contra ele
quase sempre. Uma diferença deliberada em relação ao grafo: o aggregateId é o
projeto, não a sessão — a pasta é do projeto, e gravá-lo ali poupa o
projetor de uma consulta por artefato.
A lista é de PERMITIDOS. Tipo de artefato novo nasce fora da projeção e
entra quando alguém decidir que ele é documento, em vez de aparecer sozinho
numa pasta que o usuário lê. Ficam de fora qa_verdict, secops_verdict,
task_blocked e infra_delegation_files: são desfechos OPERACIONAIS, e um
veredito de gate por task encheria docs/qa-lead/ de arquivos que ninguém
abre, custando à pasta a propriedade que a torna útil — caber numa olhada.
Versionado sobrescreve; append-only leva o seq. Os quatro tipos lidos por
redução ao maior version (module_map, module_routing, project_image,
c4_diagram) escrevem sempre o mesmo arquivo, e a pasta mostra o vigente — o
histórico continua inteiro no event log. Os demais viram arquivo próprio com o
seq do evento no nome, e sem ele dois artefatos do mesmo tipo com o mesmo
título gerariam o mesmo arquivo, com o segundo apagando o primeiro: a única
forma de esta projeção perder informação que a fonte tem.
Duas barreiras contra o filesystem, e a segunda não é redundante. O nome
sai do título que o MODELO escreveu. slugDeArquivo deriva um nome de um
alfabeto fechado (nenhum caminho de retorno deixa separador sobreviver;
devolve null quando não sobra nada), e FsArtifactFileStore confere de novo
com relative() antes de abrir. As duas respondem perguntas diferentes — "que
nome eu derivo disto" e "o que eu vou de fato abrir" —, e confiar só na
primeira faria a segurança da escrita depender de nenhuma mudança futura de
slug jamais deixar passar um separador, que é a contenção que o ADR 0130 recusa
por princípio.
A projeção nunca derruba a fonte. Nenhum throw escapa do ciclo: cada item
falha isolado, fica logado e permanece processed_at IS NULL para o ciclo
seguinte. Diferente do grafo, não há aqui um erro que pare o lote inteiro —
falha de escrita costuma ser do ITEM, e parar o lote bloquearia todos atrás
dele. Evento que sumiu do event log é marcado processado, não retentado para
sempre.
Em modo runner a pasta desvia para a raiz gerenciada, pelo mesmo motivo
físico do permissions.json (RN-478): quem escreve é a api, de
dentro do container dela, e o canal do runner só transporta comando, sem
primitiva de escrever conteúdo. Custo declarado: ali docs/ não fica ao lado
do código.
A escrita NÃO passa por proposed_action, e não é omissão: aquele pipeline
é a origem de todo efeito externo de um AGENTE PEDINDO PARA AGIR, e aqui o
artefato já foi emitido e já está no event log. Passar a projeção pela fila
encheria as aprovações de decisões rotineiras — uma por artefato — e corroeria
o teto que dá sentido ao clique nas ações que são efeito externo de verdade.
- Onde:
apps/api/src/domain/artifacts/artifact-projection-events.ts(o vocabulário e as regras de nome, puros);apps/api/src/application/artifact-projection/artifact-projector.ts(o poller);apps/api/src/application/ports/artifact-file-store.port.tseapps/api/src/infrastructure/filesystem/fs-artifact-file-store.ts(a escrita, com a segunda barreira);apps/api/src/infrastructure/filesystem/project-workspaces-root.ts(pastaDeArtefatosDoProjeto, irmã depermissionsFilePath);apps/api/src/application/use-cases/sessions/append-session-event.use-case.ts(a terceira linha de outbox) - Teste:
apps/api/test/domain/artifacts/artifact-projection-events.spec.ts(as regras de nome, o traversal e a colisão porseq) eapps/api/test/application/artifact-projection/artifact-projector.spec.ts(caminho feliz, versionado sobrescrevendo, e a degradação: falha deixa a linha não processada, o ciclo seguinte retenta, uma falha não bloqueia o lote, evento sumido não é retentado para sempre, reprocessar é idempotente) - ADR: 0148
- Origem: pedido do dono do produto — "criar apenas uma pasta por ora chamada docs, onde os artefatos estarão de cada um dos agentes"
A revogação de chave de dispositivo deixa de ser cega, e alcança a conexão viva (RN-519..520)
RN-539 — assess_implementability dispara o threat model de DESIGN em PARALELO, e uma vez só por story
O appsec (Engine.Gates.SecOpsAgentServer.run_design/2, RN-360)
nasceu implementado, testado e sem chamador de produção nenhum: a única
menção fora de teste era um comentário em artifact_schemas.ex. A própria
RN-360 declarava a lacuna, e packages/contracts/fluxo.yml (papel appsec,
campo acionamento) já NOMEAVA o gatilho que faltava — assess_implementability
do Dev Lead, gate implementavel, o mesmo ADR 0090.
Era ele. run_assessment/2 passa a chamá-lo por
Engine.Gates.Dispatcher.run_appsec_design/2 — sexto callback do behaviour,
CÓPIA estrutural de run_qa_estrategia/3 e trocável em teste pelo mesmo
motivo dos cinco vizinhos (o chamador é exercitado por um teste leve, sem
sandbox Ecto, e não deveria subir um GenServer real só para provar que
PEDIU).
Em paralelo, e o parecer não espera. O disparo acontece nas DUAS saídas
de run_assessment/2 — com plano de teste e sem — e o desfecho de cada uma
fica byte a byte como era. Fazer o parecer depender do threat model faria o
gate implementavel depender de DUAS produções assíncronas em vez de uma, e
queimaria dois turnos do Dev Lead onde hoje se queima um: o ramo :sem_plano
já devolve {:error, ...} pedindo ao modelo que chame de novo
(RN-163), e uma segunda espera empilhada nele multiplicaria a
janela. O threat model chega a quem precisa pelo caminho que a
RN-361 já definiu — handoff para arquiteto, dev-lead e
infra —, nunca por este retorno; a mensagem do :sem_plano sequer menciona
o appsec, porque prometer ao modelo algo que ele não vai receber ali é o
defeito que a RN-163 fecha.
Desde a RN-674 não há mais :sem_plano: o parecer sai na
primeira chamada, o disparo do appsec acompanha a proposta, e a leitura do
histórico serve só à guarda abaixo. O appsec segue no DESIGN — a decisão do
dono que tirou o plano de teste do pre-dev foi sobre o QA.
E uma vez só por story. O modelo é INSTRUÍDO a chamar
assess_implementability de novo enquanto o plano de teste não existe: sem
guarda, cada rechamada custaria outra rodada de LLM do appsec e mais três
handoffs sobre a MESMA story. disparar_appsec_se_preciso/3 só dispara
quando não há artifact.threat_model daquela storyId no histórico. A
pergunta aqui é EXISTE, não QUAL — o oposto do plano de teste, onde o mais
recente vence porque uma story pode ser reavaliada.
A leitura do event log é uma por chamada: as duas perguntas ("já há plano
de teste?", "já há threat model?") são respondidas sobre a mesma lista. Dois
list_events/2 na mesma invocação seriam o amplificador de tráfego que o
ADR 0060 recusa, e este caminho é
quente justamente por causa da retentativa. Histórico ILEGÍVEL não dispara
nada: ausência de resposta não é prova de ausência de artefato, e disparar ali
reabriria a rechamada por outra porta. A cláusula de args inválidos também não
dispara — ela não sabe qual é a story.
- Onde:
apps/engine/lib/engine/agents/dev_lead_tools.ex:544(run_assessment/2, a leitura única do histórico) e:576(disparar_appsec_se_preciso/3, a guarda de idempotência);apps/engine/lib/engine/gates/dispatcher.ex:29(o callback) e:86(Engine.Gates.Dispatcher.Live.run_appsec_design/2);apps/engine/test/support/fake_gate_dispatcher.ex:23 - Teste:
apps/engine/test/engine/agents/dev_lead_tools_test.exs, describe "assess_implementability dispara o appsec (RN-539)" — story sem threat model dispara, story com threat model NÃO dispara (a idempotência), threat model de outra story não conta, o desfecho de:sem_planonão muda, o parecer é proposto normalmente ao lado do disparo, args inválidos e histórico ilegível não disparam - ADR: 0090 (que declarava o gatilho automático como fora de escopo)
- Origem:
packages/contracts/fluxo.yml, papelappsec. Fica declarado e NÃO feito:artifact.threat_modelé emitido na sessão da STORY (emit_threat_model/3lêstory["sessionId"]), enquanto a guarda lê o histórico da sessão do DEV LEAD. No caminho comum são a mesma sessão — o Dev Lead avalia stories que a própria sessão de execução criou. Não sendo, a guarda não enxerga o threat model e o appsec roda outra vez: custo de uma rodada de LLM repetida, nunca dado errado (o artefato é append-only e quem o consome é o handoff). Fechar isso exigiria a api aceitar busca de artefato por story ATRAVÉS de sessões, que hoje não existe
A flag que liga um agente tem de CHEGAR ao processo (RN-540)
RN-540 — Flag booleana lida pelo runtime.exs é mapeada no environment: do serviço engine dos dois composes, com o default do código
Uma pausa só é reversível se existir o caminho de ligar. A da Anamnese e a do
Psicólogo (RN-115,
RN-117) foram declaradas reversíveis
em TRÊS lugares — o docblock de AnamneseSchedulerWorker.enabled?/0 ("Ligar de
volta é ANAMNESE_ENABLED=true e reiniciar o engine"), o de
PsychologistWorker.enabled?/0 e a tabela de
configuração — e o caminho não existia em
ambiente nenhum.
Medido, não hipótese. Com o compose de dev de pé:
$ docker compose … exec -T engine sh -lc \
'echo "[$START_ANAMNESE] [$ANAMNESE_ENABLED] [$PSYCHOLOGIST_ENABLED]"'
[true] [] []
A causa é a camada que só o Compose tem: ele não repassa o ambiente do host
ao container. Uma variável chega ao processo se, e só se, estiver escrita no
environment: (ou num env_file) daquele serviço. O engine mapeava TREZE
variáveis de Anamnese/Psicólogo — todas TETOS de custo
(ANAMNESE_INTERVAL_SECONDS, ANAMNESE_BUDGET_MICROS,
PSYCHOLOGIST_TRIAGE_THRESHOLD, …) — e nenhuma das duas que decidem se os
agentes rodam. ANAMNESE_ENABLED=true no .env era inerte:
apps/engine/config/runtime.exs caía no default "false" e os dois seguiam
desligados em silêncio, sem erro.
Origem provável do defeito: quando o default do código virou false
(2026-08-10), ninguém precisou da variável para DESLIGAR. Só quem tenta LIGAR
descobre que o fio não existe — e essa pessoa, por construção, aparece meses
depois.
A regra. Toda flag BOOLEANA que o runtime.exs lê — a forma
System.get_env("X", "true"|"false") == "true" — tem de estar mapeada no
environment: do serviço engine dos dois composes. O recorte é o das
booleanas e não o das 58 variáveis do arquivo porque uma flag existe
exatamente para ser virada, e a virada acontece pelo ambiente: flag sem linha
no compose é interruptor sem fio. Teto numérico com default bom
(READ_FILE_MAX_BYTES) e variável de release (SECRET_KEY_BASE, POOL_SIZE)
ficam legitimamente de fora, e é por isso que a lista é DERIVADA do
runtime.exs em vez de mantida à mão.
O default difere por arquivo, e a diferença é a decisão:
- em
docker/docker-compose.yml(dev) a linha repete o default do CÓDIGO. Dev é onde a promessa dos docblocks é exercida, e um default divergente ali faria o compose decidir produto por baixo doruntime.exs. - em
docker/docker-compose.prod.ymlbasta estar mapeada. Aquele arquivo desliga os agentes de fundo de propósito (START_ANAMNESE:-falsecontra otruedo código), com o motivo escrito ao lado — cobrar igualdade reprovaria uma decisão consciente.
Isto NÃO muda decisão de produto nenhuma. As duas flags continuam
false por default nos dois composes: a pausa de 2026-08-10 segue valendo,
byte a byte. O que passou a existir é o interruptor.
E ANAMNESE_ENABLED=true sozinho não basta. START_ANAMNESE (chave de
BOOT, apps/engine/lib/engine/application.ex:82,106-108) decide se o kickoff/0 que
AGENDA o tick periódico é chamado; ANAMNESE_ENABLED (flag de PRODUTO) decide
se uma rodada NOVA pode acontecer. São perguntas diferentes, e a Anamnese
periódica exige as duas em true. O Psicólogo tem só a segunda — o gatilho
automático dele é o fechamento de sessão, não um tick.
deploy/k8s/ fica de fora, e por decisão. Lá não existe a camada que
quebra: um Deployment/ConfigMap não intercepta nada, e a variável é escrita
onde toda variável é escrita. O brabo-config tem QUATRO literais (endereços
de serviço e nível de log do web), sem nenhum dos 13 tetos nem
START_ANAMNESE — o engine roda com os defaults do código, coerentemente.
Não há interruptor sem fio a consertar ali.
- Onde:
docker/docker-compose.yml:406-425(as quatro chaves de boot e as quatro flags de produto, no bloco de cada agente);docker/docker-compose.prod.yml:308-315;apps/engine/config/runtime.exs:157,158,162,168,185,229,238,264(as oito flags);.env.example(as duas perguntas, lado a lado) - Teste:
scripts/ci/flags-do-engine-no-compose.spec.ts— deriva as flags doruntime.exse reprova nomeando a que faltar em qualquer dos dois composes, e a que divergir do default do código no de dev.warnnenhum: reprova mesmo, no jobtest-packages - Origem: achado por medição direta no container do engine, 2026-09-08.
Fica declarado: a lista de exceções
FLAGS_FORA_DO_COMPOSEnasce VAZIA — ela existe para que "fora por decisão" tenha onde ser dito, e não vire indistinguível de "fora por esquecimento"
A chave de dispositivo ganha tela, e a tela diz o alcance de revogar (RN-561)
RN-566 — O Infra Lead recusa container_start por MODO antes de propor: a lacuna da RN-494 fecha na tool que a declarava
propose_container_start propunha às cegas. dispatch_container_start/2
montava o payload e chamava EngineApiClient.propose_action(...) direto — sem
uma leitura de projeto entre uma coisa e outra, e com zero ocorrências de
execution_mode na tool inteira. Num projeto runner a proposta só podia
terminar em falha: desde a RN-507/ADR 0145
container_start não atende esse modo, porque o payload dela ELEGE uma
candidata do roteamento do Arquiteto (ADR 0131)
e em runner não há roteamento contra o qual eleger — o tipo daquele modo é
container_start_via_runner.
A régua não é nova, e não nasceu uma segunda. A tool irmã já recusava
localmente (RN-508) e a página /containers já ramificava por
DESTINO (RN-521, acaoDeSubidaDoModo): container e mounted
sobem pelo BROKER, runner sobe pelo agente local (ADR 0144/RN-503).
O que esta regra faz é dar à tool que faltava a MESMA régua — uma leitura de
projeto (recusa_local_de_subida/2, que devolve nil ou motivo) e uma
CLÁUSULA por tool (recusa_por_modo/3). Duas réguas paralelas divergiriam no
primeiro modo novo do enum.
A recusa de container_start é lista de PERMITIDOS, como a do próprio
broker: container/mounted passam, runner é recusado NOMEANDO a tool
irmã, e modo novo no enum nasce recusado com mensagem em vez de proposto por
omissão. Projeto inexistente também recusa — mesma cláusula das duas tools.
A recusa é ENTRADA do laço (RN-163): texto de RESULTADO de
ferramenta, que o modelo lê e usa para chamar a tool certa. Nunca
agent.error, nunca fim de turno, nunca proposed_action — a api não é
chamada. E não é silêncio: o emit do tool.call passou a acontecer
ANTES da recusa nas DUAS tools (na irmã ele estava dentro do ramo que propõe),
como no dispatch_tool/2 genérico do mesmo módulo — recusa sem rastro no
event log é a chamada sumindo do timeline do humano.
As leituras são LOCAIS, e isso é a decisão. Project.get/1 e
Engine.Runners.Registry.connected?/1 rodam no MESMO processo BEAM do Infra
Lead; um HTTP à api aqui poria uma chamada de rede dentro do laço do agente.
O custo declarado no CLAUDE.md — "corrigir exigiria tocar o
prompt/instrução do Infra Lead" — não se confirmou: nenhuma linha de
prompt mudou, e o texto da recusa é o que o modelo lê como resultado.
O que esta regra NÃO fecha, e é metade da lacuna: a proposta cega por
AUSÊNCIA DE IMAGEM continua possível em container/mounted. A recusa aqui é
sobre MODO, e exigir imagem decidida inverteria a ordem — eleger a imagem é
justamente o que essa proposta FAZ (RN-491). A /containers checa
as TRÊS coisas (imagem decidida, modo, pasta confirmada) porque tem um humano
clicando; o agente passa a checar UMA. GetInfraContextUseCase segue sem
executionMode — enriquecer o contexto do Infra Lead é frente à parte.
container_stop/container_remove não entram: são propostas pela tela, nunca
por agente (RN-495).
Nenhum teto muda: container_start segue proposed_action de verdade,
maintainer, nunca semeada em auto-aprovação (até a RN-671, que a
semeia no aceite do handoff da Infra), e container_remove segue no
teto absoluto de git push/comando privilegiado (RN-418).
- Código:
apps/engine/lib/engine/infra/infra_lead_server.ex:599(o dispatch decontainer_startconsultando antes de propor),:889(recusa_local_de_subida/2— a leitura ÚNICA do projeto),:474(a cláusula decontainer_start: lista de permitidos),:477(a recusa nomeandocontainer_start_via_runner),:492(a cláusula da irmã, com a segunda pergunta — runner conectado),:409(oemitdotool.callmovido para antes da recusa);apps/engine/lib/engine/infra/tools/propose_container_start.ex(moduledoc — a tool deixou de propor às cegas) - Teste:
apps/engine/test/engine/infra/infra_lead_server_test.exs:422(caminho feliz emmounted— o broker atende os dois),:448(caso de falha:runnerrecusa NOMEADO,propose_actionnunca chamada, otool.callnarrado mesmo assim),:538(projeto inexistente),:380(o caminho feliz decontainer, que passou a exigir a linha no banco) - ADR: 0144, 0145
- Origem: AT-048 (EP-020/HS-032) — a lacuna declarada no
CLAUDE.mddesde a RN-494
O broker de container na instalação, com consentimento (RN-575)
RN-577 — open_adr_pr e open_infra_pr são recusadas pelo agente, antes de propor, quando o projeto não tem repositório
Numa instalação real (AT-088, 2026-09-14), um projeto sem repositório —
project_repositories, project_git_connections e repo_bootstraps vazias —
recebeu três open_adr_pr do Arquiteto e uma open_infra_pr do Infra Lead. O
humano aprovou as quatro, e as quatro terminaram failed com "Projeto sem
repositório provisionado" (ExecuteAdrPrUseCase, ExecuteInfraPrUseCase).
A pessoa aprovou quatro ações que só podiam falhar.
Não é borda, é o caso comum. Desde a RN-541 criar um projeto
não cria repositório, e o gatilho do repositório é o aceite do handoff do
Arquiteto para o Dev Lead (RN-522). O Arquiteto trabalha ANTES desse
handoff — então num projeto novo todo open_adr_pr falhava por
construção.
A regra: as duas tools perguntam, ANTES de propor, se o projeto tem
repositório, e sem ele RECUSAM com motivo NOMEADO — o que falta, e quando passa
a existir. A proposed_action não nasce, e a fila de aprovação não recebe o
que não pode dar certo.
O molde é o da RN-566, sem régua nova e sem tocar prompt. A
recusa é resultado de ferramenta (RN-163),
entrada do laço — nunca agent.error, nunca fim de turno —, e o modelo segue
o turno lendo o motivo.
A leitura é LOCAL, e o predicado é o MESMO da execução. O engine já lia
project_repositories direto do Postgres (Engine.Projects.ProjectRepository,
os consumidores de default_branch/1 e remoto_de_trabalho/1), então não
houve HTTP no laço do agente a medir nem a justificar: a pergunta nova,
recusa_de_pr_sem_repositorio/2, é a MESMA que
ProvisionedRepositoryRepository.findByProjectId faz nos dois casos de uso —
existe linha para o projeto. Se os dois divergissem, a tool deixaria passar o
que a execução recusa, ou recusaria o que ela aceitaria. As duas tools usam a
MESMA função, com o mesmo texto.
No Infra Lead a pergunta vem antes do HALT. propose_infra_pr não propõe
na hora: ela interrompe o turno para finalize/3 rodar o WorkflowsAgent e
consolidar numa PR só. Recusar depois disso (em abrir_pr/3) teria gastado um
laço de LLM inteiro e registrado duas delegações completed para uma PR que
não pode existir. A recusa acontece na interceptação, e o turno CONTINUA.
Rastro durável nas duas. A AT-048 achou que recusa local não deixava rastro
no event log; a RN-566 resolveu emitindo o tool.call antes da recusa, e aqui
vale o mesmo — no Arquiteto o dispatch_tool/2 já o emitia; no Infra Lead a
interceptação de propose_infra_pr não emitia nada e passa a emitir, no ramo
da recusa, com o título e os CAMINHOS (nunca o conteúdo dos arquivos). E o
PORQUÊ vai junto: um tool.result com ok: false e erro com o motivo — a
forma que o Criativo já usa —, porque só o tool.call diria que a chamada
existiu, não por que ela não virou proposta. No caminho que propõe nada muda:
o rastro dele continua sendo a própria proposed_action.
Os casos de uso de execução continuam recusando como antes — defesa em profundidade: a recusa do agente é uma camada ANTES, nunca no lugar.
O que esta regra NÃO decide: ONDE o repositório deveria nascer. O texto da
recusa descreve o gatilho vigente, e ele MUDOU: a AT-092 foi decidida na
RN-582 (ADR 0165) — o repositório nasce no aceite do handoff AO
Arquiteto, antes do primeiro turno dele, com o aceite ao Dev Lead como segunda
porta. O texto mudou junto, como esta regra prometia: ele nomeia o gatilho
novo e, como o Arquiteto só trabalha depois desse aceite, diz que a recusa
significa provisionamento FALHADO (o repository.provision_failed) ou projeto
anterior à regra, com as duas saídas. Deixou de ser o caso comum; a recusa
continua, porque essas duas situações existem. A open_infra_pr também é afetada no projeto novo por outro
caminho (o Infra Lead é acionado pelo handoff do Arquiteto, que pode acontecer
antes do handoff ao Dev Lead); esta regra só faz a proposta não nascer, não
reordena os handoffs.
- Código:
apps/engine/lib/engine/projects/project_repository.ex:74(recusa_de_pr_sem_repositorio/2— o predicado e o texto, únicos para as duas tools);apps/engine/lib/engine/harness/tools/propose_adr.ex:54(a recusa antes de propor),:61(otool.resultcom o motivo);apps/engine/lib/engine/infra/infra_lead_server.ex:251(a interceptação depropose_infra_prperguntando antes do HALT),:632(recusa_de_infra_pr/4),:309(otool.callcom os caminhos),:314(otool.result) - Teste:
apps/engine/test/engine/agents/arquiteto_server_test.exs:107(caso de falha: sem repositório,open_adr_prnunca proposta, motivo como resultado de ferramenta,tool.calletool.resultno event log),:80(caminho feliz: com repositório, propõe);apps/engine/test/engine/infra/infra_lead_server_test.exs:491(caso de falha: sem repositório, nempropose_actionnem delegação do Workflows, o turno segue, rastro durável),:89(caminho feliz: com repositório, a PR consolidada é proposta) - Origem: AT-088 — instalação real em 2026-09-14
RN-578 — O clique que dispara turno de agente responde ao ACEITAR, e a recusa deixa de ser calada
Numa instalação real da v6.1.0 (2026-09-14), responder o formulário de
perguntas do Criativo levou 97,3 s, confirmar a prontidão 97,3 s e
confirmar a arquitetura pronta 51,8 s — cada clique segurado pelo turno
INTEIRO do agente, porque o handle_call do conversacional só respondia ao
GenServer.call do controller do engine quando a Task do turno terminava. Turno
acima do teto (120 s no Criativo, 180 s nos outros) virava 500 num comando que
tinha funcionado. E a recusa de uma segunda mensagem com turno em curso
({:error, :turno_em_andamento}) era descartada pelo controller, que respondia
202: um "Continue" digitado durante o kickoff do Arquiteto foi aceito e nunca
lido, sem rastro nenhum.
A regra (ADR 0163, que generaliza o ADR 0086):
- O aceite sai na hora.
TurnoAssincrono.iniciar/3responde:okaofromassim que a Task sobe — depois de persistiragent.status: working, nunca antes. Vale para os seis conversacionais e para as quatro rotas de usuário que disparam turno (…/agents/:agent/messagee, por ela,…/structured-question/:id/answer;…/readiness;…/agents/arquiteto/handoff-infra) e para a devolução de história (ReturnStoryUseCase). O status e o corpo da api NÃO mudam (201 { ok: true }): sempre significaram "aceito"; o que mudou foi quando chegam. - A recusa ANTES de o turno subir é síncrona e nomeada. O engine responde
409 (
turno_em_andamento,aguardando_aprovacao) ou 422 (sem_regra_de_negocio) com{error, motivo}, e a api repassa o MESMO status com a MESMA frase.turno_em_andamentoganhaagent.errordurável (origempolitica); os outros dois já tinham. - O desfecho do turno segue pelo canal e pelo log, nunca pelo HTTP. Falha
continua
agent.errordurável, com a mesma origem de antes. - A tela deixa de tratar "a chamada resolveu" como fim de turno. Depois do
aceite, ela acompanha o fim pelo canal (
agent.done) e, como rede de segurança, pela cauda do log a cada 4 s: fecha quando oagent.statuspersistido mais recente daquele agente não éworking. Sem nenhumagent.statusdo agente na janela, não fecha — não saber não é "acabou". - O handoff ao Dev Lead continua nascendo DEPOIS do de Infra. O
:offer_dev_handoffque chega com o turno de fechamento do Arquiteto em curso fica pendente e roda quando o turno fecha — sucesso, falha, crash ou cancelamento. (Superado pela RN-672: o Arquiteto não oferece mais ao Dev Lead, e a pendência deixou de existir.)
O que esta regra NÃO fecha: o chat.message que a api grava antes de
perguntar ao engine continua no log quando o engine recusa (agora explicado pelo
agent.error, antes órfão e mudo); engine reiniciado no meio do turno não grava
idle, e a faixa fica até o "Parar" ou um recarregamento; e mensagem ao
infra pelo compositor caía no Criativo pela cláusula final de
AgentCommandController.message/2 — pré-existente, medido aqui e fechado
depois pela RN-584, que tirou o destinatário padrão.
Revisada para a MENSAGEM pela RN-673 (ADR 0191): a mensagem do
usuário que chega com turno em curso não recebe mais turno_em_andamento —
entra na fila do agente e é lida no fim do turno. A recusa do item 2 segue
valendo para os comandos que não são fala (revisão, prontidão, oferta de
handoff).
- Código:
apps/engine/lib/engine/agents/turno_assincrono.ex:120(o aceite),:130e:314(a recusa durável);apps/engine/lib/engine_web/controllers/agent_command_controller.ex:241(202),:243/:253/:263(409/409/422);apps/engine/lib/engine/agents/dev_lead_server.ex:211;apps/engine/lib/engine/agents/arquiteto_server.ex:160(adiar),:202(drenar);apps/api/src/infrastructure/http-clients/api-to-engine-client.ts:591,:612;apps/web/src/lib/session-turno.ts:33(turnoTerminouNoLog),:349(acompanharTurnoPeloLog),:316;apps/web/src/routes/SessionPage.tsx:491,:517,:578;apps/web/src/lib/session-promocao.ts:141;apps/web/src/lib/recusa-do-agente.ts:17 - Teste:
apps/engine/test/engine/agents/turno_assincrono_test.exs:66(aceite com a task viva),:84(workinggravado antes do aceite),:178(recusa durável — caso de falha);apps/engine/test/engine_web/controllers/agent_command_controller_test.exs:35,:66(409),:107(422);apps/engine/test/engine/agents/arquiteto_server_test.exs:209(ordem dos handoffs),:237(cancelamento);apps/api/test/infrastructure/http-clients/api-to-engine-client.spec.ts:520,:538,:580(500 continua genérico);apps/web/src/routes/SessionPage.turno-preso.test.tsx:181,:252(409);apps/web/src/routes/SessionPage.readiness-turno-preso.test.tsx:172,:210(422);apps/web/src/lib/fim-do-turno-pelo-log.test.ts:29,:76 - Origem: AT-089 — instalação real da v6.1.0 em 2026-09-14
RN-579 — Com o canal da sessão vivo, a tela de Sessão troca o poll curto por invalidação e um fallback longo; resposta de corpo vazio tem ETag
A instalação medida na AT-093 (v6.1.0, 14/09) mostrou UM navegador fazendo
7.088 requisições em 56 minutos — mediana de 118/min, pico de 263/min —
contra os 300/min do RATE_LIMIT_USER. Duas abas visíveis no pico passam do
teto, e o 429 cai no navegador do dono da conta. Quase tudo era poll de 3–5s
da tela de Sessão sobre dados que mudam raro (ações, handoffs e backlog
voltavam 304 em 99% das vezes), enquanto a tela já mantinha aberto o canal
session:<id> com o engine. O teto NÃO muda: o que cai é o pedido.
A regra:
- O engine avisa toda escrita confirmada. A fachada
Engine.Sessions.EngineApiClientemiteevent.appended(sótypeeactorId, nunca opayload) depois que a api CONFIRMAappend_event,append_event_returning,propose_action(proposed_action.created),create_handoff(handoff.offered) ecreate_epic/story/task(backlog.*_created). Escrita recusada não avisa. Antes, sóArtifactEmittere o Infra Lead avisavam, à mão — e avisavam mesmo quando a api recusava o append. Desde a AT-157, a escrita que a API faz por conta própria também avisa — a decisão de um humano noutra aba, uma transição de sessão, o chat respondido pelo modelo da própria api.AppendSessionEventUseCasee oSendChatMessageUseCase(os dois únicos pontos que gravam emsession_eventssem passar pelo engine) chamamSessionChannelNotifier.eventAppended, que pede ao enginePOST /internal/sessions/:id/event-appended; o engine faz o MESMOevent.appended(sótypeeactorId). O aviso sai DEPOIS do commit da transação mais externa (aposCommitnoDrizzleUnitOfWork): transação desfeita não avisa, e avisar de dentro dela faria a web buscar antes de o evento existir. Escrita que veio do engine (rota/internal/*, marcada peloOrigemEngineMiddleware) NÃO é avisada de novo pela api — a fachada já avisou. É melhor esforço e não é aguardado: o aviso perdido nunca falha a escrita, e o fallback de 15s o cobre. São só os dois pontos: outro caso de uso que passe a gravar emsession_eventssemAppendSessionEventUseCaseprecisa chamar o notifier também. - "Canal vivo" é join CONFIRMADO.
connectSessionHeartbeatmarca a sessão viva nookdo join e a desmarca em erro/fechamento do canal ou do socket e no cleanup. O estado é por sessão e por aba, fora do React, porque o intervalo é de cada OBSERVADOR e três observadores (Shell,SessionPage,ContextAside) leem a mesma chave de eventos. - Com o canal vivo, o poll vira fallback. Eventos, ações, handoffs,
backlog e a própria sessão passam de 3–5s para 15s; o orçamento, de 5s para
30s.
intervaloDaSessaoéMath.max: quem já pollava mais devagar não acelera. Canal caído devolve o intervalo curto NA HORA — a tela nunca fica pior do que era. - O TIPO do aviso decide o que invalidar, com janela. Todo aviso
invalida os eventos e o orçamento;
proposed_action.*/action.*também as ações,handoff.*os handoffs,backlog.*o backlog. Cada alvo tem janela mínima entre duas buscas (eventos 3s, ações/handoffs/backlog 2s, orçamento 10s): a primeira sai na hora, as seguintes dentro da janela viram UMA no fim dela. Sem isso, uma rajada detool.resultviraria uma busca por evento. A regra do achado C segue, e só para os EVENTOS: durante um turno em streaming eles não são antecipados; as ações são. - Aba oculta não polla.
refetchIntervalInBackground: false(o default do TanStack v5, agora escrito emOPCOES_PADRAO_DAS_QUERIES) — medido por teste, não suposto. - Corpo vazio tem validador. Handler que devolve
nullsaía com corpo vazio e SEMETag(ores.senddo Express só geraETagcom corpo), então o 304 era impossível:sessions/:id/budgetvoltou 304 em 0% das 483 chamadas medidas eexecution/sessionem 22% (o tempo com execução ativa). A api grava oETagdo corpo vazio quandosendsai sem corpo — decidido sobre o que vai ao fio, não sobre o valor do handler, para que um handler com@Res()que escreve depois nunca herde o validador do vazio. 304 NÃO reduz a contagem do rate limit (o guard conta antes do handler); reduz banda. - Compõe com o acompanhamento do turno pela cauda do log (RN-578).
O turno ACEITO continua sendo acompanhado pela leitura da cauda a cada 4s
(
acompanharTurnoPeloLog) — é a rede contra oagent.doneperdido, e o aviso do canal pode se perder junto com ele, então o tique NÃO vira fallback de 15s. O que muda: oevent.appendeddoagent.statusdo agente ACOMPANHADO roda a leitura NA HORA (avisoPedeVerificacaoDoTurno), e o turno fecha assim que o log diz que fechou, sem esperar o tique. Aviso de outro agente, de outro tipo, ou sem turno acompanhado não lê nada a mais. E o poll de 3s dos eventos continua PAUSADO durante o streaming (achados 2/7):intervaloDaSessaosó vale quando a pausa não vale, e a invalidação pelo canal respeita a mesma regra do achado C.
Números (teste de orçamento, uma aba na tela de Sessão, por minuto):
123 com o canal caído (o comportamento de antes, intacto como fallback) e
46 com ele vivo — dos quais 24 são do Shell (resumo dos projetos e sessão
de execução, que não são da sessão e seguem em 5s). Duas abas: 246 → 92.
Desde a RN-632 (AT-278) as leituras de PROJETO pollam no ritmo de
projeto (15s) e as abas Executores e Visão geral passam pelo MESMO
criarInvalidadorDoCanal — a medição com as telas de verdade e duas abas mora
lá.
O que esta regra NÃO fecha: o aviso da escrita que a api faz por conta
própria (item 1, AT-157) é melhor esforço — se o engine estiver fora ou o
aviso se perder, a tela chega pelo fallback, em até 15s (eram 3s); o aviso vai
ao nó do engine que recebeu a chamada, e o broadcast do Phoenix PubSub o
entrega aos inscritos de qualquer nó do cluster (não medido em cluster). O status
working do roster continua dependendo do event.appended do agent.status,
que sai depois de a api gravá-lo. projects-summary e execution/session
(Shell) seguem em 5s. Durante um turno aceito, a leitura da cauda de 4s da
RN-578 soma até 15 req/min, só enquanto o turno dura. Não é mudança de modelo de consistência (sem ADR): a
fonte continua sendo o GET, o canal continua sendo só gatilho — como já era
desde a Fase 4a —, e o que muda é a latência máxima das escritas sem aviso.
-
Código:
apps/engine/lib/engine/sessions/engine_api_client.ex:581,:607,:630,:1122(avisar_canal);:607,:630,:953(avisar_canal);apps/engine/lib/engine/sessions/live_broadcast.ex(event_appended/3);apps/engine/lib/engine_web/controllers/session_command_controller.ex:46(event_appended, a rota do aviso da api);apps/api/src/application/use-cases/sessions/append-session-event.use-case.ts:232(eventAppended);apps/api/src/infrastructure/persistence/drizzle/drizzle-context.ts:39(aposCommit),:53(veioDoEngine);apps/web/src/lib/canal-vivo.ts:35(fallback),:66(estado),:108(intervaloDaSessao),:134(alvosDoEvento),:150(janelas),:207(criarInvalidadorDoCanal);apps/web/src/lib/session-channel.ts:161,:165,:131,:229;apps/web/src/lib/session-turno.ts:414(o aviso: invalida e, do acompanhado, antecipa a leitura),:86(avisoPedeVerificacaoDoTurno),:344(a leitura imediata);apps/web/src/lib/hooks.ts(useSessionEvents,usePendingActions,useHandoffs,useBacklog);apps/web/src/lib/query-policy.ts:86;apps/api/src/interfaces/http/shared/etag-do-corpo-vazio.ts:40;apps/api/src/main.ts:80 -
Teste:
apps/web/src/lib/canal-vivo.orcamento.test.tsx:138(antes, 123),:155(depois, 46),:176(canal cai — caso de falha),:186(rajada),:214(aba oculta);apps/web/src/lib/session-channel.test.ts:237,:253(queda — caso de falha),:267;apps/engine/test/engine/sessions/engine_api_client_aviso_no_canal_test.exs:29,:57(append recusado não avisa — caso de falha),:71;apps/api/test/application/use-cases/sessions/aviso-ao-canal-da-sessao.spec.ts(AT-157: avisa com tipo e ator; recusada não avisa e transação desfeita não avisa — casos de falha; engine fora do ar não falha a escrita; escrita vinda do engine não é avisada em dobro),apps/api/test/infrastructure/http-clients/origem-engine.middleware.spec.ts,apps/engine/test/engine_web/controllers/session_command_controller_event_appended_test.exs;apps/api/test/interfaces/http/shared/etag-do-corpo-vazio.spec.ts:56(a causa),:69,:98(@Res()tardio — caso de falha);apps/api/test/interfaces/rate-limit.guard.spec.ts:183(2 × 263 estoura, 2 × 118 não);apps/web/src/routes/SessionPage.canal-e-acompanhamento.test.tsx:166(o predicado),:203(o aviso do acompanhado fecha o turno sem esperar o tique),:221(aviso de outro agente não lê, e o tique de 4s segue sendo a rede — caso de falha),:244(turno fechado: nem aviso nem tique leem) -
Origem: AT-093 — logs da api instalada (v6.1.0), 2026-09-14; AT-157 — o aviso da escrita da própria api
RN-580 — O agente conversacional que sobe sobre uma conversa recebe o FIM dela, com perguntas e ferramentas, e o começo que não coube é resumido com o número escrito
Os seis agentes conversacionais (Criativo, PO, Arquiteto, Dev Lead, UX
Designer e Staff) reconstroem o histórico do event log no init/1 — no
restart e, também, quando sobem numa sessão que já tem conversa (o PO no
handoff do Criativo). Medido em 13/09 (AT-073, contra a b61bc49cd), os seis
tinham a MESMA cópia de rehydrate/2, com três defeitos:
- Liam o começo.
EngineApiClient.list_events/2pedelimit=200semlatest, e a rota interna — que nem aceitavalatest— devolve os PRIMEIROS 200. Numa conversa de 201 eventos o agente acordava sem a mensagem que estava respondendo. A tela já tinha resolvido isso comlatest: truee aviso de recorte (RN-180); o engine não tinha herdado. - Reconstruíam só
chat.messageeagent.response. A pergunta feita por formulário (chat.structured_question) e as ferramentas chamadas (tool.call/tool.result) sumiam. - Cortavam calados. E as leituras dos kickoffs (PO, Arquiteto, Dev Lead, UX Designer) e das regras do product_brief (Criativo) filtravam em memória os mesmos PRIMEIROS 200 eventos de todos os tipos: numa conversa longa com o Criativo, o brief nascia depois do evento 200 e o PO recebia "(sem product brief disponível)".
A regra:
- Um caminho só.
Engine.Agents.Reidratacaosubstitui as seis cópias; os seis servidores chamamhistorico/3noinit/1. - A cauda, com o teto do ADR 0060. A leitura é
latest=true&limit=200— oMAX_LIMITda rota, nem mais (a api cortaria) nem menos (deixaria de fora o que ela entrega de graça). O teto NÃO ficou ilimitado. - O que entra.
chat.messageeagent.responsecomo sempre (de qualquer agente da sessão — é o que o PO herda do Criativo);chat.structured_questioncomo fala do agente, com rótulos e opções;tool.call/tool.resultdo PRÓPRIO agente como nota de texto com a ferramenta, os argumentos (cortados em 1.500 caracteres) e o desfecho — nunca como mensagemrole: "tool", porque o evento não guarda o id da chamada e umtool_resultsem otool_usecorrespondente é recusado pelo provider.chat.structured_question_answeredfica DE FORA de propósito: a api grava as mesmas respostas numchat.messagelogo depois (AnswerStructuredQuestionUseCasereusaSendAgentMessageUseCase), e reidratar os dois duplicaria a resposta. - O começo que não coube é dito, com o número. O
seqé gapless e começa em 1, então o número de eventos omitidos é oseqdo primeiro da cauda menos um — SUBTRAÇÃO, sem requisição a mais. Quando ele é maior que zero, o histórico abre com UMA mensagem de sistema que escreve esse número e traz o resumo da compactação de contexto mais recente do agente (quando foi gravado) e a abertura da conversa (as seis primeiras mensagens dos primeiros 40 eventos, cortadas em 500 caracteres). São mais duas leituras com teto — três no total, e uma só quando a conversa cabe. context.compactedgrava o resumo daqui em diante —summary,agentemessagesSummarized, ao lado das contagens de sempre. Conversa compactada antes disto fica DECLARADA ("compactada N vez(es) antes de o resumo passar a ser gravado; aquele resumo se perdeu"), nunca reconstruída; os eventos antigos não mudam.- Histórico ilegível não vira conversa vazia. Se a leitura falha, o agente recebe uma mensagem de sistema dizendo que não conseguiu ler o histórico.
- Kickoffs e refs do brief leem POR TIPO, pela cauda. A rota interna ganhou
types(até 20, validados; malformado é 400) — mudança de CONTRATO, aditiva: semlatest/typesa resposta é a de antes. Quando a leitura por tipo bate no teto, o kickoff diz que pode haver itens mais antigos.
Medido por agente: os seis mudaram na reidratação (init/1). Nos kickoffs,
PO, Arquiteto, Dev Lead e UX Designer mudaram; o Criativo mudou nas refs do
product_brief e no guardrail de zero regra; o Staff não tem kickoff (ADR 0088).
O que esta regra NÃO fecha: o texto que a ferramenta devolveu não estava no
log — fechado pela RN-589, daqui em diante (sessão antiga continua sem).
InfraLeadServer (que reidrata por Reidratacao.historico/3 desde a AT-150,
sem cópia própria) e DevLeadTools.run_assessment/2 (que lê a CAUDA, latest
com o mesmo teto de 200) também passam pelo caminho comum; nenhum leitor de
list_events/2 sem latest sobra em apps/engine/lib. A reidratação pode trazer MAIS do que o
contexto vivo tinha (a cauda inteira, mesmo o que já tinha sido compactado); o
ContextManager compacta de novo no próximo turno.
-
Código:
apps/engine/lib/engine/agents/reidratacao.ex:71(historico/3),:46(o teto),:104(eventos_do_tipo/3),:148(mensagens/2),:167(pergunta estruturada),:170(ferramenta),:271(omitidos por subtração),:274(o resumo do começo),:294(a compactação),:328(a abertura);apps/engine/lib/engine/harness/context_manager.ex:138(o resumo gravado);apps/engine/lib/engine/sessions/engine_api_client.ex:1077gravado);
apps/engine/lib/engine/sessions/engine_api_client.ex:1248(list_events/3); os seisinit/1—apps/engine/lib/engine/agents/criativo_server.ex:89,po_server.ex:93,arquiteto_server.ex:87,dev_lead_server.ex:132,ux_designer_server.ex:85,staff_server.ex:86; as leituras por tipo —criativo_server.ex:435,po_server.ex:352,arquiteto_server.ex:313,dev_lead_server.ex:431,ux_designer_server.ex:246;apps/api/src/interfaces/http/internal/leitura-interna-de-eventos.ts:24,apps/api/src/interfaces/http/internal/internal-sessions.controller.ts:306,apps/api/src/infrastructure/persistence/drizzle/session-event.repository.ts:56 -
Teste:
apps/engine/test/engine/agents/reidratacao_test.exs:57(pergunta estruturada),:92(a resposta entra UMA vez),:108(ferramenta com desfecho),:128(semtool.result),:136(ferramenta de outro agente fica de fora),:157(pede a cauda com teto 200),:176(o fim entra, o começo vira resumo com o número),:194(o resumo da compactação),:233(compactação antiga declarada),:248(histórico ilegível — caso de falha),:255(abertura falhando não apaga o número),:270(leitura por tipo e o aviso de teto);apps/engine/test/engine/agents/reidratacao_dos_seis_test.exs:59(os seisinit/1),:84(o brief depois do evento 200 chega ao PO),:118(a regra depois do evento 200 entra no brief do Criativo),:135(Arquiteto, Dev Lead e UX leem por tipo);apps/engine/test/engine/harness/context_manager_test.exs:74(o resumo gravado),:101(fallback do sumarizador);apps/api/test/interfaces/http/internal/leitura-interna-de-eventos.spec.ts:13(sem os parâmetros, as opções de antes),:40(tipo malformado é 400);apps/api/test/infrastructure/persistence/session-event-latest.repository.spec.ts:129(typesfiltra e olimitconta só eles),:143(typescomlatest) -
Origem: AT-073, levantada em 2026-09-13
O repositório nasce no aceite do handoff ao Arquiteto, e a execução não começa sem ele (RN-582)
RN-584 — A mensagem de chat não tem destinatário padrão: cada agente que conversa tem cláusula própria, e o resto é recusa nomeada
A última cláusula de EngineWeb.AgentCommandController.message/2 não olhava o
agente: casava QUALQUER nome que as cláusulas de cima não tivessem pegado e
entregava a mensagem ao Criativo. Uma mensagem escrita para o infra era
lida por outro agente, o clique respondia 202, e o Criativo respondia no fio —
nada quebrava, e ninguém via. Achado como adjacência da
RN-578 (AT-089) e corrigido aqui (AT-098).
A medição, antes do conserto (os três lados, na dev de 72ee8cdcf):
| Destino | A tela oferece? | A api aceita? | O engine tinha cláusula? |
|---|---|---|---|
criativo |
sim (composer: fallback da sessão criativa e AGENTES_DE_CHAT) |
sim | NÃO — era a cláusula final, sem guarda |
po, arquiteto, dev-lead, ux-designer, staff |
sim (AGENTES_DE_CHAT, o agent.activated mais recente) |
sim | sim, uma cada |
infra |
não no composer (fora de AGENTES_DE_CHAT desde o achado 9-fix e a RN-499) |
sim | não — caía no Criativo |
qualquer outro nome do roster (qa, subagentes, secops, psicologo…) |
só pelo formulário de perguntas estruturadas, que responde ao ATOR que perguntou (agent={event.actor.id}); hoje só Criativo e PO perguntam |
sim | não — caía no Criativo |
| nome desconhecido, ou nenhum | não | sim (a api não valida o agent; só o sessionId vira segmento de URL checado, RN-128) |
não — caía no Criativo |
A regra:
- Cada agente que conversa tem cláusula PRÓPRIA, o Criativo inclusive
(
"agent" => "criativo"). A cláusula final deixou de existir como entrega: ela é RECUSA. - Nome sem cláusula é 422 NOMEADO,
{error, motivo}no molde da RN-578:agente_sem_conversa, com uma frase que nomeia o agente e diz que a mensagem ficou registrada e nenhum agente a leu. A api repassa 422 comoUnprocessableEntityExceptioncom a MESMA frase (opostComandoDeTurnoda RN-578, sem código novo), e a tela a mostra pelomensagemDaRecusaDoAgenteque já existia. Sem"agent"no corpo éagente_ausente, e agente que conversa sem texto émensagem_sem_texto— três motivos, porque "não conversa" dito a quem conversa seria mentira. - O
infratem recusa PRÓPRIA, com frase que diz o que ele faz: trabalha por proposta (a PR de infra, a subida do container), e o que pede decisão aparece em Aprovações. Não é falta de código, é a mesma leitura da RN-499 — o Infra Lead é propositivo. E ouser_message/2que oInfraLeadServerainda exporta não serve para uma cláusula: ele roda o turno INTEIRO dentro dohandle_call(até 180 s), sem o aceite do ADR 0163 e sem "Parar" (via_for/2não o conhece). Ligá-lo devolveria ao clique a espera que a RN-578 tirou. - O "Parar" também não adivinha.
cancel/2sem"agent"parava o turno do Criativo; passa a ser 422agente_ausente. A api sempre manda o agente nas duas rotas, então essas duas recusas só alcançam chamador quebrado. - A tela continua não oferecendo o
infrano composer — nenhuma mudança de UI:AGENTES_DE_CHATjá o excluía, e o que mudou é que, se um dia alguém o puser lá, o engine recusa com nome em vez de entregar a outro, e a guarda do CI reprova antes.
Desde a RN-617 (ADR 0175), os itens 3 e 5 não valem mais: o turno do Infra Lead migrou para o
TurnoAssincrono, e só então ele ganhou cláusula própria demessage/2, ovia_for/2do "Parar" e o lugar emAGENTES_DE_CHAT. A recusaagente_sem_conversadele saiu; a regra de fundo desta RN — nenhum destinatário padrão, todo nome sem cláusula recusado com nome — é a mesma.
A guarda que fecha a classe mora em
scripts/ci/destinos-do-composer.spec.ts, porque o destino nasce em
TypeScript e a cláusula mora em Elixir — nenhuma das duas suítes lê a outra
linguagem. Ela DERIVA os destinos da fonte (AGENTES_DE_CHAT, os literais
agentParaEnviar = '…' do SessionPage.tsx e o AgentKey do roster) e exige:
cláusula própria que conversa e via_for/2 do "Parar" para cada destino do
composer; nenhuma cláusula sem agente fixo na cabeça que entregue a um agente;
a última cláusula de message/2 sendo recusa; o infra fora do composer e
com recusa própria; e @agentes_de_conversa igual ao conjunto das cláusulas
que conversam. Cinco mutações medidas, todas mortas (o catch-all antigo de
volta, infra em AGENTES_DE_CHAT, a cláusula do Criativo renomeada, o
via_for do Staff removido, a cláusula do infra renomeada).
O que esta regra NÃO fecha, declarado:
-
Se o Infra Lead passa a conversar pelo composer era decisão de produto, sem dono — tomada na RN-617 (AT-141): sim. Dar a ele uma cláusula exige, antes, migrar o turno dele para o
TurnoAssincrono(o aceite e a recusaturno_em_andamentoda RN-578) e registrá-lo no "Parar"; e exige decidir o que uma conversa faz com um agente cujo contrato é propor. Ouser_message/2dele segue exportado e sem chamador. -
O
chat.messagegravado pela api antes da recusa continua no log — e desde a RN-587 o engine grava oagent.errorda recusa ao lado, então o fio explica a si mesmo. -
A api não valida o
agent. Recusar lá, antes de gravar, exigiria uma segunda lista de quem conversa do lado TypeScript — e divergir dela das cláusulas é o defeito que esta regra fecha. Quem decide é quem tem as cláusulas (decisão mantida na RN-587). -
Código:
apps/engine/lib/engine_web/controllers/agent_command_controller.ex:42(@agentes_de_conversa),:177(a cláusula do Criativo),:192(a recusa doinfra),:283(mensagem_sem_texto),:249(agente_sem_conversa),:312(agente_ausente),:310(o "Parar" sem agente);apps/api/src/interfaces/http/agents/agents.controller.ts:115e:174(a resposta 422 documentada);apps/web/src/lib/session-readiness.ts:37(AGENTES_DE_CHAT, a fonte que a guarda lê) -
Teste:
scripts/ci/destinos-do-composer.spec.ts(a guarda da classe);apps/engine/test/engine_web/controllers/agent_command_controller_test.exs:124(caminho feliz: o Criativo pela cláusula própria),:231(oinfra: 422 e nenhum dos dois sobe — caso de falha),:258(todo nome do catálogo de áreas e do roster fora de conversa),:286,:302,:317;apps/api/test/infrastructure/http-clients/api-to-engine-client.spec.ts:575(o destino viaja como escolhido, e o 422 chega com a frase) -
Origem: AT-098 — adjacência medida no PR #594 (AT-089)
RN-585 — O fim do turno só fica visível depois de o turno fechar no agente
Desde o ADR 0163
(RN-578) o turno dos seis conversacionais roda numa Task, e o
desfecho que a Task grava — agent.response, ou agent.error quando a api
narra orçamento, credencial ou binding no frame final — sai ANTES de o
resultado chegar ao GenServer do agente e zerar turno_assincrono. Um teste
que lia o state logo depois do agent.error reprovava em ~1 de 4 rodadas
(AT-099: 8 de 30 medidas). A pergunta era se a MESMA janela existia para
o usuário: a tela via o turno acabar e a mensagem seguinte ouvia 409
turno_em_andamento?
Não existe, e a regra é o que faz ela não existir:
- Os dois sinais de fim de turno saem só de
finalizar/1, que roda no processo do GenServer, dentro dohandle_infoque JÁ zerouturno_assincrono:agent.doneno canal eagent.status: idlepersistido. São os dois, e só eles, que a tela usa para fechar o turno (onAgentDoneeturnoTerminouNoLog, RN-578 item 4). - A mensagem de quem viu um desses sinais é atendida depois desse
handle_info— o GenServer atende uma mensagem por vez —, então encontra o agente livre e é aceita. - O que a Task grava não é sinal de fim.
agent.responseeagent.erroraparecem no fio com o turno ainda aberto no agente. Tela ou cliente que passasse a fechar o turno por eles reabriria a janela; emitiragent.doneouidlede dentro da Task também.
Não houve mudança de comportamento: a ordem já era esta desde a RN-578. O que
esta RN acrescenta é a ordem ESCRITA e travada por teste nos seis
conversacionais — o teste para o GenServer com a Task terminada e prova que
nenhum dos dois sinais saiu, sem timeout — e a correção do teste do PO, que
passa a esperar o idle persistido em vez do agent.error.
- Código:
apps/engine/lib/engine/agents/turno_assincrono.ex:158(zera o turno nohandle_info),:495(finalizar/1, único emissor dos dois sinais);apps/web/src/lib/session-turno.ts:30(turnoTerminouNoLog) - Teste:
apps/engine/test/engine/agents/turno_assincrono_test.exs:434(os seis conversacionais: com o GenServer suspenso e a Task terminada, oagent.responsejá está no log e nemagent.donenemidlesaíram; depois doidle, a mensagem seguinte é aceita — o caso de falha é a recusaturno_em_andamento, que o teste nega);apps/engine/test/engine/agents/po_server_test.exs:478(espera oidleantes de ler o state) - Origem: AT-099 — teste intermitente do
PoServerTest, medido em 2026-09-19
RN-586 — O turno que o reinício do engine interrompeu fecha com desfecho durável, e nunca é refeito
Desde a RN-578 o agent.status: working é gravado ANTES do aceite, e
o fim do turno (agent.status: idle e agent.done) só sai de finalizar/1,
dentro do processo do agente (RN-585). Se o engine cai no meio do
turno, o processo e a Task morrem juntos e ninguém grava o fim: o último
agent.status do agente fica working para sempre (AT-156, reproduzido —
subir o servidor de novo sobre esse log não gravava nada, nos seis). Os dois
leitores desse sinal o tomam por turno em curso: a tela (turnoTerminouNoLog,
RN-578 item 4) mantém a faixa de atividade, e o
GetSessionPendingWorkUseCase conta trabalho pendente SEM teto (o terceiro
sinal da RN-064), então a sessão não fecha por heartbeat. Só a
próxima mensagem do usuário destravava, e só porque ela sobe o agente.
- O
workingque ficou sem turno vivo é fechado por evento NOVO, nunca reescrito (evento é imutável): umagent.errorcom origeminfraereason: turno_interrompido_por_reinicio— a frase diz que o engine foi reiniciado, que o turno não foi concluído e que quem decide tentar de novo é o usuário (RN-059: falha nunca vira resposta vazia) — seguido deagent.status: idlepersistido. O canal recebeagent.erroreagent.done, para a faixa de quem está com a tela aberta sair sem recarregar. Vale para os que compartilhamTurnoAssincrono— seis na origem, sete desde a RN-617. - O turno NUNCA é reexecutado. Refazê-lo gastaria token em nome de uma mensagem que o usuário talvez já tenha esquecido, sobre um histórico reconstruído; nenhuma chamada ao modelo acontece no fechamento.
- Só o ÚLTIMO
agent.statusde cada agente conta, e sóworking:idlejá está fechado, eawaiting_approval(Dev Lead suspenso, RN-284) é decisão pendente, não turno morto. - "Sem turno vivo" olha o cluster, e na dúvida não fecha. O registro dos
agentes é local; num rollout o pod antigo pode estar rodando o turno, e
fechá-lo dali mataria um turno saudável. Nó que não responde conta como vivo.
O repasse de sessão do drain (RN-588,
Monitor.expect_handoff/1) é doSessionServer, não dos conversacionais: enquanto o pod antigo está de pé o agente dele segue registrado lá, e a varredura do pod novo o enxerga; oAdopternão varre — quem chega aoworkingórfão depois que o pod antigo morre é oinit/1do agente. - Dois pontos de entrada, uma função (
Engine.Agents.TurnoOrfao): a varredura de boot em cada sessão reidratada (Rehydrator, fora do caminho do readiness — o boot não espera a api), que fecha o turno mesmo que ninguém mande mensagem, e oinit/1de cada servidor, rede de segurança para quando o boot não conseguiu escrever (api ainda fora do ar). Falha de leitura ou de escrita é logada e o agente sobe.
Declarado e não fechado: o Infra Lead ( — fechado na
RN-617, que o pôs no infra) roda o turno dentro do
handle_call, sem TurnoAssincrono, e fica de foraTurnoAssincrono e em TurnoOrfao; a janela de leitura é a
das 200 mais recentes agent.status da sessão; se a api estiver fora no boot E
ninguém acordar o agente, o working fica até a próxima subida dele; e o
fechamento não recupera o que o turno já tinha gravado antes da queda.
- Código:
apps/engine/lib/engine/agents/turno_orfao.ex:89(varrer/2),:99(fechar_ao_subir/3),:122(encerrar/3);apps/engine/lib/engine/sessions/rehydrator.ex:75(varredura no boot); oinit/1dos seis*_server.exchamaTurnoOrfao.fechar_ao_subir/3 - Teste:
apps/engine/test/engine/agents/turno_orfao_test.exs:72(nos seis: oworkingsem turno vivo viraagent.errorinfra+idle, e o LLM não é chamado),:108(awaiting_approvalnão é órfão;idlee oworkingde outro agente não são fechados),:124(varrer/2pula o agente com processo vivo em ESTE nó ou num nó:peerde verdade do cluster; leitura e escrita que falham não derrubam a subida);apps/engine/test/engine/sessions/rehydration_test.exs:46(o boot dispara a varredura) - Origem: AT-156, herdada da AT-089 (RN-578) — reproduzida em 2026-09-19
Revisada pela RN-920 (ADR 0226). O item 2 ("o turno NUNCA é reexecutado") deixou de valer para o turno recente: a varredura de boot o retoma UMA vez, se a última atividade do agente tem até 900 s, com a mensagem efêmera do diário; o turno velho ou já retomado continua só fechado, e o botão "Retomar" (RN-921) é o caminho humano. Os itens 1, 3, 4 e 5 seguem.
RN-587 — A mensagem que nenhum agente leu fica no fio dizendo isso: o engine grava o agent.error da recusa
A api grava o chat.message ANTES de perguntar ao engine (o engine reconstrói a
conversa lendo o log, e a mensagem precisa ser durável mesmo com ele fora do
ar). Quando o engine recusava, a mensagem ficava no fio do usuário com cara de
entregue e o motivo morava só no toast — a tela afirmando o que a fonte negou.
As duas RNs de origem deixaram isso declarado (RN-578, RN-584);
esta o fecha (AT-132).
A medição, antes do conserto, por recusa:
| Recusa | Quem a produz | agent.error durável ao lado do chat.message? |
|---|---|---|
409 turno_em_andamento |
TurnoAssincrono (RN-578) |
JÁ SIM — emitir_recusa_por_turno_em_andamento/1, com teste |
409 aguardando_aprovacao |
DevLeadServer.handle_call (RN-578) |
JÁ SIM — reason: "aguardando_aprovacao_de_plano" |
422 agente_sem_conversa, mensagem_sem_texto, agente_ausente |
o controller (RN-584), sem *Server |
NÃO — a explicação vivia só no toast |
A regra:
- O engine é a fonte da recusa E do registro. As três recusas 422 gravam
agent.error(origempolitica,mensagemigual à frase do 422,reasonigual aomotivo) e o transmitem no canal da sessão — o mesmo par durável/efêmero das duas 409. O fio já desenhaagent.error(SessionPage.tsx,lerFalhaDeTurno), então nenhuma tela mudou. - Só há o que gravar com sessão nomeada. Sem
projectId/sessionIdno corpo (chamador quebrado) a recusa segue só como resposta. - O ator não vem da rota.
infrae os seis que conversam entram comoagent; qualquer outro nome (que a rota pública deixa passar) é osystemengine— um nome digitado nunca vira ator no log. - Não se apaga nem se adia o
chat.message. Evento é imutável, e gravá-lo só depois do aceite reordenaria o log: o turno roda numa Task e oagent.responsepoderia preceder a mensagem que responde (e uma falha entre o aceite e o append rodaria um turno sobre mensagem não gravada).
A api continua sem validar agent, e agora isso é decisão e não lacuna. A
validação que a AT-098 pedia é a recusa 422 do engine, e ela é o mecanismo
ÚNICO: a api a repassa (postComandoDeTurno) e o engine a registra. Uma lista
do lado TypeScript seria a segunda cópia que diverge no primeiro agente novo, e
uma lista GERADA a partir das cláusulas exigiria parsear Elixir — sem ganho
para o usuário, que já vê a mensagem e a explicação no fio.
- Código:
apps/engine/lib/engine_web/controllers/agent_command_controller.ex:573(recusar_mensagem/5) e:574(registrar_recusa_de_mensagem/3) - Teste:
apps/engine/test/engine_web/controllers/agent_command_controller_test.exs:215(qa, o agente do roster que segue sem conversa — até a RN-617 o caso era oinfra, que passou a conversar: oagent.errorcom a frase e o motivo),:178(nome desconhecido não vira ator),:194,:209,:224(sem sessão: nada gravado — caso de falha); mutação medida: com o registro removido, 4 dos 5 falham;scripts/ci/destinos-do-composer.spec.ts(a recusa demessage/2érecusar_mensagem/5e ela grava oagent.error; seis mutações mortas) - Origem: AT-132 — declarado aberto nas RN-578 (AT-089) e RN-584 (AT-098)
RN-588 — Repassar uma sessão não deixa o pod antigo apagar a linha do par
No rollout, o drain do pod antigo para o SessionServer e pede a um par que
assuma a sessão (Engine.Shutdown); o init do par regrava a linha de
engine.session_states. O :DOWN do processo parado, porém, chega ao
Engine.Sessions.Monitor do pod antigo por mensagem — e o Monitor é um
GenServer único, que serializa o :DOWN de cada sessão com um DELETE por ida
ao banco, sem esperar pelo repasse seguinte. Quando o :DOWN era processado
DEPOIS do upsert do par, o DELETE ... WHERE session_id — que não olha dono
nem geração — levava a linha DO PAR: a sessão ficava com dono e sem linha,
invisível ao Adopter, ao Rehydrator e ao local_sessions/0 do drain do
par. Se o par era OUTRO pod antigo (.33 → .28 → novo), ele não a via ao
drenar, e o SIGTERM dele a matava: active na api e sem dono (AT-078, o
rollout-test do propriedades.yml, ~3 órfãs em 13 rodadas).
A regra: o drain marca o stop com Monitor.expect_handoff/1 (o
expect_stop/1 de sempre MAIS "a linha não é minha"), e o Monitor NÃO apaga a
linha de uma sessão repassada — registra em Logger.info que a manteve. Sem
adoção, quem apaga é o próprio drain (terminate_unadopted), como já era; o
Monitor continua apagando a linha de toda sessão que morre por outra causa e a
do expect_stop/1 do SessionLifecycleWorker (a api já encerrou a sessão).
Sem migration: a linha não ganhou coluna de dono, e a marca vive no estado do
Monitor, por sessão. Toda apagada do Monitor passou a deixar UMA linha de log
com sessão, nó e linhas removidas — o log da drenagem nunca chega ao log do
pod (o preStop o descarta), o do Monitor chega.
Inferência que fica: o instante em que a linha sumiu nunca foi medido no
k3d; a leitura vem do artefato da rodada 35452845830 (sem linha + segundo pod
antigo) e do código, e a corrida foi provada em ExUnit, não observada. A
próxima falha do k3d, se houver, traz a linha do Monitor.
- Código:
apps/engine/lib/engine/sessions/monitor.ex:45(expect_handoff/1),:119(apagar_linha/1);apps/engine/lib/engine/shutdown.ex:130(release/1, o drain marca o repasse) - Teste:
apps/engine/test/engine/sessions/monitor_repasse_test.exs:52(Monitor suspenso: o:DOWNtardio não apaga a linha regravada pelo par),:67(cadeia antigo → antigo → novo: o segundo drain ainda vê a sessão),:87(sem adoção a linha continua saindo pelo drain),:100(Shutdown.release/1marca o repasse — trocar a chamada de volta porexpect_stop/1reprova SÓ este teste) - Origem: AT-078 — sessão órfã intermitente do rollout do engine
RN-589 — O texto que a ferramenta devolveu entra no tool.result, cortado com o total dito
A RN-580 declarou aberto: só o Criativo gravava tool.result, e sem o texto —
nos outros cinco a nota da reidratação dizia que o log não registra o
desfecho, e o agente reidratado sabia QUE chamou create_epic, não o id do
épico que ela respondeu.
A regra:
- Os seis gravam. Criativo, PO, Arquiteto, Dev Lead, UX Designer e Staff
emitem
tool.resultdepois de TODA chamada que devolve texto. O payload sai de UM lugar,Engine.Agents.ResultadoDeFerramenta.payload/2:tool,oke — no sucesso —resultado; na falha,erro(como o Criativo já fazia). - Com teto, e o teto diz o total. O texto é cortado em 2.000 caracteres;
quando corta,
resultadoTotaltraz o tamanho REAL (ADR 0060). O motivo do teto: o log é lido por todo membro do projeto e entra no backup, e uma listagem (listar_backlog) pode ser grande. - Aditivo. Campo novo em tipo existente, sem migration;
tool/ok/erroseguem como eram. Evento é imutável: sessão anterior continua sem o texto, e a nota da reidratação segue dizendo isso para a chamada semtool.result. - A reidratação usa o texto.
desfecho: ok, devolveu: <texto>(com aviso[cortado; o total real tinha N caracteres]quando houverresultadoTotal). Continua nota de texto, nuncarole: tool— o evento não guarda o id da chamada. propose_adrdeixou de gravar o própriotool.resultde recusa: o servidor do Arquiteto o grava para toda ferramenta, e os dois duplicariam o evento.
O que fica aberto: (a) e (b) fecharam na RN-593 — (a) o Dev
Lead que SUSPENDE ({:pending, _},
propose_execution_plan/assess_implementability) não gravava tool.result na
suspensão nem no action_settled; (b) o InfraLeadServer tinha o próprio
tool.result e não passava por este módulo; (c) o teto de 2.000 é ESCOLHA, não medida sobre o
acervo — nenhum resultado real foi amostrado; (d) decisão de produto
pendente (TODO humano da AT-151): as ferramentas dos seis devolvem ids,
listas de regras/backlog/métricas e mensagens de erro — nenhuma lê arquivo do
repositório nem saída de comando —, mas se uma ferramenta assim nascer, ela
gravaria conteúdo dele no log; o corte é a única contenção.
- Código:
apps/engine/lib/engine/agents/resultado_de_ferramenta.ex:24(payload/2),criativo_server.ex:382,po_server.ex:259,arquiteto_server.ex:336,dev_lead_server.ex:407,ux_designer_server.ex:232,staff_server.ex:198;apps/engine/lib/engine/agents/reidratacao.ex:244(desfecho/1) - Teste:
apps/engine/test/engine/agents/reidratacao_test.exs:128(texto e aviso de corte na nota;:145sessão antiga semtool.result),apps/engine/test/engine/agents/po_server_test.exs:252(o evento gravado, cortado, comresultadoTotal) - Origem: AT-151 — declarado aberto na RN-580 (AT-073)
RN-593 — O Dev Lead suspenso e o Infra Lead gravam o tool.result pelo módulo comum
A RN-589 declarou dois abertos: o Dev Lead que suspende numa
aprovação não gravava tool.result em momento nenhum, e o InfraLeadServer só
gravava o de recusa de propose_infra_pr, com payload montado à mão. Nos dois
casos o agente reidratado (RN-580) lia "o log não registra o
desfecho desta chamada" sobre um plano que o usuário já tinha decidido, ou sobre
toda chamada de validate_infra_file e de subida de container.
A regra:
- Dev Lead: o
tool.resultnasce na RETOMADA, nunca na suspensão. Quandopropose_execution_plan/assess_implementabilitysuspende ({:pending, _}), o log fica só com otool.call: não há desfecho, e gravar "pending" ali seria a mesma mentira que seria para o modelo. Quando o{:action_settled, …}da MESMA ação chega, o servidor grava otool.resultcom o MESMO texto que entra na mensagemrole: "tool".oksegue a partição detexto_do_desfecho/1:executed/auto_approved/approvedsãotrue; recusa, falha e status que o módulo não conhece sãofalse—ok: truesobre desfecho que ninguém classificou seria afirmar sucesso sem saber. Desfecho de OUTRA ação continua ignorado e não grava nada. - Infra Lead: toda ferramenta despachada inline grava.
validate_infra_file,propose_container_startecontainer_start_via_runnergravamtool.resultporEngine.Agents.ResultadoDeFerramenta.payload/2— proposta aceita pela api éok: true; recusa local por modo (RN-566), recusa da api (RN-591) e runner ausente sãook: falsecom o motivo. A recusa depropose_infra_prsem repositório (RN-577) passa pelo mesmo módulo, com o payload de antes (tool,ok: false,erro) e o corte. propose_infra_praceita segue SEMtool.result, de propósito: ela não emitetool.call(o rastro dela é aproposed_action), e um resultado sem chamada viraria nota órfã na reidratação.
O que segue aberto: a suspensão atravessada por um restart do engine
continua perdendo a inscrição no Wake (ADR 0086) — ali o tool.result nunca
nasce, e a reidratação diz que o log não tem o desfecho, o que é verdade; o
teto de 2.000 caracteres continua ESCOLHA, não medida: nenhum acervo real foi
amostrado nesta mudança (o item (c) da RN-589); a saída do
validate_infra_file (hadolint/actionlint) pode citar trechos do arquivo que o
PRÓPRIO agente escreveu, e o corte é a única contenção — é o item (d) da
RN-589 com o primeiro caso concreto.
- Código:
apps/engine/lib/engine/agents/dev_lead_server.ex:357(handle_info/2doaction_settled, otool.resultem:271),:821(sentido_do_desfecho/1);apps/engine/lib/engine/infra/infra_lead_server.ex:1120(registrar_resultado/4),:317(a recusa depropose_infra_pr) - Teste:
apps/engine/test/engine/agents/dev_lead_server_test.exs:292(a suspensão não grava),:364(a retomada grava o texto que o modelo leu),:488(recusado éok: falsecom o motivo);apps/engine/test/engine/infra/infra_lead_server_test.exs:253(o desfecho devalidate_infra_file),:409(proposta aceita),:485(recusa por modo),:863e:914(container_start_via_runnercom e sem runner) - Origem: AT-190 — declarado aberto na RN-589 (AT-151)
RN-601 — O engine e o broker aplicam aos dois tokens de serviço a régua da api
Os três processos que comparam o token de serviço usam a mesma régua da
RN-598, cada um na sua linguagem. O espaço em volta é descartado, e
um valor feito só de espaço conta como ausente. Em produção,
BRABO_SERVICE_TOKEN ausente, o literal público de desenvolvimento
(dev-service-token-change-me) ou um valor com menos de 16 caracteres depois
do trim DERRUBAM o boot, com uma mensagem que nomeia a variável. As duas
últimas recusas valem também para BRABO_SERVICE_TOKEN_PREVIOUS quando ele
está definido. Um anterior igual ao atual não conta como rotação. Fora de
produção só o trim se aplica, e o atual vazio cai no default de dev.
Antes, o engine lia os dois crus em config/runtime.exs: sem trim, sem piso,
e em produção subia com o default público quando a variável faltava (e com a
string vazia quando o compose a passava vazia). Só a api recusar subir
protegia a instalação. O broker já aplicava a régua ao atual, mas ao anterior
só o trim.
Duas divergências de forma, declaradas. No engine, "produção" é
config_env() == :prod (a release), e não NODE_ENV, que ele não lê; a
imagem de produção é sempre uma release :prod. E a régua fica inline no
runtime.exs, e não num módulo de lib/, porque numa release esse arquivo
roda num config provider antes de o código da aplicação estar carregado.
Consequência: todo processo que avalia esse runtime.exs numa release
:prod precisa do token, inclusive a migração (bin/engine eval Engine.Release.migrate()). Por isso o serviço migrate-engine dos composes de
produção e de instalação passou a receber BRABO_SERVICE_TOKEN e
BRABO_SERVICE_TOKEN_PREVIOUS, sem default. O smoke de CI mostrou isso: sem
eles, a migração morria antes de migrar. O Job de migração do Kubernetes já os
recebia pelo envFrom de brabo-secrets.
- Onde:
apps/engine/config/runtime.exs:85(exigir_token_de_servico_de_producao),:105(service_token),:121(service_token_previous);apps/broker/src/config.ts:89(anterior),:125(exigirTokenDeProducao) - Teste:
apps/engine/test/engine/runtime_service_token_test.exs(avalia oruntime.exsde verdade porConfig.Reader.read!/2, comenv: :prode:dev);apps/broker/src/config.spec.ts(describe "lerConfiguracao — produção, o token ANTERIOR (RN-601)") - Borda: a régua é copiada nos três processos, não compartilhada: são três
linguagens ou runtimes, e nenhum teste confere que as três cópias continuam
iguais. O piso conta caracteres (
String.lengthno Elixir,.lengthno TS), o que só difere para token fora do ASCII. - Origem: AT-216 (achado da AT-205)
RN-610 — O Infra Lead recusa a subida por ESTADO, na ordem da /containers: já de pé, sem imagem, pasta nunca confirmada
A RN-566 deu às duas tools de subida do Infra Lead a recusa por
MODO e declarou o resto: a /containers checa três coisas antes de oferecer o
botão (imagem decidida, modo, pasta confirmada) e o agente checava uma. A
decisão do mantenedor (AT-142, 2026-09-27) foi fechar essa distância por
recusas locais no despacho, no molde da RN-566/RN-577, e NÃO por
contexto enriquecido — GetInfraContextUseCase não muda.
recusa_local_de_subida/2 continua lendo o projeto UMA vez. Primeiro o MODO
(recusa_por_modo/2, a régua da RN-566 sem mudança de texto) — é ele que diz
qual tool usar, e responder "falta imagem" a quem chamou a tool errada
apontaria a porta errada. Depois o ESTADO (recusa_por_estado/3), uma
cláusula por motivo, na MESMA ordem de decidirSubida
(apps/web/src/routes/containers-subida.ts), avaliadas só até a primeira
recusa:
| Cláusula | propose_container_start |
container_start_via_runner |
Leitura local |
|---|---|---|---|
container já REGISTRADO running/provisioning (ja_esta_de_pe) |
recusa | recusa | project_containers.status |
nenhuma imagem decidida (sem_imagem_decidida) |
não recusa | recusa | existe artifact.project_image em alguma sessão do projeto |
pasta nunca confirmada (runner_nunca_confirmou) |
— | recusa | projects.workspace_verified_at nulo |
| nenhum runner conectado agora | — | recusa (já existia, RN-508) | Engine.Runners.Registry |
Por que propose_container_start não recusa por imagem. Eleger a imagem é
o que essa proposta FAZ (RN-491): exigir decisão antes inverteria a
ordem. A tela checa imagem nos dois modos porque o botão dela não elege nada.
container_start_via_runner não elege — sobe a imagem JÁ decidida, e
ExecuteContainerStartViaRunnerUseCase falha sem ela —, então ali a cláusula
vale. O predicado é o MESMO da api (ObterContainerDoProjetoUseCase):
EXISTÊNCIA do evento, nunca validade do payload, porque a api degrada payload
ilegível para o default em vez de tratá-lo como ausência.
Por que "já de pé" recusa, se a execução não falharia.
SubirCicloDeVidaDoContainerUseCase é idempotente sobre provisioning/
running, então a proposta aprovada seria um nada que gastou uma decisão
humana — e em container/mounted pior que nada: elegeria uma versão nova de
artifact.project_image que o container de pé, com a versão congelada na
linha (RN-245), não usaria. Registrado não é observado
(RN-486): o texto não afirma que o container está vivo, diz o que
fazer quando ele morreu por fora. stopped, failed e removed não recusam.
Erro de consulta devolve nil e cai no caminho de sempre — "não consegui
olhar" nunca vira recusa.
Os textos, que são o resultado de ferramenta que o modelo lê
(RN-163) — nunca agent.error, nunca
fim de turno, com tool.call/tool.result (ok: false) no event log pelo
mesmo registrar_resultado/4 da RN-593:
- já de pé: "o container deste projeto já está REGISTRADO como
<status>— subir não é a próxima ação, e<tool>não foi proposta. O registro não é observação (RN-486): se o container morreu por fora, ou se a imagem precisa mudar, diga ao usuário que parar ou remover é pela página/containers; só depois disso uma nova subida faz sentido. Não repita a chamada agora." - sem imagem: "nenhuma imagem de container foi decidida para este projeto
(
artifact.project_image, RN-105) —container_start_via_runnersobe a imagem JÁ decidida e não elege nenhuma, então aprovada ela só poderia falhar, e não foi proposta. Quem decide a imagem é o Arquiteto (choose_project_image): diga isso ao usuário e não repita a chamada até haver decisão." - pasta nunca confirmada: "a pasta deste projeto nunca foi confirmada por um
agente local (
workspace_verified_atvazio, RN-423) — nenhumbrabo-runnerjamais conectou a ele, econtainer_start_via_runnernão foi proposta. Peça ao usuário para rodarbrabo-runner --project <id> --dir <pasta>na máquina dele: a confirmação acontece quando o runner conecta."
Nenhuma linha de prompt mudou, e nenhuma medição com modelo real foi feita nesta regra: a premissa de que o modelo entende a recusa é a da RN-566.
O que NENHUMA cláusula checa, declarado: instalação sem broker (o engine
não lê BROKER_URL; quem recusa é a api ao propor, com 409
sem_broker_na_instalacao, RN-591) e papel/sessão (o agente não é
quem clica). A cláusula de pasta usa o carimbo, que não é batimento
(RN-468) — por isso a de runner conectado continua depois dela.
- Código:
apps/engine/lib/engine/infra/infra_lead_server.ex:889(recusa_local_de_subida),:945(recusa_por_estado),:1025(recusa_ja_de_pe),:1027(recusa_sem_imagem_decidida),:1028(recusa_pasta_nunca_confirmada),:1029(recusa_runner_desconectado);apps/engine/lib/engine/containers/project_container_lifecycle.ex:83(status_registrado);apps/engine/lib/engine/session_events/event.ex:120(imagem_decidida?) - Teste:
apps/engine/test/engine/infra/infra_lead_server_test.exs:742(via runner,runningrecusa),:808(provisioningrecusa),:818(stoppednão recusa),:779(sem imagem),:790(pasta nunca confirmada),:854(propose_container_startcom containerrunning),:854(propose_container_startsem imagem PROPÕE),:863(caminho feliz do via runner),:901(sem runner conectado) - Origem: AT-142
RN-617 — O Infra Lead conversa pelo composer, e o turno dele passa pelo TurnoAssincrono: aceite imediato, "Parar", turno órfão fechado
A decisão que a RN-584 deixou aberta — "se o Infra Lead passa a
conversar pelo composer" — foi tomada pelo mantenedor (AT-141, 2026-09-27):
sim, e ele vira o SÉTIMO conversacional
(ADR 0175). A condição que a
RN-584 escreveu veio primeiro: o turno dele deixou de rodar DENTRO do
handle_call/handle_cast e passou pelo MESMO Engine.Agents.TurnoAssincrono
dos outros seis; só depois a cláusula de message/2 nasceu.
Etapa 1 — o turno.
- Os três turnos do Infra Lead sobem numa Task — o kickoff (ainda
cast, só no start FRESCO do handoff aceito), a correção de gate ({:correct, _}) e a mensagem do composer. A mensagem responde ao ACEITE, com oagent.status: workinggravado ANTES (RN-578); o desfecho vai pelo canal e, quando é falha, peloagent.errordurável. Ouser_message/2que rodava o turno inline SAIU: o nome ficou e é o aceite. - Segunda mensagem com turno em curso é
409 turno_em_andamento, com oagent.errorde origempoliticano fio — inclusive durante o kickoff, que antes prendia a mensagem na fila do processo pelo turno inteiro. - "Parar" alcança o Infra Lead (
via_for("infra")nocancel/2): mata a Task — e com ela oWorkflowsAgent, que roda dentro da consolidação —, gravaagent.errorcancelado_pelo_usuarioe devolveidle. - A consolidação com o Workflows é parte da função de turno e devolve o
state; os sinais de fim (agent.done,agent.status: idle) saem só deTurnoAssincrono, no processo do servidor (RN-585). - A correção de gate que chega com turno em curso entra numa FILA
(
correcoes_pendentes) e sobe no fecho do turno, na ordem de chegada — oTurnoAssincronosemfromDESCARTA o pedido que encontra turno em curso, e o gate pediria mudança a ninguém. - O turno órfão do reinício fecha pela RN-586:
infraentrou emTurnoOrfao(@agentes), no boot e noinit/1. - O histórico vem de
Engine.Agents.Reidratacao(RN-580), como já vinha, e otool.resultdeResultadoDeFerramenta(RN-593). - O teto de iterações é o dele, 14 (o mesmo de Arquiteto, Dev Lead, UX
Designer e Staff), e esgotá-lo passa a emitir
toolloop.limit_reached(RN-166) — antes o 14º passo terminava calado.
Etapa 2 — o composer.
message/2tem cláusula PRÓPRIA parainfra, e a recusaagente_sem_conversadele saiu — um nome, um caminho.@agentes_de_conversao inclui; a mensagem a um Infra Lead que não está de pé (engine reiniciado) o reergue SEM kickoff, e ele reidrata noinit/1.- A tela o oferece pela regra de sempre:
infraentrou emAGENTES_DE_CHAT, então ele é o destinatário quando é oagent.activatedmais recente — sem destinatário padrão (RN-584). O aceite do handoff DELE continua no card próprio da RN-499; o card do fio o exclui por NOME (offeredHandoff), senão o convite aparecia duas vezes e, sendo o mais antigo, voltava a esconder o do Dev Lead (RN-136). A faixa de atividade ganhou frase para as quatro ferramentas dele. - Conversar não abre efeito externo. As ferramentas são as mesmas
quatro; a PR de infra e as duas subidas de container continuam nascendo
proposed_action, com as recusas locais das RN-566, RN-577 e RN-610 intactas.
O teto de 180 s deixou de importar para o turno. Ele era menor que os
225 s do propose_action de container (RN-605), e uma mensagem que
levasse o modelo a propor container_start podia estourar o GenServer.call
com a proposta de pé. Com o aceite imediato, o GenServer.call só espera o
aceite; o propose_action corre dentro da Task, sem ninguém esperando
síncrono. O 180 000 ficou, igual ao dos outros seis, como teto do ACEITE.
Não muda, declarado: SOLO_CONVERSATIONAL_AGENTS (gerada por
pnpm --filter api gerar:areas) é a lista dos agentes SEM área que o handoff
manual endereça; o Infra Lead é lead de área e continua fora dela, e
AGENTES_DE_CHAT não é gerada. O Infra Lead não ganhou perguntas estruturadas
nem leitura de backlog — conversar com ele é conversar com o que o kickoff lhe
deu e com as quatro ferramentas.
-
Código:
apps/engine/lib/engine/infra/infra_lead_server.ex:187(user_message),:211(TurnoOrfao.fechar_ao_subir),:248(handle_cast(:kickoff),:270(handle_cast({:correct, a fila),:280(handle_cast(:cancel),:296(handle_call({:user_message),:368(handle_info),:373(drenar_correcao_pendente),:456(toolloop.limit_reached),:1164(concluir);(
toolloop.limit_reached),:1057(concluir);apps/engine/lib/engine_web/controllers/agent_command_controller.ex:44(@agentes_de_conversa),:192(a cláusula doinfra),:618(via_for);apps/engine/lib/engine/agents/turno_orfao.ex:57(@agentes);apps/web/src/lib/session-readiness.ts:37(AGENTES_DE_CHAT);apps/web/src/lib/session-handoffs.ts:120(ofertasAcionaveis) -
Teste:
apps/engine/test/engine/infra/infra_lead_server_test.exs:1033(aceite imediato,workingantes),:1048(409 com turno em curso — caso de falha),:1077("Parar"),:1103("Parar" sem turno),:1107(a correção de gate na fila),:1792(toolloop.limit_reached);apps/engine/test/engine/agents/turno_orfao_test.exs:76(o órfão, agora nos sete);apps/engine/test/engine/agents/reidratacao_dos_seis_test.exs:62(a reidratação, nos sete);apps/engine/test/engine_web/controllers/agent_command_controller_test.exs:236(202, 409 e "Parar" pela rota),:283(reergue sem kickoff);apps/web/src/routes/SessionPage.cancelar-turno.test.tsx:256(o composer envia ao Infra Lead e o "Parar" é dele);apps/web/src/lib/session-readiness.test.ts:123;scripts/ci/destinos-do-composer.spec.ts:174(a guarda:infraé destino, com UMA cláusula que conversa) -
Origem: AT-141 (decisão do mantenedor), aberta pela AT-098
Revisada pela RN-924 (ADR 0226). A fila correcoes_pendentes, só em memória, passou a ser durável (infra.correction_queued/_delivered), no molde da fila de mensagens; o turno órfão do Infra Lead segue a retomada da RN-920.
RN-621 — O resumo da compactação mantém o idioma original de cada turno, e não traduz citação nem código
Quando o contexto de um agente passa do limiar, o ContextManager troca os
turnos antigos por UM resumo, escrito por outra chamada de LLM (o ator
context-manager) e reinserido como mensagem system. O prompt dessa chamada
é pt-BR e não dizia nada sobre idioma: numa conversa em inglês, ou com dois
idiomas, o resumo saía traduzido, e o modelo seguinte lia a própria memória na
língua do prompt de resumo — o desvio de idioma que o épico de idioma persegue,
nascido fora do turno do usuário. A orientação de idioma do turno não protege
isto: ela nunca está em messages, então nunca é resumida, mas também nunca é
vista pelo sumarizador.
A regra é uma frase no prompt de resumo, nas DUAS trilhas (o texto inline e o
template versionado prompts/context-manager-summarize.md, que sobe para a
versão "2"):
Mantenha cada turno no idioma original; não traduza citações nem código.
- O idioma é POR TURNO, nunca um idioma da sessão: numa sessão com dois autores de línguas diferentes, os dois sobrevivem ao resumo.
- Citação e código são texto literal e não se traduzem — um identificador ou uma mensagem de erro traduzida no resumo deixa de ser encontrável.
- A frase vem ANTES dos turnos, para que o texto de um turno no fim do prompt não seja lido como continuação dela.
- Custo: o acréscimo é de 76 bytes — 19 tokens pela heurística do engine (bytes/4), no máximo 38 por uma régua pessimista de 1 token a cada 2 bytes —, dentro do teto de 50 tokens de entrada por chamada decidido pelo mantenedor para a orientação de idioma (AT-169, resposta 8). O teste reprova a frase que passar dele pelas duas réguas.
- Não é
agent_instructions: é texto de código e de template versionado, fora do teto deinstruction_patch(RN-007), porque o sumarizador não é agente de projeto e não tem instrução por projeto.
Decidido sem linha de base, declarado: o mantenedor decidiu mudar o prompt já, sem esperar a validação paga mostrar que o resumo em pt-BR desvia (AT-169, resposta 4). A consequência é que a validação da AT-167 mede o CONJUNTO (orientação + resumo novo), sem medir o resumo antigo isolado.
O que esta regra NÃO fecha, declarado:
-
Com
graph_templates_enabled?ligada, vale a versão ATIVA no grafo. Uma instalação que semeou a versão"1"continua mandando o prompt antigo até rodarscripts/dev/seed-prompts.tsde novo — o hash do corpo mudou, então o seeder cria a versão nova. Com a flag desligada (o default) vale o inline, que já tem a frase. Semear de novo também tira do corpo a seção de documentação que fazia o sumarizador receber os turnos em dobro (AT-244, ver RN-413); o template está na versão"3". -
O fallback de quando o sumarizador falha (
"(N turnos anteriores omitidos)") não é resumo e não ganha a frase: não há texto de turno nele. -
A frase é pt-BR. Ela pede o idioma de cada turno, não o do prompt; se um modelo a obedece em conversa de outra língua é o que a validação da AT-167 mede.
-
Código:
apps/engine/lib/engine/harness/context_manager.ex:214(@instrucao_de_idioma),:276(prompt_inline),:233(prompt, a escolha entre template e inline);prompts/context-manager-summarize.md -
Teste:
apps/engine/test/engine/harness/context_manager_test.exs:417(caminho feliz: o prompt enviado ao sumarizador tem a frase, antes dos turnos),:433(o teto de 50 tokens, pelas duas réguas),:443(o template versionado carrega a MESMA frase),:453(falha do sumarizador: o fallback não finge resumo nem ganha a frase) -
Origem: AT-166, especificada na AT-081 e destravada pela AT-169 (decisões do mantenedor de 2026-09-28)
O idioma das respostas dos agentes (RN-618..620, RN-622..624, ADR 0177)
RN-622 — O idioma chega ao modelo como a ÚLTIMA mensagem de cada chamada do turno, efêmera: o do AUTOR quando há um, o do PROJETO quando não há
As RN-618..620 decidiram EM QUE idioma o agente responde e mostraram isso na tela; nada disso chegava ao modelo. Esta regra é o transporte (AT-164, sobre a especificação da AT-081 e o mecanismo verificado por provider na AT-161).
- Uma mensagem
role: "system"no FIM da lista, a cada chamada de LLM do turno — inclusive as iterações de ferramenta —, e NUNCA no histórico: ela não entra emstate.messages, então não se acumula de turno em turno, não é resumida pela compactação (RN-621) e some com o turno. No OpenAI-compatível ela vai no fim da conversa; no Anthropic é içada para o fim dosystemdo topo (AT-161). Não éagent_instructions: é texto de código, fora do teto deinstruction_patch(RN-007). - Um lugar só. Quem acrescenta é a fachada
EngineApiClient(llm_turn/5ellm_turn_stream/6), por onde passa TODA chamada de LLM do engine — os sete conversacionais, oToolLoop(dev agents e gates) e o Infra Lead. Nenhum servidor monta a orientação. - Turno COM autor humano (a mensagem que alguém digitou a um agente): a
api resolve o idioma DAQUELA pessoa naquela sessão
(
ResolverIdiomaDaRespostaUseCase, RN-618 — override da sessãoconta > detectado confirmado > interface) e o manda no comando
agent/messagecomoidiomaDaResposta, campo OPCIONAL. O servidor do agente o põe no estado do TURNO durante ohandle_call, e a Task do turno o herda do dicionário de processo (TurnoAssincrono); o GenServer não o guarda para o turno seguinte. Dois autores na mesma sessão recebem cada um o seu. - Turno SEM autor humano (kickoff, dev agents, gates, commit, corpo de
PR, a retomada do Dev Lead depois da aprovação): o idioma do PROJETO
(RN-619), lido de
projects.languagea cada chamada (decisão do mantenedor, AT-169 resposta 1). - O sumarizador da compactação (
context-manager) fica fora: "responda em X" traduziria o resumo, e preservar o idioma de cada turno nele é a RN-621. - Falha nunca derruba o turno. A resolução que falha na api vira log e a
mensagem segue SEM
idiomaDaResposta; turno com autor sem idioma segue SEM orientação — nunca o do projeto no lugar do da pessoa. A leitura do idioma do projeto que falha no engine virawarninge a chamada segue sem orientação. Valor que não tem forma de código BCP-47 também não vira orientação: nada que não seja um código é interpolado no prompt. - Os textos são constantes de código, um por idioma:
pt-BR("Responda em português brasileiro (pt-BR), salvo pedido explícito do usuário por outro idioma nesta mensagem."),en, e um genérico em inglês que nomeia o código BCP-47 para qualquer outro idioma da lista aberta (AT-168 resposta 2). O texto não depende do modelo: trocar de modelo no meio da sessão não muda nada, e não há cascata por modelo nem caso especial de DeepSeek. - Custo — teto decidido de 50 tokens de ENTRADA por chamada (AT-169
resposta 8). Medido com
gpt-tokenizer, só o conteúdo, cl100k / o200k:pt-BR29 / 23,en18 / 18, genéricoes-MX26 / 26 e o PIOR caso (o genérico com o código mais longo que a forma aceita) 42 / 42; a moldura de mensagem soma ~4, então o pior caso fica em ~46. O teste trava o texto em 160 caracteres como vigia do teto. - Medição: o
chat.messagegravado pela api carregaidiomaAlvoeorigemquando a resolução deu certo — o evento é imutável e diz, para sempre, o que foi pedido ao modelo naquele turno (AT-082).
O que esta regra NÃO fecha, declarado:
-
Chat humano sem agente (
SendChatMessageUseCase) fica fora, por decisão do mantenedor (AT-169 resposta 3). -
Só a MENSAGEM a um agente carrega autor (e a resposta ao formulário de pergunta estruturada, que a reusa). Os outros comandos que uma pessoa dispara — confirmar prontidão ao Criativo, aceitar handoff — sobem turno sem autor e caem no idioma do PROJETO: o product brief do
confirmReadinesssai no idioma do projeto. -
Artefato de turno com autor: esta regra o declarava seguindo o idioma de quem escreveu; desde a RN-623 a mesma mensagem ganha a cláusula do artefato, no idioma do projeto.
-
Não medido: o tokenizador do DeepSeek e o da Anthropic; se a orientação basta contra o prompt de sistema em pt-BR é o que a validação paga da AT-167 mede. No Anthropic, a orientação içada ao topo muda o prefixo do cache a cada troca de autor ou de idioma — cache não é observável hoje.
-
Código:
apps/engine/lib/engine/harness/idioma_da_resposta.ex:294(anexar),:253(com_idioma_do_autor),:161(orientacao),:94(@sem_orientacao),:301(texto_do_turno),:326(idioma_do_projeto);apps/engine/lib/engine/sessions/engine_api_client.ex:730(IdiomaDaResposta),:846(IdiomaDaResposta);apps/engine/lib/engine/sessions/engine_api_client.ex:730(IdiomaDaResposta),:846(IdiomaDaResposta);apps/engine/lib/engine/projects/project.ex:64(idioma);apps/engine/lib/engine_web/controllers/agent_command_controller.ex:527(idioma_da_resposta);apps/engine/lib/engine/agents/criativo_server.ex:140(handle_call, e a MESMA cláusula nos outros seis);apps/engine/lib/engine/agents/turno_assincrono.ex:134(copiar_dicionario);apps/api/src/application/use-cases/agents/send-agent-message.use-case.ts:151(resolverIdioma),:155(idiomaAlvo);apps/api/src/infrastructure/http-clients/api-to-engine-client.ts:215(idiomaDaResposta) -
Teste:
apps/engine/test/engine/harness/idioma_da_resposta_test.exs:78(sem autor: o idioma do projeto, no fim),:123(com autor: o do autor vence o do projeto),:134(autor sem idioma resolvido: sem orientação),:98(a consulta que falha — caso de falha: lista intacta e log),:93(o sumarizador fica fora),:67(o vigia do teto de 50 tokens),:340(ponta a ponta: o idioma do autor no fim de cada chamada e fora do histórico),:370(a fachada também anexa nollm_turn/5);apps/engine/test/engine_web/controllers/agent_command_controller_test.exs:402(o comando leva o idioma até o modelo),:414(campo inválido: aceito, sem orientação);apps/api/test/application/use-cases/agents/send-agent-message.use-case.spec.ts:109(resolve pelo autor e pela sessão — o override da sessão vence),:125(a resolução que falha não derruba a mensagem — caso de falha);apps/api/test/infrastructure/http-clients/api-to-engine-client.spec.ts:528(o campo viaja só quando resolvido) -
Origem: AT-164, especificada na AT-081 e destravada pelas decisões das AT-168 e AT-169 (mantenedor, 2026-09-28); mecanismo verificado na AT-161
RN-623 — Num turno COM autor, a resposta é no idioma da pessoa e o ARTEFATO compartilhado é no idioma do PROJETO
A RN-619 prometeu o idioma do projeto para o artefato compartilhado, e a RN-622 pôs o idioma do AUTOR em todo turno com autor — então o que um agente gravava DURANTE a conversa com uma pessoa saía no idioma dela. Decisão do mantenedor (AT-168 resposta 8, AT-245): o artefato segue o PROJETO; a resposta de chat continua no idioma do autor.
- O mecanismo é a mesma mensagem da RN-622, com uma segunda cláusula.
Num turno com autor, quando a chamada leva ao menos uma ferramenta que
grava artefato compartilhado e o idioma do projeto (
projects.language) DIFERE do do autor, a orientação vira "Responda em português brasileiro (pt-BR), salvo pedido… Artefatos do projeto: em en." — no idioma do texto base: a cláusula dopt-BRé em português, a dos outros em inglês ("Write project artifacts in X."). Continua UMA mensagemsystemefêmera no fim, montada pela fachada; quem decide é o conjunto de ferramentas que a MESMA chamada leva ao modelo, que a fachada já tem. - Por que a orientação, e não a descrição da ferramenta. A descrição viaja em TODA chamada, em cada ferramenta, com os idiomas iguais ou não; a cláusula existe só quando eles diferem, e mora no único lugar que já decide o idioma. Sem acréscimo nenhum quando os idiomas são iguais, quando a chamada não tem ferramenta de artefato, e em turno SEM autor (que já recebe o idioma do projeto).
- O que é artefato compartilhado — o que a ferramenta PERSISTE é lido por
outras pessoas e pelos próximos agentes:
emit_artifact(business_rule,note,decision_record) — Criativo, PO, Arquiteto, Dev Lead, UX Designer, Staff;create_epic,create_story,create_task— PO;create_module_map,assign_story_modules,choose_project_image,create_c4_diagram,route_modules_to_infra,propose_adr(o ADR deopen_adr_pr),emit_insight— Arquiteto;propose_execution_plan,assess_implementability— Dev Lead;propose_prototype— UX Designer;propose_rfc— Staff;propose_infra_pr— Infra Lead. O product brief NÃO passa por aqui: ele é emitido pelo SERVIDOR noconfirmReadiness, que é turno sem autor e já sai no idioma do projeto (como o handoff e o revise, que esta regra não muda).
- O que NÃO é:
ask_structured_questions(é pergunta à PESSOA do turno),offer_handoff(não leva texto), as de leitura (listar_*,rag_*,read_file,search_workspace),validate_infra_file(só valida) e as propostas de subir container (pedem decisão, não gravam artefato). Os dev agents e os gates não têm turno com autor. - Falha nunca derruba o turno. Ler o idioma do projeto que falha vira
warninge segue SÓ a orientação do autor — nunca sem orientação, nunca a do projeto no lugar da da pessoa. Autor sem idioma resolvido segue sem orientação, como na RN-622. - Custo — o teto de 50 tokens de entrada da AT-169 vale para a mensagem
inteira. Medido com
gpt-tokenizer, só o conteúdo, cl100k / o200k, sobre todos os pares dept-BR,en,es,es-MXezh-Hant-TW: pior casopt-BR→zh-Hant-TW42 / 36;en→pt-BR26 / 26; genéricoes-MX→zh-Hant-TW36 / 36. A cláusula só entra com os DOIS códigos na forma canônica curta (idioma[-Escrita][-Região], ex.:pt-BR,zh-Hant-TW,es-419), porque é a FORMA que segura o teto: o pior dela (pt-BR→ código de 12 caracteres) mede 44 / 37, ~48 com a moldura da mensagem. Código fora dessa forma fica só com a orientação do autor, e o log diz. O teste trava todo par em 165 caracteres como vigia.
O que esta regra NÃO fecha, declarado: a lista é por NOME de ferramenta — ferramenta nova que grave artefato entra nela no mesmo PR (o teste amarra cada nome a uma spec que existe, mas não descobre ferramenta nova). Se o modelo SEGUE as duas cláusulas na mesma mensagem é o que a validação paga da AT-167 mede; os tokenizadores do DeepSeek e da Anthropic seguem não medidos.
-
Código:
apps/engine/lib/engine/harness/idioma_da_resposta.ex:174(orientacao),:127(@ferramentas_de_artefato),:118(@forma_curta),:301(texto_do_turno),:319(grava_artefato?),:320(idioma_do_projeto_para_o_artefato);apps/engine/lib/engine/sessions/engine_api_client.ex:730(IdiomaDaResposta),:846(IdiomaDaResposta);apps/engine/lib/engine/sessions/engine_api_client.ex:730(IdiomaDaResposta),:846(IdiomaDaResposta);apps/web/src/routes/settings/ProjectLanguageSection.tsx -
Teste:
apps/engine/test/engine/harness/idioma_da_resposta_test.exs:157(idiomas diferentes: a orientação diz os dois),:168(iguais: só um),:175(código fora da forma curta: só o do autor, com log),:190(o vigia do teto),:204(cada nome da lista é uma spec que existe),:235(autor pt-BR, projeto en, numa mensagem só),:259(sem ferramenta de artefato: só o autor),:290(idioma do projeto ilegível — caso de falha: só o do autor, com log),:316(sem autor: nada muda),:340(ponta a ponta pelo Criativo);apps/web/src/routes/settings/idioma-do-projeto.test.tsx:84 -
Origem: AT-245, sobre a decisão da AT-168 (resposta 8) e as do mantenedor de 2026-09-29
RN-625 — O laço pergunta ao Jev qual ferramenta, e o modelo do usuário vê só o menu que sobra; o Jev restringe e nunca decide
Decisão do mantenedor (AT-236, 2026-09-28) e, sobre o critério, de 2026-09-29:
antes de cada chamada de chat de um agente, a api pergunta ao Jev
(typesafe/jev-1.13, Decisions API do OpenRouter) qual ferramenta cabe no
passo, e o provider de chat recebe só o menu que a política P3 tira dessa
resposta. O mecanismo está no ADR 0179;
a decisão de adoção, com os números, também.
- Quando. Só com o provider do modelo do turno igual a
openrouter, com duas ou mais ferramentas no pedido, com agente no turno (fora a Anamnese e o sumarizador da compactação), semcatalogoCompletopedido pelo engine e com o workspace ligado (workspaces.tool_router_enabled, padrão ligado,PUT workspaces/:workspaceId/tool-router,owner). Qualquer condição falha: nenhuma chamada, nenhum evento. Com provider ≠openrouternem o workspace é lido. A chave é a do titular do workspace, a mesma do chat (RN-058). - A política é a P3 e só restringe. Menu = {escolha do Jev, ferramenta
anterior da MESMA execução}.
responder_sem_ferramentado Jev, ou nenhuma ferramenta anterior no catálogo do passo, deixa o catálogo INTEIRO. Não hátool_choice(o modelo ainda pode responder em texto) e não há limiar de confiança na v1: a confiança é gravada e é da AT-239 propor um. Critério de adoção: 80%, decisão do dono de 2026-09-29 — top-1 do Jev 73% (66–79), cobertura de menu da P3 91% (85,1–94), top-2 90%; cobertura é o TETO da acurácia ponta a ponta (62%–91% em replay), e a validação real é a AT-239. - O
stateé o que o produto tem antes do passo, recortado dasmessagesque o engine já manda, sem leitura de banco:agente= oagentId;contexto= o começo (1.500 caracteres) da mensagemsystemque oToolLooppõe na frente (apps/engine/lib/engine/harness/tool_loop.ex:98,system_msg, montada porsystem_prompt/1em:316);pedido= a última mensagemuser— nos agentes de execução a mensagem inicial do laço (apps/engine/lib/engine/dev/dev_agent_server.ex:651,initial_message/2, ou a de correção em:523), nos conversacionais a fala da pessoa; epassos_recentes= as 6 últimas chamadas de ferramenta depois dessa mensagem, uma entrada por chamada, argumento e resultado cortados em 500 caracteres — o resultado vem da mensagemtoolque o laço anexa (tool_loop.ex:293,concluir_despacho/5, comtoolCallId). A execução corrente é "depois da últimauser" porquectx.messagesrecomeça a cadaToolLoop.run. Teto de 8 mil tokens; estourou, quedaestado_grande. - O turno nunca falha por causa do Jev. Erro HTTP, timeout (2.000 ms,
TOOL_ROUTER_TIMEOUT_MS), erro de rede, JSON inesperado, escolha fora das opções, estado grande e colisão com o nome reservado: o provider recebe o catálogo inteiro e o evento levamotivoDaQuedaeorigemDaQueda(infra,modelooucodigo— RN-059), nunca calado. - A política não muda. O Jev não aprova, não nega e não escolhe modelo. A
ferramenta escolhida que exige aprovação continua exigindo; a chamada a uma
ferramenta fora do cardápio é despachada como sempre e o evento só registra
foraDoCardapio. O teto de iteração é o de antes (a chamada ao Jev não é uma iteração). - Registro por passo. O engine grava
tool_router.decided, na fachadaEngineApiClientpara todos os agentes, com menu antes e depois, escolha, confiança, segunda opção,aplicado, queda, latência, custo real eforaDoCardapio. A api grava UMA linha detoken_usagedo Jev: ator = o próprio agente (o gasto entra no orçamento de área, ADR 0110),modelName = typesafe/jev-1.13,modelIdnulo,estimated = false,costMicros= ousage.costda resposta, preço por milhão IMPLÍCITO (custo ÷ tokens) comprice_implicit = true— mantémtokens × preço = custoda RN-044 sem inventar preço de catálogo. Semusage.costna resposta não há linha. O gate de orçamento não é reconsultado entre o Jev e o chat. - Sair do beco. Menu restringido e o modelo responde sem chamar ferramenta
(nem em texto recuperável), sem erro do provider: a fachada repete o passo UMA
vez com
catalogoCompleto: true, que pula o Jev. No stream, só se nada foi escrito para a pessoa. Pede mais ferramentas, nunca uma que o agente não tinha.
O que esta regra NÃO fecha, declarado: a tela (mostrar que o passo foi
roteado, com a confiança, e a faixa "escolhendo ferramenta" pelo frame
tool_routing_started) é de apps/web; o ganho é da AT-239; um timeout que o
OpenRouter cobrou sem devolver usage.cost não entra em token_usage; o
endpoint é ALPHA e o smoke manual
(jev-tool-router.smoke.spec.ts, teto US$ 0,05) é quem acusa a mudança.
-
Código:
apps/api/src/domain/llm/tool-router.ts:27(MODELO_DO_JEV),:109(montarPedidoAoJev),:166(lerRespostaDoJev),:216(recortarEstado),:272(menuP3);apps/api/src/application/use-cases/llm/decidir-ferramenta-do-passo.use-case.ts:118(preparar),:143(executar),:305(registrarGasto);apps/api/src/application/use-cases/llm/run-llm-turn.use-case.ts:190(decisaoDoJev);apps/api/src/application/use-cases/llm/stream-llm-turn.use-case.ts:212(preparar);apps/api/src/infrastructure/llm/jev-tool-router.ts:32(decidir);apps/api/src/db/schema/iam.ts:169(toolRouterEnabled);apps/api/src/db/schema/llm.ts:375(priceImplicit);apps/engine/lib/engine/harness/roteamento_de_ferramenta.ex:60(registrar),:112(custo_micros),:123(repetir_com_catalogo_inteiro?),:146(mesclar_repeticao);apps/engine/lib/engine/sessions/engine_api_client.ex:746(llm_turn),:837(llm_turn_stream);(
mesclar_repeticao);apps/engine/lib/engine/sessions/engine_api_client.ex:746(llm_turn),:837(llm_turn_stream);apps/engine/lib/engine/harness/tool_loop.ex:132(o custo do Jev no orçamento local) -
Teste:
apps/api/test/domain/llm/tool-router.spec.ts:25(questionsé objeto),:86(forma inesperada vira queda — caso de falha),:138(ostate),:210(a P3);apps/api/test/infrastructure/llm/jev-tool-router.contract.spec.ts:60(a resposta gravada),:121(erro HTTP),:130(timeout),:142(rede),:149e:154(JSON inesperado),:163(escolha fora das opções);apps/api/test/application/use-cases/llm/decidir-ferramenta-do-passo.use-case.spec.ts:186(provider ≠ openrouter não chama),:222(workspace desligado),:255(estado grande),:277(a P3),:318(sem ferramenta = catálogo inteiro),:331(sem anterior = inteiro),:352(a política não muda),:369(as quedas),:456(o metering, no orçamento de área);apps/engine/test/engine/harness/roteamento_de_ferramenta_test.exs:67(o evento),:103(a queda no evento),:180(sair do beco),:288(o custo no laço),:299(a aprovação continua exigida),:325(o roteamento não conhece a política) -
Origem: AT-238, sobre a especificação da AT-235, as dezesseis decisões da AT-236, a medição da AT-237/AT-280 e a decisão do dono de 2026-09-29 (critério de 80%)
Decidir no chat, onde o dono está (RN-626)
RN-629 — O QA Lead trata o subagente que SUSPENDE de novo na retomada do mesmo jeito que na primeira vez, e resultado que não conhece vira bloqueio nomeado
O qa-automacao roda vários comandos e cada um pode pedir aprovação humana. O
QaLeadServer já parava a área quando run/5 devolvia {:awaiting, _} (achado
AB, ADR 0052), mas a RETOMADA (retomar/3, quando a decisão chega) devolvia o
resultado de um laço que podia suspender OUTRA vez — e esse segundo
{:awaiting, _} entrava na lista de resultados colhidos como se fosse parecer.
Ao consolidar, registrar_resultado/5 não tinha cláusula para ele e o
GenServer caía (FunctionClauseError, medido em 29/09/2026: 06:55:01 e
07:00:10), deixando as três tasks entregues em awaiting_qa para sempre, sem
veredito e sem desfecho nomeado.
- Um só lugar decide o que fazer com o resultado de um subagente
(
tratar_resultado/6), venha ele derun/5ou deretomar/3.{:awaiting, _}guarda o estado em voo (chaveado pela nova ação), mantém a linha degate_statesemin_progresscom o subagente e PARA a área: sem veredito, sem bloquear a task, sem registrar a delegação. Quem retoma é o mesmo{:action_settled, _}doEngine.Dev.Wake(ADR 0086), quantas vezes forem necessárias. - Resultado que o lead não conhece nunca derruba nem some (RN-059): vira
{:blocked, …}com origemcodigo(falta uma cláusula aqui, não é infra, modelo nem política), queregistrar_resultado/5grava como delegaçãofailedeQaLead.consolidar/1transforma em bloqueio da task com a origem.
Declarado, não resolvido: o restart do engine com o lead suspenso perde a
inscrição no Wake (lacuna aceita do ADR 0086); o GateRescuer reinicia a área
inteira (ADR 0067), não retoma o ctx. Esta regra não muda o ADR 0090 (o
momento do QA-estratégia) nem o teto de iterações do subagente.
- Código:
apps/engine/lib/engine/gates/qa_lead_server.ex:127(handle_info),:237(tratar_resultado),:230(resultado desconhecido) - Testes:
apps/engine/test/engine/gates/qa_lead_server_test.exs:325(segunda suspensão na retomada: fica suspenso, não decide nada, e a segunda decisão conclui a área);:278(uma suspensão, o caminho feliz) - Origem: AT-248, sobre o achado E2 da análise de uso real de 2026-09-29
RN-636 — O AppSec só oferece o threat model a quem ainda não recebeu oferta nem está ativo
O AppSec criava três ofertas por história com run_design, aos mesmos três
destinos — N histórias, 3N ofertas pendentes (AT-292, que funde a AT-273 do
vault por decisão do dono; ADR 0182).
- Pergunta no modo
seAusente.criar_handoffs_appsec/3chamaEngineApiClient.create_handoff_if_absent/5, que mandaseAusente: trueà MESMA rota interna. A api decide sob o lock do destino (RN-635): com oferta pendente em QUALQUER sessão do projeto, devolve a existente (desfecho: ja_oferecido) e não a substitui; com o destino ativo, 409agente_ja_ativo. Perguntar antes, pelo engine, seria corrida com outra oferta. - "Já atendido" não é falha. As duas respostas acima seguem sem
agent.error. Falha de verdade (5xx, rede) continua narrada por alvo, com origem (RN-116, RN-361). - O artefato fica. O
artifact.threat_modelé gravado ANTES e de qualquer jeito; o que deixa de nascer é a oferta repetida.
-
Onde:
apps/engine/lib/engine/gates/secops_agent_server.ex:377(criar_handoffs_appsec/3);apps/engine/lib/engine/sessions/engine_api_client.ex:1142apps/engine/lib/engine/sessions/engine_api_client.ex:1313(create_handoff_if_absent);apps/api/src/domain/sessions/ciclo-de-vida-do-handoff.ts:71(decidirOferta) -
Teste:
apps/engine/test/engine/gates/secops_agent_server_test.exs("run_design (RN-636): destino com oferta pendente ou já ativo não recebe outra, e isso não é erro", "run_design (RN-636): falha de verdade ao oferecer segue narrada, só para aquele destino");apps/api/test/application/use-cases/agents/ciclo-de-vida-do-handoff.spec.ts("seAusente(AppSec, RN-636)…") -
Origem: AT-292, AT-273
A PR do dev agent mira dev, e o gate julga o diff dessa PR (RN-664)
RN-664 — O trabalho dos agentes nasce de dev, a PR deles mira dev, e o gate julga o diff contra dev — os três juntos
Revisada pela RN-1021 (AT-617): no repositório do projeto a branch de trabalho agora é a
main(trunk), e a constante@branch_de_trabalhodo código vale"main". O texto abaixo descreve a política dedevque ela substituiu; onde ele dizdevpara a branch de trabalho do projeto, leiamain.
A política de branches do produto manda o trabalho nascer de dev (o bootstrap
cria dev e qa a partir de main, RN-582), mas até aqui ninguém
apontava os agentes para ela: o pr_open do dev agent não levava
targetBranch e a api completava com repo.defaultBranch (main); a PR de
ADR do Arquiteto nascia de main e mirava main; o working tree do projeto
abria a default_branch; o gate calculava o diff contra ela; e o contexto do
QA dizia "Repositório · branch main" (AT-250, item A4 do uso real de 29/09).
Toda PR de agente pulava a esteira dev → qa → main.
Decisão do dono (30/09): os TRÊS passam a usar dev, juntos — mudar só um
deixaria o gate julgando um diff que não é o da PR. A branch de TRABALHO é
dev e NÃO é a default_branch do provider (que segue sendo a que a promoção
alcança por último e continua lida onde ela é a pergunta):
- o working tree do projeto abre
dev(ensure_remoto/2ignora odefault_branchdo remoto para escolher a base), e o worktree de cada dev agent nasce dedevEXPLICITAMENTE, não do HEAD — workspace inicializado antes desta regra está parado nadefault_branch(a marca de pronto impede re-inicializar) e ganha adevlocal a partir deorigin/dev, semfetchnovo; - o
pr_opendo dev agent levatargetBranch: "dev"no payload, e a api, sem o campo (ação proposta antes), completa comdeve não mais com adefaultBranch; a PR de ADR nasce dedeve miradev; - o diff do gate é
dev...HEAD, e a linha de contexto do projeto diz "Repositório · branch de trabalho dev".
Repositório SEM dev (adotado sem bootstrap, ou bootstrap que não chegou a
create_dev_branch): NÃO há queda para a default_branch — seria o agente
trabalhando e o gate julgando sobre uma base que não é a da PR. Cada ponto
falha NOMEADO, pelo comportamento que já tinha para branch ausente, com uma
exceção nova: o working tree recusava nada e criava uma dev LOCAL VAZIA
(o caminho do bare que nunca recebeu push), marcava pronto e nunca mais
re-inicializava. Agora esse caminho só vale para o remoto SEM branch nenhuma;
com branches e sem origin/dev, a inicialização levanta com a mensagem
nomeada (mensagem_sem_branch_de_trabalho/1, "o repositório do projeto não
tem a branch dev… crie a branch… e tente de novo") e o .git é desfeito
(AT-112), então a próxima tentativa, com a dev criada, inicializa normal. O
worktree recusa com a mesma mensagem quando não há dev nem origin/dev; o
diff falha com o erro do git nomeando a revisão; o provider recusa a PR (e a
criação da branch do ADR) nomeando a ref. O infra (open_infra_pr) NÃO
mudou: segue nascendo e mirando a defaultBranch.
O valor mora em DOIS lugares, de propósito, um por linguagem, e mudam juntos:
BRANCH_DE_TRABALHO na api e branch_de_trabalho/0 no engine.
- Código:
apps/engine/lib/engine/projects/project_repository.ex:166(branch_de_trabalho),:177(branch_de_trabalho),:191(mensagem_sem_branch_de_trabalho);apps/engine/lib/engine/actions/workspace.ex:71(ensure_remoto),:224(init_from_bare!),:273(remoto_vazio?);apps/engine/lib/engine/actions/workspace/runner_git.ex:160(add_worktree),:183(garantir_base),:330(remoto_vazio?);apps/engine/lib/engine/dev/worktree_manager.ex:37(create),:311(add_worktree),:516(garantir_base);apps/engine/lib/engine/dev/agent_io.ex:427(propose_pr);apps/engine/lib/engine/gates/diff.ex:29(compute);apps/engine/lib/engine/harness/project_context.ex:29(repo_line);apps/api/src/domain/actions/protected-branches.ts:26(BRANCH_DE_TRABALHO);apps/api/src/application/use-cases/actions/execute-git-action.use-case.ts:208(targetBranch);apps/api/src/application/use-cases/actions/execute-adr-pr.use-case.ts:86(fromRef),:99(targetBranch) - Teste:
apps/engine/test/engine/gates/diff_test.exs:64(o diff é só a mudança do agente, nada damain— caminho feliz),:73(semdevo diff falha nomeando a revisão — caso de falha),:91(o contexto diz a branch de trabalho);apps/engine/test/engine/actions/workspace_test.exs:87,:98,:110(repositório adotado semdev: recusa nomeada,.gitdesfeito, e passa depois de criada a branch — caso de falha),:130;apps/engine/test/engine/dev/worktree_manager_test.exs:119,:127(workspace de antes da regra),:147(semdevnemorigin/dev);apps/engine/test/engine/dev/dev_agent_server_test.exs:114;apps/api/test/application/use-cases/actions/execute-git-action.use-case.spec.ts:184,:213;apps/api/test/application/use-cases/actions/execute-adr-pr.use-case.spec.ts:143,:151(semdev,failedcom o motivo do provider) - Origem: AT-250 (item A4 da análise do uso real de 29/09), decisão do dono de 30/09
O idioma das respostas dos agentes (RN-618..620, RN-622..624, ADR 0177)
RN-667 — O formulário estruturado segue o idioma da resposta: a descrição da ferramenta não fixa idioma nenhum
A validação paga da AT-167 achou, para autor com preferência en (C06/C12),
a chamada ask_structured_questions com a prosa em inglês e as perguntas do
formulário em português — no DeepSeek tratado, 8 das 15 respostas de
C06+C12. A causa é do produto, e mora num lugar só: a descrição da ferramenta
dizia que label era "o texto da pergunta, em pt-BR". Era uma SEGUNDA
orientação de idioma, fixa, fora da fachada da RN-622, competindo
com ela em toda chamada do Criativo e do PO (as duas usam a MESMA spec).
- O mecanismo é tirar a orientação concorrente, não acrescentar outra. A
descrição passou a dizer "o texto da pergunta, no idioma da sua
resposta (vale também para
options)", e o exemplo, que segue em português, é marcado "os textos seguem o idioma da sua resposta". Ela não nomeia idioma NENHUM: QUAL é o idioma continua decidido só pela mensagem efêmera deEngine.Harness.IdiomaDaResposta, e a fachadaEngineApiClientnão muda. - A orientação da RN-622/623 fica byte a byte. Zero token a mais nos 50
de teto.
ask_structured_questionssegue FORA de@ferramentas_de_artefato(é pergunta à pessoa do turno, RN-623 item 4), então autorennum projetopt-BRrecebe as perguntas em inglês — e é o que se quer. - Custo, onde ele cai. A descrição viaja em toda chamada que leva a
ferramenta, com qualquer autor: medido com
gpt-tokenizer, só o texto que mudou, +23 tokens em cl100k e +19 em o200k (a linha delabel18/17 → 29/27; a do exemplo 8/6 → 20/15). Não é a orientação indo para a descrição — a frase aponta para ela sem dizer idioma, e o que foi embora era justamente a frase que dizia um. - Guarda. Nenhuma descrição de ferramenta do harness (as 22 de
Engine.Harness.Tools.*) pode citarpt-BR,(en), "in English", "em português" ou "em inglês" — a doemit_artifact, que exige as CHAVES do payload "em INGLÊS", fica de fora porque fala de contrato, não de texto para a pessoa.
O que esta regra NÃO fecha, declarado: a medição paga antes/depois
não rodou (sem chave do OpenRouter na máquina da mudança) — o "antes" é o
da AT-167 (DeepSeek tratado: 8 de 15 respostas de C06+C12 com o formulário em
português), e o "depois" é pendência do dono:
pnpm --filter @brabo/scripts idioma:validar --arquivo-de-chave ~/.config/brabo/openrouter-test.env --bracos tratamento --casos C06,C12 --rodadas 5 --teto-usd 0.5 --saida ~/.cache/brabo/validacao-idioma/at-282,
seguido de idioma:validar --relatorio --saida na mesma pasta e da revisão do
formulário (rótulo formulario do idioma:revisar). Teto sugerido US$ 0,50:
o braço tratado inteiro custou US$ 0,21 (DeepSeek) e US$ 0,94 (Haiku) em
17 casos, e estes são 2. O script lê a descrição do arquivo do produto, então
mede o texto novo sem cópia. Se o formulário continuar em português com a
descrição neutra, o passo seguinte é uma cláusula condicional na orientação
(só quando a chamada leva ask_structured_questions), e ela tem de caber no
pior caso da RN-623 (44 / 37), que já está a ~48 com a moldura.
- Código:
apps/engine/lib/engine/harness/tools/ask_structured_questions.ex:193(descricao),:44(spec) - Teste:
apps/engine/test/engine/harness/tools/ask_structured_questions_test.exs:199(a descrição manda seguir o idioma da resposta),:207(não nomeia idioma — caso de falha: a forma antiga "em pt-BR" é reprovada pela mesma régua),:214(nenhuma das ferramentas do harness fixa idioma);scripts/idioma/validacao.spec.ts:60(a medição lê a descrição nova) - Origem: AT-282, da revisão humana da AT-167 (PR #750, 29/09); entrada na rodada aprovada pelo mantenedor em 2026-09-30
O Infra Lead não anuncia subida que não fez (RN-668)
RN-668 — O que o turno do Infra Lead anuncia sobre a subida do container é decidido pelo código, não pelo texto do modelo
É a RN-163 ("nada se anuncia que o
código não vá executar") aplicada ao Infra Lead. No uso real de 29/09 (sessão
be70, seq 282) ele escreveu que subiria o container "em paralelo" à PR de
infra, e nenhum tool.call de subida veio depois (AT-264). Havia duas causas no
código, e as duas fecham aqui:
- O lote inteiro da resposta é despachado antes do encerramento.
propose_infra_praceita encerra o turno sem nova ida ao modelo — é o único ponto em que o CÓDIGO corta o laço com o modelo querendo continuar —, mas ela parava o despacho no MEIO do lote: umapropose_container_start(oucontainer_start_via_runner) pedida na MESMA resposta, depois da PR, sumia semtool.call, semtool.resulte sem aviso. Agora é a forma do Dev Lead (propose_execution_plan): todas as chamadas da resposta rodam, e só depois a PR aceita encerra o turno. Uma segundapropose_infra_prna mesma resposta é recusada com motivo (tool.call+tool.resultcomok: false) — só a primeira consolida com o Workflows. - O fecho do turno diz, com frase do SERVIDOR, quando a subida NÃO foi
proposta — a forma do desfecho consolidado do Criativo (
encerrar/2). A decisão é pelo que o laço executou (alguma proposta de subida ACEITA pela api neste turno?), nunca pelo que o modelo escreveu; não há leitura do texto dele. Fala-se em dois casos: o turno terminou pela PR e nenhuma subida foi proposta; ou uma subida foi tentada, recusada, e nenhuma proposta aceita veio depois. Turno que nunca tocou na subida (uma pergunta, uma correção de gate) não ganha frase nenhuma, e container já REGISTRADOrunning/provisioningtambém não (a mesma leitura local derecusa_ja_de_pe/2, sem HTTP).
O kickoff e a descrição de propose_infra_pr passam a dizer o que o código faz:
a subida vem ANTES da PR ou na MESMA resposta, porque a PR encerra o turno. Isso
é ajuda ao modelo, não a garantia — a garantia é o fecho do item 2.
Nada aqui sobe container nem propõe subida, com as recusas por modo e estado
intactas (RN-566/RN-610). Desde a RN-671 (AT-260) a primeira subida
é passo do SERVIDOR no kickoff, marcada pelo MESMO registrar_subida: no caso
comum ela foi proposta e o fecho do item 2 não tem o que dizer; ele segue
falando quando a subida do servidor foi recusada (ou NEGADA pela política, que
desde então conta como recusa) e quando ela não cabia ao servidor (sem
roteamento, runner) e o modelo não a propôs.
-
Código:
apps/engine/lib/engine/infra/infra_lead_server.ex:505(dispatch_calls),:541(recusa_pr_repetida_no_lote),:1252(registrar_subida),:1254(fechar_subida),:1155(
dispatch_calls),:539(recusa_pr_repetida_no_lote),:1140(registrar_subida),:1150(fechar_subida),:1154(desfecho_da_subida);apps/engine/lib/engine/infra/tools/propose_infra_pr.ex(a descrição da tool) -
Teste:
apps/engine/test/engine/infra/infra_lead_server_test.exs:1075(a subida pedida depois da PR na mesma resposta é despachada — caminho feliz),:1107(a PR encerra o turno sem subida e o fio diz — caso de falha),:1147(subida recusada e prometida, sem proposta: o fecho diz),:1132(container já de pé: nada a dizer),:1169(recusa corrigida no turno: sem frase),:1190(turno sem subida: sem frase),:1199(segundapropose_infra_prno lote é recusada) -
Origem: AT-264 (uso real de 2026-09-29, item A18 da análise)
O git credenciado roda no host do runner (RN-676)
RN-673 — A mensagem que chega com turno em curso entra numa fila persistida no log e é lida no fim do turno, N mensagens num turno só
Até aqui a mensagem que chegava a um agente conversacional no meio de um turno
era 409 turno_em_andamento (RN-578): gravada como chat.message
(a api grava antes de falar com o engine) e nunca lida. O uso real de 29/09
mediu três numa sessão só — PO, Infra Lead e Dev Lead. Decisão do dono (01/10),
para os SETE conversacionais pelo mesmo caminho:
- Fila, não recusa. Com turno em curso, a mensagem entra na fila do agente
(
TurnoAssincrono.receber_mensagem/4), o agente gravachat.message_queued(mensagemId,texto,idioma,posicao) e o clique recebe 202 com{entrega: "enfileirada", posicao}. Sem turno, o turno sobe na hora, como sempre (entrega: "lida"). A rota da api devolve também omensagemId— o id dochat.message. - N mensagens = 1 turno. No FIM do turno (
finalizar/1, nuncasuspender/1) as pendentes viram UM turno, na ordem de chegada, num texto só (FilaDeMensagens.texto_do_turno/1: uma é ela mesma; várias, numeradas), e o agente gravachat.message_deliveredcom os ids. A entrega é uma mensagem a si mesmo, depois do fecho do servidor (a correção de gate do Infra Lead e o handoff do Arquiteto vêm primeiro). A mensagem que chega entre o fim do turno e a entrega entra no FIM da fila, e a fila é entregue na hora. - Teto de 10 pendentes por agente e sessão. A 11ª é 409
fila_de_mensagens_cheia, comagent.errordurável de origempolitica— ela está gravada e não será lida. - Cancelar.
POST …/agents/:agent/messages/:messageId/cancel(developer, o mínimo da rota de mensagem): só quem ENVIOU (403mensagem_de_outra_pessoa), só em sessão que aceita conversa (RN-581). Quem decide se ela ainda está na fila é o processo do agente, que serializa a corrida com a entrega: 409mensagem_fora_da_filaquando já foi lida ou cancelada. O engine gravachat.message_cancelledem nome de quem cancelou; com o agente fora do ar, grava direto (a fila é o log). - Persistida no log, nunca em tabela. Pendente = enfileirada e nem
entregue nem cancelada — a mesma derivação no engine e na tela. O
init/1dos sete reconstrói a fila e agenda a entrega; a RN-580 tira do histórico a cancelada e a pendente do próprio agente; no boot, depois de fechar o turno órfão (RN-586), o agente com fila pendente é acordado. O turno interrompido continua nunca refeito. - Turno suspenso em aprovação (Dev Lead, RN-284) segue recusando com
aguardando_aprovacao— sem fim previsto, enfileirar prometeria leitura sem data. A fila que já existia espera a retomada terminar. - A tela. Com turno em curso o composer não trava mais para mensagem a
agente: o botão vira "Pôr na fila" e a mensagem não arma turno novo. No fio,
"na fila de
" com Cancelar para o autor, e "cancelada" riscada. O chat sem agente segue travando.
A recusa turno_em_andamento continua para o que não é fala: revisão de
história do PO, prontidão do Criativo, oferta de handoff do Arquiteto.
- Código:
apps/engine/lib/engine/agents/turno_assincrono.ex:322(receber_mensagem),:245(tratar_resultado),:519(entregar_fila),:380(cancelar_mensagem),:425(fila_ao_subir),:559(emitir_recusa_por_fila_cheia);apps/engine/lib/engine/agents/fila_de_mensagens.ex:71(pendentes),:111(fora_do_historico),:158(texto_do_turno),:182(acordar_pendentes);apps/engine/lib/engine/agents/reidratacao.ex:178(fora_do_historico);apps/engine/lib/engine/agents/dev_lead_server.ex:245(_mensagem_id);apps/engine/lib/engine_web/controllers/agent_command_controller.ex:405(cancel_queued_message),:468(responder_ao_aceite);apps/api/src/application/use-cases/agents/cancel-queued-agent-message.use-case.ts:35(CancelQueuedAgentMessageUseCase);apps/api/src/infrastructure/http-clients/api-to-engine-client.ts:69(entregaDaResposta);apps/api/src/domain/sessions/conversa-em-sessao-encerrada.ts:72;apps/web/src/lib/fila-de-mensagens.ts:27(estadosNaFila);apps/web/src/lib/enfileirar-mensagem.ts:25(useEnfileirarMensagem);apps/web/src/routes/PainelDaFila.tsx:20(PainelDaFila, que substituiu o selo — RN-978) - Teste:
apps/engine/test/engine/agents/fila_de_mensagens_test.exs:156(entra na fila — caminho feliz),:181(N mensagens = 1 turno),:223(a que chega na janela vai ao fim),:246(teto de 10 — caso de falha),:276e:306(cancelar e cancelar fora da fila),:322(restart),:349(Dev Lead suspenso),:78/:120(derivação e reidratação);apps/engine/test/engine_web/controllers/agent_command_controller_test.exs:70;apps/api/test/application/use-cases/agents/cancel-queued-agent-message.use-case.spec.ts:40,:61(outra pessoa — caso de falha),:95,:106;apps/api/test/application/use-cases/agents/send-agent-message.use-case.spec.ts:74;apps/api/test/infrastructure/http-clients/api-to-engine-client.spec.ts:531;apps/web/src/routes/SessionPage.fila-de-mensagens.test.tsx:163,:189(fila cheia),:207,:233;apps/web/src/lib/fila-de-mensagens.test.ts:17 - Origem: AT-267 (item A21 da análise do uso real de 2026-09-29), decisão do dono de 01/10; ADR 0191
Complementada pela RN-924. A mesma ideia (estado da fila no log, reconstruído no init/1) vale para a correção de gate do Infra Lead, e a fila de mensagens espera o fim do turno retomado (RN-920).
O plano de teste nasce depois da entrega do dev (RN-674, ADR 0192)
RN-674 — A QA-estratégia escreve o plano sobre o código ENTREGUE, no começo do ciclo do qa-verificada, e o implementavel se julga sem ele
No uso real de 29/09 o plano de teste PRE-DEV (RN-341) terminou
duas vezes em toolloop.limit_reached 8/8 sem emit_plano_de_teste: o laço
leu docs, RAG vazio e pastas até concluir que "o código ainda não existe". O
dono decidiu (01/10) pela saída (b): o plano nasce DEPOIS da entrega. O
ADR 0192 revisa o 0090 nesse
ponto, e a regra tem quatro partes:
- O plano é o primeiro passo do ciclo de revisão.
run_area/3doQaLeadServerobtém o plano da entrega ANTES das subespecialidades (plano_de_teste_da_entrega/5): reencontra na cauda da sessão (teto da reidratação, RN-580) oartifact.plano_de_testeda MESMAtaskId— a rodada de correção não paga outro laço —, e senão rodaQaEstrategiaAgent.run/6com o worktree do dev comoworkspace_root, as regras de negócio da task e a lista de arquivos que a entrega tocou (git diff dev...HEAD, até 40 nomes). Diff que falha vira texto ("não consegui listar: motivo"), nunca lista vazia. - O plano é INSUMO, nunca veredito. Ele chega à Automação no FIM da
mensagem dela, para o passo de achar qual teste cobre cada regra; a régua do
veredito não muda (uma linha de
coverageMatrixpor regra da story,approvedsó com suite verde e toda regra coberta) e o contrato externo segue sendo UMqa_verdictpor ciclo. Plano que FALHA não segura a revisão: a QA-estratégia gravaagent.errorcom origem (RN-059) e a Automação recebe a mensagem de sempre, byte a byte. - O
implementavelse julga sobre a história e omodule_map.run_assessment/2não lê nem dispara plano: propõe o parecer na PRIMEIRA chamada, sobre o que o kickoff do Dev Lead já lhe dá, com payloadstoryId/parecer/justificativa. Gate, dono, severidadewarne aprovação humana não mudam; só aentrada([story-ready, module_map]). O appsec (RN-539) segue disparando ali, no design. - O teto continua 8.
"qa-estrategia"não ganha cláusula emIteracoes.tipo/1; o que mudou foi o insumo. O agente passa a rodar sob otask_budget_microsque as subespecialidades compartilham (RN-036).
run_design/3, Dispatcher.run_qa_estrategia/3 e QaEstrategiaContext
saíram, e artifact.plano_de_teste passa a exigir taskId. Declarado e não
decidido aqui: se os critérios executáveis do plano devem virar linhas
OBRIGATÓRIAS da coverageMatrix (e portanto reprovar entrega).
- Onde:
apps/engine/lib/engine/gates/qa_lead_server.ex:190(plano_de_teste_da_entrega),:741(plano_de_teste_da_entrega),:742(plano_ja_emitido),:747(arquivos_alterados);apps/engine/lib/engine/gates/qa_estrategia_agent.ex:87(run),:104(token_budget_micros),:189(descrever_arquivos);apps/engine/lib/engine/gates/qa_automacao_agent.ex:226(com_o_plano);apps/engine/lib/engine/agents/dev_lead_tools.ex:544(run_assessment),:577(propor_parecer);apps/engine/lib/engine/harness/artifact_schemas.ex:59(taskId);apps/api/config/gates.yml(qa-verificada,implementavel);packages/contracts/fluxo.yml(qa-estrategia,area-qa,dev-lead) - Teste:
apps/engine/test/engine/gates/qa_lead_server_test.exs, describe "plano de teste da entrega (ADR 0192)" — o plano antes da Automação e dentro da mensagem dela (caminho feliz), o reuso portaskId, o plano de outra task que não conta, e o plano que falha sem segurar a revisão (caso de falha);apps/engine/test/engine/gates/qa_estrategia_agent_test.exs(os arquivos da entrega na mensagem, o diff que falhou,taskIdno artefato);apps/engine/test/engine/agents/dev_lead_tools_test.exs, describe "assess_implementability (ADR 0090, insumo do ADR 0192)" (parecer na primeira chamada, payload sem plano);dev_lead_server_test.exs("assess_implementability pending TAMBÉM suspende");apps/engine/test/engine/harness/artifact_schemas_test.exs("sem taskId é rejeitado"). ExUnit: provado só no CI (orepo.hex.pmdeu 403 no ambiente da rodada). - Origem: AT-269 (item A23 da análise do uso real de 29/09); decisão do dono em 01/10
A Anamnese religada: sujeito elegível e fato do perfil (RN-680)
RN-680 — A Anamnese não roda sem sujeito elegível, e a hipótese que a própria pessoa aceitou vira fato do perfil dela no grafo e entra no contexto dos agentes que conversam com ela
Decisão do dono em 01/10 (AT-277): religar a Anamnese com correções. No
uso real de 29/09 ela rodou seis vezes e as seis terminaram em
anamnese.run_skipped ("nenhum membro elegível"), escritos pelo MODELO depois
de uma chamada paga (8 933 micros), e as três hipóteses aceitas ficaram na fila
sem destino. Ver o ADR 0196.
- Sem sujeito elegível, a rodada não roda. Sujeito é o membro EFETIVO do
projeto (
projectRole ?? workspaceRole, a régua da RN-471), fora do opt-out, com interação PRÓPRIA no que a rodada mostra ao modelo — evento dele comactor_kind: "user"na janela, ou decisão dele. A guarda vem ANTES da triagem e nem a hipótese na fila a atravessa. Sem sujeito: nenhuma chamada ao LLM e nenhuma busca no RAG; o motivo vai NOMEADO para o log do engine (sem_sujeito_elegivel, comnenhum_membroounenhuma_interacao_propriae os números) e, na rodada pedida à mão, também para o event log (anamnese.run_skippedcomcausa: "sem_sujeito_elegivel"). Pelo tick é só log — seria um aviso a cada 15 minutos por projeto. O prompt passa a listar só os sujeitos. - Membro do projeto é o EFETIVO. Criar projeto não grava
project_members, e a Anamnese lia só essa tabela — o dono do workspace nunca era membro. Os três casos de uso da Anamnese passam a lerlistEffectiveMembers;listMemberscontinua sóproject_members. - A hipótese ACEITA pela PRÓPRIA pessoa vira fato do perfil. O
psychologist.hypothesis_acceptedcarregaprojectId,sujeito(o autor da sessão analisada),hipotese,sugestaoefatoDoPerfil, e oGraphEventTranslator— o mesmo do projetor e degrafo:reprojetar— o traduz para(:FatoDoPerfil {hypothesisId}), ligado à pessoa (SOBRE) e ao projeto (NO_PROJETO).fatoDoPerfilsó étruequando quem aceitou é o sujeito: um fato sobre a pessoa nunca nasce do clique de outra, e o payload diz por quê (motivoSemFato). A recusada fica só registrada, como antes. Os aceites gravados antes desta RN não têm os campos e não viram fato. - O fato entra no contexto dos agentes que conversam com a pessoa. A
mensagem do autor a um agente conversacional leva os fatos DELE neste
projeto — lidos por
QueryUserContextUseCase, escopados ao projeto, os 5 mais recentes, com o total ao lado e o recorte dito —, e o engine os põe no turno como mensagemsystemEFÊMERA, pelo mesmo caminho do idioma (RN-622): nunca no histórico, antes da orientação de idioma, com teto de 2 000 caracteres. Grafo fora do ar vira log, e o turno segue sem os fatos. Turno sem autor não recebe fatos. - Religada.
ANAMNESE_ENABLEDvolta ao defaulttruenoruntime.exse nos três composes;START_ANAMNESEnão muda (o de produção seguefalse, divergência já declarada). O Psicólogo segue pausado.
- Código:
apps/engine/lib/engine/anamnese/elegibilidade.ex:59(avaliar);apps/engine/lib/engine/workers/anamnese_worker.ex:63(maybe_analyze),:97(narrar_sem_sujeito);apps/api/src/domain/iam/membros-efetivos.ts:16(membrosEfetivos);apps/api/src/infrastructure/persistence/drizzle/project.repository.ts:209(listEffectiveMembers);apps/api/src/application/use-cases/execution/accept-hypothesis.use-case.ts:75(fatoDoPerfil);apps/api/src/application/graph-projection/graph-event-translator.ts:171(projectProfileFact);apps/api/src/application/use-cases/graph/query-user-context.use-case.ts:68(fatos);apps/api/src/domain/graph/perfil-do-autor.ts:30(textoDoPerfilDoAutor);apps/api/src/application/use-cases/agents/send-agent-message.use-case.ts:152(resolverPerfil);apps/engine/lib/engine/harness/perfil_do_autor.ex:68(anexar);apps/engine/lib/engine/harness/idioma_da_resposta.ex:253(com_idioma_do_autor);apps/engine/config/runtime.exs:315(anamnese_enabled?) - Teste:
apps/engine/test/engine/anamnese/elegibilidade_test.exs:17(caminho feliz),:35e:46(sem sujeito — casos de falha);apps/engine/test/engine/workers/anamnese_worker_test.exs:305(sem membro, nem com fila, não chama o LLM),:324,:343(rodada à mão narra),:360;apps/engine/test/engine/anamnese/context_builder_test.exs:82;apps/api/test/infrastructure/persistence/drizzle/project-effective-members.repository.spec.ts:69;apps/api/test/application/use-cases/execution/accept-dismiss-hypothesis.use-case.spec.ts:136,:156(aceite de terceiro não vira fato);apps/api/test/application/graph-projection/graph-projector.spec.ts:361,:386;apps/api/test/application/use-cases/agents/send-agent-message.use-case.spec.ts:181,:207(grafo fora do ar);apps/engine/test/engine/harness/perfil_do_autor_test.exs:14;apps/engine/test/engine_web/controllers/agent_command_controller_test.exs:428 - Origem: AT-277 (item A31 da análise do uso real de 29/09; decisão do dono em 01/10)
O teto da RN-418 vale também pela porta tipada (RN-689)
RN-690 — O assistente guarda os toolCalls da resposta: o resultado da ferramenta não chega órfão ao provider
Os agentes conversacionais com laço próprio (Criativo, PO, Arquiteto, Dev Lead,
UX Designer, Staff e Infra Lead) guardavam no histórico a mensagem do
assistente SEM os toolCalls da resposta e, em seguida, a mensagem
role: "tool" com o toolCallId. No fio, o provider recebia um resultado que
respondia a chamada nenhuma e o descartava: o modelo nunca via o que a
ferramenta devolveu e repetia a mesma chamada até o teto do laço (medido na
v7.0.0: o PO chamou listar_regras_de_negocio 12 de 12 vezes em dois turnos).
A mensagem do assistente passa a levar os toolCalls NATIVOS da resposta
quando há algum — o mesmo formato que o ToolLoop já guardava e que o
ContextManager agrupa na compactação —, montada num lugar só. Resposta sem
toolCalls não ganha a chave. Chamada recuperada do texto (ToolCallRecovery,
id nulo) fica de fora: não há id para casar. A api já repassava o campo aos
providers (toWireMessage no OpenAI-compatível, tool_use no Anthropic); a
reidratação continua trazendo ferramentas como texto.
- Onde:
apps/engine/lib/engine/agents/mensagem_do_assistente.ex:17(de);apps/engine/lib/engine/agents/po_server.ex:280, e o mesmo ponto nos outros seis servidores - Teste:
apps/engine/test/engine/agents/po_server_test.exs:93(a segunda chamada leva oassistantcomtoolCallslogo antes dotoolque o responde; a resposta final, sem ferramenta, não ganha a chave — caso de falha) - Origem: AT-350
RN-695 — A análise da Anamnese não entra no fio da sessão
Medido no uso real da v7.0.0: o texto que o modelo da Anamnese escreve durante a rodada ("Analisando a janela do log… usuário (09824667-…)… Decisão de proficiência") aparecia como BOLHA no fio da sessão de ideação, entre o Criativo e o PO, com o UUID cru do usuário. A Anamnese não conversa com ninguém: ela analisa a PESSOA, e o que ela produz é o perfil (RN-680).
A regra: o texto da rodada é gravado como anamnese.analysis, nunca como
agent.response. O ToolLoop aceita response_event no contexto (padrão
agent.response, inalterado para todo outro consumidor) e a Anamnese o põe.
O fio da sessão não desenha NENHUM evento cujo ator é o agente anamnese —
inclusive o agent.response gravado antes da correção. O painel de log da
sessão continua mostrando os eventos dela; o resultado mora em Configurações ›
Perfil de proficiência, sem id cru. Nenhum marcador "perfil atualizado" entra
no fio: fica de fora até alguém pedir.
- Onde:
apps/engine/lib/engine/harness/tool_loop.ex:128(loop);apps/engine/lib/engine/workers/anamnese_worker.ex:157(build_ctx);apps/web/src/routes/session-timeline-montagem.tsx:147(ehDaAnamnese) - Teste:
apps/engine/test/engine/workers/anamnese_worker_test.exs:253(o texto saianamnese.analysis;agent.responsenunca é gravado — caso de falha);apps/web/src/routes/session-timeline-montagem.falha-de-turno.test.tsx:86(a resposta da Anamnese some do fio, a do Criativo fica) - Origem: AT-355
RN-697 — O turno da prontidão do Criativo só sintetiza: sem ferramentas
O turno que confirm_readiness dispara consolida as regras num resumo e grava o
product_brief. No uso real (v7.0.0, Haiku 4.5) ele levou 25 s e gravou mais
quatro artifact.business_rule e um chat.structured_question que ninguém
responderia: o PO já tinha entrado pelo aceite implícito (RN-658). O turno passa
a ir ao modelo SEM catálogo de ferramentas, e nenhuma chamada é atendida nele —
nem a que o modelo copie do histórico. O catálogo volta intacto no turno
seguinte; o turno normal continua com emit_artifact e
ask_structured_questions.
- Onde:
apps/engine/lib/engine/agents/criativo_server.ex:223(executar_confirm_readiness);:268(ferramentas_da_prontidao) - Teste:
apps/engine/test/engine/agents/criativo_server_test.exs:172(o turno vai comtools: []e não grava regra nem pergunta) e:192(regressão: o turno normal mantém as duas ferramentas) - Origem: AT-352
RN-698 — O texto do turno vira UMA resposta, e o teto de iterações aparece no fio
Com ferramentas, o modelo escreve em pedaços — um trecho antes da chamada, outro
na volta seguinte, muitas vezes continuando a mesma frase. Cada volta virava a
sua agent.response, e o fio recolhe as anteriores em "Passos do turno": à
mostra ficava só o último pedaço (" histórias e tarefas que cubram tudo.",
começando com espaço). O transporte não perdia nada — o frame final da api
leva o texto inteiro da VOLTA —; o que se partia era o TURNO. Nos sete
conversacionais, o texto de uma volta com ferramenta é acumulado no processo do
turno e gravado numa agent.response só quando o turno fecha (sem ferramenta,
teto, falha, suspensão do Dev Lead, PR da Infra). Pedaço que começa ou termina
com espaço entra como veio; dois pedaços colados viram parágrafos.
O toolloop.limit_reached dos conversacionais (RN-166) passa a ser uma linha
no fio, na bolha de falha, origem modelo, dizendo o teto ("parou no teto de N
passos sem terminar o turno"); sem o número no payload a linha diz ?.
A agent.response do turno leva o modelName (RN-146) em TODO desfecho, não
só no fecho sem ferramenta: quando o turno fecha sem uma chamada final que o
traga (entregou um formulário ask_structured_questions, esgotou o teto,
falhou), vale o modelo da última volta acumulada. Sem chamada de LLM que tenha
devolvido modelo, o campo fica nil — nunca um modelo inventado (AT-384; o
Infra Lead não grava modelName em desfecho nenhum e segue fora).
Revisão (decisão do dono, 03/10, AT-395): só o fecho do turno na bolha. No
uso real (loja-teste-2, 02/10) a resposta única juntava o texto de TODAS as
voltas ("Vou registrar… Excelente! Registrei…", dois resumos seguidos no PO).
A agent.response do turno continua UMA, mas o content passa a ser só o
FECHO — o texto da última volta que escreveu algo (o turno que termina num
formulário, sem texto na última volta, fica com a anterior) — e o texto das
voltas anteriores vai, na ordem, no campo passos, ausente quando não há.
Nada some: o fio desenha cada passo como narração do mesmo turno+autor antes
do fecho, e o colapso de "Passos do turno" que já existia os recolhe, fechado
por padrão; os passos não contam no corte de mensagens do fio (RN-644). A
reidratação (RN-580) devolve passos ANTES do content, no mesmo
assistant, para o modelo não perder o que disse no meio do turno. Vale nos
sete conversacionais (o Infra Lead também, sem modelName). Sem teto próprio
para passos: o acúmulo já era gravado inteiro antes da revisão.
- Onde:
apps/engine/lib/engine/agents/texto_do_turno.ex:88(descarregar);:123(juntar);:51(descarregar_com_modelo);:65(payload_do_turno);apps/engine/lib/engine/agents/reidratacao.ex:183(acumular);apps/web/src/routes/session-timeline-montagem.tsx:880(passos) - Teste:
apps/engine/test/engine/agents/texto_do_turno_test.exs:43(três voltas: fecho e dois passos),:56(uma volta, sempassos),:60(termina em formulário);apps/engine/test/engine/agents/reidratacao_test.exs(passos antes do fecho);apps/web/src/routes/SessionPage.ordenacao-e-avisos.test.tsx:502(só o fecho à mostra, passos ao expandir) e:524(sem passos, nada extra);apps/engine/test/engine/agents/criativo_server_test.exs:209(duas voltas, uma resposta inteira; nenhuma segunda),apps/engine/test/engine/agents/texto_do_turno_test.exseapps/web/src/routes/session-timeline-montagem.falha-de-turno.test.tsx:85(a linha do teto; sem número, não inventa um); o modelo no formulário emapps/engine/test/engine/agents/criativo_server_test.exs:703(com modelo) e:724(sem modelo,nil) - Origem: AT-354, AT-384, AT-395
RN-699 — O Dev Lead lê o module_map vigente e o backlog do PROJETO, e relê os dois quando o plano é recusado
Medido no uso real (02/10): aceito o Dev Lead, ele respondeu que faltava o
module_map — e o mapa existia. O kickoff procurava o evento
architecture.module_map_created, que NENHUM código emite (o Arquiteto grava o
mapa pela api), e caía sempre em "(sem module_map)". O mapa agora vem da MESMA
leitura de projeto que o AppSec usa (get_infra_context, o vigente da api),
nunca de evento da sessão; sem mapa, o kickoff diz que não há.
No mesmo uso, as tarefas nasceram DEPOIS do kickoff, o Dev Lead inventou um
task_id, a api recusou o plano e o turno acabou sem nova tentativa. Ele ganha
read_backlog: leitura CONTIDA, sem parâmetro, escopada ao projeto (as tarefas
pendentes pela mesma rota da listar_backlog do PO, teto de 200 linhas dizendo
o total real, e o module_map vigente). A recusa da api volta ao laço dizendo
para chamar read_backlog e corrigir; e só o propose_execution_plan
bem-sucedido encerra o turno — antes, qualquer ferramenta bem-sucedida na
última chamada (uma leitura, um emit_artifact) encerrava. A instrução do
kickoff proíbe inventar task_id e pede que ele só diga ter avaliado as
histórias em que chamou assess_implementability.
-
Onde:
apps/engine/lib/engine/agents/dev_lead_tools.ex:418(module_map_vigente);:353(spec_read_backlog);:419(tarefas_do_backlog);apps/engine/lib/engine/agents/dev_lead_server.ex:647(kickoff_instruction); -
Onde:
apps/engine/lib/engine/agents/dev_lead_tools.ex:418(module_map_vigente);:353(spec_read_backlog);:419(tarefas_do_backlog);apps/engine/lib/engine/agents/dev_lead_server.ex:647(kickoff_instruction);:393(run_turn) -
Teste:
apps/engine/test/engine/agents/dev_lead_server_test.exs:164(o mapa vigente chega ao kickoff e o tipo antigo não é lido), o teste seguinte (sem mapa, diz que não há — caso de falha) e:202(plano recusado pela api →read_backlogno mesmo turno, com as tarefas reais) -
Origem: AT-362, AT-363
RN-700 — O PO não anuncia o handoff ao Arquiteto sem fazê-lo, e história sem tarefa não é entregue
No uso real o PO escreveu "Handoff ao Arquiteto" e encerrou o turno sem chamar
offer_handoff (fere a RN-163), e entregou cinco histórias ready com zero
tarefas — o Dev Lead não tinha o que distribuir. No fim de todo turno do PO,
se o servidor ainda não registrou oferta ao Arquiteto naquele processo, ele lê
as regras do projeto (uncoveredCount, a mesma cobertura do aceite automático
da RN-660) e o backlog: com toda regra coberta e toda história com ≥ 1 tarefa,
o SERVIDOR oferece o handoff (molde da Infra que propõe sozinha, ADR 0190,
RN-671/672), e a RN-660 segue decidindo o aceite. Com regra coberta e história
sem tarefa, não oferece e grava backlog.handoff_not_offered (origem modelo)
nomeando as histórias; leitura ou oferta que falha grava o mesmo evento com
origem infra. Regra descoberta, ou backlog sem regra/história, não é julgado.
A instrução do PO passa a exigir tarefa por história. Quem cria as tarefas é o
PO (decisão do agente principal na AT-364: ele já tem create_task).
- Onde:
apps/engine/lib/engine/agents/po_server.ex:515(fechar_handoff);:558(oferecer_handoff) - Teste:
apps/engine/test/engine/agents/po_server_test.exs:592(oferece e não repete),:610(história sem tarefa: não oferece, nomeia) - Origem: AT-364
O instalador oferece os modelos locais, e não confunde o dev com uma instalação (RN-691, RN-692)
RN-702 — A compactação de contexto respeita a janela padrão de 128k e nunca troca turnos por um marcador vazio
- Regra: sem
context_windowno contexto (os agentes conversacionais não o declaram), a janela de compactação éDEFAULT_CONTEXT_WINDOW, cujo padrão passou de 8 192 para 128 000, o mesmo valor que os agentes de gate e de dev declaram. Quando o sumarizador (context-manager) falha ou devolve texto vazio, o resumo é DETERMINÍSTICO (um trecho de até 300 caracteres de cada turno substituído) e ocontext.compactedgravasummaryOrigin: "deterministico"efalhacomorigememotivo(RN-059). Com o sumarizador respondendo,summaryOrigin: "modelo"efalha: null. - Medição (AT-366): sessão 17d41c20, Arquiteto em
anthropic/claude-haiku-4.5(janela de 200k): 6 799 tokens passavam de0,7 × 8 192 = 5 734e compactavam 35 turnos; o sumarizador falhou e o resumo gravado foi o marcador"(35 turnos anteriores omitidos)". No turno seguinte o Arquiteto pediu de novo uma confirmação que já tinha recebido. - Onde:
apps/engine/lib/engine/harness/context_manager.ex:157(summarize),apps/engine/lib/engine/harness/context_manager.ex:188(resumo_deterministico),apps/engine/config/runtime.exs(default_context_window) - Teste:
apps/engine/test/engine/harness/context_manager_test.exs("sem context_window declarado: 6,8k tokens NÃO compactam"; "sumarizador respondendo: summaryOrigin modelo, sem falha"; caso de falha: "sumarizador falhando: falha NOMEADA com origem e resumo determinístico com o conteúdo") - Lacuna declarada: a janela continua não sendo lida do catálogo por
modelo (o engine não recebe o
context_windowdo binding); 128k é o padrão da instalação, ajustável porDEFAULT_CONTEXT_WINDOW. - Origem: AT-366
RN-706 — O agente de execução sabe onde está: pasta, imagem, rede, git tipado e o próprio módulo
- Regra: os dev agents (
dev-*) e os subagentes de QA (qa-*) recebem em toda chamada de LLM uma mensagemsystemEFÊMERA de ambiente, pelo mesmo caminho do idioma (RN-622) e do perfil do autor (RN-680) — nunca emstate.messages, nunca por servidor. Ela diz a pasta real em que oterminalroda (oworkspace_rootdo laço, traduzido para/workpela mesma regra doTerminalExecutorquando há containerrunningREGISTRADO), a imagem e a rede do últimoartifact.project_image, que no terminal só rodamgit fetchegit merge, no worktree da task, sem push e sem outro git (RN-1019 revisa esta regra: antes ela proibia git no terminal de todo; commit, push e PR seguem com o sistema noreport_done) e, para o dev agent, o MÓDULO dele: o que consome de outro módulo vem do contrato (listar_contratos_de_modulos) e se usa por stub/mock, nunca reimplementado, e arquivos de raiz que já existem nadev(package.json, lockfile) não se recriam. Sem containerrunning, a mensagem DIZ isso e não afirma/work. Teto de 1 200 caracteres; falha de leitura vira mensagem ausente, nunca turno derrubado. - Medição (AT-379/AT-378, 02/10, loja-teste):
dev-apieqa-automacaorodaramcd /workspace(nenhum prompt cita essa pasta);git statusdeu exit 127 numa imagem sem git; odev-api(móduloapi) criousrc/catalog.jsepackage.json, os mesmos dodev-catalog, e o merge conflitou. - Onde:
apps/engine/lib/engine/harness/ambiente_do_agente.ex:102(montar),:48(com_ambiente),:65(anexar),apps/engine/lib/engine/harness/tool_loop.ex:87(com_ambiente) - Teste:
apps/engine/test/engine/harness/ambiente_do_agente_test.exs("caminho feliz: com container running, a pasta é /work/.worktrees/"; caso de falha: "sem container running, não afirma /work") - Lacuna declarada: nenhum aviso no resultado do
git_commitpara arquivo de outro módulo (omodule_mapnão diz quais caminhos são de cada módulo, então o aviso não seria determinístico) e nenhum bloqueio de commit, por decisão do dono. - Origem: AT-379, AT-378
RN-707 — O semgrep do gate de SecOps tem $HOME gravável, e a falha sem saída é nomeada
- Regra: nos composes de produção e de instalação, o tmpfs
/home/enginedo engine nasce com o dono do usuário da imagem (/home/engine:uid=1000,gid=1000,mode=0700). OSemgrepDetector.Livedistingue o exit1com JSON (achados) do exit0/1com stdout vazio, que vira{:error, {:sem_saida, exit_code}}em vez de:invalid_output. O veredito do gate NÃO muda: scanner que não rodou continua "pulado" no resumo do parecer. - Medição (AT-380): dentro de
brabo-engine-1(imagem de produção,read_only: true), o tmpfs/home/enginenasciaroot2755; osemgrep scando gate saía1com stdout vazio ePermissionError: '/home/engine/.semgrep'no stderr, e o parser chamava isso de:invalid_output. Com o tmpfsuid=1000o mesmo comando devolve o JSON (exit 0). O Kubernetes não tinha o defeito (fsGroup: 1000noemptyDir). - Onde:
apps/engine/lib/engine/actions/semgrep_detector.ex:73(interpretar),docker/docker-compose.prod.yml,docker/docker-compose.install.yml - Teste:
apps/engine/test/engine/actions/semgrep_detector_test.exs("JSON com results vira findings, inclusive com exit 1 (achou algo)"; caso de falha: "exit 1 com stdout vazio é falha de execução nomeada, não :invalid_output") - Decisão (revista pela RN-714): a frase "o veredito do gate NÃO muda" deixou de valer: o dono decidiu (02/10) que SAST que não rodou deixa o gate PENDENTE e bloqueando.
- Origem: AT-380
RN-710 — O Workflows não é delegado sem CI que rode, e não pergunta a ninguém
No aceite da Infra, o infra-workflows respondia com uma pergunta ("qual é o
provider do repositório do projeto?") que nenhum humano recebe — subagente não
conversa —, e a delegação terminava failed com origem modelo e um
dev.error genérico no fio (medido no loja-teste, 02/10). O projeto era
local: repositório bare no servidor, sem runner de CI.
-
Provider
localou ausente: a delegação ao Workflows é dispensada (delegationdispensed, com a justificativa nomeada — "repositório local … não tem CI que rode"), e a PR de infra sai só com o que o Lead produziu. Sógithubegitlabdelegam. -
Quando delega, o prompt diz que o provider recebido JÁ é o do projeto e proíbe perguntar.
-
O subagente que para com TEXTO em vez de chamar a ferramenta continua falha nomeada reportada ao lead, mas o diagnóstico diz o que ele pediu: "pediu informação que não recebeu, em vez de chamar emit_infra_delegation_result:
" (até 300 caracteres). -
O título da PR de infra leva o prefixo
infra:só quando o resumo do modelo ainda não começa com ele (sem diferenciar maiúsculas) — antes saía "infra: infra: Dockerfile…" (AT-396). -
Onde:
apps/engine/lib/engine/infra/workflows_agent.ex:61(dispensa),apps/engine/lib/engine/infra/infra_lead.ex:69(com_prefixo),apps/engine/lib/engine/infra/workflows_agent.ex:183(parada_sem_ferramenta),apps/engine/lib/engine/infra/infra_lead_server.ex:1288(finalize)apps/engine/lib/engine/infra/infra_lead_server.ex:1288(finalize) -
Teste:
apps/engine/test/engine/infra/workflows_agent_test.exs("RN-710: …") eapps/engine/test/engine/infra/infra_lead_server_test.exs("RN-710: repositório local dispensa o Workflows …") eapps/engine/test/engine/infra/infra_lead_test.exs("com_prefixo/1 (AT-396)") -
Origem: AT-368, AT-396
RN-711 — O conversacional sabe onde está no fluxo, e a restrição técnica do usuário vira decision_record
-
Regra: os sete agentes conversacionais (Criativo, PO, Arquiteto, Infra, Dev Lead, UX Designer, Staff) recebem em toda chamada de LLM uma mensagem
systemEFÊMERA com o fluxo de entrega — as sete etapas na ordem (Criativo → PO → Arquiteto → Infra → Dev Lead → dev agents → QA/SecOps → merge manual), ONDE o agente está, o PRÓXIMO passo real e QUEM o dispara (humano × sistema) —, pela fachadaEngineApiClient, depois do ambiente (RN-706) e antes do perfil (RN-680) e do idioma (RN-622, sempre o último). A fonte é UMA,Engine.Harness.FluxoDoTime, conferida por teste contrapackages/contracts/fluxo.yml: todo papel citado existe ali comoactive, e o texto não cita etapa fora dele (deploy, pipeline, Dockerfile, InfraAgent). O Criativo registra restrição técnica EXPLÍCITA do usuário (linguagem, "nenhuma dependência", "sem rede", "um módulo só") comodecision_record(emit_artifact, RN-505), sem sermão; o kickoff do Arquiteto lê osartifact.decision_recordda sessão (a mesma leitura por tipo, RN-580) e manda respeitá-los, dizendo QUAL e POR QUÊ quando contrariar. O fechamento do Arquiteto não pede de novo a confirmação já dada, e a Infra não cola os arquivos no chat nem promete deploy. -
Medição (AT-369/AT-370, 02/10, loja-teste): o Criativo anunciou o Arquiteto como próximo (é o PO); o PO disse que o Arquiteto ia "implementar"; o Arquiteto, que "dev agents começam" após os ADRs, e pediu "Confirme: Arquitetura pronta" depois da confirmação; a Infra despejou ~80 linhas de compose no fio. O pedido "Node puro, nenhuma dependência, sem rede, um módulo só" não virou artefato e o Arquiteto criou três módulos.
-
Onde:
apps/engine/lib/engine/harness/fluxo_do_time.ex:120(texto),:97(anexar),apps/engine/lib/engine/sessions/engine_api_client.ex:647:97(anexar),apps/engine/lib/engine/sessions/engine_api_client.ex:746(llm_turn),apps/engine/lib/engine/harness/agents.ex:23(identity),apps/engine/lib/engine/agents/arquiteto_server.ex:404(build_kickoff) -
Teste:
apps/engine/test/engine/harness/fluxo_do_time_test.exs("todo papel citado existe no fluxo.yml como ativo"; caso de falha: "nenhum texto cita etapa/agente fora do fluxo"),apps/engine/test/engine/agents/arquiteto_server_test.exs("kickoff lê os decision_record e manda respeitar "um módulo só""),apps/engine/test/engine/harness/agents_test.exs -
Lacuna declarada: a leitura de
decision_recorddo Arquiteto é da SESSÃO (como as regras e as histórias do kickoff), não do projeto inteiro; e nada RECUSA umcreate_module_mapque contrarie a restrição — a instrução pede a justificativa, não a impõe. -
Origem: AT-369, AT-370
RN-714 — SAST que não rodou deixa o gate de SecOps pendente, nunca aprovado
- Regra: quando o semgrep do gate de SecOps não roda — binário ausente,
saída inválida (
:invalid_output),:sem_saida, exceção ou teto de tempo —, nenhum veredito é gravado: a PR continua emawaiting_secops, o gate fica PENDENTE e bloqueia, e a PR não chega ao usuário como aprovada pela SecOps. O motivo vai ao fio comoagent.errorde origeminfra("SAST não rodou:— … reexecute o gate depois de corrigir"). O contrato externo não muda: continuam os dois vereditos de sempre ( approved/changes_requested), um por gate. O gitleaks ausente continua só "pulado" no resumo. - Reexecução: o ciclo fica
in_progressemgate_states, e oGateRescuero reexecuta no boot do engine e depois do limiar de staleness (gate_rescue_stale_after_seconds, 15 min). Não há botão de reexecutar gate na tela. - Sem repetição (AT-386): o resgate que reencontra o MESMO motivo não
grava outro
agent.error: se o último evento do SecOps daquela task (agent.errorouartifact.secops_verdict, lidos por tipo, cauda com teto) já é este erro com o mesmoreason, fica só a linha de Logger do servidor. Motivo diferente, um veredito no meio ou leitura que falha gravam de novo; oagent.errorpassa a levartaskId. - Onde:
apps/engine/lib/engine/gates/secops_agent_server.ex:116(run_secops),apps/engine/lib/engine/gates/secops_agent_server.ex:144(sast_nao_rodou),apps/engine/lib/engine/gates/secops_agent_server.ex:209(erro_ja_registrado?) - Teste:
apps/engine/test/engine/gates/secops_agent_server_test.exs("sem achados (gitleaks e semgrep limpos): approved"; casos de falha: "semgrep com saída inválida: gate pendente, sem veredito, motivo nomeado" e "semgrep com binário ausente: gate pendente, sem veredito, motivo nomeado"; AT-386: "primeiro resgate grava; o segundo, com o mesmo motivo, não", "motivo diferente, ou veredito depois do erro, grava de novo", "o SAST que volta a rodar aprova como sempre") - Revisa: RN-707 ("o veredito do gate NÃO muda")
- Origem: AT-380
RN-717 — O desvio do agente que a Anamnese observa vira sinal do produto, sem dado pessoal
- Regra: laço de agente, turno cancelado pelo usuário e mensagem do
usuário que repara falha de agente são registrados pela Anamnese como
anamnese.agent_deviation(ferramentareport_agent_deviation), nunca como perfil da pessoa. O payload é montado no engine por lista de permitidos —agente,tipo(laco|cancelamento_pelo_usuario|reparo_pelo_usuario) eevidenceEventIds(deduplicados, teto de 20) —, sem texto livre e sem id, nome, e-mail ou traço do usuário; campo extra é descartado. O tipo não é projetado no grafo do usuário. Tipo fora do vocabulário ou sem evidência é recusado, sem gravar. - Onde:
apps/engine/lib/engine/anamnese/tools/report_agent_deviation.ex:73(payload),apps/engine/lib/engine/anamnese/tools/report_agent_deviation.ex:52(run),apps/engine/lib/engine/anamnese/tools.ex, regra no kickoff (prompts/anamnese-kickoff.mdv4) - Teste:
apps/engine/test/engine/anamnese/report_agent_deviation_test.exs("reparo do usuário vira anamnese.agent_deviation do AGENTE, só com agente, tipo e evidência"; falhas: "tipo fora do vocabulário é recusado e nada é gravado", "sem evidência é recusado"),apps/api/test/application/graph-projection/desvio-do-agente-fora-do-grafo.spec.ts - Lacuna declarada: a api não confere que a evidência existe; a leitura é por consulta ao event log, sem tela (ADR 0205).
- Origem: AT-356 (parte 2), decisão do dono de 02/10
Uma chave de cache, uma pergunta (RN-718)
RN-719 — Ferramenta de agente aceita lista enviada como texto JSON, e exceção dentro dela volta ao laço
Contexto: medido ao vivo (02/10, Haiku 4.5 via OpenRouter), o modelo manda
lista e objeto como STRING JSON ("business_rule_ids": "[\"01M…\"]"). Três
desfechos: create_story descartava os ids calado (história ficava draft),
outras ferramentas recusavam e custavam voltas, e create_c4_diagram fazia
Enum.map numa string e derrubava o turno do Arquiteto com agent.error.
A regra: toda ferramenta dos sete conversacionais e do ToolLoop roda por
UM ponto, Engine.Harness.ArgumentosDeFerramenta.executar/4. Argumento cujo
schema declara array/object e chega como string JSON válida do tipo vira o
valor; string que não é recusa NOMEADA ("o campo X precisa ser uma lista"),
antes de a ferramenta rodar. Campo declarado string nunca é tocado. Exceção
levantada dentro da ferramenta vira tool.result ok: false com
origem: "codigo", e o erro entra no laço como qualquer outro (RN-163): o
turno continua.
- Onde:
apps/engine/lib/engine/harness/argumentos_de_ferramenta.ex:26(executar),:73(coagir),:56(falha_interna?);apps/engine/lib/engine/agents/resultado_de_ferramenta.ex:29(payload);apps/engine/lib/engine/harness/tool_loop.ex:409(run_direct);apps/engine/lib/engine/agents/arquiteto_server.ex:357(executar_ferramenta, o mesmo helper nos outros seis servidores) - Teste:
apps/engine/test/engine/harness/argumentos_de_ferramenta_test.exs(create_storycom ids em string JSON grava os ids;create_c4_diagramcomactorsem string JSON funciona; string não-JSON num campo array é recusa nomeada; ferramenta que lança não encerra o turno doToolLoope gravatool.resultok: false) - Origem: AT-387
RN-721 — O id encurtado de história ou tarefa é resolvido dentro do projeto
O Arquiteto passava a assign_story_modules os 8 primeiros caracteres do id da
história, a api respondia 400 "storyId must be a UUID" e, depois de oito
tentativas, nenhuma história ganhava módulo (loja-teste-2, 02/10). Toda
ferramenta de agente que recebe id de história ou tarefa —
assign_story_modules e create_task (story_id), assess_implementability
(storyId) e propose_execution_plan (tarefas[].taskId) — passa o id por
Engine.Harness.IdDoBacklog antes de chamar a api:
-
UUID completo segue sem consulta.
-
Prefixo de no mínimo 8 caracteres hexadecimais é resolvido contra o backlog do PROJETO (
list_backlog): um só id que começa com ele vira o UUID; nenhum é recusa nomeada ("nenhuma história do projeto tem id começando com …"); mais de um é recusa nomeada ("o prefixo … é ambíguo"). A recusa volta ao modelo como resultado da ferramenta (RN-163). -
O que não é nem UUID nem prefixo segue como veio, e quem recusa é a api.
-
Onde:
apps/engine/lib/engine/harness/id_do_backlog.ex:21(resolver),:43(resolver_em);apps/engine/lib/engine/agents/dev_lead_tools.ex:180(resolver_tarefas) -
Teste:
apps/engine/test/engine/harness/id_do_backlog_test.exs(prefixo único resolve; ambíguo e inexistente recusam nomeados) -
Origem: AT-390
A Anamnese não paga duas vezes pela mesma janela, e o resgate de gate não reinicia ciclo que espera alguém (RN-722)
RN-722 — A Anamnese fecha a janela que avaliou e só roda com interação nova; o resgate de gate pergunta pela task, respeita a decisão humana pendente e estaciona o ciclo velho
- Regra (Anamnese): (1) a rodada que AVALIOU a janela e encerrou sem
perfil (
skip_proficiency) gravaanamnese_runscom zero perfis, e a próxima começa emwindow_to— revisa a cláusula da RN-680/do moduledoc "rodada que não conclui não grava": rodada que FALHA (teto, orçamento, provider) continua sem gravar, e a que nem chega ao LLM (sem sujeito, triagem, teto diário) não fecha nada; (2) os eventos que a própria Anamnese escreve (anamnese.*e todo evento de atoragent/anamnese) e os de atorsystemficam fora da janela e da contagem da triagem; (3) decisão do dono de 02/10: sujeito é quem tem ao menos 5 interações próprias NOVAS (eventos deactor_kind: "user"mais decisões, na janela que já começa no fim da última tentativa) — revisa o "ao menos UMA" da RN-680 —, e o projeto tem no máximo 4 rodadas pagas por dia UTC (concluída, falha ourun_skippedescrito pelo modelo; o pulo sem sujeito não conta). Abaixo de qualquer dos dois, nenhuma chamada ao LLM nem ao RAG; o motivo vai ao log e, só na rodada pedida à mão, aanamnese.run_skipped(causa: "teto_diario"ou"sem_sujeito_elegivel"). - Regra (resgate de gate, revisa a RN-140): o
GateRescuer(a) considera vivo o ciclo cuja TASK está em voo no lead, perguntando a ele ({:em_voo?, task_id}), e não mais o processo do projeto — lead ocioso não segura o resgate; lead ocupado demais para responder conta como vivo; (b) não reinicia ciclo cuja sessão temproposed_actionPENDENTE de ator do gate (qa*,secops*,appsec) — só registra no log; (c) decisão do dono de 02/10: ciclo parado há mais de 2 h desde a ÚLTIMA ATIVIDADE (updated_at) é ESTACIONADO (parked_at) comgate.rescue_parkedno log da sessão (task, gate, idade, motivo), nem no boot nem no tick é retomado, e retomar exige gesto humano (GateRescuer.retomar_estacionado/3). - Onde:
apps/engine/lib/engine/workers/anamnese_worker.ex:189(fechar_janela),apps/engine/lib/engine/workers/anamnese_worker.ex:104(dentro_do_teto_diario?),apps/engine/lib/engine/session_events/event.ex:194(project_window_query),apps/engine/lib/engine/session_events/event.ex:202(count_anamnese_rounds_since),apps/engine/lib/engine/anamnese/elegibilidade.ex:67(min_interacoes_proprias),apps/api/src/application/use-cases/anamnese/record-proficiency.use-case.ts:160(fecharJanelaSemPerfil),apps/engine/lib/engine/gates/gate_rescuer.ex:379(task_em_voo?),apps/engine/lib/engine/gates/gate_rescuer.ex:403(aguardando_humano?),apps/engine/lib/engine/gates/gate_rescuer.ex:321(estacionar),apps/engine/lib/engine/gates/gate_rescuer.ex:359(retomar_estacionado),apps/engine/lib/engine/gates/qa_lead_server.ex:95(handle_call) - Teste:
apps/engine/test/engine/workers/anamnese_worker_test.exs(describe "a janela avaliada fecha (RN-722)": "skip_proficiency grava a rodada vazia; a segunda começa em window_to e, sem interação nova, não chama o LLM"; falhas: "falha ao fechar a janela não vira falha da rodada", "rodada que FALHA não fecha a janela"; describe "piso de interações e teto diário (RN-722)"),apps/engine/test/engine/anamnese/elegibilidade_test.exs,apps/engine/test/engine/session_events/event_window_test.exs("dez eventos da Anamnese sozinhos não passam a triagem"),apps/engine/test/engine/gates/gate_rescuer_test.exs("lead do projeto vivo e OCIOSO não segura o resgate de uma task perdida", "ciclo com ação PENDENTE de ator do gate espera o humano", "ação pendente de OUTRO ator não segura o resgate", "ciclo parado há mais de 2 h é ESTACIONADO", "ciclo parado há MENOS de 2 h segue sendo resgatado"),apps/api/test/application/use-cases/anamnese/record-proficiency.use-case.spec.ts(describe "fecharJanelaSemPerfil (RN-722)") - Lacuna declarada: retomar um ciclo estacionado não tinha tela nem rota
— era chamada de operador (
bin/engine rpc); fechou na RN-724 (rota e botão no eventogate.rescue_parked). A pendência humana é lida pela SESSÃO do ciclo (aproposed_actionnão temtask_id) (ADR 0207). - Origem: AT-391 (uso real de 02/10: ~US$ 8,90 de Anamnese num dia sem ninguém pedir; ciclo de gate reiniciado com ação pendente e outro 8 h sem resgate), decisões do dono de 02/10
Revisada pela RN-923 (ADR 0226). O resgate AUTOMÁTICO (tick) de um ciclo vale uma vez: o segundo estaciona com motivo: "retomada_automatica_ja_usada" e espera o "Retomar gate", que zera o contador. O estacionamento de 2 h segue como está.
Uma chave de cache, uma pergunta (RN-718)
RN-723 — A Infra herda a rede que o Arquiteto declarou para o projeto
Decisão do dono em 02/10 (ADR 0208),
revisando a rede literal none da subida no aceite da RN-671. A
eleição da Infra usa o network da artifact.project_image mais recente
emitida pelo Arquiteto (actor_id: "arquiteto", qualquer sessão do projeto),
lido localmente pelo engine; sem decisão dele, ou com valor fora de
none/egress, segue none. Vale para a subida do servidor no aceite e para
a tool propose_container_start sem network. A autonomia semeada não muda:
egress sobe sem clique, e o preço (a rede egress alcança as portas
publicadas da máquina, ADR 0189) está declarado no ADR.
- Código:
apps/engine/lib/engine/session_events/event.ex:139(rede_do_arquiteto),apps/engine/lib/engine/infra/infra_lead_server.ex:803(rede_herdada) - Testes:
apps/engine/test/engine/infra/infra_lead_server_test.exs— "RN-723: a subida herda a redeegressque o Arquiteto declarou" e "RN-723: rede do Arquiteto fora do vocabulário — a subida seguenone" - Origem: AT-392
A Anamnese não paga duas vezes pela mesma janela, e o resgate de gate não reinicia ciclo que espera alguém (RN-722)
RN-724 — O ciclo de gate estacionado volta por um botão no fio, não só pelo terminal do operador
- Regra: o ciclo de gate ESTACIONADO pelo resgate
(RN-722, ADR 0207)
é retomado por
POST /projects/:projectId/tasks/:taskId/gates/:gate/resume, com o MESMO papel mínimo de quem decide a PR (developer). A api só autoriza e pede ao engine (POST /internal/projects/:id/tasks/:taskId/gates/:gate/resume), que delega aGateRescuer.retomar_estacionado/3: ciclo que não existe, não está estacionado ou gate fora deqa/secopsé 409gate_nao_estacionadoe nada é despachado. No fio da sessão, o eventogate.rescue_parkedvira um aviso com o botão "Retomar gate", inerte com o motivo em texto quando o papel não alcançadeveloper; a recusa mostra a frase da api. Nenhum poll novo: o andamento do gate retomado chega pelo log da sessão. - Onde:
apps/engine/lib/engine_web/controllers/gate_command_controller.ex:15(resume),apps/api/src/application/use-cases/gates/resume-parked-gate.use-case.ts:18(execute),apps/api/src/infrastructure/http-clients/api-to-engine-client.ts:379(resumeParkedGate),apps/api/src/interfaces/http/gates/gate-resume.controller.ts:40(resume),apps/web/src/routes/RetomarGateEstacionado.tsx:31(RetomarGateEstacionado) - Teste:
apps/engine/test/engine_web/controllers/gate_command_controller_test.exs("ciclo estacionado: 202 e despacha o gate de novo"; falha: "ciclo NÃO estacionado: 409 nomeado e nada despachado"),apps/api/test/interfaces/http/gates/gate-resume.controller.spec.ts(feliz, 409gate_nao_estacionado, papel abaixo dedeveloper),apps/web/src/routes/RetomarGateEstacionado.test.tsx(o clique chama a rota; a recusa vira toast com a frase da api; papel insuficiente deixa o botão inerte) - Origem: AT-397, fechando a lacuna declarada na RN-722
RN-725 — Texto com escape literal é decodificado, e a recusa de argumento diz o que chegou
Contexto: no TP-01 (03/10, Claude Haiku 4.5) o PO gravou títulos de
história com o escape unicode LITERAL (código): a normalização da
RN-719 só decodificava campo array/object. Recriadas com o título certo,
as histórias passaram pela recusa de duplicata (RN-720/RN-081), que não casou
o escapado com o normal. E as recusas "exige X" não diziam o que tinha
chegado: o PO gastou 28+ passos (48% do custo do teste) até o teto, e
complete_story respondia "ainda faltam RF, DoD, DoR ou business_rule_ids"
sem dizer qual.
A regra:
- No mesmo ponto da RN-719, string com
\uXXXXLITERAL (par surrogate incluído) é decodificada, e com ela\n,\t,\"e\\da MESMA string. String sem\uXXXXvolta byte a byte — código numcontentcom"\n"literal não muda. Isso revisa o "campostringnunca é tocado" da RN-719 só nesse caso. - A duplicata EXATA de história (api) e de regra (engine) compara o título com o escape decodificado, antes da caixa e do NFD de sempre.
- Recusa de ferramenta que diz "exige
X" ganha, no mesmo ponto, as CHAVES que chegaram (nomes, nunca valores), ou que chegou string/lista em vez de objeto.emit_artifactcom chave faltando nopayloaddiz as chaves faltantes e as recebidas, em vez de{:missing_keys, …}. complete_storynomeia exatamente o que falta (falta: business_rule_ids), pela mesma régua demissingForReadyda api, lida da história devolvida.
- Onde:
apps/engine/lib/engine/harness/argumentos_de_ferramenta.ex:102(decodificar_escapes),:152(chaves_recebidas),:166(com_chaves_recebidas);apps/engine/lib/engine/harness/tools/complete_story.ex:80(faltam);apps/api/src/domain/backlog/story-overlap.ts:39(decodificarEscapesLiterais),:25(normalizarTitulo);apps/engine/lib/engine/harness/artifact_dedupe.ex(normalizar) - Teste:
apps/engine/test/engine/harness/argumentos_de_ferramenta_test.exs(describe "RN-725": escape decodificado, texto sem\ue lista intactos; recusa com as chaves e sem os valores;complete_storynomeia o campo);apps/api/test/domain/backlog/story-overlap.spec.ts(título escapado casa com o normal; título diferente segue diferente) - Origem: AT-405, AT-408
RN-726 — Crédito do provedor esgotado pausa o dev agent, com origem infra, sem queimar as tarefas seguintes
- Regra: quando o provedor do modelo recusa por falta de CRÉDITO (HTTP 402,
ou o texto "available credits"/"add credits" do OpenRouter), o turno do dev
agent não é "parou sem concluir": a tarefa é bloqueada com o motivo
"crédito do provider esgotado" e origem
infra(nuncacodigo), o contador da parada automática (RN-047) NÃO avança, e o agente NÃO reivindica a próxima tarefa — fica emidle_trippede emitedev.credit_exhausted(motivo: credito_esgotado), nuncadev.idle_tripped. A retomada é o Rearmar que já existia no painel do time, depois de recarregar o crédito; a tarefa bloqueada volta pelo desbloqueio de sempre. A tela de Executores mostra o agentetravadocom "crédito do provedor do modelo esgotado — recarregue e rearme para retomar", e o sino diz o mesmo. O mesmo reconhecimento fazFalhaDeTurno.origem/1devolverinfrapara o frame final de crédito (vale para os agentes de gate, como o QA). A sessão contadev.credit_exhaustedcomo trabalho pendente, comodev.idle_tripped. Desde a RN-730 o 402 chega comcodepróprio (insufficient_credit) e é ele que decide; o reconhecimento pelo texto ficou só como rede. - Onde:
apps/engine/lib/engine/agents/falha_de_turno.ex:142(credito_esgotado?),apps/engine/lib/engine/dev/dev_agent_server.ex:903(handle_outcome),apps/engine/lib/engine/dev/agent_io.ex:271(pausar_por_credito),apps/web/src/lib/agent-status.ts:198(breakerReasonFor),apps/api/src/application/use-cases/sessions/get-session-pending-work.use-case.ts - Teste:
apps/engine/test/engine/dev/dev_agent_server_test.exs("402 do provider: origem infra, pausa sem idle_tripped e sem queimar a próxima task"; falha: "erro que não é de crédito segue como antes: parou sem concluir, conta no disjuntor"),apps/engine/test/engine/agents/falha_de_turno_test.exs("402 do provider é infra e é reconhecido"),apps/web/src/lib/agent-status.test.ts("RN-726: dev.credit_exhausted é travado com motivo de crédito…") - Origem: AT-407 (TP-01 de 03/10: 402 do OpenRouter virou três tarefas
bloqueadas com origem
codigoe a parada automática)
RN-731 — O fecho do turno diz o que foi gravado com o número do servidor
- Regra: o número do que um agente conversacional gravou num turno é FATO
do servidor, nunca texto do modelo (RN-163). O Criativo, o PO e o Arquiteto
contam, no processo do turno, cada ferramenta de escrita que voltou
ok(emit_artifactpor tipo — regra de negócio, decisão, nota, outro —,create_epic,create_story,update_story,archive_story,create_task), e o fecho daagent.responseganha, DEPOIS do texto do modelo e sem reescrevê-lo, a linha "Gravado neste turno: N …". Ferramenta recusada e leitura não contam; turno sem escrita não ganha linha; a contagem morre com o turno. A linha sai no idioma do turno (RN-622): o do autor, senão o do projeto;pt*em português ("Gravado neste turno: 1 regra de negócio"), qualquer outro idioma — ou nenhum resolvido — em inglês ("Recorded this turn: 2 business rules"), com singular e plural nos dois. - Onde:
apps/engine/lib/engine/agents/gravado_no_turno.ex:62(anotar),:95(descarregar),apps/engine/lib/engine/harness/idioma_da_resposta.ex:272(idioma_do_turno),apps/engine/lib/engine/agents/texto_do_turno.ex:65(payload_do_turno) - Teste:
apps/engine/test/engine/agents/gravado_no_turno_test.exs(feliz: seis regras e uma decisão gravadas, uma recusada — o fecho "8 regras" do modelo fica e ganha "6 regras de negócio, 1 decisão" ao lado; sem escrita: nada acrescentado; pt-BR e en, singular e plural; idioma fora dos dois cai em en),apps/engine/test/engine/agents/criativo_server_test.exs - Origem: AT-414 (TP-01 de 03/10: o Criativo fechou com "Registrei as 8 regras de negócio" e o painel tinha 6)
RN-732 — O texto do turno passa pela mesma régua de escape dos argumentos
- Regra: o texto que o modelo escreve no turno de um agente conversacional
— o
content(fecho) e cadapassosdaagent.response— passa pela MESMAdecodificar_escapes/1da RN-725 antes de ser gravado:\u00e3literal viraã. Uma régua só, sem cópia, e com o mesmo limite: só age quando há\uXXXXliteral, então texto sem escape — inclusive código com"\n"literal num bloco cercado — volta byte a byte. Oagent.deltado streaming NÃO é decodificado: um escape pode chegar partido entre dois deltas, e a bolha gravada substitui o texto em curso no fim do turno. Regra própria, e não cláusula da RN-725, porque a superfície é outra (resposta do modelo, não argumento de ferramenta) e a decisão sobre o delta é desta. - Onde:
apps/engine/lib/engine/agents/texto_do_turno.ex:65(payload_do_turno),apps/engine/lib/engine/harness/argumentos_de_ferramenta.ex:102(decodificar_escapes) - Teste:
apps/engine/test/engine/agents/texto_do_turno_test.exs(feliz: os cinco escapes medidos, no fecho e num passo; intacto: texto sem escape e bloco de código com\nliteral) - Origem: AT-417 (TP-01 de 03/10, 2ª rodada: a resposta final do Criativo gravada com "colis\u00e3o", "usu\u00e1rio" e mais três escapes na bolha)
RN-733 — A falha de crédito diz o que fazer, e o reason nunca é a tupla
- Regra: o
reasongravado noagent.errordos conversacionais, do Infra Lead e doToolLoop(dev agent) é o TEXTO do frame final quando há um — nunca a tupla{:final, …}inspecionada; outras formas seguem cominspect/1. Com crédito do provider esgotado (errorCode: insufficient_credit, RN-730, ou o reconhecimento por texto da RN-726), amensagemda bolha é curta e acionável no idioma do turno (RN-622): pt "Crédito do provedor do modelo esgotado — recarregue a chave e tente de novo.", qualquer outro idioma "The model provider's credit is exhausted — top up the key and try again."; sem idioma resolvido, pt-BR. O JSON do provider fica só noreason(diagnóstico). Os demais erros mantêm a frase de sempre. - Onde:
apps/engine/lib/engine/agents/falha_de_turno.ex:166(mensagem),:189(diagnostico) - Teste:
apps/engine/test/engine/agents/dev_lead_server_test.exs(402 em pt-BR e en: frase curta,reasonsem{:final; erroupstream: frase de sempre),apps/engine/test/engine/agents/falha_de_turno_test.exs - Origem: AT-421 (TP-01 de 03/10, 2ª rodada: o Dev Lead gravou
reason: "{:final, …"e a bolha despejou o JSON do 402)
RN-735 — A rede none da imagem do Arquiteto diz que não instala dependências
- Regra: a descrição de
choose_project_imagee o RESULTADO dela dizem que, com redenone, nenhum gerenciador de pacotes (npm, pnpm, pip, mix, cargo, go mod...) instala dependência no container, e que a Infra herda essa rede (RN-723). Comnone, o resultado pede ao modelo que chame a ferramenta de novo comegressse a stack instala dependências. É AVISO: a api não adivinha a stack, e a contenção do broker (ADR 0130) não muda. (Desde a RN-906 a api RECUSAnonequando a stack declarada de um módulo baixa dependências; fora desse caso, segue só o aviso.) - Código:
apps/engine/lib/engine/harness/tools/choose_project_image.ex:96(mensagem_de_fixada),apps/engine/lib/engine/harness/tools/choose_project_image.ex:23(spec) - Teste:
apps/engine/test/engine/harness/tools/choose_project_image_test.exs - Origem: AT-420 (TP-01 de 03/10:
node:22-bookworm-slimcom redenone, e o dev agent não conseguiunpm install)
RN-742 — O dev agent não morre calado, e a resposta HTTP tardia não fica na caixa dele
- Regra: toda chamada HTTP não-streamada do engine à api
(
post_returning/3e os GETs deEngineApiClient.Live) roda numaTaskdescartável; a resposta que chega depois doreceive_timeoutcai num processo que já acabou, nunca na caixa de mensagens de quem chamou, e exceção dentro do pedido volta como{:error, _}. E o dev agent que CAI (saída que não é:normalnem:shutdown) não é tratado como terminado: oEngine.Dev.Monitorgravaagent.errorDURÁVEL com origeminfra(reason: processo_do_dev_caiu,religado,taskId) e o RELIGA a partir da própria linha dedev_agent_states, até 3 vezes por agente; oresumepassa pelohandle_continue({:restart_recovery, _})que já existia, que bloqueia a task interrompida com diagnóstico (origeminfra) e tenta o próximo claim. Sem linha, ou no teto, a linha sai e o erro diz que não houve religamento. Ollm_turn_stream/6fica fora do isolamento (ointo:grava no dicionário do chamador); os conversacionais já o chamam de dentro da Task do turno (TurnoAssincrono), então o padrão "HTTP dentro do GenServer" não os alcança — declarado. A web não mudou: a aba Executores passa a ver oagent.errore odev.blockedda recuperação. - Onde:
apps/engine/lib/engine/sessions/engine_api_client.ex:2222(isolado),apps/engine/lib/engine/dev/monitor.ex:69(ao_terminar),:107(registrar_e_religar) - Teste:
apps/engine/test/engine/sessions/engine_api_client_isolado_test.exs(mensagem tardia não chega ao chamador; exceção vira{:error, _}),apps/engine/test/engine/dev/monitor_test.exs(crash gravaagent.errorinfra e religa;:normalapaga a linha; sem linha não religa) - Origem: AT-428 (08/10:
CaseClauseErroremFinch.HTTP1.Conn.receive_response/8depois do timeout donpm install)
Revisada pela RN-922 (ADR 0226). O dev que cai com working recente e sem retomada anterior na tarefa deixa de ser bloqueado: preserva o worktree como commit e retoma a mesma tarefa uma vez. Fora disso o bloqueio descrito acima segue.
RN-743 — Bloquear a task preserva o trabalho do worktree, e a próxima parte dele
- Regra: antes de bloquear uma task (qualquer origem, inclusive o teto de
orçamento
taskBudgetMicros, cujo VALOR não muda aqui), o que o dev agent deixou no worktree vira commit na branch da própria task, com a identidade<agente>[bot](git add -A+commit --no-verify). Odev.blockedlevatrabalhoPreservado({branch, commit}ounull) e o diagnóstico DIZ onde o trabalho ficou — ou que não foi possível preservá-lo, com o motivo; a falha ao preservar nunca impede o bloqueio. A próxima task do MESMO agente (logo, do mesmo módulo) nasce dessa branch (create_from/4) em vez dedev, uma vez só. Worktree limpo não gera commit. Declarado: o ponteiro mora só em memória (restart do engine perde o ponteiro, não o commit), e no modorunnero worktree está na máquina do usuário e o engine não preserva nada. - Onde:
apps/engine/lib/engine/dev/worktree_manager.ex:73(preservar_em),:56(create_from),apps/engine/lib/engine/dev/agent_io.ex:533(preservar_trabalho),apps/engine/lib/engine/dev/agent_io.ex:321(base_preservada) - Teste:
apps/engine/test/engine/dev/worktree_manager_test.exs(preservar_em/3 e create_from/4: commit comdev-api[bot]e a próxima task enxerga o arquivo; limpo é:nada; pasta sem repositório é erro),apps/engine/test/engine/dev/bloqueio_preserva_trabalho_test.exs(odev.blockeddiz a branch; falha ao preservar não impede o bloqueio) - Origem: AT-429 (08/10:
feature/task-86dade03sem commit,package.jsonesrcperdidos depois do bloqueio por orçamento)
RN-744 — O dev sabe que o shell é sh e começa vendo o próprio worktree
- Regra: a descrição da ferramenta
terminaldiz que o shell do container ésh(dash), não bash — sem brace expansion, sem[[ ]], sem arrays, semsource— e dá a forma que funciona. O kickoff de cada task do dev agent ganha uma segunda mensagem fixada com o RETRATO do worktree: a branch e os arquivos que já existem (rastreados + não rastreados não ignorados), com teto de 80 linhas e o total real quando corta (ADR 0060); é o servidor que monta, sem parâmetro do modelo. Pasta que o engine não alcança (modorunner) diz que o retrato está indisponível e manda listar peloterminal. Declarado e NÃO corrigido: o worktree do dev pode nascer antes do merge da PR de infra, então o que a Infra entregou por PR pode não estar nadevde onde ele parte. - Onde:
apps/engine/lib/engine/harness/tools/terminal.ex:12(spec),apps/engine/lib/engine/dev/worktree_manager.ex:113(retrato),apps/engine/lib/engine/dev/dev_agent_server.ex:642(retrato_do_worktree) - Teste:
apps/engine/test/engine/dev/worktree_manager_test.exs(retrato/2: branch e arquivos; pasta inalcançável diz indisponível; a descrição do terminal dizsh) - Origem: AT-444 (TP-01 de 08/10:
mkdir -p src/{a,b}no dash e o dev recriando o que já existia)
RN-745 — Na resposta cortada, só a chamada incompleta é recusada
- Regra: quando a resposta do modelo vem cortada pelo teto de saída
(
truncated: true, RN-737), só a ÚLTIMA chamada de ferramenta da resposta é tratada como incompleta e recusada com o erro nomeado (que agora diz "a última da resposta", que as anteriores foram executadas e pede "uma por chamada"). As chamadas anteriores da mesma resposta executam normalmente — medido no código: o stream OpenAI-compatível monta as chamadas em ordem, o corte cai na última, e oparseArgumentosda api devolve{}para o JSON partido. A chamada é identificada por nome e argumentos. Resposta cortada sem chamada conhecida continua recusando toda ferramenta (régua da RN-737). A descrição depropose_adrorienta um ADR por resposta. - Onde:
apps/engine/lib/engine/harness/resposta_cortada.ex:44(registrar),:64(incompleta?),apps/engine/lib/engine/harness/argumentos_de_ferramenta.ex:26(executar),apps/engine/lib/engine/harness/tools/propose_adr.ex:27(spec) - Teste:
apps/engine/test/engine/harness/resposta_cortada_test.exs("só a última chamada da resposta cortada é recusada; as completas executam"; "ferramenta de resposta cortada NÃO executa", sem chamada conhecida) - Origem: AT-430 (08/10: o Arquiteto mandou 5
propose_adrnuma resposta cortada e as cinco foram recusadas; 0 ADRs)
RN-746 — O fecho do Arquiteto diz o que gravou, e as escritas recusadas são contadas
- Regra: a linha factual "Gravado neste turno: …" da
RN-731 passa a contar as escritas do Arquiteto (ADR proposto,
mapa de módulos, atribuição de módulos, imagem decidida, diagrama C4,
roteamento para a infra, contrato de módulos, insight), a RFC do Staff e o
protótipo do UX Designer, e conta à parte as chamadas de ESCRITA recusadas
(
{:error, _}) do turno: "…; N chamadas de escrita recusadas". Turno que tentou escrever e não gravou nada fecha com "Gravado neste turno: nada; N chamadas de escrita recusadas." (en: "Recorded this turn: nothing; N write calls refused."). O Criativo passa a anotar a recusa também; Staff e UX Designer passam a anotar. Leitura nunca conta. O texto do modelo não é reescrito (RN-163: o agente não anuncia como feito o que o servidor recusou — quem lê vê o fato ao lado). Fora, declarado: Dev Lead (o plano suspende o turno, RN-284) e Infra Lead. - Onde:
apps/engine/lib/engine/agents/gravado_no_turno.ex:77(anotar),:96(descarregar),:141(classe),apps/engine/lib/engine/agents/arquiteto_server.ex:330(dispatch_tool) - Teste:
apps/engine/test/engine/agents/gravado_no_turno_test.exs("Arquiteto com as cinco propose_adr recusadas fecha com nada gravado"; "escritas do Arquiteto contadas, leitura e ferramenta desconhecida não") - Origem: AT-431 (08/10: com os 5
propose_adrok: false, o Arquiteto fechou com "✅ ADRs Propostas (5 decisões críticas)" e nenhuma linha factual)
RN-756 — A Anamnese só roda com interação de conteúdo técnico do sujeito; aprovar não conta
- Regra: revisa a contagem de "interação própria" da RN-680/RN-722. Só
contam para fazer alguém sujeito da rodada (a) evento
actor_kind: "user"do próprio membro cujo tipo está emtipos_de_evento_com_conteudo/0—chat.message(mensagem escrita por ele) echat.structured_question_answered— e (b) decisão de ação com motivo de recusa escrito (rejectionReasonnão vazio, uma correção). Aprovação, aceite de handoff, clique e qualquer outro evento de usuário NÃO contam: não são evidência de competência. O piso de 5 (RN-722), o teto diário e o desfecho sem sujeito (motivo no log;anamnese.run_skippedsó na rodada pedida à mão) não mudam, e a triagem segue LOCAL, antes do LLM e do RAG. - Código:
apps/engine/lib/engine/anamnese/elegibilidade.ex:61(avaliar),:102(recusa_com_motivo?),:108(tipos_de_evento_com_conteudo) - Teste:
apps/engine/test/engine/anamnese/elegibilidade_test.exs(só interação com conteúdo técnico conta (RN-756, AT-439): mensagem e resposta estruturada fazem sujeito; só aprovações, handoff e recusa sem motivo não fazem) - Origem: AT-439 (TP-01, 08/10, projeto
c1dc053d: duas rodadas durante a execução, US$ 0,20, ambasskip_proficiencycom "o usuário apenas aprovou propostas")
RN-757 — O terminal auto-aprovado espera o comando, e o teto do comando é 2 min
- Regra: a ação
terminalque a api auto-aprova é EXECUTADA dentro da mesma requisição dopropose_action(api → engine/internal/actions/execute→ apicontainer-exec→ broker), e o engine espera essa requisição porTERMINAL_ACTION_TIMEOUT_MS+ 120 s — não pelo default de 15 s doReq. O teto do comando (TERMINAL_ACTION_TIMEOUT_MS) passa de 15 s para 120 s nos três composes e noruntime.exs. A cadeia fica broker (T) < api → broker (T + até 45 s, RN-604) < engine →container-exec(T + 90 s) < engine →propose_action(T + 120 s), e com T = 120 s o salto de fora (240 s) segue abaixo dos 300 s de cabeçalhos dofetchdo Node. Se a espera ainda estourar, o modelo recebe texto NOMEADO — o teto em segundos, que o processo pode seguir rodando e a sugestão denohup … &com log —, nunca%Req.TransportError{reason: :timeout}cru. Exec assíncrono/streaming foi considerado e não feito: a menor mudança que faznpm installcomum caber é o teto. - Onde:
apps/engine/lib/engine/sessions/engine_api_client.ex:1985(opcoes_do_propose_action) eapps/engine/lib/engine/harness/hooks/action_pipeline.ex:85(resultado_de_teto_do_terminal) - Teste:
apps/engine/test/engine/sessions/engine_api_client_propose_terminal_test.exs(api falsa que demora 16 s ainda é esperada; ordem dos tetos abaixo de 300 s; o texto de estouro nomeia o teto) - Origem: AT-440 (TP-01 de 08/10:
npm install 2>&1 | tail -50num containernode:22-bookworm-slimvoltou "falha no pipeline: %Req.TransportError{reason: :timeout}"; medido nesta máquina, o mesmonpm installdebcrypt+sqlite3+expresslevou 5,3 s com a imagem em cache e prebuilds disponíveis — a cadeia, não o comando, cortava em 15 s)
RN-760 — A próxima task do dev agent parte da última branch dele ainda não mergeada
- Regra: generaliza a RN-743. Ao terminar uma task — aprovada
nos gates (
nextAction: "done") ou bloqueada por QUALQUER motivo, inclusive o "ciclo de correção esgotado (gate)" que a api decide e chega ao engine só comogate_resolved—, o dev agent guarda a branch dela e a próxima task do MESMO agente (logo, do mesmo módulo) nasce dessa branch, e não dedev. O que sobrou sem commit no worktree é preservado antes, como na RN-743; o ponteiro gravado peloblock_task/4não é sobrescrito. Quando a branch anterior já é ancestral dadevlocal (mergeada), a próxima parte dadev, que já a contém. QUANDO a fila anda não muda: a próxima task continua sendo reivindicada ao fim dos gates, antes do merge do usuário (decisão do dono). Declarado: merge por squash num remoto não deixa a branch ancestral dadev, então a próxima segue partindo dela; o ponteiro continua só em memória, e no modorunnera checagem de merge não roda. - Onde:
apps/engine/lib/engine/dev/agent_io.ex:343(levar_branch_para_a_proxima),:324(criar_worktree),apps/engine/lib/engine/dev/worktree_manager.ex:542(ja_mergeada_volta_ao_trabalho) - Teste:
apps/engine/test/engine/dev/bloqueio_preserva_trabalho_test.exs(levar_branch_para_a_proxima/1 e criar_worktree/2: aprovada leva a branch para a próxima; o ponteiro do bloqueio não é sobrescrito),apps/engine/test/engine/dev/worktree_manager_test.exs(add_worktree/4 com a branch da task anterior: não mergeada parte dela, mergeada parte dadev) - Origem: AT-447 (TP-01 de 09/10: tasks
762833bde5ebad64b— a segunda nasceu dedevsem a rota de login da primeira, e o QA reprovou)
RN-763 — Encerrar a sessão de execução para os dev agents e os gates, e "Parar execução" na aba Executores
- Regra: o fechamento da sessão (
session.closed/closed_abnormally, peloSessionLifecycleWorker, o ponto por onde todo fechamento passa) PARA os dev agents dela em todos os nós e os gates (QA lead e SecOps) do projeto — a régua que já valia para os conversacionais (RN-581). O processo é derrubado na hora, sem gravar o turno em curso, e oEngine.Dev.Monitoré avisado antes (esquecer/1): a linha durável sai e o agente NÃO é religado (RN-742). Para cada dev com task em curso (working,awaiting_approvalouawaiting_gate), o trabalho do worktree vira commit na branch da task (RN-743) e a task é BLOQUEADA com motivo "sessão de execução encerrada", origempolitica, e o diagnóstico dizendo onde ficou o trabalho — volta à fila quando um humano a libera, nunca perdida. As linhas degate_statesda sessão saem, para oGateRescuernão reabrir ciclo da sessão encerrada. A aba Executores ganha "Parar execução" (papeldeveloper, o de encerrar sessão), cujo mecanismo é o MESMO fechamento (closing→closed), sem estado novo; a confirmação diz antes do clique o que fica: as tarefas em curso (com a contagem), as PRs abertas, as aprovações pendentes e que a sessão não reabre. Declarado: ação pendente de dev agent continua pendente; PR aberta continua aberta; sessão comexecution.activatednão reabre (RN-650), então retomar é ativar a execução numa sessão nova; a contagem de tarefas da confirmação vem da janela de eventos da aba. A confirmação no "Encerrar" da barra da sessão é de outra atividade (AT-452). - Onde:
apps/engine/lib/engine/dev/encerramento_da_sessao.ex:40(parar_da_sessao),:74(parar_processos),:104(devolver_task),apps/engine/lib/engine/dev/monitor.ex:34(esquecer),apps/engine/lib/engine/workers/session_lifecycle_worker.ex:94(parar_execucao),apps/web/src/components/PararExecucao.tsx - Teste:
apps/engine/test/engine/dev/encerramento_da_sessao_test.exs(o dev morre sem ser religado, a linha sai, a task é bloqueada com origempolitica; sessão sem dev é no-op e o dev de outra sessão fica de pé),apps/web/src/components/PararExecucao.test.tsx(a confirmação diz o que fica e fechaclosing→closed; a recusa da api fica na confirmação; sem papel o botão fica inerte com o motivo) - Origem: AT-456 (TP-01 de 09/10: com a sessão de execução encerrada, o
dev-encurtador-apibloqueou a tarefa 3, pegou a 4 e abriu PR; o único freio foi parar o container)
RN-765 — O gate de QA julga a TAREFA, e RF de tarefa irmã é observação
-
Regra: a QA-estratégia (plano de teste, RN-674) e a QA-automação recebem, além da história, o RECORTE da tarefa: título e descrição, as regras de negócio da história e as OUTRAS tarefas da mesma história com o status (
siblingTasksemGET .../dev-context). O veredito é sobre o que é DESTA tarefa: requisito que pertence a uma tarefa irmã (feita ou pendente) não reprova — vai aoresumocomo observação, fora deitens, e não impedeapproved. A régua é de PROMPT, não de código: oemit_qa_verdictcontinua exigindo só a suite verde para aprovar. Desde a AT-479, quando a descrição da tarefa ou uma regra quantifica POR item ("por X", "cada", "de todos os" — "por exemplo"/"por padrão" não contam), o recorte ganha a seção "Forma da resposta": conferir que a resposta traz o valor de CADA item, não só o total (o painel aprovado com só{totalCliques}sobre "a contagem… de todos os links"), e que a recusa de entrada nomeia o campo — esta última é OBSERVAÇÃO noresumo, fora deitens, salvo regra que a exija. Continua régua de prompt: nenhuma reprovação nova no código. -
Onde:
apps/engine/lib/engine/gates/recorte_da_tarefa.ex:18(texto),apps/engine/lib/engine/gates/recorte_da_tarefa.ex:82(secao_da_forma),apps/api/src/application/use-cases/execution/get-dev-task-context.use-case.ts:195(tarefasIrmas) eapps/engine/lib/engine/gates/qa_automacao_agent.ex:170apps/engine/lib/engine/gates/recorte_da_tarefa.ex:82(secao_da_forma),apps/api/src/application/use-cases/execution/get-dev-task-context.use-case.ts:195(tarefasIrmas) eapps/engine/lib/engine/gates/qa_automacao_agent.ex:225(com_o_recorte) -
Teste:
apps/engine/test/engine/gates/recorte_da_tarefa_test.exs(desde a AT-479 também "regra que quantifica por item pede a conferência da forma da resposta" e o caso sem quantificador) eapps/api/test/application/use-cases/execution/get-dev-task-context.use-case.spec.ts(as irmãs sem a própria tarefa). O golden-set do QA (ADR 0168) NÃO ganhou caso: ele roda o modelo de verdade, e provar o veredito sem chamada paga não é possível — declarado. -
Origem: AT-448 (TP-01 de 09/10: o gate de QA da tarefa "Modelar tabela de usuários e hash bcrypt" reprovou três vezes por "RF01: login HTTP com JWT não existe nesta entrega", que é de outra tarefa, e a tarefa bloqueou;
artifact.qa_verdictdas tasks42bea901e762833bd)
RN-772 — O Arquiteto lê o status ATUAL das ADRs que propôs
- Regra: o Arquiteto tem a leitura
listar_adrs_propostas: as açõesopen_adr_prdo PROJETO, lidas agora deproposed_actions(mesmo Postgres, sem HTTP no laço), com o status de cada uma — pendente, aprovada, aprovada automaticamente, executada (com a URL da PR), recusada (com o motivo) ou falhada. Contida (ADR 0060, RN-164): sem parâmetro, escopo fechado no projeto do contexto, uma contagem e uma consulta por chamada, as 20 mais recentes, e o total real dito quando corta. Falha de leitura étool.resultok: falsecom o motivo, nunca texto vazio. Desde a RN-784 ela ESTÁ no piso do Jev. - Onde:
apps/engine/lib/engine/harness/tools/listar_adrs_propostas.ex:37(run),:40(ler),apps/engine/lib/engine/agents/arquiteto_server.ex:619(ListarAdrsPropostas) - Teste:
apps/engine/test/engine/harness/tools/listar_adrs_propostas_test.exs(aprovada aparece aprovada, executada com a PR, recusada com o motivo, só as do projeto; teto com o total real; sem proposta; leitura que falha vira erro nomeado) - Origem: AT-454 (TP-01 de 09/10, sessão
771131f4: com as 3 ADRs aprovadas, o fecho seguinte disse "As 3 ADRs seguem pendentes de aprovação")
RN-773 — O Arquiteto lê o backlog, e consulta antes de afirmar estado
- Regra: o Arquiteto ganha a mesma
listar_backlogdo PO (RN-164), com a contagem de tarefas por história. O kickoff dele e o resultado dopropose_adrdizem que o status das ADRs e as tarefas mudam fora da conversa e mandam consultar as duas leituras antes de afirmá-los, em vez do retrato de um resultado anterior. Nada de fluxo do time é narrado aqui (RN-711). - Onde:
apps/engine/lib/engine/agents/arquiteto_server.ex:620(ListarBacklog),apps/engine/lib/engine/harness/tools/propose_adr.ex:94(propor) - Teste:
apps/engine/test/engine/harness/tools/listar_backlog_test.exs(a contagem de tarefas por história),apps/engine/test/engine/agents/arquiteto_server_test.exs(as duas leituras estão no catálogo do Arquiteto) - Origem: AT-454 (TP-01 de 09/10: com 17 tarefas no backlog, o Arquiteto disse "faltam as tarefas" e "não consigo verificá-las daqui")
RN-774 — A primeira tarefa de cada módulo tem o dobro do teto por tarefa
A primeira tarefa de um módulo monta o esqueleto inteiro (manifesto, app,
banco, testes) e custa mais que as seguintes: no TP-01 de 08/10 ela gastou
US$ 0,53 com o Haiku 4.5 e foi bloqueada pelo teto fixo de US$ 0,50
(taskBudgetMicros: 500000). Por decisão do dono (09/10), ela ganha
2× o teto por tarefa (US$ 1,00 com o padrão); as demais seguem o teto
configurado. O multiplicador é constante nomeada no engine, não configuração.
"Primeira" é medida pelo log, nunca pelo estado da tarefa: é a tarefa cujo
backlog.task_claimed é o MAIS ANTIGO do projeto para aquele módulo. Bloquear
zera assigned_to e reiniciar o engine perde a memória do processo; o log não
muda, então a mesma tarefa reivindicada de novo depois de liberada continua a
primeira, e uma correção pedida por gate também recebe o 2×. A api responde
task.primeiraDoModulo no contexto do dev (false sem module, e false sem
claim nenhum do módulo — o teto normal é o conservador), e o teto EFETIVO é
resolvido num ponto só do engine. O bloqueio por orçamento diz o teto que valeu
para aquela tarefa ("teto da primeira tarefa do módulo: 1000000 micro-USD,
US$ 1.00"). Os gates de QA seguem com o task_budget_micros base — fora do
recorte do card.
- Onde:
apps/engine/lib/engine/dev/teto_da_tarefa.ex:32(efetivo);apps/engine/lib/engine/dev/dev_agent_server.ex:718(diagnostico_de_orcamento);apps/api/src/application/use-cases/execution/get-dev-task-context.use-case.ts:182(ehPrimeiraTarefaDoModulo) - Teste:
apps/engine/test/engine/dev/dev_agent_server_test.exs(describe "RN-774": a primeira com US$ 0,53 não bloqueia, a segunda do mesmo módulo bloqueia — caso de falha —, a primeira de outro módulo também ganha 2×);apps/engine/test/engine/dev/teto_da_tarefa_test.exs;apps/api/test/application/use-cases/execution/get-dev-task-context.use-case.spec.ts(describe "RN-774") - Origem: AT-433
RN-778 — O dev agent órfão fecha por evento novo no boot
- Regra: no boot, para cada sessão não terminal, o dev agent cujo último
dev.*na sessão é de espera (dev.working,dev.blocked,dev.idle_trippede os demais que oGetSessionPendingWorkUseCaseconta) e que NÃO tem linha emdev_agent_statesNESTA sessão ganhadev.errorcom origeminfra(reason: dev_agent_sem_processo) seguido dedev.idle, que o tira da régua do trabalho pendente (RN-064). É o molde da RN-586 para o vocabuláriodev.*: nunca reexecuta, nunca reescreve evento, e a linha durável é a prova de dono — com ela oDevRehydratorreliga o agente, e fechá-lo seria matar um agente saudável. Não toca a tarefa: tarefa bloqueada ou com PR esperando o merge continua segurando a sessão de execução vigente pela RN-776 (AT-457), que tem dono humano. - Onde:
apps/engine/lib/engine/dev/dev_orfao.ex:49(varrer),apps/engine/lib/engine/sessions/rehydrator.ex:81(varrer) - Teste:
apps/engine/test/engine/dev/dev_orfao_test.exs(o órfão fecha comdev.errorinfra +dev.idle; o que tem linha durável e o que já estáidlenão; leitura que falha não fecha nada) - Origem: AT-465 (TP-01 de 09/10: a sessão
63a4e1aareagendada pelo heartbeat a cada 30 s por umdev.workingsem processo)
Citada pela RN-922 (ADR 0226), sem mudar: o dev sem linha durável em dev_agent_states continua só fechado, porque não há tarefa a retomar.
RN-779 — O worktree da task (re)pegada parte da dev ATUAL e a integra
- Regra: antes de criar o worktree de uma task (primeira vez ou retomada),
o engine faz
git fetch originno working tree do projeto (serializado por projeto) — é no remoto que as PRs são mergeadas, e adevlocal só andava no checkout da inicialização. A ponta dadevéorigin/devquando a local ficou para trás. A task nasce: (1) da branch da task anterior não mergeada (RN-760), (2) da PRÓPRIA branch quando ela tem trabalho que adevnão contém — a task retomada num processo novo, que perdeu o ponteiro em memória (RN-743) —, ou (3) da ponta atual dadev. Nos casos (1) e (2) o worktree INTEGRA a ponta dadev(merge --no-ff, identidade<agente>[bot]) antes do primeiro passo. Conflito nessa integração vai para o agente (RN-1010); só o que o agente não resolve é recusa NOMEADA ("conflito ao integrar adevatual", com os arquivos): o merge é abortado, a branch de origem fica intacta e a task bloqueia com motivo próprio e origemcodigo.fetchque falha bloqueia a task com o motivo do git, nunca cai nadevvelha. A readoção depois de conflito de merge (RN-715) não integra sozinha. Medido no TP-01: a AT-447 DISPAROU no 1º claim (reflog:Created from feature/task-b3cc92b8); quem perdeu a base foi o reclaim, que redefiniu a branch para adevLOCAL parada no merge da infra (Reset to dev). Declarado: no modorunnernada disso roda (o git é da máquina do usuário). - Onde:
apps/engine/lib/engine/dev/worktree_manager.ex:156(atualizar_remoto),:311(add_worktree),:388(ponta_do_trabalho),:451(retomada),:461(integrar_trabalho) - Teste:
apps/engine/test/engine/dev/worktree_manager_test.exs(integração da dev atual (RN-779): retomada com branch antiga parte da dev nova do remoto; trabalho preservado integra a dev nova; anterior emawaiting_useré a base com a dev integrada; conflito é recusa nomeada) - Origem: AT-458 (TP-01 de 09/10:
feature/task-c80daf6cnasceu de1fa2241sem as 12 tarefas mergeadas depois, e o merge conflitou)
RN-780 — Resposta só de raciocínio cortada pelo teto é corte, não desistência
- Regra: no
ToolLoop(caminho de todo agente de execução e de gate), a resposta sem texto e sem chamada de ferramenta que a api marcatruncated: true(ofinish_reason: lengthda RN-737) é o raciocínio que comeu o teto de saída. A volta é retentada UMA vez, sem a resposta vazia no histórico e contando iteração — o teto de iterações e o orçamento da tarefa continuam valendo. Se repetir, o laço para marcandocorte_pelo_teto, e o dev agent bloqueia a task com o motivo "resposta só de raciocínio, cortada pelo teto" e origemmodelo, em vez de "parou sem concluir nem reportar bloqueio". Uma chamada de ferramenta no meio zera a retentativa. Declarado: o engine recebe da apitruncatedeusage.outputTokens, mas NÃO osreasoningTokens(ficam só emtoken_usage), então a resposta vazia que bate no teto semfinish_reason: lengthnão é reconhecida. - Onde:
apps/engine/lib/engine/harness/tool_loop.ex:256(parada_sem_chamada),apps/engine/lib/engine/dev/dev_agent_server.ex:1190(motivo_da_parada),:1209(stop_diagnosis) - Teste:
apps/engine/test/engine/harness/tool_loop_test.exs(corte só de raciocínio: retentada uma vez e segue; repetido para comcorte_pelo_teto),apps/engine/test/engine/dev/dev_agent_server_test.exs(resposta só de raciocínio cortada pelo teto bloqueia com motivo nomeado) - Origem: AT-459 (TP-01 de 09/10: a tarefa
c80daf6cbloqueou em 40 s com 8.192 tokens de saída, todos de raciocínio)
RN-781 — A mensagem de commit do dev não grava pendência que é do sistema
- Regra: o resumo do
report_doneé a mensagem de commit do dev agent, e commit, push e PR são feitos pelo sistema depois dele. Antes de propor ogit_commit, o engine corta do marcador de pendência ("Pendente", "Pendência", "Pending") ao fim quando o resto fala de commit, push ou PR, e tira o parêntese "(ainda sem commit…)"; pendência que não fala disso fica. A descrição do camposummarydiz que ele vira a mensagem de commit e que não se narra commit, push nem PR. Resumo que fica vazio cai na mensagem padrão<agente>: <task>. - Onde:
apps/engine/lib/engine/dev/agent_io.ex:398(mensagem_de_commit),:373(propose_commit),apps/engine/lib/engine/dev/tools/report_done.ex:27(summary) - Teste:
apps/engine/test/engine/dev/mensagem_de_commit_test.exs(corta a narração; tira o parêntese; pendência alheia ao sistema fica) - Origem: AT-464 item 2 (TP-01 de 09/10:
git log feature/task-9d6dcd53com "Pendente: commit… as ações git_commit, git_push e pr_open não estavam disponíveis neste passo")
RN-787 — O gate conta o diff da PR contra a dev da origem, e o parecer de QA grava lista
- Regra: (1) o diff que o QA e o SecOps julgam (
dev...HEAD, RN-664) usa adevMAIS NOVA que o working tree enxerga: com origem local (caminho no disco) o gate faz antesgit fetch origin dev, melhor esforço, e entreorigin/deve adevlocal vale a que contém a outra. Adevlocal do clone não anda quando a PR é mergeada na origem, e o parecer do SecOps contava o acumulado das tarefas já mergeadas. Com origem remota não há fetch no gate (a credencial é doWorkspace) — declarado. O semgrep e o gitleaks seguem varrendo o worktree inteiro, não só o diff: estreitar o que o scanner vê muda o que o gate reprova, e isso fica para o dono. (2)itensecoverageMatrixdo parecer de QA chegam aoartifact.qa_verdictsempre como LISTA: string JSON de lista é decodificada (o molde da RN-719), e string que não é lista vira[]. - Onde:
apps/engine/lib/engine/gates/diff.ex:44(base_mais_nova),apps/engine/lib/engine/gates/hooks/termination.ex:54(lista) - Teste:
apps/engine/test/engine/gates/diff_test.exs(a tarefa B nascida da A, com a A mergeada na origem: o diff é só B) eapps/engine/test/engine/gates/hooks/termination_test.exs(string JSON vira lista; lista intacta; string inválida vira[]) - Origem: AT-464 itens 1 e 3 (TP-01 de 09/10, sessão
80e3ce09: "17 arquivo(s) alterado(s) nesta PR" numa PR de 3 arquivos contra adev, ecoverageMatrix/itensgravados como"[]"nos últimos pareceres)
RN-790 — O README de como subir o entregável é critério do DoD que o PO escreve
- Regra: a instrução de kickoff do PO manda incluir, no DoD da história que
cria a entrada da aplicação (servidor, CLI, configuração ou persistência) — ou
numa história própria —, o critério "README diz como instalar, configurar
(variáveis de ambiente obrigatórias e opcionais, com o padrão) e subir a
aplicação, e como criar o primeiro acesso, quando houver". O dev agent cumpre
o DoD da história, e o gate o julga por ele. Régua de PROMPT, como a
RN-765: nenhuma ferramenta recusa história sem esse critério. A
outra variante — o dev agent atualizar o README em toda tarefa que mexe em
configuração/entrada — NÃO foi feita: exigiria classificar a tarefa no
AmbienteDoAgenteou noreport_done, regra nova de código — declarado. - Onde:
apps/engine/lib/engine/agents/po_server.ex:600(kickoff_instruction) - Teste:
apps/engine/test/engine/agents/po_server_test.exs:438(a instrução de kickoff traz o critério do README) - Origem: AT-466 (TP-01 de 09/10: o
encurtador-apisaiu com um README só comnpm test, semJWT_SECRETobrigatório,DB_PATH/PORT,npm startnem como criar o primeiro membro)
RN-792 — Ao declarar o contrato, o Arquiteto recebe as rotas das histórias que o contrato não declara
- Regra: depois de
declare_module_contractsgravar a versão (RN-684), a ferramenta lê o backlog do projeto e extrai, de forma determinística e sem LLM, todoMÉTODO /caminho(GET, POST, PUT, PATCH, DELETE) do título, descrição, RF e DoD das histórias não arquivadas, e o mesmo padrão dasassinaturas do contrato. Rota citada por história e ausente da UNIÃO das rotas do contrato (a história do front cita a rota do back) volta no resultado como DIVERGÊNCIA, com o que o contrato declara no mesmo método — entrada do laço (RN-163), para o Arquiteto corrigir o contrato ou registrar a decisão. Parâmetro (:id,{id}), barra final e query string são normalizados; teto de 10 linhas, com o total. A leitura do backlog falhar NÃO recusa a declaração (já gravada): o resultado diz que não conferiu. Fica de fora, declarado: rota que só o contrato declara (não é divergência), história editada DEPOIS da declaração (só é conferida na próxima), e o contrato acompanhar a rota que o DEV renomeou (o QA nomeia a divergência no parecer, RN-786, mas nada a leva de volta ao Arquiteto). - Onde:
apps/engine/lib/engine/harness/rotas_do_contrato.ex:37(divergencias),apps/engine/lib/engine/harness/tools/declare_module_contracts.ex:125(conferencia_com_historias) - Teste:
apps/engine/test/engine/harness/tools/declare_module_contracts_test.exs(GET /painelna história ×GET /panelno contrato acusa; rota igual com parâmetro e barra normalizados não acusa; backlog ilegível diz que não conferiu) - Origem: AT-468 (TP-01 de 09/10: a história pedia
GET /painel, o contrato declarouGET /panel, e ninguém acusou na arquitetura)
RN-796 — O item do gitleaks diz a regra e o trecho com o segredo mascarado
- Regra: o item que o
gitleakspõe no parecer do SecOps leva, além do caminho e da linha, a regra que acusou (RuleID) e o trecho acusado (Match) com o segredo (Secret) trocado por***— nunca o segredo em claro. Sem oSecretpara mascarar, o trecho não entra. E o ambiente do agente de execução (RN-706) diz em uma linha que senha, token ou chave de teste são gerados em tempo de execução, nunca literais. Sem allowlist nova. - Onde:
apps/engine/lib/engine/actions/gitleaks_detector.ex:121(mensagem),apps/engine/lib/engine/harness/ambiente_do_agente.ex:102(montar) - Teste:
apps/engine/test/engine/actions/gitleaks_relatorio_test.exs,apps/engine/test/engine/harness/ambiente_do_agente_test.exs - Origem: AT-470
RN-797 — A task cuja branch não tem diff contra a dev fecha como "já na dev"
- Regra: no
report_done, antes de propor commit, push e PR, o dev agent pergunta se a branch do worktree traz algo que adevnão tem: árvore limpa egit diff --quiet <ponta da dev>...HEADsem diferença. É o caso da task reintegrada (RN-779) cujo código já entrou por outra PR. Sem diff, a task fecha comodonecom o eventodev.task_already_in_dev(agente, task, branch, motivo) — sem commit, sem PR, sem gates e sem clique. Qualquer dúvida (pasta que o engine não alcança, como no modorunner; git que falha; ref dadevausente) é "tem diff": a task segue para os gates, nunca fecha sozinha. Mede DIFF e não ancestralidade, então cobre também o merge por squash que a RN-760 declara não ver. Declarado: a PR antiga da branch, se existir, não é fechada no provider. - Onde:
apps/engine/lib/engine/dev/worktree_manager.ex:407(sem_diff_contra_a_dev?),apps/engine/lib/engine/dev/dev_agent_server.ex:1122(ja_na_dev) - Teste:
apps/engine/test/engine/dev/tarefa_ja_na_dev_test.exs - Origem: AT-474
RN-798 — O QA confere os comandos do README tocado pela entrega contra os scripts
- Regra: quando o diff da entrega contra a
devtoca um README, o QA confere, sem LLM, que todonpm run <s>,npm test,npm start,pnpm [run] <s>eyarn [run] <s>citado nele existe emscriptsdopackage.jsonda mesma pasta (senão o da raiz do worktree). A conferência entra na consolidação como mais um parecer ("Conferência do README"): divergência échanges_requestedcom um item por comando, nomeando o script que falta; sem divergência, aprova. Sem README no diff, diff que falhou oupackage.jsoninalcançável, nada é afirmado. De FORA, declarado: subcomando embutido do gerenciador (pnpm install,pnpm dlx…), comando com flag antes do script (pnpm --filter x test), flags de CLI do próprio projeto e comando que não é de Node. - Onde:
apps/engine/lib/engine/gates/conferencia_do_readme.ex:28(conferir),apps/engine/lib/engine/gates/qa_lead_server.ex:818(conferencia_do_readme) - Teste:
apps/engine/test/engine/gates/conferencia_do_readme_test.exs - Origem: AT-475
RN-805 — O plano de teste gravado é o que a ferramenta aceitou
- Regra:
emit_plano_de_testevalida os argumentos JÁ normalizados (RN-719:criteriosExecutaveisem string JSON vira lista), e o hook que encerra o laço da QA-estratégia lêcriteriosExecutaveispela MESMA normalização (Termination.lista/1, a da RN-787). Antes, a tool dizia "plano registrado: 9 critérios", o hook levava a string crua, o artefato era recusado (:criterios_vazios), o agente devolvia{:ok, plano}mesmo assim e a Automação caía noEnumda string — o QA Lead morria e o gate ficava parado até o resgate (18 min no uso real). Artefato recusado agora é falha NOMEADA da QA-estratégia (agent.error, origemcodigo) e a revisão segue sem plano, como já seguia quando o plano falha; a Automação também normaliza os critérios que recebe. - Onde:
apps/engine/lib/engine/gates/hooks/termination_plano_de_teste.ex:23(criterios_executaveis),apps/engine/lib/engine/gates/qa_estrategia_agent.ex:231(emit_returning) - Teste:
apps/engine/test/engine/gates/qa_estrategia_agent_test.exs("critérios em string JSON viram lista: o plano é gravado e devolvido como lista") - Origem: AT-478 (TP-01 de 10/10)
RN-806 — Um ciclo de QA por tarefa: pedido anterior ao veredito é descartado
- Regra: o QA Lead roda UM ciclo por task. Pedido
{:run, task}para a task cujo ciclo está suspenso esperando decisão é ignorado, e ao fim de um ciclo (veredito ou bloqueio) todo{:run, task}que esperava na caixa do processo é descartado — foi pedido antes de o veredito existir, e a correção legítima do dev só nasce depois dele. No uso real o resgate (RN-722) disparou o ciclo duas vezes no mesmo instante, e o segundo, rodando depois do aprovado, gravou um parecer contrário sobre a tarefa já mergeada. De FORA, declarado: duas réplicas do engine (a caixa é por processo, a mesma ressalva doGateRescuer) e a causa da dupla varredura do resgate, que não foi corrigida aqui. - Onde:
apps/engine/lib/engine/gates/qa_lead_server.ex:106(handle_cast),apps/engine/lib/engine/gates/qa_lead_server.ex:157(descartar_pedidos_obsoletos) - Teste:
apps/engine/test/engine/gates/qa_lead_server_test.exs("pedido repetido que esperava na caixa é descartado ao fim do ciclo"; "pedido para a task com ciclo suspenso não abre um segundo") - Origem: AT-478 (TP-01 de 10/10)
RN-810 — Falha de rede passageira do provider é retentada, e o bloqueio dela é infra sem contar no disjuntor
- Regra: quando a chamada ao modelo volta com erro de REDE do provider —
o
codeconnection/timeoutque a api normaliza (ADR 0041) ou, sem código, o texto do transporte (EAI_AGAIN,ECONNRESET,ETIMEDOUT,ECONNREFUSED,ENOTFOUND) —, oToolLoopa retenta até duas vezes, com espera de 1 s e 3 s, gravandotoolloop.network_retrya cada uma. Vale para todo consumidor doToolLoop(dev agents e subagentes de gate). Esgotado o teto, a origem do desfecho éinfra, nuncacodigo, e o bloqueio da task do dev agent por essa causa NÃO incrementa o contador de bloqueios seguidos (RN-047): o agente segue reivindicando. No uso real umgetaddrinfo EAI_AGAIN openrouter.aide 17 s bloqueou duas tarefas comocodigoe desarmou o dev. A correção pós-gate segue a mesma régua (AT-621, RN-864). - Onde:
apps/engine/lib/engine/harness/tool_loop.ex:285(llm_turn_com_retentativa),apps/engine/lib/engine/agents/falha_de_turno.ex:119(falha_de_rede?),apps/engine/lib/engine/dev/dev_agent_server.ex:1043(bloquear_parada),apps/engine/lib/engine/dev/agent_io.ex:228(finish_task) - Teste:
apps/engine/test/engine/harness/tool_loop_test.exs("falha de rede do provider: retenta e segue quando a rede volta"; "falha de rede que persiste esgota o teto"),apps/engine/test/engine/dev/dev_agent_server_test.exs("erro de rede do provider: origem infra e o disjuntor não conta"),apps/engine/test/engine/agents/falha_de_turno_test.exs - Origem: AT-484 (TP-01 de 10/10)
RN-811 — O "já na dev" compara com a dev do remoto atualizada
- Regra: antes da checagem "já na dev" (RN-797) no
report_done, o dev agent atualiza adevdo remoto pelo MESMOfetchque a RN-779 faz no claim, serializado por projeto. Sem ele a comparação usava adevdo dia em que o worktree nasceu: a branch da task cujos commits já tinham entrado nadevremota pela PR de outra task passou por QA e SecOps com 0 arquivos.fetchque falha não decide nada — compara com o que há, e na dúvida a task segue para os gates. - Onde:
apps/engine/lib/engine/dev/worktree_manager.ex:407(sem_diff_contra_a_dev?),apps/engine/lib/engine/dev/worktree_manager.ex:438(sem_diff_atualizado?),apps/engine/lib/engine/dev/dev_agent_server.ex:903(handle_outcome) - Teste:
apps/engine/test/engine/dev/tarefa_ja_na_dev_test.exs("caminho feliz: com a dev atualizada, a branch não tem diff"; "falha: remoto inalcançável não fecha a task sozinha") - Origem: AT-486 (TP-01 de 10/10)
RN-813 — Tarefa que toca o ponto de entrada exige evidência de que ele sobe
- Regra: extensão do recorte da RN-765. Quando o título ou a
descrição da tarefa toca o ponto de entrada, a fiação (rotas, migração,
registro) ou o README de execução, o recorte do gate de QA ganha a seção
"Ponto de entrada": exigir evidência de que o entrypoint REAL sobe (o
start/maindo projeto rodado, ou um teste que importe o entrypoint real, não um app montado no próprio teste) e responde numa rota ou comando. Sem essa evidência, a lacuna vai paraitens. É régua de PROMPT: o contrato externo dos gates (apps/api/config/gates.yml) e o código doemit_qa_verdictnão mudam, e nenhum gate novo nasce. Tarefa que não toca nada disso não ganha a seção. - Onde:
apps/engine/lib/engine/gates/recorte_da_tarefa.ex:121(secao_do_ponto_de_entrada) - Teste:
apps/engine/test/engine/gates/recorte_da_tarefa_test.exs("tarefa que toca a fiação exige evidência do ponto de entrada real" e "tarefa que não toca entrypoint nem fiação não ganha a seção") - Origem: AT-483 (TP-01 de 10/10: o
server.tsnunca chamava a migração da tabelalinkse/:codesombreavaGET /links; os 53 testes passavam porque cada um montava o próprio banco e app)
RN-814 — O parecer do QA lista todas as lacunas numa volta
- Regra: extensão do recorte da RN-765: o gate de QA lista em
itensTODAS as lacunas da tarefa que achar na volta, de uma vez, nunca uma por volta — cada volta é uma correção paga e um passo do teto do ciclo. O prompt anterior só mandava listar "quais regras faltam", e lacuna que não era regra descoberta ficava para a volta seguinte. Régua de prompt. - Onde:
apps/engine/lib/engine/gates/recorte_da_tarefa.ex:20(texto) - Teste:
apps/engine/test/engine/gates/recorte_da_tarefa_test.exs("o recorte pede todas as lacunas numa volta só") - Origem: AT-487 (TP-01 de 10/10, tarefa
c7ced5a2: duas reprovações por duas lacunas que já existiam na entrega original)
RN-817 — O QA roda a suíte em estado limpo, e teste pulado não é verde
- Regra: extensão do recorte da RN-765, para toda tarefa. Antes
de rodar a suíte, o gate de QA encerra o que o PRÓPRIO projeto deixou de pé
(servidor do
startescutando numa porta do projeto), só processos daquela pasta, nunca de fora. Teste que sobe processo deve encerrar o GRUPO (ou subirnodedireto, não vianpm); o que não encerra trava a suíte num clone limpo e é lacuna emitens. Teste que se PULOU por ambiente (porta ocupada, ferramenta ausente) não conta como verde: vai aoresumocomo observação. É instrução do recorte, não passo do harness: quem roda a suíte é o próprio QA peloterminal, que já passa pela política e pelo roteamento de container dos três modos — umpkillno harness precisaria de um segundo caminho de execução por modo. Régua de prompt; o contrato dos gates não muda. - Onde:
apps/engine/lib/engine/gates/recorte_da_tarefa.ex:178(secao_do_ambiente_limpo) - Teste:
apps/engine/test/engine/gates/recorte_da_tarefa_test.exs("RN-817: pede ambiente limpo, encerrar o grupo e pulado como observação") - Origem: AT-496 (TP-01 de 10/10:
npm testtravava num clone limpo — o teste matava onpme onodeneto ficava na 3000 —, e no container o QA viu exit 0 porque o teste se pulava com a porta ocupada por um servidor órfão)
RN-818 — A evidência do ponto de entrada é a que o ambiente do dev produz
- Regra: extensão da RN-813. A seção "Ponto de entrada" diz o
que CONTA como evidência no ambiente do dev — subir o processo do
entrypoint na pasta do projeto, rodar o script no container do projeto,
bater numa rota do processo de pé — e o que NÃO cabe ali: construir imagem
Docker,
docker compose, rede externa. Exigência desse segundo tipo nunca vai paraitens; vai aoresumocomo observação. Régua de prompt. - Onde:
apps/engine/lib/engine/gates/recorte_da_tarefa.ex:121(secao_do_ponto_de_entrada) - Teste:
apps/engine/test/engine/gates/recorte_da_tarefa_test.exs("RN-818: com ponto de entrada diz o que conta e o que não cabe" e "RN-818: sem ponto de entrada não traz a régua de evidência") - Origem: AT-492 (TP-01 de 10/10, task
c708f8b2: a 4ª volta exigiu construir a imagem a partir do Dockerfile, o container do projeto não tem Docker, e o ciclo esgotou em bloqueiopolitica)
RN-819 — O QA varre regra por regra antes de emitir
- Regra: extensão da RN-814. Além de listar todas as lacunas, o recorte pede uma VARREDURA POR REGRA antes de emitir: percorrer cada regra de negócio e cada critério da tarefa, um por um, apontando onde está a prova ou que ela falta, e só emitir depois de passar por todas. "Liste todas" sozinho não segurou lacuna pré-existente aparecendo na volta seguinte. Régua de prompt.
- Onde:
apps/engine/lib/engine/gates/recorte_da_tarefa.ex:20(texto) - Teste:
apps/engine/test/engine/gates/recorte_da_tarefa_test.exs("RN-819: pede varredura por regra antes de emitir") - Origem: AT-490 (TP-01 de 10/10: tarefas 2 e 11 trouxeram na 2ª volta lacuna que já estava na entrega)
RN-823 — O ciclo de gate que o reinício do engine matou é retomado no boot, e o fio diz isso
- Regra: no BOOT do engine, a linha
in_progressdegate_statesRECENTE (abaixo degate_rescue_stale_after_seconds, 15 min, e por isso invisível para o tick) sem processo vivo NESTE nó nem nos OUTROS nós do cluster é órfã do reinício, não um ciclo lento: o boot a retoma na hora (Dispatcher.run_qa/run_secops), sem esperar o limiar. "Vivo" é a régua da RN-722 (a task em voo no lead), perguntada também aos outros nós por:erpc.multicall— nó que não responde conta como VIVO, a régua doTurnoOrfao(RN-586). Valem as guardas de sempre: ação pendente de ator do gate não reinicia, e dev dono ausente só apaga a linha. Antes de redespachar, o boot gravagate.interruptedno log da sessão (taskId,gate,ultimaAtividadeEm,retomadoEm), e o fio o mostra como aviso, nos dois idiomas. O tick periódico NÃO muda: fora do boot, linha recente é ciclo em voo. Resíduo declarado: nó que ainda não entrou no cluster no instante do boot não é perguntado, e o pior desfecho é o de sempre (trabalho duplicado e barato; o segundo veredito é recusado pela api). - Onde:
apps/engine/lib/engine/gates/gate_rescuer.ex:231(run_boot),:240(interrompido_no_boot),apps/web/src/routes/RetomarGateEstacionado.helpers.ts:21(cicloInterrompido) - Teste:
apps/engine/test/engine/gates/gate_rescuer_test.exs("boot com ciclo RECENTE órfão do reinício: grava gate.interrupted e retoma já (RN-823)"; falhas: "boot com ciclo recente esperando ação PENDENTE do gate: não reinicia nem grava (RN-823)" e o processo local vivo intocado),apps/web/src/routes/RetomarGateEstacionado.test.tsx("cicloInterrompido (RN-823)") - Origem: AT-495 (TP-01 de 10/10: o reinício das 18:51 matou o ciclo de QA
da tarefa 10, e o dev ficou
awaiting_gate15 min sem tela nenhuma dizer)
Revisada pela RN-923 (ADR 0226). A retomada do boot conta como a automática do ciclo (auto_resumes): se o ciclo já usou a sua, o boot o estaciona em vez de redespachá-lo, e o subagente retomado leva o plano do último agent.turn_resumed.
RN-832 — O dev agent sabe que report_done faz commit, push e PR, e que teste novo entra no script test
- Regra: o dev agent não tem nem precisa de
git_commit,git_pushoupr_open: commit, push e PR acontecem quando ele chamareport_done. A descrição dereport_done, a dereport_blocked("falta ferramenta de git" não é motivo de bloqueio), o kickoff do dev agent e a mensagem de ambiente (RN-706) dizem isso — a de ambiente mandava, até aqui, usar as ações tipadas de git, que o dev não tem. O kickoff e o ambiente dizem também que, se o scripttestdopackage.jsonlista arquivos um a um, todo arquivo de teste novo entra nele. Só texto: nenhuma regra nova de código, e o Jev segue sem piso para dev agents (RN-758) — até a RN-839, que pôsreport_done/report_blockedno piso deles. - Onde:
apps/engine/lib/engine/dev/tools/report_done.ex:16(spec),apps/engine/lib/engine/dev/tools/report_blocked.ex:12(spec),apps/engine/lib/engine/dev/dev_agent_server.ex:651(initial_message),apps/engine/lib/engine/harness/ambiente_do_agente.ex:102(montar) - Teste:
apps/engine/test/engine/dev/report_done_instrucao_test.exs("caminho feliz: report_done, kickoff e ambiente carregam as duas instruções"; falha: "o ambiente não manda mais usar as ações tipadas de git") - Origem: AT-499 (TP-01, 11ª: tarefa bloqueada com a suíte verde por
"git_commit não existe"; quatro voltas de QA por teste fora do script
test)
RN-845 — A leitura das ADRs propostas diz se a PR já foi mergeada
- Regra: na linha de uma ADR
executed,listar_adrs_propostas(RN-772) não afirma mais "PR aberta" pelo status da AÇÃO: cruza opullRequestIddo resultado com ogit_mergeEXECUTADO do mesmo projeto (a ação pela qual todo merge passa, RN-418) e diz "PR MERGEADA em <data da decisão>" ou "PR aberta (nenhum merge dela registrado no Brabo)". Resultado sem id de PR, ou consulta de merge que falha, vira "estado da PR: NÃO CONSEGUI LER" — nunca "aberta". Contida (ADR 0060): uma consulta a mais, só sobre os ids listados, no mesmo Postgres. Limite declarado: PR fechada sem merge FORA do Brabo (no provider remoto) não é vista, e segue lida como aberta — perguntar ao provider seria HTTP no laço do agente. - Onde:
apps/engine/lib/engine/harness/tools/listar_adrs_propostas.ex:88(cruzar_com_merges),:102(merges_por_pr),:155(estado_da_pr). - Teste:
apps/engine/test/engine/harness/tools/listar_adrs_propostas_test.exs("PR mergeada aparece como mergeada…", "estado da PR ilegível diz que não leu, nunca aberta"). - Origem: AT-514 (TP-01, 11/10: o Arquiteto disse "aberta como PR" 30 s depois do merge).
RN-846 — A rota HTTP vai ao README e o erro de entrada é do projeto
- Regra: extensão da RN-819. O kickoff do dev agent de
execução pede que rota HTTP criada ou mudada entre no README (método,
caminho, corpo, respostas e códigos) e que erro de entrada (JSON malformado,
campo inválido) responda com mensagem própria no formato do projeto, nunca a
mensagem crua da biblioteca. O recorte do QA confere as duas coisas na
varredura por regra, e o que faltar vai a
itens. Régua de prompt. - Onde:
apps/engine/lib/engine/dev/dev_agent_server.ex:651(initial_message) eapps/engine/lib/engine/gates/recorte_da_tarefa.ex:20(texto) - Teste:
apps/engine/test/engine/dev/report_done_instrucao_test.exs("RN-846: kickoff pede a rota no README e erro de entrada próprio") eapps/engine/test/engine/gates/recorte_da_tarefa_test.exs("RN-846: confere rota no README e erro de entrada próprio") - Origem: AT-515 (TP-01, 12ª: o README não documentava a API, e JSON malformado respondia 400 com "Expected property name…" do parser)
RN-849 — O turno de prontidão da arquitetura abre com o estado atual das ADRs
- Regra: extensão da RN-845. Quando o usuário confirma que a
arquitetura está pronta, o servidor do Arquiteto LÊ o estado das ADRs pelo
mesmo código de
listar_adrs_propostas(Postgres local, sem HTTP, mesmo teto de 20 linhas) e o põe na mensagem de abertura desse turno, com a instrução de não afirmar estado de ADR diferente. Leitura que falha diz "NÃO CONSEGUI LER" e manda dizer que não sabe — nunca "pendente". - Onde:
apps/engine/lib/engine/harness/tools/listar_adrs_propostas.ex:50(contexto_de_prontidao),apps/engine/lib/engine/agents/arquiteto_server.ex:223(contexto_de_prontidao). - Teste:
apps/engine/test/engine/harness/tools/listar_adrs_propostas_test.exs("contexto_de_prontidao/2 (RN-849)"). - Origem: AT-519 (TP-01: no turno de prontidão o Arquiteto disse "pendente de aprovação" sobre ADR já mergeada, repetindo a leitura do turno anterior).
RN-850 — A rota entra na seção "Rotas HTTP" e o README não leva nota de processo
- Regra: extensão da RN-846. O kickoff do dev agent pede que a rota entre DENTRO da seção "Rotas HTTP" do README (criada antes de "Testes" se faltar), nunca no fim do arquivo, e que o README, que é do usuário, não ganhe nota de processo nem instrução para agentes. O recorte do QA confere as duas coisas. Régua de prompt.
- Onde:
apps/engine/lib/engine/dev/dev_agent_server.ex:651(initial_message) eapps/engine/lib/engine/gates/recorte_da_tarefa.ex:20(texto) - Teste:
apps/engine/test/engine/dev/report_done_instrucao_test.exs("RN-850: a rota vai dentro de Rotas HTTP e o README não leva nota de processo") eapps/engine/test/engine/gates/recorte_da_tarefa_test.exs("RN-850: confere a seção Rotas HTTP e a ausência de nota de processo") - Origem: AT-520 (TP-01: cada tarefa acrescentou rotas no fim do README, depois de "Testes", e sobrou um parágrafo de processo para agentes)
RN-851 — O compose da Infra sobe o APP na máquina do usuário, e a PR declara como rodá-lo
- Regra: o kickoff do Infra Lead pede o compose que SOBE O APP (não só
ambiente de dev):
builddo Dockerfile de cada módulo comFROM= a imagem decidida (projectImagedo contexto da Infra; sem ela, a candidata do roteamento, dito no README), porta${APP_PORT:-8080}:<porta interna>, volume para dados persistentes,.env.examplecommitado (nunca.env) e a seção "Rodar na sua máquina" no README. O container de TRABALHO dos agentes segue subindo pelo broker, e não há deploy.propose_infra_prganha o campo OBRIGATÓRIOrun(commandaté 300 caracteres;urlsde 1 a 5,labelaté 60,kindemapi_docs | web,urlcomeçando comhttp://localhost:ouhttp://127.0.0.1:;envExamplebooleano), validado no engine ANTES do HALT com recusa nomeada que entra no laço (RN-163) e repassado no payload deopen_infra_pr. Decisão no ADR 0213. - Onde:
apps/engine/lib/engine/infra/instrucoes_de_execucao.ex:27(validar),apps/engine/lib/engine/infra/infra_lead_server.ex:1414(build_kickoff) e:1327(abrir_pr), eapps/engine/lib/engine/infra/tools/propose_infra_pr.ex:24(spec) - Teste:
apps/engine/test/engine/infra/infra_lead_server_test.exs(describe "o compose sobe o app e a PR levarun(RN-851)") - Origem: AT-517 (o entregável não tinha como ser rodado pelo usuário)
RN-853 — O dev expõe OpenAPI e Swagger quando a tarefa toca rotas
- Regra: parte do ADR 0213. Quando a
tarefa toca ponto de entrada ou rotas (a MESMA régua do recorte do QA,
toca_ponto_de_entrada?, nunca uma cópia), o kickoff do dev ganha a cláusula: módulo que expõe HTTP serve/openapi.json(OpenAPI 3) e/docs(UI Swagger); sem rede ou sem pacote,/docsé página HTML que carrega o swagger-ui de CDN; módulo web serve a página inicial; a URL entra na seção "Rodar na sua máquina" do README. Tarefa que não toca rotas não paga a cláusula (a mensagem é fixada e volta a cada iteração). - Onde:
apps/engine/lib/engine/dev/dev_agent_server.ex:691(clausula_de_openapi),apps/engine/lib/engine/gates/recorte_da_tarefa.ex:113(toca_ponto_de_entrada?) - Teste:
apps/engine/test/engine/dev/report_done_instrucao_test.exs("RN-853: tarefa que toca rotas pede /openapi.json e /docs"; falha: "RN-853 falha: tarefa sem rota nem entrypoint não paga a cláusula") - Origem: AT-517
RN-854 — O QA prova /openapi.json e /docs dentro do container
- Regra: parte do ADR 0213. Na seção
de ponto de entrada do recorte (RN-813), o QA sobe o app pelo
comando do README em background DENTRO do container, bate em
localhost:<porta interna>/openapi.jsone/docs(curl,wgetounode -e fetch) e encerra o grupo de processos (RN-817). Falha vai aitens; odocker compose upreal fica noresumocomo "provado só na máquina do usuário". - Onde:
apps/engine/lib/engine/gates/recorte_da_tarefa.ex:121(secao_do_ponto_de_entrada) - Teste:
apps/engine/test/engine/gates/recorte_da_tarefa_test.exs("tarefa que toca rotas pede /openapi.json e /docs, compose em resumo"; falha: "falha: tarefa sem rota não recebe a seção") - Origem: AT-517
RN-862 — A pergunta em aberto do lado do engine: ferramenta, veredito e decisão no contexto
- Regra: o Criativo, o PO, o Arquiteto e o Dev Lead declaram toda dúvida
deixada para o usuário com
declare_open_question(perguntaaté 500,sugestaoaté 300 opcional,afeta: {regras, historias, tarefas}), que gravaartifact.open_questioncomquestionId(uuid),autoresessionId; payload inválido é recusa nomeada e não grava. A instrução é UMA frase central, no texto do fluxo dos quatro (RN-711), nunca por servidor. No gate de QA, requisito "não confirmado pelo usuário" (ou contradito por decisão vigente) éneeds_user_decision, nuncachanges_requested:emit_qa_verdicto aceita só comperguntaouopenQuestionId, o QA Lead o consolida acima deapproved, o veredito vai à api com a pergunta e o ciclo fica ESTACIONADO (ADR 0207,gate.rescue_parked, sem resgate automático) até a api pedir a retomada pela rota da RN-724. Asdecisoesdo dev-context entram no kickoff do dev e no recorte do QA como "Decisões do usuário — vencem RF/DoR quando conflitam" (pergunta → decisao); vazio não escreve nada. A pergunta gravada conta no fecho do turno (RN-731), e o PO passaquestion_idemupdate_story/complete_storyquando corrige a história a partir de uma resposta (ADR 0214). - Onde:
apps/engine/lib/engine/harness/tools/declare_open_question.ex:54(run),apps/engine/lib/engine/harness/fluxo_do_time.ex:116(frase_da_pergunta),apps/engine/lib/engine/gates/tools/emit_qa_verdict.ex:81(run),apps/engine/lib/engine/gates/qa_lead.ex:89(consolidar_decisao),apps/engine/lib/engine/gates/qa_lead_server.ex:595(estacionar_por_decisao),apps/engine/lib/engine/dev/context_builder.ex:121(secao_de_decisoes) - Teste:
apps/engine/test/engine/harness/perguntas_em_aberto_test.exs("válida grava artifact.open_question…"; "inválida é recusa nomeada…"; "consolidação: needs_user_decision vence approved"; "o recorte diz a régua…"; "ContextBuilder: a história leva as decisões…"),apps/engine/test/engine/gates/qa_lead_server_test.exs("grava a pergunta na api, estaciona o ciclo e só o gesto humano o retoma") - Origem: AT-521, ADR 0214
RN-863 — api_docs é sempre /docs, toda rota entra no OpenAPI, e o QA reprova a que falta
- Regra: módulo HTTP declara em
run.urlsSEMPRE/docs(a UI Swagger,kind: "api_docs") mesmo antes de existir, e módulo web a página inicial — nunca uma rota de negócio; o kickoff da Infra diz isso no passo do README (estende a RN-851). A validação dorunrecusa, nomeada e com a MESMA régua nos dois lados,api_docscuja URL não termina em/docs(estende a RN-852). A cláusula do dev diz que TODA rota HTTP criada ou mudada na tarefa entra no/openapi.json— path com método, corpo e respostas (estende a RN-853). No recorte do QA, cada rota da seção "Rotas HTTP" do README precisa estar empathsdo/openapi.jsonservido; rota faltando é item que reprova, nunca observação (estende a RN-854). - Onde:
apps/engine/lib/engine/infra/instrucoes_de_execucao.ex:60(url),apps/api/src/domain/infra/run-instructions.ts:77(recusaDoRun),apps/engine/lib/engine/infra/infra_lead_server.ex:1414(build_kickoff),apps/engine/lib/engine/dev/dev_agent_server.ex:691(clausula_de_openapi),apps/engine/lib/engine/gates/recorte_da_tarefa.ex:20(texto) - Teste:
apps/engine/test/engine/infra/infra_lead_server_test.exs("run api_docs em rota de negócio é recusado com motivo, sem propor"),apps/api/test/application/use-cases/execution/get-run-instructions.use-case.spec.ts("recusaDoRun (RN-852)"),apps/engine/test/engine/dev/report_done_instrucao_test.exs("RN-853: …"),apps/engine/test/engine/gates/recorte_da_tarefa_test.exs("RN-854: …") - Origem: AT-524, ADR 0213
RN-864 — Falha passageira de rede volta sozinha em até 3 tentativas, e o dev pausa
- Regra: a task bloqueada pelo dev agent com origem
infrapor falha PASSAGEIRA de rede/provedor — lista de permitidos explícita: DNS (EAI_AGAIN,ENOTFOUND,getaddrinfo),ECONNRESET,ETIMEDOUT,aborted, HTTP 5xx/429 do provedor (códigosrate_limit/upstream,connection/timeout) — é RETOMADA sozinha até 3 vezes, depois de 1, 3 e 10 min (decisão do dono), por um job Oban que chama o MESMO desbloqueio do humano (UnblockTaskUseCase, pela rota interna.../tasks/:taskId/unblock, atorsystem, no-op se a task já não está bloqueada). O motivo do bloqueio diz qual retomada foi agendada; depois da 3ª retomada a falha seguinte fica bloqueada como antes, e o motivo diz que as 3 já foram tentadas. E o agente PAUSA 1 min em vez de pegar a próxima task (o wake é ignorado durante a pausa). Crédito esgotado (402, RN-726/730) e erro de código NÃO entram: bloqueiam na hora. O contador de retomadas é só em memória: reiniciar o engine o zera. Vale também para a correção pós-gate (AT-621): o 503 do provider numa correção segue o mesmo caminho do laço principal (crédito RN-726 + retomada), com o motivo marcado "(correção)". - Onde:
apps/engine/lib/engine/dev/dev_agent_server.ex:815(handle_correction_outcome),apps/engine/lib/engine/agents/falha_de_turno.ex:140(passageira?),apps/engine/lib/engine/dev/dev_agent_server.ex:1062(bloquear_passageira),apps/engine/lib/engine/dev/agent_io.ex:293(pausar_por_rede),apps/engine/lib/engine/workers/retomada_de_tarefa_worker.ex:33(agendar),apps/api/src/application/use-cases/execution/unblock-task.use-case.ts:33(ator) - Teste:
apps/engine/test/engine/dev/dev_agent_server_test.exs("falha passageira: agenda a retomada, pausa e não queima a fila", "fim da pausa volta a reivindicar"; falha: "falha passageira depois de 3 retomadas fica bloqueada pedindo o clique", "402 não agenda retomada"; correção: "correção com 503: infra, retomada agendada, pausa e disjuntor intacto", "correção sem crédito: pausa por crédito, sem retomada", "correção com erro não passageiro: bloqueia na hora, sem retomada"),apps/engine/test/engine/workers/retomada_de_tarefa_worker_test.exs - Origem: AT-525
RN-865 — Endpoint e verbo HTTP com caminho também ligam a conferência do OpenAPI, e ela cobre as rotas do código
- Regra: a régua única de "a tarefa toca ponto de entrada/rotas"
(
toca_ponto_de_entrada?, usada pelo recorte do QA e pela cláusula do dev da RN-853) casa também "endpoint" e verbo HTTP seguido de caminho (POST /links). Sem isso, tarefa descrita como "Endpoint protegido…" não recebia nem a cláusula do OpenAPI (dev) nem a conferência do/openapi.json(QA) — foi assim quePOST /linkseGET /panelficaram fora do OpenAPI com as duas tarefas aprovadas no TP-01. A conferência do QA passa a exigir empathsdo/openapi.jsonservido, além das rotas da seção "Rotas HTTP" do README, cada rota que o CÓDIGO registra; a que falta é item que reprova (estende a RN-863). - Onde:
apps/engine/lib/engine/gates/recorte_da_tarefa.ex:103(@ponto_de_entrada),apps/engine/lib/engine/gates/recorte_da_tarefa.ex:114(toca_ponto_de_entrada?) - Teste:
apps/engine/test/engine/gates/recorte_da_tarefa_test.exs("RN-865: tarefa que diz só endpoint ou verbo com caminho recebe a seção"; falha: "falha: tarefa sem rota não recebe a seção") - Origem: AT-527
RN-868 — O tick do resgate de gates é uma corrente só
- Regra: o
GateRescueSchedulerWorkerreagenda o próprio sucessor com a MESMA unicidade dokickoff/0(estadosavailable/scheduled/retryable, semexecuting, janela de dois intervalos). Antes só okickoff/0era único: o job em execução não conta, então o kickoff do boot com o tick anterior em voo, ou um retry que re-roda operform/1, abria uma SEGUNDA corrente, e elas nunca se fundiam. Medido no uso real (14/10): 11 jobs agendados ao mesmo tempo e a linha "espera decisão humana" da RN-722 11 vezes por tick para UMA linha degate_states. Não era defeito de dado. Com a unicidade no reagendamento as correntes duplicadas colapsam numa só no tick seguinte. - Onde:
apps/engine/lib/engine/workers/gate_rescue_scheduler_worker.ex:42(unicidade) - Teste:
apps/engine/test/engine/workers/gate_rescue_scheduler_worker_test.exs("o tick se reagenda e mantém a corrente viva"; falha: "com um sucessor já agendado, o tick não abre uma segunda corrente") - Origem: AT-529
RN-874 — O PO recebe no turno as decisões do usuário e as perguntas abertas do projeto
- Regra: todo turno do PO leva, como mensagem
systemEFÊMERA acrescentada pela fachadaEngineApiClientlogo depois do fluxo do time (RN-711) — nunca emstate.messagesnem no prompt do servidor —, as decisões do usuário (artifact.decision_record,choiceecontext) e as perguntas AINDA abertas do projeto (artifact.open_questionsemopen_question.answeredcom o mesmoquestionId), de TODAS as sessões. O texto manda escrever a RESPOSTA decidida na história, regra, DoR, DoD e tarefa, nunca "pergunta em aberto N"; pergunta ainda aberta é citada peloquestionId, sem inventar resposta. A leitura é LOCAL (Postgres direto, sem HTTP no laço), dos 200 eventos mais recentes desses tipos, com teto de 10 decisões, 10 perguntas, 240 caracteres por item e 3 000 no texto. Sem decisão nem pergunta aberta, ou com a leitura falhando, o turno segue sem a mensagem, nunca cai. Só oporecebe. Cada decisão leva o seuquestionIde as histórias deafeta.historias, para o PO citar o id emcomplete_story/update_storye corrigir uma históriaready(RN-859, AT-752). - Onde:
apps/engine/lib/engine/harness/decisoes_do_projeto.ex:34(anexar),:49(texto),apps/engine/lib/engine/session_events/event.ex:224(list_types_for_project),apps/engine/lib/engine/sessions/engine_api_client.ex:745(DecisoesDoProjeto) - Teste:
apps/engine/test/engine/harness/decisoes_do_projeto_test.exs("o turno do PO leva a decisão e só a pergunta AINDA aberta", "a decisão sai com o questionId e as histórias de afeta", "custo de entrada tem teto"; falha: "sem decisões nem perguntas não acrescenta nada", "leitura que falha deixa o turno sem a mensagem, sem queda")
RN-876 — O needs_user_decision recusado pela api volta ao QA, e só o aceite estaciona o gate
- Regra: o gate de QA só ESTACIONA (
gate.rescue_parked) quando a api aceita o vereditoneeds_user_decisioncomnextAction: "await_user". Se a api RECUSA (400, 404, ou o 409pergunta_ja_respondida), a recusa volta ao laço da QA de Automação como mensagem nova (RN-163: erro é entrada do laço), retomando o MESMO contexto em que ela emitiu — no 409, com a decisão já tomada para o QA aplicar em vez de perguntar de novo. As outras delegações não rodam de novo. A volta conta no teto de iterações do próprio laço; esgotado, a tarefa é bloqueada com o motivo e a origemmodelo(RN-059), nunca estacionada em silêncio. Antes, o retorno da api era descartado e o gate estacionava sem pergunta no fio (medido:openQuestionId: "1", 400 do@IsUUID). A ferramentaemit_qa_verdictrecusa, DENTRO do laço e antes da api,openQuestionIdque não é uuid. - Onde:
apps/engine/lib/engine/gates/qa_lead_server.ex:601(devolver_ao_qa),apps/engine/lib/engine/gates/qa_lead_server.ex:312(devolver_ao_qa),apps/engine/lib/engine/gates/qa_lead_server.ex:697(texto_da_recusa),apps/engine/lib/engine/gates/qa_automacao_agent.ex:186(devolver),apps/engine/lib/engine/gates/tools/emit_qa_verdict.ex:75(uuid) - Teste:
apps/engine/test/engine/gates/qa_lead_server_test.exs("400: nada estaciona, o QA recebe o motivo e uma nova volta acontece"; "409 no laço: a volta seguinte do QA lê a decisão"; falha: "teto esgotado depois da recusa: bloqueio narrado, nunca estacionamento"),apps/engine/test/engine/harness/perguntas_em_aberto_test.exs("AT-526 (RN-876): openQuestionId que não é uuid é recusa nomeada; uuid passa") - Origem: AT-526
RN-877 — O recorte do QA manda conferir as decisões antes de pedir decisão
- Regra: a instrução do recorte do QA diz que, antes de
needs_user_decision, ele confere as decisões listadas; requisito que uma delas já responde não é pergunta e a decisão se aplica.openQuestionIdsó com o uuid de uma pergunta ABERTA; sem ele, a pergunta nova vai empergunta/sugestao. É lembrete de leitura: não muda a régua do veredito. - Onde:
apps/engine/lib/engine/gates/recorte_da_tarefa.ex:20(texto) - Teste:
apps/engine/test/engine/gates/recorte_da_tarefa_test.exs("AT-526 (RN-877): manda conferir as decisões antes de needs_user_decision"; a falha da instrução é coberta pelo teste da RN-876 da ferramenta) - Origem: AT-526
RN-878 — O idioma do agente de execução vale desde a primeira frase
- Regra: nos turnos SEM autor humano dos agentes de execução (
dev-*,qa-*), a orientação de idioma da RN-622 diz que vale desde a PRIMEIRA frase, inclusive a narração antes de cada ferramenta, e perde a ressalva "salvo pedido do usuário" (não há usuário no turno). Medido na sessão6c68fbc0(projeto pt-BR,anthropic/claude-haiku-5.5): a orientação chegava, e mesmo assim a iteração 0 doqa-automacaoe dodev-encurtador-apiabria em inglês ("I'll start by running…"). O caminho continua UM (a fachada, RN-622): nada em prompt de servidor,agent_instructionsou descrição de ferramenta (RN-667). O teto de 50 tokens (AT-169) segue: medido comgpt-tokenizer,pt-BR32/26,en19/19 e o genérico com código na forma curta no pior caso 30/30 (cl100k/o200k); código fora da forma curta cai na orientação de sempre, que já cabia. Turno com autor e os conversacionais não mudam. Sem idioma resolvido, nada é acrescentado. A eficácia se mede no próximo TP-01. - Onde:
apps/engine/lib/engine/harness/idioma_da_resposta.ex:164(orientacao_de_execucao),:154(agente_de_execucao?) - Teste:
apps/engine/test/engine/harness/idioma_da_resposta_test.exs("agente de execução: vale desde a primeira frase, dentro do teto"; falha: "agente de execução sem idioma resolvido: nada é acrescentado") - Origem: AT-530
RN-879 — URL absoluta do entregável com a porta, e o mesmo code para o mesmo erro
- Regra: quando a tarefa toca ponto de entrada ou rotas, a cláusula do dev
manda que URL absoluta devolvida pela API do entregável (ex.: o link curto)
venha de
PUBLIC_BASE_URLou doHostda requisição COM a porta — nuncahttp://localhostfixo nem o hostname sem porta —, e que o mesmo caso de erro responda o mesmocodeem todas as rotas. O recorte do QA confere as duas coisas e trata a divergência (ex.:not_found×link_not_found) como ITEM. Decisão do dono de 14/10: regra geral. - Onde:
apps/engine/lib/engine/dev/dev_agent_server.ex:691(clausula_de_openapi);apps/engine/lib/engine/gates/recorte_da_tarefa.ex:122(secao_do_ponto_de_entrada) - Teste:
apps/engine/test/engine/dev/report_done_instrucao_test.exs("RN-879/880: tarefa que toca rotas leva URL com porta, code único e CORS" e a falha "RN-879/880 falha: tarefa sem rota não recebe CORS nem PUBLIC_BASE_URL");apps/engine/test/engine/gates/recorte_da_tarefa_test.exs("RN-879/880: URL, code de erro e CORS") - Origem: AT-531
RN-880 — O entregável responde CORS configurável, com servers relativo e swagger-ui pinado
- Regra: na mesma cláusula, a API do entregável responde CORS com origens
de
CORS_ORIGINS(padrãohttp://localhost:<porta>ehttp://127.0.0.1:<porta>),OPTIONS204 eAccess-Control-Allow-*comAuthorizationeContent-Type; o/openapi.jsondeclaraserverssó relativo (/) — URL absoluta criaria o erro de CORS quando a página é aberta por outro host —, e o swagger-ui de CDN vai com versão exata, nunca@5. O QA prova com um preflight de outra origem (curl -X OPTIONS) e reprova sem os cabeçalhos, comserversabsoluto ou com o CDN flutuante. - Onde:
apps/engine/lib/engine/dev/dev_agent_server.ex:691(clausula_de_openapi);apps/engine/lib/engine/gates/recorte_da_tarefa.ex:122(secao_do_ponto_de_entrada) - Teste: os mesmos da RN-879 (caminho feliz e falha nos dois arquivos)
- Origem: AT-532
RN-881 — O engine imprime as outras decisões do projeto para o dev e o QA
- Regra: o
decisoesDoProjetoque a api devolve nodev-context(RN-872, ADR 0215, já com teto) vira a seção "Outras decisões do projeto", separada e DEPOIS das decisões da tarefa, na unidade da story do dev e no recorte do QA. Vazio ou ausente não escreve nada; o engine não aplica teto próprio. - Onde:
apps/engine/lib/engine/dev/context_builder.ex:133(secao_de_decisoes_do_projeto);apps/engine/lib/engine/gates/recorte_da_tarefa.ex:20(texto) - Teste:
apps/engine/test/engine/dev/context_builder_test.exs("RN-881: decisoesDoProjeto vira a seção..." e "RN-881 falha: sem decisoesDoProjeto a seção some");apps/engine/test/engine/gates/recorte_da_tarefa_test.exs("RN-881: outras decisões do projeto") - Origem: AT-526 (fatia F4, lado do engine)
RN-885 — O CORS do entregável usa a porta PUBLICADA, e o QA confere o header nas duas origens
- Regra: revisa a RN-880 (que continua valendo no resto). O padrão de
CORS_ORIGINSdo entregável usa a porta PUBLICADA —APP_PORT, que o compose passa ao app —, nunca a internaPORT: no TP-01 (16ª) o padrão saiu comPORT=3000e, com o compose publicando 8080, nenhuma origem real recebiaAccess-Control-Allow-Origin(medido com curl::3000recebe,:8080não; no Chrome,Failed to fetch). O QA faz o preflight contra a porta PUBLICADA, uma vez parahttp://localhost:<APP_PORT>e outra parahttp://127.0.0.1:<APP_PORT>, e reprova sem o headerAccess-Control-Allow-Origin— 204 sozinho não basta, e CORS só na porta interna é ITEM. - Onde:
apps/engine/lib/engine/dev/dev_agent_server.ex:691(clausula_de_openapi);apps/engine/lib/engine/gates/recorte_da_tarefa.ex:122(secao_do_ponto_de_entrada) - Teste:
apps/engine/test/engine/dev/report_done_instrucao_test.exs("RN-885: o padrão do CORS usa a porta PUBLICADA, nunca a interna"; falha: "RN-879/880 falha: tarefa sem rota não recebe CORS nem PUBLIC_BASE_URL");apps/engine/test/engine/gates/recorte_da_tarefa_test.exs("RN-885: o preflight é na porta PUBLICADA, nas duas origens, e confere o header") - Origem: AT-535
RN-890 — O QA cobra o código de saída do comando de linha, na recusa também
- Regra: a instrução do QA de Automação manda conferir, no teste que
prova um comando de linha da entrega (CLI, script do
package.json), o código de saída 0 no caminho feliz e diferente de 0 quando o comando recusa a entrada; teste sem essa conferência na recusa é regra sem cobertura e vai paraitens. A instrução da QA-estratégia pede, emcriteriosExecutaveis, esses dois critérios para todo comando que recusa entrada. É instrução de agente: nenhum gate novo, nenhuma mudança de veredito ou de estrutura. - Onde:
apps/engine/lib/engine/gates/qa_automacao_agent.ex:250(instrucao_de_codigo_de_saida),apps/engine/lib/engine/gates/qa_estrategia_agent.ex:130(instrucao_de_codigo_de_saida) - Teste:
apps/engine/test/engine/gates/qa_automacao_agent_test.exs("a instrução cobra código de saída 0 no feliz e diferente de 0 na recusa");apps/engine/test/engine/gates/qa_estrategia_agent_test.exs("os arquivos da entrega entram na mensagem") - Origem: AT-542 (TP-01, 17ª:
npm run user:createrecusava a senha e saía com 0)
RN-893 — O PO declara a borda do valor informado no RF e no DoD
- Regra: a instrução de kickoff do PO (com ou sem event log) traz uma
terceira obrigação: todo RF que recebe um valor informado pelo usuário
(data, número, texto, lista, limite) declara, no RF e no DoD, se a borda —
passado/futuro, zero, vazio, negativo, o próprio limite — é ACEITA ou
RECUSADA (com o status HTTP/erro); o exemplo é a expiração no passado. Sem
saber, o PO chama
declare_open_questioncomafeta.historiase uma sugestão, nunca inventa nem omite. Régua de PROMPT, no molde da RN-790: nenhuma ferramenta recusa história sem borda. O PO NÃO passa a reescreverbusiness_rule; Criativo, QA, schema e gate não mudam. - Onde:
apps/engine/lib/engine/agents/po_server.ex:703(obrigacoes) - Teste:
apps/engine/test/engine/agents/po_server_test.exs:438(a instrução de kickoff traz a obrigação da borda) e:460(sem event log, o kickoff ainda a carrega) - Origem: AT-541 (TP-01 de 14/10: o entregável aceitou
expiresAtno passado e o link nasceu expirado, 410; o RF da história só dizia que a expiração é opcional), decisão do dono de 15/10
RN-895 — A recusa de argumento diz o que chegou e o formato esperado
- Regra: revisa a RN-719 e a RN-725. Campo declarado
array/objectque chega como texto fora de JSON, ou como número/booleano/tipo trocado, é recusado antes da ferramenta com "o campo X chegou como; envie ", com exemplo ( ["item 1","item 2"],{"chave":"valor"}), nunca com o valor. A frase doemit_plano_de_testepassou a pôr as chaves entre crases, e por isso a recusa leva as chaves recebidas. - Onde:
apps/engine/lib/engine/harness/argumentos_de_ferramenta.ex:84(coagir);apps/engine/lib/engine/gates/tools/emit_plano_de_teste.ex:73(run) - Teste:
apps/engine/test/engine/harness/argumentos_de_ferramenta_test.exs:193e:201(o formato e o tipo recebido) e:210(as chaves recebidas) - Origem: AT-583 (TP-01 18ª e 19ª: o
dodchegou como checklist markdown e o plano de teste foi recusado 4 vezes sem dizer o que veio)
RN-896 — O epic_id é validado antes da api
- Regra:
create_storyrecusaepic_idque não é UUID ("epic_id inválido; …"). Quando o PO já criou épicos neste processo (epicos_conhecidos) e o id não é um deles, relê o backlog UMA vez (list_backlog): se o id está lá, aceita; senão recusa listandoid=títulodo backlog (teto de 20 linhas, dizendo o total se truncar, ADR 0060). Mapa vazio ou leitura do backlog que falha é "não sei" e não recusa — a api decide. - Onde:
apps/engine/lib/engine/harness/tools/create_story.ex:57(epico_valido) - Teste:
apps/engine/test/engine/harness/argumentos_de_ferramenta_test.exs:224e:233 - Origem: AT-584 (o qwen mandou
EPIC_ID_PLACEHOLDER_NAO_USAR_AQUIe a api respondeu com o nome do DTO)
RN-897 — O corte do provider por max_tokens num gate é origem modelo
- Regra: o
agent.errordos gatesqa-estrategia,qa-automacao,qa-performance-segurancaeappsecclassifica "Tool calls cutoff by max_tokens" comomodelo(o raciocínio comeu o teto, RN-059); qualquer outro erro de LLM segueinfra. - Onde:
apps/engine/lib/engine/gates/origem_do_erro.ex:10(de_llm) - Teste:
apps/engine/test/engine/gates/origem_do_erro_test.exs:7 - Origem: AT-586 (BaseTen 502 gravado como
infra)
RN-898 — Resposta cortada pelo teto, sem texto algum, fecha o turno dita
- Regra: o turno de agente conversacional cuja última resposta de LLM foi
cortada pelo teto de saída (RN-737) e que não escreveu texto em
volta nenhuma NÃO fecha calado: a linha "Resposta cortada pelo limite de
tamanho." (en: "Response cut off by the length limit.") vira o fecho de uma
agent.responsecommodelName, e a bolha mostra "Continuar de onde parou" (RN-749). Vale para os sete conversacionais, que gravam o fecho porpayload_do_turno/2. Fere a RN-059 e a RN-163 quando falta. - Onde:
apps/engine/lib/engine/agents/texto_do_turno.ex:65(payload_do_turno) - Teste:
apps/engine/test/engine/harness/resposta_cortada_test.exs:104 - Origem: AT-570 (TP-01 18ª: a 3ª chamada do PO saiu 8192/8192 e o turno
fechou em
idlesemagent.response)
RN-899 — Resposta vazia sem corte vira agent.error de origem modelo
- Regra: o fecho do laço conversacional sem texto, sem ferramenta e sem
corte grava
agent.errorcom origemmodeloe a frase "o modelo não devolveu resposta" (FalhaDeTurno), nos sete servidores. É fala do agente para o heartbeat (RN-581): a sessão com turno calado fechava porheartbeat_timeout26 s depois de o usuário sair da tela. Presença por aba do projeto NÃO entra (RN-064). - Onde:
apps/engine/lib/engine/agents/falha_de_turno.ex:63(origem),apps/engine/lib/engine/agents/po_server.ex:327(gravar_texto_do_turno) - Teste:
apps/engine/test/engine/agents/po_server_test.exs:194eapps/engine/test/engine/agents/texto_do_turno_test.exs:96 - Origem: AT-572 (TP-01 18ª, achado 4)
RN-900 — write_file recusa reescrever o README e apagar a seção de uso da Infra
- Regra:
write_filesubstitui o arquivo inteiro. Quando opathé um README (README*), o arquivo atual tem a seção## Rodar na sua máquinae o conteúdo novo não a traz, a ferramenta recusa com erro nomeado e não escreve nada. Trocar um trecho exige reescrever o README completo, com a seção. O título é uma constante só (titulo_secao_de_uso/0), usada também pelos prompts da Infra e do dev, para que a recusa e a instrução não divirjam. Não é gate: a recusa é da própria ferramenta, e não cobre escrita peloterminal(cat >,sed -i). - Onde:
apps/engine/lib/engine/harness/tools/write_file.ex:62(recusa_de_secao_perdida) - Teste:
apps/engine/test/engine/harness/tools/write_file_test.exs:41(a reescrita sem a seção é recusada e o arquivo fica intacto); o caminho feliz (README com a seção reescrito com a seção, e arquivo que não é README) está nas primeiras duas provas do mesmo arquivo - Origem: AT-571 (TP-01 18ª de 19/10: o dev agent reescreveu o README ao corrigir um parecer do QA e a seção de uso que a Infra escrevera sumiu; nenhum gate notou)
RN-903 — O 429 do provider é limite de uso do modelo, não falha de rede
- Regra: a task que o dev agent bloqueia porque o modelo respondeu 429
(código
rate_limit, ou texto com(429)ou "limite de uso") diz no motivo "limite de uso do modelo atingido (429)", e a retomada automática diz que, se repetir, é para trocar o modelo do dev agent. Nunca diz "falha passageira de rede". A origem segueinfrae a retomada da RN-864 não muda (até 3, depois de 1, 3 e 10 min; pausa de 60 s, que odev.idlenomeia como "depois de limite de uso do modelo"). A falha de rede segue com o texto de sempre. - Onde:
apps/engine/lib/engine/dev/dev_agent_server.ex:1202(limite_de_uso?),apps/engine/lib/engine/dev/dev_agent_server.ex:1190(motivo_da_parada),apps/engine/lib/engine/dev/dev_agent_server.ex:1062(bloquear_passageira),apps/engine/lib/engine/dev/agent_io.ex:293(pausar_por_rede) - Teste:
apps/engine/test/engine/dev/dev_agent_server_test.exs("429 do provider: o motivo diz limite de uso do modelo, não rede"; falha: "falha de rede continua com o texto de rede, não de limite") - Origem: AT-594 (TP-01 20ª, 19/10: o 429 do
laguna-s-2.1:freebloqueou as tarefas com "falha passageira de rede", e o diagnóstico dizia limite de uso)
RN-904 — O corte do teto de saída não repete a chamada com o catálogo inteiro
- Regra: quando o Jev restringe o menu e o modelo responde SEM ferramenta,
o passo é repetido UMA vez com o catálogo inteiro (RN-625) — exceto quando a
resposta veio CORTADA pelo teto de saída (
truncated). O corte não é erro de menu: a volta com o catálogo inteiro só aumentaria o prompt e gastaria outro teto de saída. Vale nas duas fachadas (llm_turnellm_turn_stream, esta só sem texto já entregue). Quem trata o corte é a retentativa da RN-780 noToolLoop, que não muda. - Onde:
apps/engine/lib/engine/harness/roteamento_de_ferramenta.ex:126(repetir_com_catalogo_inteiro?) - Teste:
apps/engine/test/engine/harness/roteamento_de_ferramenta_test.exs:274(resposta vazia cortada não repete, nos dois caminhos; sem corte, repete) - Origem: AT-578 (EP-026, levantamento de 21/10): o 1º turno do PO na 18ª rodada gastou duas voltas de 8192 tokens de saída.
RN-905 — O teto de passos diz o que foi gravado e o que falta, e oferece continuar
- Regra: quando um agente conversacional para no limite de passos, o evento
do teto (
toolloop.limit_reached; no Criativo, oagent.errorcomreason: "limite_de_iteracoes") levagravado, a linha da RN-731 lida ANTES de o texto do turno ser gravado (ela sai ali e não naagent.response, nunca nas duas). O PO leva tambémregrasSemHistoria, ouncoveredCountquefechar_handoffjá lê; a leitura que falha deixa a chave ausente, nunca erro. O fio mostra o motivo sem jargão, a linha gravada, as regras sem história e o botão "Continuar de onde parou" (RN-749, mensagem do usuário, que a fila da RN-673 absorve com turno em curso) só para os sete conversacionais com a sessão ativa;dev-*/qa-*ganham o texto, sem botão. O botão nunca continua sozinho. - Onde:
apps/engine/lib/engine/agents/po_server.ex:254(run_turn);apps/web/src/routes/session-timeline-montagem.tsx:1094(conversacional) - Teste:
apps/engine/test/engine/agents/po_server_test.exs:548(gravado e regrasSemHistoria; sem a leitura, sem a chave);apps/web/src/routes/session-timeline-montagem.falha-de-turno.test.tsx:108(bolha, botão para o mesmo agente; sem botão emdev-*e sessão inativa) - Origem: AT-590 (EP-031).
RN-908 — O kickoff do PO pede o backlog em lote, depois de ler, em camadas
- Regra: o kickoff do PO manda, nesta ordem: ler
listar_regras_de_negocioelistar_backlogna mesma resposta, sem escrita junto; depois todos os épicos numa resposta só; depois todas as histórias, com oepic_iddevolvido; depois todas as tarefas, com ostory_iddevolvido. Nunca recria épico que já existe, e no máximo cerca de 10 chamadas de ferramenta por resposta. Motivo medido: sem o pedido de lote, o nemotron faz 1 ferramenta por resposta e o qwen faz 3; com ele, 9 em cada (parallel_tool_callsnão mudou nada). Ler primeiro é o que impede o qwen de criar épicos sem ler o backlog. Não altera o teto@max_iterations(RN-166). - Onde:
apps/engine/lib/engine/agents/po_server.ex:617(build_kickoff) - Teste:
apps/engine/test/engine/agents/po_server_test.exs:518(leitura antes da escrita; lote em camadas épicos → histórias → tarefas) - Origem: sonda da AT-591 (21/10), implementada na AT-596 (EP-026).
RN-913 — Falha igual repetida de ferramenta no turno corrige na 2ª e para na 3ª
- Regra: em cada turno, o harness conta as falhas IGUAIS de ferramenta (mesma ferramenta e mesmos argumentos,
tool.resultok: false). Na 2ª, o resultado devolvido ao modelo ganha a correção "esta chamada já falhou igual… mude os argumentos ou use outra abordagem". Na 3ª, o laço PARA e gravaagent.errorcom origemmodelo(reason: falha_repetida_de_ferramenta), nunca em silêncio (RN-059, RN-163). Falhas diferentes não somam, e o contador zera a cada turno. Vale para os sete conversacionais e para oToolLoop, pelo despacho único da RN-719; cada laço para pelo desfecho que já tinha (os conversacionais pelo caminho do teto de iterações, com oagent.errorno lugar dotoolloop.limit_reached; oToolLoopdevolvendo{:ok, ctx}comlast_error). - Onde:
apps/engine/lib/engine/harness/argumentos_de_ferramenta.ex:26(executar),apps/engine/lib/engine/harness/falha_repetida.ex:30(registrar),:51(parou?),:56(narrar),apps/engine/lib/engine/harness/tool_loop.ex:123(parou_ou_loop) - Teste:
apps/engine/test/engine/harness/falha_repetida_test.exs(2ª corrige, 3ª para noToolLoop, falhas diferentes não somam, zera entre turnos) - Origem: AT-588 (EP-026), decisão do dono de 22/10 (K=2 corrige, K=3 para).
RN-918 — O agente escreve o próprio plano com registrar_plano, e o último agent.plan é o vigente
- Regra:
registrar_planogravaagent.plancom o objetivo (até 200 caracteres) e de 1 a 12 passos de até 160 caracteres, cada umpendente,em_curso,feitooudescartado. Cada chamada manda o plano INTEIRO e substitui o anterior (o vigente é o últimoagent.plando ator). Acima do teto a ferramenta RECUSA nomeando o limite, nunca corta. Não éproposed_action(escrever o plano não é efeito externo) nem artefato (fora deferramentas_de_artefato/0) e a descrição não nomeia idioma (RN-667). Têm a ferramenta os seis conversacionais com o Infra Lead, o dev e os subagentes de QA e AppSec; ficam fora o subagente de validação de arquivo da Infra e ocontext-manager. AReidratacaomostra só o último plano de cada ator, como uma nota, e pula otool.call/tool.resultda ferramenta. - Onde:
apps/engine/lib/engine/harness/tools/registrar_plano.ex:84(run),apps/engine/lib/engine/agents/reidratacao.ex:182(so_o_ultimo_plano_de_cada_ator) - Teste:
apps/engine/test/engine/agents/diario_vivo_test.exs(registrar_plano: grava, substitui, recusa acima do teto;Reidratacao: só o último plano) - Origem: AT-582 (EP-031), decisão do dono de 27/10; ADR 0226.
RN-919 — O diário é a projeção do log mais o plano, sem tabela, e diz o que está confirmado e o que ficou sem desfecho
- Regra:
Engine.Agents.Diariolê a cauda do log do agente, a partir do marco do turno (agent.status: working, odev.workingda tarefa ou oagent.turn_resumeddo subagente de gate), e devolve o plano vigente, os efeitos confirmados (tool.resultok,proposed_action.executed, artefatos, commit preservado), as chamadas sem desfecho, as ações pendentes (proposed_action.createdsem decisão), a última atividade e se a retomada automática já foi usada. A fonte de verdade continua o event log.RetomadaDoTurnotransforma o diário numa mensagemsystemEFÊMERA (teto de 2 000 caracteres, depois deDecisoesDoProjetoe antes dePerfilDoAutor; noToolLoop, porctx[:retomada]): onde parou, o plano, o que não repetir, o que conferir antes e as ações pendentes que não se propõem de novo. Nunca é mensagem falsa do usuário. Push e PR são repetíveis (a PR aberta é reaproveitada, RN-812); merge NUNCA na retomada (RN-418); o engine não reexecuta chamada sem desfecho, quem decide é o modelo, depois de conferir o estado. - Onde:
apps/engine/lib/engine/agents/diario.ex:85(de_eventos),:255(retomada_usada?),apps/engine/lib/engine/harness/retomada_do_turno.ex:69(texto) - Teste:
apps/engine/test/engine/agents/diario_vivo_test.exs(Diario.de_eventos/2,RetomadaDoTurno, marco por opção e retomada usada) - Origem: AT-582 (EP-031); ADR 0226.
RN-920 — O turno conversacional que o reinício matou é retomado UMA vez no boot, se recente
- Regra: revisa a RN-586. A varredura de boot (
TurnoOrfao.varrer/2) retoma o turno órfão cujo agente teve atividade há no máximo 900 s (:retomada_automatica_janela_segundos, a mesma régua da RN-823, medida pela última atividade do agente e não peloworking) e que ainda não foi retomado: gravaagent.error(turno_interrompido_por_reinicio,retomada: "automatica"),agent.status: idleeagent.turn_resumed(automatica: true) e acorda o agente, cujoinit/1agenda:retomar_turno_interrompidoANTES da entrega da fila (RN-673). O teto é durável no log: umagent.turn_resumeddo agente entre o último status não-workinge oworkingórfão. Turno velho ou já retomado é só fechado, com a frase apontando o botão (RN-921). Ofechar_ao_subir/3doinit/1continua só fechando, e nó mudo conta como vivo (na dúvida nem fecha nem retoma). Resíduo declarado: dois nós subindo juntos, sem se enxergarem, podem retomar o mesmo turno duas vezes. - Onde:
apps/engine/lib/engine/agents/turno_orfao.ex:90(varrer),:181(retomar),apps/engine/lib/engine/agents/turno_assincrono.ex:430(retomada_ao_subir),:253(tratar_resultado) - Teste:
apps/engine/test/engine/agents/diario_vivo_test.exs(TurnoOrfao.varrer/2: retomada automática 1× no boot,TurnoAssincrono: retomada no init);apps/engine/test/engine/agents/reidratacao_dos_seis_test.exs - Origem: AT-582 (EP-031), decisão do dono de 27/10; ADR 0226.
RN-922 — O dev agent que cai dentro da janela retoma do commit da MESMA tarefa, uma vez
- Regra: revisa a RN-742 (e cita a RN-778, que continua só fechando o dev sem linha durável). O dev
workingreidratado, com a última atividade há no máximo 900 s e semdev.workingcause: "retomada"nesta tarefa, preserva o worktree como commit (RN-743), readota a MESMA tarefa (a branch dela, RN-779), gravadev.workingcomcause: "retomada"e o commit, e roda o laço com a mensagem do diário. A tarefa nunca sai dein_progress. A 2ª queda, a queda fora da janela, oawaiting_approvale qualquer falha da retomada bloqueiam como antes, e o "desbloquear" manual vira o "retomar do commit". Merge nunca ocorre aqui (RN-418). O contador de falha passageira da RN-864 é outra régua e não muda. - Onde:
apps/engine/lib/engine/dev/dev_agent_server.ex:1290(retomada_do_commit),:1316(retomar_do_commit) - Teste:
apps/engine/test/engine/dev/retomada_do_commit_test.exs(queda recente retoma; 2ª queda, fora da janela eawaiting_approvalbloqueiam) - Origem: AT-582 (EP-031), decisão do dono de 27/10; ADR 0226.
RN-923 — O resgate automático de gate vale uma vez por ciclo, no boot e no tick, e o "Retomar gate" zera o contador
- Regra: revisa a RN-823 e a RN-722.
engine.gate_states.auto_resumesconta os redespachos automáticos do ciclo (no boot e no tick); com 1 já usada, o segundo resgate ESTACIONA (gate.rescue_parked,motivo: "retomada_automatica_ja_usada") e espera o "Retomar gate" (RN-724), que não tem teto e ZERA o contador. O contador fica fora doon_conflictdo upsert (sobrevive aos passos do ciclo) e some com a linha quando o ciclo termina. O estacionamento de 2 h não muda. Os subagentes de gate ganhamregistrar_plano; no redespacho oToolLooprecebectx[:retomada]com o plano do últimoagent.turn_resumeddo subagente, e a área continua sendo redespachada inteira. Resíduo declarado: um ciclo novo pode pegar um marco pendente com menos de 15 min, e o subagente de gate não abreworking, então a retomada dele leva só o plano. - Onde:
apps/engine/lib/engine/gates/gate_rescuer.ex:212(retomar_ou_estacionar),:359(retomar_estacionado),apps/engine/lib/engine/gates/gate_state.ex:37(auto_resumes) - Teste:
apps/engine/test/engine/gates/gate_rescuer_test.exs:501(teto de 1 retomada automáticano tick e no boot, e o manual zerando) - Origem: AT-582 (EP-031), decisões do dono de 27 e 28/10; ADR 0226.
RN-924 — A correção de gate que chega ao Infra Lead com turno em curso é durável
- Regra: revisa a RN-617, no molde da fila da RN-673.
InfraLeadServer.correct/3gravainfra.correction_queued(id,gate,reason, diagnóstico até 2 000 caracteres) ANTES do cast e, sem processo, acorda o agente;iniciar_correcao/2gravainfra.correction_delivered(id). Oinit/1reconstrói as pendentes (queued menos delivered) e as entrega depois do turno retomado e da fila de mensagens, e oRehydratoracorda o Infra Lead que tem correção pendente no boot. Entregue não é reentregue. O Infra Lead também temregistrar_planoe a retomada da RN-920. - Onde:
apps/engine/lib/engine/infra/infra_lead_server.ex:1649(correct),:408(iniciar_correcao),:200(init) - Teste:
apps/engine/test/engine/infra/infra_lead_server_test.exs:1980(correct/3 grava infra.correction_queued antes do cast; a pendente sobrevive ao reinício e a entregue não é reentregue) - Origem: AT-582 (EP-031); ADR 0226.
RN-940 — O gate de QA-automação sobe uma instância de QA efêmera, testa nela e a remove
- Regra: em
container/mounted, antes do laço oQaAutomacaoAgentpropõeqa_environment_start(semeadaauto_approve); a subida que falha termina o gate com desfecho nomeado de origeminfra(RN-059). Oterminaldo QA levaalvo: {qa: true, taskId}vindo do ctx do agente, nunca do modelo, e roda na instância; os demais agentes não mudam. A instância é removida em sucesso, falha, exceção e teto esgotado, salvo laço parado esperando decisão.runnersegue sem instância. O boot e o tick doGateRescuerchamam osweepcom as tarefas degate_statesqanão estacionadas; falha só loga.run.qaé validado no engine com a mesma régua da api (RN-935). - Onde:
apps/engine/lib/engine/gates/qa_automacao_agent.ex:78(subir_instancia),apps/engine/lib/engine/gates/qa_automacao_agent.ex:120(com_remocao),apps/engine/lib/engine/harness/hooks/action_pipeline.ex:46(maybe_put_alvo),apps/engine/lib/engine/gates/gate_rescuer.ex:102(varrer_instancias_de_qa),apps/engine/lib/engine/infra/instrucoes_de_execucao.ex:133(qa). - Teste:
apps/engine/test/engine/gates/qa_automacao_agent_instancia_test.exs,apps/engine/test/engine/infra/instrucoes_de_execucao_qa_test.exs. - Origem: AT-612 (EP-030), ADR 0229; depende da api (#1146).
RN-944 — O QA de automação recebe o pedido da volta anterior, o plano é lido por tipo e o dev sabe onde o write_file escreve
- Regra: (a) o
changes_requestedmais recente do atorqapara a MESMA task entra nodev_contextcomo:pedido_anterior, e oQaAutomacaoAgentacrescenta à mensagem a lista de itens e a instrução de dizer noresumose cada um foi atendido; sem pedido anterior (primeira volta,approved, outra task, lista vazia) a mensagem é a de sempre. É insumo, nunca régua nova de veredito (molde do ADR 0192): o teto de correções (RN-015) e oqa_verdictúnico por ciclo não mudam. (b)plano_ja_emitido/3lê por tipo (Reidratacao.eventos_do_tipo/3), não a janela dos 200 mais recentes: o plano da task não sai da janela nem é pago de novo (cumpre a RN-674). (c) O ambiente dedev-*/qa-*diz quewrite_file/read_fileusam caminho relativo à raiz do repositório e quecdnoterminalnão persiste (cada chamada ésh -cnovo emSystem.cmd ... cd: dir,terminal_executor.ex). - Onde:
apps/engine/lib/engine/gates/qa_lead_server.ex:784(pedido_anterior),apps/engine/lib/engine/gates/qa_lead_server.ex:743(plano_ja_emitido),apps/engine/lib/engine/gates/qa_automacao_agent.ex:323(com_o_pedido_anterior),apps/engine/lib/engine/harness/ambiente_do_agente.ex:102(montar). - Teste:
apps/engine/test/engine/gates/qa_automacao_agent_test.exs,apps/engine/test/engine/gates/qa_lead_server_test.exs,apps/engine/test/engine/harness/ambiente_do_agente_test.exs. - Origem: AT-616 (EP-030), TP-01 (21ª) de 30/10/2026.
RN-945 — Os agentes de QA nomeiam o corte pelo teto de saída em vez de "o modelo parou"
- Regra: estende a RN-780 aos dois agentes de QA: quando o
ToolLoopdevolvecorte_pelo_teto: true,QaEstrategiaAgenteQaAutomacaoAgentnarram, com origemmodelo, que a resposta foi só raciocínio e o teto de saída a cortou duas vezes (texto único emEngine.Gates.MotivoDoCorte). Sem o marcador, o diagnóstico antigo ("parou sem chamar ...") continua. A falha do plano segue sem segurar a revisão (RN-674) e o teto de iterações não muda. - Onde:
apps/engine/lib/engine/gates/qa_estrategia_agent.ex:265(diagnostico_de_parada),apps/engine/lib/engine/gates/qa_automacao_agent.ex:427(diagnostico_de_parada),apps/engine/lib/engine/gates/motivo_do_corte.ex:8(texto). - Teste:
apps/engine/test/engine/gates/qa_estrategia_agent_test.exs,apps/engine/test/engine/gates/qa_automacao_agent_test.exs. - Origem: AT-618 parte 1 (EP-030), TP-01 (21ª) de 30/10/2026.
RN-946 — O corte pelo teto no meio do texto também é nomeado pelos agentes de QA
- Regra: estende a RN-945/RN-780: resposta
truncatedCOM texto e sem chamada (o raciocínio comeu o teto e o texto saiu cortado) não é "o modelo parou": oToolLoopdevolvecorte_com_texto: true, chave PRÓPRIA (nãocorte_pelo_teto, que o dev agent e a RN-780 casam), eQaEstrategiaAgenteQaAutomacaoAgentnarram, com origemmodelo, que o teto cortou a resposta no meio do texto antes da ferramenta de término (MotivoDoCorte.texto_com_texto). Não há retentativa nova (mudaria laço e custo); resposta não cortada com texto segue "parou sem chamar ...". A mesma lacuna no dev agent fica fora. - Onde:
apps/engine/lib/engine/harness/tool_loop.ex:256(parada_sem_chamada),apps/engine/lib/engine/gates/qa_estrategia_agent.ex:265(diagnostico_de_parada),apps/engine/lib/engine/gates/qa_automacao_agent.ex:427(diagnostico_de_parada),apps/engine/lib/engine/gates/motivo_do_corte.ex:9(texto_com_texto). - Teste:
apps/engine/test/engine/harness/tool_loop_test.exs,apps/engine/test/engine/gates/qa_estrategia_agent_test.exs,apps/engine/test/engine/gates/qa_automacao_agent_test.exs. - Origem: AT-620 (EP-030), TP-01 (22ª) de 31/10/2026.
RN-955 — GET interno com conexão envenenada do pool é repetido UMA vez
- Regra: complementa a RN-742 (AT-428). A resposta atrasada de um pedido que estourou o
receive_timeoutfica no socket de uma conexão do pool do Finch; o GET seguinte que a pega lê a resposta ANTIGA e lançaCaseClauseError {:status, ref, _}, que a checagem de trabalho pendente da RN-064 lia como "api fora do ar" e encerrava a sessão ativa. Oreq_getrepete o MESMO GET uma vez quando o erro é esse (o Finch já descartou a conexão); só GET, porque POST pode ter tido efeito. Erro de transporte verdadeiro NÃO é repetido, e a RN-064 não muda. - Onde:
apps/engine/lib/engine/sessions/engine_api_client.ex:2237(get_repetindo). - Teste:
apps/engine/test/engine/sessions/engine_api_client_isolado_test.exs(repete e devolve; falha na segunda não tem terceira; timeout verdadeiro não repete). - Origem: AT-642 (EP-030).
RN-956 — readiness e offer-infra-handoff sobem o agente antes de chamar, como message
- Regra: depois do fechamento e da reabertura (RN-649) os conversacionais não estão de pé, e as duas rotas chamavam o servidor sem
start_agent: o:noprocvirava 500 mascarado por "Stale conn". Agora recebemprojectIde sobem o Criativo/Arquiteto antes (idempotente); semprojectId, 400 nomeado. - Onde:
apps/engine/lib/engine_web/controllers/agent_command_controller.ex:348(readiness),:355(offer_infra_handoff). - Teste:
apps/engine/test/engine_web/controllers/agent_command_controller_test.exs(agente parado sobe; semprojectIdé 400). - Origem: AT-642 (EP-030).
RN-957 — "aborted" é falha de origem infra
- Regra: complementa a RN-059 e a RN-810. "aborted" é o socket da resposta do provider morrendo no meio do stream; o frame final sem
errorCodecaía emcodigo.FalhaDeTurno.origem/1o classificainfra(sem tocarfalha_de_rede?; a retentativa segue pela RN-864). - Onde:
apps/engine/lib/engine/agents/falha_de_turno.ex:87(origem),:110. - Teste:
apps/engine/test/engine/agents/falha_de_turno_test.exs("RN-957"). - Origem: AT-642 (EP-030).
RN-962 — O compose do entregável usa volume nomeado, nunca bind-mount relativo
- Regra: o Infra Lead pede VOLUME NOMEADO para dados persistentes e
.env.examplesó com as variáveis obrigatórias, sem valor.propose_infra_prrecusa, ANTES de abrir a PR, arquivo de compose com bind-mount relativo (- ./pasta:/destinoousource: ./pasta): a pasta nascerootna primeira subida e o app non-root não grava nela. A recusa é resultado de ferramenta (entra no laço, RN-163) e registratool.resultcomok: false. - Onde:
apps/engine/lib/engine/infra/infra_lead_server.ex:669(recusa_de_bind_mount_relativo). - Teste:
apps/engine/test/engine/infra/infra_lead_server_test.exs(volume nomeado passa; formas curta, com aspas e longa são recusadas). - Origem: AT-644 (EP-031).
RN-982 — As fontes de uma resposta são as leituras ok do próprio turno, registradas pelo servidor, nunca pelo texto do modelo
- Regra: irmã da RN-731. O servidor anota cada leitura
okdo turno (listagens de regras, backlog, métricas, ADRs e contratos,read_filee os trechos dorag_search) e oagent.responselevafontesFORA docontent:{tipo, rotulo, ref?, searchId?, chunkId?}, teto de 10, comfontesTotalquando corta (ADR 0060). Sem leitura, o campo não existe. Leitura com falha não é fonte. A tela diz "consultado", nunca "usado": é o que o agente leu, não o que ele aproveitou. Não muda ocontent, a reidratação (RN-580), otool.result(RN-589) nem o voto do RAG (RN-479..481). - Onde:
apps/engine/lib/engine/agents/fontes_do_turno.ex:37(anotar),apps/engine/lib/engine/agents/texto_do_turno.ex:65(payload_do_turno),apps/engine/lib/engine/harness/tool_loop.ex:419(anotar). - Teste:
apps/engine/test/engine/agents/fontes_do_turno_test.exs,apps/engine/test/engine/harness/tool_loop_test.exs("RN-982"),apps/engine/test/engine/agents/retomada_dos_seis_test.exseapps/engine/test/engine/infra/infra_lead_server_test.exs(a ferramenta nos sete). - Conversacionais (fatia 2): os SETE agentes conversacionais oferecem o MESMO
rag_searchdos dev agents (Engine.Harness.Tools.RagSearch, escopo do projeto, sem parâmetro de escopo, teto detop_kda ferramenta, ADR 0060), despachado porArgumentosDeFerramenta.executar/4(RN-719) e com o resultado notool.result(RN-589). Entra na spec e no despacho de cada servidor:criativo_server.ex:121/:469,po_server.ex:153/:437,arquiteto_server.ex:125/:402,dev_lead_server.ex:187/:580,ux_designer_server.ex:121/:350,staff_server.ex:121/:304,infra/infra_lead_server.ex:239/:1149. - Origem: AT-716 fatias 1 e 2 (EP-031).
RN-985 — A citação inline é marcada pelo modelo e validada contra as fontes do próprio turno; índice inventado não é mostrado
- Regra: quando o turno já tem fonte (RN-982), uma mensagem
systemEFÊMERA no fim da chamada (mesmo caminho da RN-622, nunca emstate.messages, sem nomear idioma, RN-667) lista as fontes como[n] rótulo(até 10, na ordem defontes) e pede "cite com [n]; não cite o que não está na lista" (teto de 250 tokens; 10 fontes medem ~55). No fecho, o servidor extrai os[n]do texto e gravacitacoesnoagent.responsesó com os que existem emfontes(1 <= n <= fontes.length), sem repetição e em ordem. OcontentNÃO é reescrito (RN-731): o marcador inválido fica no texto e a tela o descarta. Sem fontes, ou sem marcador válido, o campo não existe. - Onde:
apps/engine/lib/engine/harness/citacao_das_fontes.ex:22(anexar) e:43(marcar),apps/engine/lib/engine/sessions/engine_api_client.ex:760(llm_turn),apps/engine/lib/engine/agents/texto_do_turno.ex:65(payload_do_turno),apps/engine/lib/engine/harness/tool_loop.ex:206(marcar). - Teste:
apps/engine/test/engine/harness/citacao_das_fontes_test.exs,apps/engine/test/engine/harness/tool_loop_test.exs("RN-985"). - Origem: AT-716 fatia 3 (EP-031).
RN-990 — A imagem anexada vai ao LLM só no turno em que a mensagem chegou
- Regra: o
anexosdoagent/message([{id, nome, mime, tamanho}], até 4 por mensagem) entra no turno e na fila de mensagens (RN-673) JUNTO com o texto: ochat.message_queuedguarda os anexos, e as pendentes que viram UM turno juntam os anexos na ordem. Na PRIMEIRA chamada de LLM do turno, ocontentda última mensagemuservira a lista de partes (text+ uma{type:'anexo',id}por anexo); a chamada CONSOME os anexos, então as iterações seguintes do laço de ferramentas e os turnos seguintes mandam só o texto com o rótulo[anexos: a.png, b.txt](teto de 300 caracteres), que é também o que a reidratação devolve a partir dopayload.anexosdochat.message. A imagem nunca é reenviada (custo × iterações × turnos; decisão do dono, 02/11). O caminho é UM só para os sete conversacionais (TurnoAssincrono+Reidratacao), sem código por servidor; mensagem sem anexos continuastring. - Onde:
apps/engine/lib/engine/harness/anexos_do_turno.ex:56(anexar),apps/engine/lib/engine/agents/turno_assincrono.ex:324(receber_mensagem),apps/engine/lib/engine/agents/reidratacao.ex:461(usuario),apps/engine/lib/engine_web/controllers/agent_command_controller.ex:545(autor_do_turno). - Teste:
apps/engine/test/engine/agents/anexos_do_turno_test.exs. - Origem: AT-715 fatia 1, lane L3 (EP-031), ADR 0246.
RN-996 — O parecer de QA leva o resultado por teste lido do relatório JUnit pelo engine; sem relatório, diz por quê
- Regra:
emit_qa_verdictaceitarelatorioDeTestes(caminho relativo, em geral.brabo-qa/junit.xml). O ENGINE lê o arquivo (nunca o modelo declara), o apaga e põeexecucaoDeTestesnoqa_verdictdo subagente e no consolidado:{fonte: "junit", total, passaram, falharam, pulados, duracaoMs, suites, casosOmitidos}, ou{indisponivel: "sem_relatorio" | "relatorio_invalido" | "modo_runner", origem, detalhe?}. Sem relatório NUNCA vira "0 testes" (RN-470), e falha ao ler nunca muda o veredito (ADR 0054; só registra, a régua de aprovação segueexit 0mais a matriz). Teto (ADR 0060): 200 casos gravados, falhas primeiro; 20 mensagens de 500 caracteres; nomes de 200; contadores do relatório inteiro,casosOmitidospor subtração. O parser (SAX do:xmerl) recusa DOCTYPE/ENTITY (XXE), lê no máximo 2 MiB e 20 000 casos. Emrunnero arquivo vem pelo canal do runner (oexecjá existente, sem operação nova). Quem instala o reporter é o dev agent; o QA só lê. (A parte "como TAREFA que o Dev Lead inclui no módulo" foi superada pela RN-1016: o reporter é configurado dentro da primeira tarefa de código do módulo, nunca como tarefa própria.) - Onde:
apps/engine/lib/engine/gates/relatorio_junit.ex:34(ler),apps/engine/lib/engine/gates/relatorio_junit.ex:111(parse),apps/engine/lib/engine/gates/hooks/termination.ex:49(com_execucao_de_testes). - Teste:
apps/engine/test/engine/gates/relatorio_junit_test.exs(vitest, pytest, XXE recusado, teto de tamanho/casos/gravação, arquivo ausente/inválido/apagado),apps/engine/test/engine/gates/hooks/termination_test.exs,apps/engine/test/engine/gates/qa_lead_test.exs. - Origem: AT-687 (EP-031), lane engine.
RN-999 — A chamada irmã da pendente recebe recusa, para todo tool_use ter o seu tool_result
Revisa a RN-284 e a RN-593. Elas diziam que o laço PARA na chamada pendente "sem processar as chamadas seguintes". Parar continua; o que mudou é que as seguintes não ficam mudas.
- Regra: quando o modelo pede várias ferramentas na mesma resposta e uma delas fica pendente de aprovação (
{:pending, …}no Dev Lead,aguardando_aprovacaonoToolLoopdos dev agents e dos QAs), cada chamada IRMÃ que veio DEPOIS dela não roda, mas recebe, antes de suspender, uma mensagemtoolcom "não executada: o turno parou esperando a aprovação de; repita se ainda precisar" e um tool.resultok: falsecom o mesmo texto, peloResultadoDeFerramenta(RN-589). A pendente continua sem mensagem até o desfecho (RN-593). - Por quê: a Anthropic recusa com 400 a retomada que traz
tool_usesem otool_resultcorrespondente (achado 1 do TP-01 de 05/11:propose_execution_planmaisdeclare_open_question). A irmã não é executada, então a recusa diz isso e deixa o modelo repetir. - Onde:
apps/engine/lib/engine/agents/dev_lead_server.ex:582(recusar_irmas),apps/engine/lib/engine/harness/tool_loop.ex:225(recusar_irmas),apps/engine/lib/engine/agents/resultado_de_ferramenta.ex:44(recusa_de_irma). - Teste:
apps/engine/test/engine/agents/dev_lead_server_test.exs("irmã da pendente recebe recusa") eapps/engine/test/engine/harness/tool_loop_test.exs("irmã da chamada pendente recebe recusa"); mutação provada: sem a recusa os dois reprovam. - Origem: AT-739 (achado 1), Rodada 191, lane L1.
RN-1003 — O agente fecha a pergunta em aberto que o usuário já respondeu no chat, só quando a resposta é inequívoca
Estende a RN-862. Até aqui só a api fechava uma pergunta em aberto, pelo botão; o agente não tinha como.
- Regra: os quatro perguntadores (Criativo, PO, Arquiteto, Dev Lead) têm a ferramenta
answer_open_question(questionId,respostaopcional; sem ela, vale a sugestão do próprio agente). Ela chamaPOST /internal/projects/:projectId/open-questions/:questionId/answercom{sessionId, agentId, resposta?}. Quem fecha é a api (o mesmo núcleo do clique:decision_recordmaisopen_question.answered, rearmando o gate, RN-860), então o engine não grava evento.already_answeredvolta ao modelo como sucesso ("já fechada"); um 404 vira recusa nomeada (a pergunta não é deste projeto) e um 400, recusa citando a api. A frase central emFluxoDoTimemanda o modelo fechar só quando a resposta do usuário no chat é INEQUÍVOCA, e manter a pergunta aberta caso contrário. Para isso ser possível, as perguntas abertas (comquestionIde a sugestão) passam a chegar ao turno dos quatro perguntadores, não só ao do PO (DecisoesDoProjeto, a mesma leitura local com teto, ADR 0060). - Por quê: achado do TP-01 de 05/11: o usuário respondeu quatro perguntas no chat, o Criativo registrou as regras, e as perguntas ficaram na fila com "Aceitar sugestão". Decisão do dono (AT-741): o agente fecha.
- Onde:
apps/engine/lib/engine/harness/tools/answer_open_question.ex:43(run),apps/engine/lib/engine/harness/fluxo_do_time.ex:109(@frase_da_pergunta),apps/engine/lib/engine/harness/decisoes_do_projeto.ex:23(@agentes). - Teste:
apps/engine/test/engine/harness/tools/answer_open_question_test.exseapps/engine/test/engine/harness/decisoes_do_projeto_test.exs; mutação provada: quebrar a cláusula do 404, oalready_answered, o campoagentId, a frase da resposta inequívoca, a lista de perguntadores ou o registro de um agente reprova um teste. - Origem: AT-741 (lado do engine), Rodada 193, lane L3; o lado da api é a RN-1002.
RN-1006 — O Dev Lead cria a task de fundação do módulo de que outros dependem
Revisa a RN-678 (o papel do Dev Lead). ADR 0259.
- Regra: o Dev Lead ganha
create_task(oEngine.Harness.Tools.CreateTaskdo PO, com ummoduleobrigatório a mais). Ele mandaagentRole: "dev_lead"no corpo dePOST /internal/sessions/:id/taskspara o histórico da sessão creditar o Dev Lead; o PO não manda o campo e a api assumepo. Só roda para história cujomoduleIdscontémmodule; fora disso a chamada é recusada com mensagem nomeada ("a história não lista o módulo emmoduleIds") e nada é criado. Omodule_map_vigentemostra odependsOnde cada módulo, e o kickoff diz ao Dev Lead que módulo de que outro depende precisa de uma task de FUNDAÇÃO (esqueleto,package.json, entrypoint, contrato exposto) e que, se ninguém a tem, ele deve criá-la comcreate_taskANTES de propor o plano. A api recusa plano que deixa um módulo desses sem task ("X depende de Y, que não tem task: crie a task de fundação de Y", RN-1004); a recusa chega ao Dev Lead como qualquer outro erro depropose_execution_plan, então o laço segue. - Por quê: achado do TP-01 de 05/11: o plano deixou
persistenciaeapi-httpsem task e as tasks dependentes bloqueadas. O PO cria tasks antes de omodule_mapexistir, então só o Dev Lead consegue ver a lacuna. Decisão do dono (AT-742). - Onde:
apps/engine/lib/engine/agents/dev_lead_tools.ex(spec_create_task,run_create_task,module_map_vigente),apps/engine/lib/engine/agents/dev_lead_server.ex(tool_specs,run_tool("create_task"),build_kickoff). - Teste:
apps/engine/test/engine/agents/dev_lead_tools_test.exs("create_task do Dev Lead") eapps/engine/test/engine/agents/dev_lead_server_test.exs(kickoffdependsOn,create_tasknas specs); mutação provada: tirar odependsOn, a spec ou a checagem de módulo reprova um teste. - Origem: AT-742 (lado do engine), Rodada 194, lane L2; o lado da api são as RN-1004 e RN-1005.
RN-1010 — Conflito ao integrar a dev no claim vai para o agente; só o que ele não resolve bloqueia
Revisa a RN-779 (o conflito abortava e bloqueava) e espelha a RN-715 (conflito de merge na PR).
- Regra: quando a integração da
devatual no worktree da task conflita em arquivos, o engine NÃO aborta: o merge fica em curso com os marcadores nos arquivos, eadd_worktreedevolve{:error, {:conflito_de_integracao, worktree, files, reason}}. Orun_tasksegue então para oimplementnormal (registradev.workingcomcause: "integration_conflict") com UMA mensagem fixada a mais entre o prompt da task e o retrato do worktree: resolver os arquivos listados mantendo o que a task anterior fez e o que adevtrouxe, depoisgit addegit commit --no-editpeloterminal; NÃO rodargit mergenemgit merge --abort; só então implementar a task. Conflito sem arquivo conflitado mantém o comportamento antigo (aborta, string nomeada, bloqueia). - Guardas: (1) no
report_done, se oMERGE_HEADainda existe ou algum dos arquivos conflitados tem linha<<<<<<</>>>>>>>(o=======não é checado: é sublinhado de título em Markdown), o merge é abortado e a task bloqueia com o motivo da RN-779 ("conflito ao integrar a dev atual no worktree da task", origemcodigo), com o diagnóstico dizendo que o agente não resolveu;report_blockedbloqueia como antes. (2)preservar_emaborta um merge em curso ANTES do commit de WIP (RN-743), então um bloqueio no meio do conflito (teto, orçamento,report_blocked) nunca commita marcador; o caminho da RN-715 também fica protegido. - Por quê: 26ª rodada do TP-01 (06/11): duas tasks bloquearam no mesmo segundo pedindo ao usuário que integrasse a
devà mão, e desbloquear só repetia o ciclo. O conflito acontece ANTES do trabalho da task, então é o laço da task, não o de correção; os gates seguem julgando a branch no fim. - Onde:
apps/engine/lib/engine/dev/worktree_manager.ex(integrar_trabalho,conflito_nao_resolvido?,abortar_merge_em_curso,preservar_em),apps/engine/lib/engine/dev/dev_agent_server.ex(run_task,conflito_message,handle_outcomedoreport_done). - Teste:
apps/engine/test/engine/dev/worktree_manager_test.exs("conflito deixa o merge em curso com os marcadores", "preservar_em com merge em curso aborta e não commita marcador") eapps/engine/test/engine/dev/conflito_de_integracao_test.exs(git real: o agente resolve e chega aawaiting_gate; sem resolver, bloqueia sem marcador commitado); mutação provada: restaurar o abort, tirar a guarda dopreservar_em, a cláusula dorun_taskou a guarda doreport_donereprova um teste cada. - Origem: AT-747, Rodada 203, lane L2.
RN-1011 — O plano do Dev Lead é recusado quando uma task cita rota que o contrato declara em outro módulo
Complementa a RN-792 (rotas da história contra o contrato) e a RN-678 (a task é do módulo que o Dev Lead atribui). ADR 0200.
- Regra:
propose_execution_planlê o contrato de módulos vigente (list_module_contracts) e o backlog (list_backlog) depois de resolver os ids das tasks e ANTES dopropose_action. Para cada task planejada, extrai as rotasMÉTODO /caminhodo título e da descrição da task (RotasDoContrato.extrair/1); os donos de uma rota são os módulos cujo contrato a declara. Se o módulo atribuído à task não está entre os donos, a chamada devolve erro nomeando a task, a rota e o(s) módulo(s) dono(s), e mandando o Dev Lead mover a task para o dono ou separar a parte de domínio numa task do módulo de domínio comcreate_task. Rota ausente do contrato é ignorada (a RN-792 cobre). Falha ao ler o contrato ou o backlog não recusa: o plano segue e o resultado leva um aviso. - Por quê: 26ª rodada do TP-01 (06/11): o Dev Lead atribuiu "Implementar POST /horarios" a
scheduling, mas o contrato v3 declara essa rota emapi; o agente dev-scheduling bloqueou por falta de um validador de auth que sóapiconsome. A recusa vem antes da proposta porque, no modo automático, o plano roda assim que é proposto. - Onde:
apps/engine/lib/engine/harness/rotas_do_contrato.ex(donos_divergentes/3),apps/engine/lib/engine/agents/dev_lead_tools.ex(conferir_donos). - Teste:
apps/engine/test/engine/harness/rotas_do_contrato_test.exseapps/engine/test/engine/agents/dev_lead_tools_test.exs("tarefa que cita rota de OUTRO módulo"); mutação provada: comparar com qualquer módulo, ignorar os donos ou tirar a checagem dorun/2reprova um teste. - Origem: AT-748 (lane a), Rodada 203, lane L5.
RN-1012 — Task bloqueada pelo modelo acorda o Dev Lead, que pede ao Arquiteto o contrato que falta
- Regra: quando um dev agent encerra uma task com
report_blocked(origemmodelo), o engine entrega{:tarefa_bloqueada, %{task_id, modulo, reason, diagnosis}}ao Dev Lead peloEngine.Dev.Wake. Conflito de merge (codigo), gates esgotados (politica), crédito/infra e o teto de iterações NÃO o acordam. O Dev Lead transforma o aviso em mensagem de sistema: com o Dev Lead ocioso, um turno começa; com turno em curso, aprovação de plano pendente ou fila não vazia, o aviso vai para a fila de mensagens (limitada pelo teto da RN-673) e é lido quando o turno termina. O aviso manda conferir o contrato e o mapa de módulos (read_backlog), pedir ao Arquiteto compedir_ao_arquitetoe criar a task no módulo dono comcreate_task, ou dizer ao usuário o que falta. pedir_ao_arquiteto: grava o pedido como evento durávelchat.messagede atordev-leadna sessão (a mesma sessão do Arquiteto; aReidratacaolêchat.messagede qualquer ator) e o entrega ao Arquiteto como mensagem prefixada[Pedido do Dev Lead]. É um canal de agente para agente que não é handoff (handoff para agente ativo é recusado com 409). Com o Arquiteto ocupado, a mensagem entra na fila dele.- Contrato publicado:
declare_module_contracts, numa versão aceita pela api, entrega{:contrato_publicado, version}ao Dev Lead, que então cria a task no módulo dono e repropõe o plano. Recusa da api não entrega nada. - Limite (atalho): o Wake entrega no máximo uma vez (ADR 0045): sem processo do Dev Lead vivo (reinício do engine, ou fase de arquitetura) o aviso se perde, e resta o caminho humano (a lista de tasks bloqueadas no chat, RN-826). Desbloquear a task parada depois que a dependência é mergeada segue humano (RN-826/RN-777).
- Por quê: 26ª rodada do TP-01 (06/11): o dev bloqueou, corretamente, e ninguém agiu até o condutor pedir ao Arquiteto e ao Dev Lead à mão.
- Onde:
apps/engine/lib/engine/dev/dev_agent_server.ex(avisar_dev_lead),apps/engine/lib/engine/agents/dev_lead_server.ex(handle_infode:tarefa_bloqueadae:contrato_publicado,avisar),apps/engine/lib/engine/harness/tools/pedir_ao_arquiteto.ex,apps/engine/lib/engine/harness/tools/declare_module_contracts.ex. - Teste:
apps/engine/test/engine/dev/dev_agent_server_test.exs,apps/engine/test/engine/agents/dev_lead_server_test.exs,apps/engine/test/engine/harness/tools/pedir_ao_arquiteto_test.exs,apps/engine/test/engine/harness/tools/declare_module_contracts_test.exs; mutação provada: tirar oWake.deliverdoreport_blocked, movê-lo parablock_task/4(todas as origens), sempre começar o turno, apagar a cláusula dohandle_info, tirar oappend_eventda ferramenta e tirar oWake.deliverdo contrato reprova um teste cada. - Origem: AT-748 (b), Rodada 203, lane L6.
RN-1013 — Cada mensagem assistant do turno guarda os próprios blocos de raciocínio, e o prefixo enviado só cresce
- Regra:
Engine.Agents.RaciocinioDoTurnoguarda osthinkingBlocksda Anthropic de TODA chamada de LLM do turno, indexados pelo id do primeirotoolCallda mensagemassistantdaquela chamada.anexar_blocos/1dá a cada mensagemassistantcomtoolCallsos blocos da chamada que a produziu, não só os da última. Nada do que já foi enviado muda entre duas chamadas do mesmo laço: o prefixo da chamada N é prefixo exato da chamada N+1. Os blocos ficam em memória (nunca no log nem naReidratacao, RN-993) edescarregar/0os limpa no fim do turno. - Por quê: 26ª rodada do TP-01 (06/11): agentes que raciocinam tinham de 19% a 46% de cache de prompt, os que não raciocinam tinham de 62% a 82%. Os blocos iam só na última mensagem
assistant, então a anterior os perdia na chamada seguinte e toda entrada de cache gravada depois de uma rodada de raciocínio deixava de valer. - Onde:
apps/engine/lib/engine/agents/raciocinio_do_turno.ex(registrar,anexar_blocos), chamado porapps/engine/lib/engine/sessions/engine_api_client.ex(llm_turn,llm_turn_stream). - Teste:
apps/engine/test/engine/agents/raciocinio_do_turno_test.exs("3 chamadas seguidas"); mutação provada: guardar só os blocos da última chamada, ou não anexá-los às mensagensassistantanteriores, reprova um teste. - Origem: AT-749 (engine), Rodada 205, lane L2.
RN-1016 — O reporter JUnit é configurado na primeira task de código do módulo, nunca numa task própria
Revisa a RN-996 (quem instala o reporter e como o quê).
- Regra: o kickoff do Dev Lead (
build_kickoff) manda NÃO criar task só para o relatório JUnit. Oinitial_message/2do dev agent diz ao dev que, se o módulo ainda não produz relatório JUnit, ele configura o reporter nesta mesma task, com saída em.brabo-qa/junit.xml(vitest--reporter=junit --outputFile=.brabo-qa/junit.xml, pytest--junitxml=.brabo-qa/junit.xml), põe.brabo-qa/no.gitignoree nunca commita o relatório..brabo-qa/junit.xmlé o caminho que o QA lê e apaga (RN-996). - Por quê: 26ª rodada do TP-01 (06/11): cinco tasks "configurar JUnit no módulo X" rodaram em paralelo com as tasks de código sobre o mesmo
package.json, lock e README; a PR de scheduling foi refeita duas vezes por conflito e a de auth esgotou o ciclo de correção versionandotest-results/junit.xml. - Onde:
apps/engine/lib/engine/agents/dev_lead_server.ex(build_kickoff),apps/engine/lib/engine/dev/dev_agent_server.ex(initial_message/2). - Teste:
apps/engine/test/engine/agents/dev_lead_server_test.exs("o kickoff proíbe tarefa só para o relatório JUnit") eapps/engine/test/engine/dev/report_done_instrucao_test.exs("RN-1016"); mutação provada: restaurar o parágrafo antigo ou tirar a frase do dev reprova um teste. - Origem: AT-750, Rodada 205, lane L3.
RN-1020 — O Criativo e o PO retiram sozinhos uma regra de negócio obsoleta, citando a substituta ou a resposta do usuário
- Regra: a ferramenta
retire_business_rule(rule_id,reason,replaced_byequestion_idopcionais) só existe nos catálogos do Criativo e do PO. Ela chamaPOST /internal/projects/:projectId/business-rules/:ruleId/retirecom{sessionId, agentRole, reason, replacedBy?, questionId?}; a api valida e gravaartifact.business_rule_retired(RN-1018 da api). O agente retira por conta própria, sem perguntar ao usuário nem ao condutor, e sempre citareplaced_byouquestion_id. Toda recusa da api (papel_nao_retira_regra,retirada_sem_citacao,substituta_invalida,pergunta_nao_respondida,regra_ja_retirada,historia_ficaria_sem_regra,historia_com_tarefa_em_execucao, 404) volta ao modelo comoreason — message. Os prompts de identidade dos dois agentes dizem isso. - Leitores no engine: as listas de regras que o engine monta a partir do log de eventos da sessão (
CriativoServer.business_rule_refs, os kickoffs do PO e do Arquiteto) também leemartifact.business_rule_retirede descartam os ids retirados (Reidratacao.sem_regras_retiradas/1). Os leitores que vêm da api (listar_regras_de_negocio, backlog) já filtram. - Onde:
apps/engine/lib/engine/harness/tools/retire_business_rule.ex,apps/engine/lib/engine/sessions/engine_api_client.ex(retire_business_rule),apps/engine/lib/engine/agents/criativo_server.ex(tool_specs,dispatch_tool,business_rule_refs),apps/engine/lib/engine/agents/po_server.ex(tool_specs,run_tool,kickoff_instruction),apps/engine/lib/engine/agents/arquiteto_server.ex(kickoff_instruction),apps/engine/lib/engine/agents/reidratacao.ex(sem_regras_retiradas),apps/engine/lib/engine/harness/agents.ex. - Teste:
apps/engine/test/engine/harness/tools/retire_business_rule_test.exs; mutação provada: registrar a ferramenta em outro agente, tirar o papel ou oreplacedBydo corpo, engolir a recusa da api e não filtrar os ids retirados reprova um teste cada. - Origem: AT-755, Rodada 205, lane L8.
RN-1021 — Depois de um merge na main do projeto, a pasta do projeto avança até ela
Revisa as RN-664, RN-729, RN-779, RN-787, RN-797, RN-811 e RN-1010: o repositório do projeto é trunk, então a branch de trabalho que elas chamam de
devé amain.
- Regra: a branch de trabalho do repositório de um projeto é a
main(ProjectRepository.@branch_de_trabalho): o claim, a base do worktree, o alvo dopr_opene o diff do gate a usam. Quando a api mergeia uma PR nela (evento de outboxproject.trunk_merged, agregadoproject, payload{projectId, sessionId, mergedSha}), oEngine.Outbox.Draino encaminha aoEngine.Workers.TrunkMergedWorker, que chamaWorktreeManager.avancar_pasta/1: sob o mesmo lock:globaldofetchdo claim,git fetch originegit merge --ff-only origin/mainna pasta do projeto (modoscontaineremounted). Nunca rodareset,checkout -f,stashnem merge de verdade. Para projetosrunnerpassa pelo runner (RN-1027); é no-op para pasta sem a marca de pronto (o merge da infra vem antes do primeiro claim). - Recusas (a pasta não é tocada): branch atual não é
main(other_branch); árvore rastreada suja (dirty_tree, arquivos não rastreados ignorados);mainlocal não é ancestral deorigin/main(diverged); o próprio git recusa porque um arquivo não rastreado seria sobrescrito (untracked_conflict); ou ofetchfalha (fetch_failed). Cada uma é registrada na sessãosessionIdcomoworkspace.checkout_not_advancedcomreason,branchedetail; o sucesso éworkspace.checkout_advancedcomfrometo. - Por quê: a pasta ficava no commit da infra enquanto o trabalho andava (21ª e 22ª rodadas do TP-01); um usuário que seguisse o README rodava código velho.
- Onde:
apps/engine/lib/engine/dev/worktree_manager.ex:244(avancar_em),apps/engine/lib/engine/dev/worktree_manager.ex:207(avancar_pasta),apps/engine/lib/engine/workers/trunk_merged_worker.ex:21(perform),apps/engine/lib/engine/outbox/drain.ex:26(run_once),apps/engine/lib/engine/actions/workspace.ex:183(pronto?). - Teste:
apps/engine/test/engine/dev/worktree_manager_test.exs("trunk: guarda de projeto legado e avanço da pasta"),apps/engine/test/engine/workers/trunk_merged_worker_test.exs,apps/engine/test/engine/outbox/drain_test.exs("project.trunk_merged"); mutação provada: tirar o merge--ff-only, a checagem de árvore suja, de branch ou de divergência, a recusa de não rastreado, a cláusula do drain ou os eventos do worker reprova um teste cada. - Origem: AT-617, Rodada 208, lane L1.
RN-1023 — Projeto cuja dev está à frente da main recusa o claim, nomeando a migração
- Regra: em
WorktreeManager.create/3, depois dofetch, seorigin/devexiste eorigin/mainé ancestral estrito dela (adevtem commits que faltam namain), o claim falha com "projeto em gitflow antigo … Rode o script de migração para trunk da api (pnpm migrar-projeto-para-trunk, nobrabo-api) antes de continuar." e nenhum worktree é criado.devausente, igual, atrás ou divergente damainnão recusa. O script de migração mora na api (RN-1022). - Por quê: com a branch de trabalho fixada em
main, um projeto criado no gitflow antigo seria pego de umamainque nunca recebeu o trabalho mergeado: um erro silencioso. - Onde:
apps/engine/lib/engine/dev/worktree_manager.ex:176(recusar_projeto_legado),apps/engine/lib/engine/dev/worktree_manager.ex:139(criar). - Teste:
apps/engine/test/engine/dev/worktree_manager_test.exs("RN-1023"); mutação provada: tirar a checagem de "à frente", a de ancestral ou a recusa reprova um teste. - Origem: AT-617, Rodada 208, lane L1.
RN-1027 — No modo runner a pasta do projeto avança pelo canal do runner, e o avanço é reaplicado quando o runner entra
Estende a RN-1021: o mesmo fast-forward e as mesmas recusas, agora também para
execution_mode: runner.
- Regra:
WorktreeManager.avancar_pasta/1de um projetorunnerchamaRunnerGit.avancar_pasta/2, que manda os comandos git peloRunnerRouter.exec/5do canalterminal:<projectId>(sem mensagem nova; obrabo-runnernão muda): ofetchcom credencial da RN-676 (ADR 0193), depois a decisão. A decisão éWorktreeManager.decidir_avanco/1, uma função sobre um transportegitinjetado e compartilhado com o caminho local, então os dois nomeiam as mesmas recusas (other_branch,dirty_tree,diverged,untracked_conflict) efetch_failed, todas sob o mesmo lock:globaldofetchdo claim. Sem runner pronto (RunnerReadinessdiferente de:pronto) devolve{:recusado, :runner_offline, nil}sem levantar exceção, e o worker registraworkspace.checkout_not_advancedcomreason: runner_offline. O avanço é idempotente ("levar a pasta atéorigin/main"), então oEngineWeb.TerminalChannelo reenfileira (TrunkMergedWorker.reaplicar/1, único por projeto por 30 s) quando o runner entra, e oEngine.Outbox.Draintambém encaminhacontainer.runningaoTrunkMergedWorker. A reaplicação não levasessionId, então não registra evento. - Por quê: a pasta da máquina do runner ficava no commit da infra como a do servidor (AT-617); com o runner desligado nada podia ser feito na hora do merge, então o avanço espera por ele.
- Onde:
apps/engine/lib/engine/actions/workspace/runner_git.ex:231(avancar_pasta),apps/engine/lib/engine/dev/worktree_manager.ex:252(decidir_avanco),apps/engine/lib/engine/dev/worktree_manager.ex:207(avancar_pasta),apps/engine/lib/engine/workers/trunk_merged_worker.ex:53(reaplicar),apps/engine/lib/engine_web/channels/terminal_channel.ex:261,apps/engine/lib/engine/outbox/drain.ex:106(handlers_for). - Teste:
apps/engine/test/engine/dev/avancar_pasta_runner_test.exs,apps/engine/test/engine/workers/trunk_merged_worker_test.exs,apps/engine/test/engine/outbox/drain_test.exs("container.running"),apps/engine/test/engine_web/channels/terminal_channel_test.exs("RN-1027"); mutação provada: tirar a checagem de árvore suja, de branch ou de divergência, o merge--ff-only, a respostarunner_offline, o despachorunner, o fetch, o enfileiramento na entrada, a cláusula do drain ou a unicidade reprova um teste cada. - Origem: AT-617, Rodada 208, lane L7.
RN-1028 — Depois que a pasta do projeto avança, o processo do app é reiniciado no lugar, uma vez por rajada de merges
Estende as RN-1021 e RN-1027: o reinício só acontece depois que a pasta de fato andou.
- Regra: o
TrunkMergedWorkeragenda oEngine.Workers.ReiniciarAppWorkersó quandoavancar_pastadevolveu{:avancou, from, to}e o evento levasessionId; recusa, pasta já atualizada, no-op ou reaplicação (container.running, entrada do runner) nunca reiniciam e não registram eventoapp.*. O job é único por projeto (uniqueemprojectId) e atrasado 20 s, então merges em sequência viram UM reinício, feito com a pasta já namainmais recente. O worker chamaEngineApiClient.reiniciar_app/1(POST /internal/projects/:projectId/app/restart, corpo vazio, sempre 200; o cliente HTTP espera 130 s porque a api espera até 100 s) e registra na sessão do job:app.restarted;app.restart_skippedcom{motivo: "sem_app"}(semrun.app, ou modorunner); ouapp.restart_failedcom{motivo, exitCode?}. O payload é uma lista fechada (sem_container,tempo_esgotado,falha_no_exec,app_saiucom o seuexitCode; qualquer outro viradesconhecido; chamada que falha éfalha_na_chamada): NUNCA leva texto de log, porque o comando do app pode conter segredo. Falha só avisa: a pasta nunca é revertida. - Por quê: o app que o usuário testa roda a partir da pasta do projeto; sem reinício, ele segue servindo o código velho depois de um merge (AT-617).
- Onde:
apps/engine/lib/engine/workers/trunk_merged_worker.ex:21(perform),apps/engine/lib/engine/workers/reiniciar_app_worker.ex:20(agendar),apps/engine/lib/engine/workers/reiniciar_app_worker.ex:30(perform),apps/engine/lib/engine/sessions/engine_api_client.ex:2069(reiniciar_app). - Teste:
apps/engine/test/engine/workers/reiniciar_app_worker_test.exs; mutação provada: agendar sem o casamento:avancou, tirar a unicidade ou chaveá-la na sessão, tirar a cláusulasem_app, ou deixar texto livre no payload de falha reprova um teste cada. - Origem: AT-617, Rodada 208, lane L8.