Engine — parte 6 (desde RN-880)
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-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:698(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:698(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:708(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:1222(limite_de_uso?),apps/engine/lib/engine/dev/dev_agent_server.ex:1218(motivo_da_parada),apps/engine/lib/engine/dev/dev_agent_server.ex:1075(bloquear_passageira),apps/engine/lib/engine/dev/agent_io.ex:302(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:1101(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:622(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:1318(retomada_do_commit),:1344(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:2258(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:754(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:605(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, editando os arquivos comwrite_file; NÃO rodar git noterminal(o container não tem git; AT-798); o commit do merge é do SISTEMA, noreport_done(GitExecutor.commit/1, commit de dois pais); só então implementar a task. A readoção da RN-715 (adopt_integrando) integra amainatual do mesmo jeito e devolve a mesma tupla; a mensagem de correção diz que amainjá foi integrada e quais arquivos têm marcadores. Conflito sem arquivo conflitado mantém o comportamento antigo (aborta, string nomeada, bloqueia). - Guardas: (1) no
report_done, se algum dos arquivos conflitados ainda tem linha<<<<<<</>>>>>>>(o=======não é checado: é sublinhado de título em Markdown), (oMERGE_HEADNÃO conta: sem marcador, segue para a entrega e o sistema conclui o merge) 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 eterminalSEM git, exit 127: o agente resolve comwrite_file, chega aawaiting_gatee o HEAD tem dois pais; a readoção da RN-715 idem; sem resolver, bloqueia sem marcador commitado; as mensagens não citam git); mutação provada (AT-798): devolver oMERGE_HEADà guarda, recolocarnot adotando?ou tirar a guarda doreport_donereprova um teste cada; e também: 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:254(avancar_em),apps/engine/lib/engine/dev/worktree_manager.ex:217(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:186(recusar_projeto_legado),apps/engine/lib/engine/dev/worktree_manager.ex:149(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:217(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:2090(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.
RN-1058 — As perguntas abertas têm precedência no texto do turno, nunca somem caladas, e os perguntadores as listam com list_open_questions
- Regra: no texto de
DecisoesDoProjetoas perguntas abertas vêm ANTES das decisões. O orçamento de 3 000 caracteres serve primeiro as abertas (até 10, com oquestionIdinteiro) e depois as decisões, as mais recentes primeiro; o corte é por LINHA inteira, nunca no meio de umquestionId. Com pergunta fora do texto (pelo teto de itens ou de caracteres) o texto diz "Há N perguntas abertas no total; M listadas acima; uselist_open_questionspara ver todas", e o cabeçalho das abertas traz o total. A ferramentalist_open_questions(sem parâmetros; Criativo, PO, Arquiteto e Dev Lead) lê LOCAL o event log e devolve todas as abertas do projeto, de qualquer autor, mais antigas primeiro, até 50, cada uma comquestionId, pergunta, sugestão e autor, terminando em "total: N" (ou "nenhuma pergunta aberta"). A frase central do fluxo manda listar e fechar "todas" comanswer_open_question, de qualquer autor. Os tetos da RN-874 não mudam (ADR 0060). - Por quê: TP-01 27ª (07/11): com "aceito todas as 7", o Arquiteto fechou 5. O corte de 3 000 caracteres caiu dentro da linha da quinta pergunta, em silêncio, e duas ficaram fora do texto.
- Onde:
apps/engine/lib/engine/harness/decisoes_do_projeto.ex:116(montar),apps/engine/lib/engine/harness/decisoes_do_projeto.ex:76(abertas),apps/engine/lib/engine/harness/tools/list_open_questions.ex:34(run),apps/engine/lib/engine/harness/fluxo_do_time.ex:109(@frase_da_pergunta). - Teste:
apps/engine/test/engine/harness/decisoes_do_projeto_test.exs("as 7 do TP-01 27ª", "com mais abertas do que cabe"),apps/engine/test/engine/harness/tools/list_open_questions_test.exs,apps/engine/test/engine/harness/tools/answer_open_question_test.exs("os quatro perguntadores anunciam a ferramenta"); mutação provada: pôr as decisões antes das abertas reprova um teste; tiraropen_question.answeredda leitura da ferramenta reprova um teste. - Origem: AT-804, Rodada 250, lane L1.
RN-1059 — A execução com todos os dev agents parados e tarefa pendente atrás de bloqueio ou de agente travado grava execution.stalled e acorda o Dev Lead
Estende a RN-1012 (o aviso ao Dev Lead passa a cobrir o bloqueio de origem
codigoe o disjuntor, mas só quando a execução INTEIRA parou, nunca por tarefa) e preserva a RN-047 (o disjuntor segue gesto humano).
- Regra: o
AgentIoenfileira oEngine.Workers.ExecucaoParadaWorkerdepois dedev.idle,dev.idle_trippededev.credit_exhausted: um job por sessão (uniqueemprojectIdesessionId), com espera de 5 s para juntar a rajada. Operformsó age se TODAS as condições valem: (1) a sessão tem dev agents e todos estãoidleouidle_tripped(umworking,awaiting_gateouawaiting_approvalbasta para não fazer nada); (2) o backlog tem tarefa comstatus != done; (3) há tarefablockedou agenteidle_tripped. PR esperando merge sem bloqueio nem disjuntor não conta (AT-810). Então gravaexecution.stalled(actorKind: "system",actorId: "execucao") com{agentesOciosos: [agentId], agentesTravados: [{agentId, module}], tarefasBloqueadas: [{taskId, title, module, reason}] (até 10, reason até 200), tarefasPendentes: N}(contrato com a web) e entrega{:execucao_parada, payload}ao Dev Lead peloEngine.Dev.Wake, que abre turno (ou entra na fila) com o aviso que nomeia o que segura a execução e manda dizer ao usuário o que destrava. Não repete: se o último evento da sessão entreexecution.stalled,dev.working,dev.rearmedebacklog.task_unblockedé umexecution.stalledcom os mesmos ids de tarefa e de agente, nada é gravado. Falha de leitura vira log. O Dev Lead não ganha ferramenta de rearm nem de desbloqueio. - Por quê: TP-01 27ª (07/11): o disjuntor do
dev-catalogoabriu e os outros quatro agentes ficaramidlepor 57 min sem nenhum aviso na sessão; cada agente decide sozinho e ninguém olhava o conjunto. - Limite (atalho): o job
uniquecobre os estados incompletos, então umdev.idleque chega durante operformnão gera job novo; sem processo do Dev Lead vivo o aviso a ele se perde (RN-1012), mas o evento na sessão não depende dele. A parte da web (feed e Rearmar/Desbloquear) é outra PR. - Onde:
apps/engine/lib/engine/workers/execucao_parada_worker.ex:38(agendar),apps/engine/lib/engine/workers/execucao_parada_worker.ex:51(perform),apps/engine/lib/engine/dev/agent_io.ex:178(claim_e_rodar),apps/engine/lib/engine/dev/agent_io.ex:273(finish_task),apps/engine/lib/engine/dev/agent_io.ex:279(pausar_por_credito),apps/engine/lib/engine/agents/dev_lead_server.ex:379(handle_info). - Teste:
apps/engine/test/engine/workers/execucao_parada_worker_test.exs,apps/engine/test/engine/dev/dev_agent_server_test.exs("sem task pegável"),apps/engine/test/engine/agents/dev_lead_server_test.exs("execucao_parada"); mutação provada: tirar a condição dos agentes parados, a deduplicação, oWake.deliverou o enfileiramento doAgentIoreprova um teste cada. - Origem: AT-800 (engine), Rodada 250, lane L1.
RN-1070 — O Dev Lead lê o estado de ADR, PR e tarefa antes de afirmá-lo, e cita tarefa pelo título no porque
Estende a RN-772 (
listar_adrs_propostasdeixa de ser só do Arquiteto) e aplica ao Dev Lead a regra da RN-849 (o estado muda fora da conversa; a leitura vale mais que a lembrança).
- Regra: o menu do Dev Lead ganha
listar_adrs_propostas(a mesma ferramenta do Arquiteto, lida local do Postgres, sem rota nova na api). O prompt dele traz, depois da instrução dodecision_record, "Não afirme estado de ADR, PR ou tarefa sem lerlistar_adrs_propostas/read_backlogno mesmo turno; se não leu, diga que não sabe." A descrição do campoporquedopropose_execution_planmanda citar tarefas pelo TÍTULO e deixar os ids só emtarefas[].taskId, que o sistema já confere (IdDoBacklog, RN-699). Não há parser de prosa nem marcação de afirmação sem leitura no resumo do turno. - Por quê: TP-01 27ª (07/11, achado 9): o Dev Lead escreveu "e72dc064 (e97dc064)" no
porque(texto livre que nada valida) e afirmou uma ADR como mergeada sem ter como lê-la, porque a ferramenta só estava no menu do Arquiteto. - Onde:
apps/engine/lib/engine/agents/dev_lead_server.ex:770(system_prompt),apps/engine/lib/engine/agents/dev_lead_server.ex:644(run_tool),apps/engine/lib/engine/agents/dev_lead_server.ex:905(tool_specs),apps/engine/lib/engine/agents/dev_lead_tools.ex:98(spec). - Teste:
apps/engine/test/engine/agents/dev_lead_server_test.exs("lê o estado da ADR por listar_adrs_propostas", "pede tarefa pelo título no porque"); mutação provada: tirar a ferramenta do menu, a cláusula dorun_tool, a regra do prompt ou a frase doporquereprova um teste cada. - Origem: AT-809, Rodada 271, lane L3.
AT-812 (parte do engine). A instrução do decision_record, idêntica nos cinco conversacionais (PO, Arquiteto, Dev Lead, UX Designer, Staff), passa a pedir, quando a decisão orienta o dev de um módulo, afeta: {modulos: ["<módulo>"]} e a citação modules/<módulo>; a api usa isso para pôr a decisão na lista da tarefa daquele módulo (EP-030). No contexto do dev agent, a seção das decisões da tarefa tem o cabeçalho "Decisões registradas para esta tarefa/módulo — vencem RF/DoR quando conflitam", porque agora também leva decisão de agente. O teste é apps/engine/test/engine/agents/instrucao_decision_record_test.exs; mutação provada: tirar afeta.modulos de um dos servidores ou voltar o cabeçalho reprova um teste cada.
AT-805 (parte do engine, RN-1068 da api). O PO e o Dev Lead ganham update_task (apps/engine/lib/engine/harness/tools/update_task.ex:47, run): corrige título e/ou descrição de tarefa todo por POST /internal/sessions/:sessionId/tasks/:taskId/update, com o agentId po ou dev-lead. O prefixo do task_id vira uuid antes da chamada (RN-721); o 409 nomeado (tarefa_em_execucao, historia_arquivada, historia_nao_draft) volta ao modelo pelo texto de RecusaDeHistoria (RN-727); GravadoNoTurno conta "tarefa corrigida" (RN-731). Teste: apps/engine/test/engine/harness/tools/update_task_test.exs.
AT-806 (parte do engine, RN-1069 da api). O propose_adr aceita supersedes (o slug da ADR mergeada que a nova substitui; docs/adr/<slug>.md é normalizado), repassa ao payload.supersedes do open_adr_pr e escreve Substitui: docs/adr/<slug>.md no topo do content (apps/engine/lib/engine/harness/tools/propose_adr.ex:79, propor). O 400 adr_supersedes_invalido da api volta ao modelo como texto. A spec da ferramenta e a instrução do Arquiteto mandam ler list_open_questions e listar_adrs_propostas antes de substituir (RN-1058, RN-772). Teste: apps/engine/test/engine/harness/tools/propose_adr_supersedes_test.exs.
AT-839 (exceção da AT-833 na RN-502). O NoopDevAgentServer ganha a cláusula {:action_settled, _} (ignora, como o agente real em apps/engine/lib/engine/dev/dev_agent_server.ex:308): o git_push e o pr_open nascem pending (RN-689) e, aprovados, entregam action_settled (ADR 0052); sem a cláusula o Noop morria com FunctionClauseError e nunca chegava a dev.awaiting_gate. Teste: apps/engine/test/engine/dev/noop_dev_agent_server_test.exs ("pending -> action_settled -> pr_settled").
AT-844 (lacuna do Noop na RN-715). O NoopDevAgentServer não tinha cláusula para {:merge_conflict, _}: o wake task.merge_conflict o derrubava com FunctionClauseError e a task ficava in_progress sem dono. Agora ele bloqueia a task (mark_task_blocked, origem codigo) com o motivo "conflito de merge: o NoopDevAgent não readota a branch", já que não tem LLM para resolver; o agente real segue readotando. Uma cláusula final de handle_info ignora com Logger.debug qualquer outra mensagem. Teste: apps/engine/test/engine/dev/noop_dev_agent_server_test.exs ("merge_conflict bloqueia a task", "mensagem desconhecida").