API — parte 9 (desde RN-677)
O git credenciado roda no host do runner (RN-676)
RN-677 — Aceitar o Dev Lead só o traz para planejar; APROVAR o plano dele é o que ativa a execução
Revisa a RN-161 (decisão do dono, 01/10): aceitar o handoff ao Dev Lead deixou de encadear
POST .../execution/activate, para TODO papel. O atalho explícito "Ativar execução" do card (RN-137) e o da Visão Geral continuam — são um clique próprio, não o aceite.
O uso real de 29/09 mediu a inversão: a execução foi ativada no aceite do
handoff às 06:45:01 — o web encadeava a ativação para maintainer/owner
(session-acoes-de-handoff.ts, RN-161) —, e o plano do Dev Lead, proposto às
06:47:47, ficou pending e nunca foi usado. O plano não tinha consumidor:
propose_execution_plan não tinha pipeline de execução e, aprovado, ficava
approved para sempre. Medido em dev antes de mudar: o backend do aceite
(AcceptHandoffUseCase) NUNCA emitiu execution.activated; quem o emite é só
ActivateExecutionUseCase, chamado só pela rota execution/activate.
A regra:
- O aceite do Dev Lead só o ativa para planejar.
handleAcceptHandoffaceita e mais nada; oAcceptHandoffUseCasenão muda — segue provisionando o repositório como segunda porta idempotente (RN-582). - Aprovar o
propose_execution_planATIVA a execução. A ação ganhou executor na api,ExecuteExecutionPlanUseCase, chamado pela aprovação manual (ApproveActionUseCase) e pela auto-aprovação (ProposeActionUseCase— o tipo não está nos tetos absolutos dedecide.ts, então PODE ser configurado auto-aprovável). Ele grava o módulo das tarefas (RN-678) e chama o MESMOActivateExecutionUseCasedo botão — nenhuma segunda régua: o 409 sem repositório (RN-582), o 409 deexecution.activatedem sessão consultiva (RN-097) e a reativação idempotente porfindActiveExecutionSessioncontinuam lá, intocados. - Quem ativa é quem APROVOU (
decidedBy); na auto-aprovação, quem abriu a sessão — contra quemdecide()resolveu o papel. Os dois exigemmaintainer, o mesmo mínimo deexecution/activate, então a resolução do papel efetivo dosgit_*dos dev agents (ocreatedByda sessão de execução) não muda de dono. - A sessão do Dev Lead NÃO fecha (não é passada como
originSessionId, RN-135): é nela que ele retoma o turno suspenso e narra o desfecho. - O desfecho é nomeado e durável. A ação termina
executedcom{ sessaoDeExecucao, modulos, tarefasAtribuidas }oufailedcom omotivo(a frase da api, ex.: o 409 sem repositório), mais o eventoexecution.plan_applied/execution.plan_failedna timeline. O Dev Lead lê o desfecho real ao retomar ("execução ATIVADA na sessão …" ou "a ativação falhou: …"), nunca "plano registrado". Falhar não desfaz a aprovação: a decisão do humano já está gravada.
- Onde:
src/application/use-cases/execution/execute-execution-plan.use-case.ts:89(execute);src/application/use-cases/actions/approve-action.use-case.ts:221(executeExecutionPlan);src/application/use-cases/actions/propose-action.use-case.ts:427(executeExecutionPlan);apps/web/src/lib/session-acoes-de-handoff.ts:91(handleAcceptHandoff);apps/engine/lib/engine/agents/dev_lead_tools.ex:274(classificar);apps/engine/lib/engine/agents/dev_lead_server.ex:811(texto_do_desfecho);apps/web/src/lib/aprovacoes.ts:359(propose_execution_plan) - Teste:
test/application/use-cases/execution/execute-execution-plan.use-case.spec.ts:106(aprovado ativa como quem aprovou — caminho feliz),:129(auto-aprovado: quem abriu a sessão),:151(ativação recusada por falta de repositório virafailedcom a frase — caso de falha);test/application/use-cases/actions/approve-deny-action.use-case.spec.ts:399(aprovar chama o executor e avisa o Dev Lead);apps/web/src/routes/SessionPage.handoff-devlead-e-colapso.test.tsx:396(aceitar não ativa, nos três papéis),:416(o atalho explícito continua);apps/web/src/lib/aprovacoes.test.ts:118(a frase diz que aprovar ativa);apps/engine/test/engine/agents/dev_lead_tools_test.exs:134,:141;apps/engine/test/engine/agents/dev_lead_server_test.exs:426,:457 - Decisão arquitetural: ADR 0194
- Origem: AT-263 (item A17 da análise do uso real de 29/09)
RN-678 — A tarefa pertence a um módulo, atribuído pelo Dev Lead no plano, e só o dev daquele módulo a pega
No uso real de 29/09 dev-input-keyboard pegou "Implementar loop de queda
automática da peça" e dev-board-engine pegou "Mapear teclas…": a tarefa não
tinha módulo, a história tinha vários, e claimNext deixava qualquer dev de
qualquer módulo da história pegar qualquer tarefa dela (s.module_ids ? $1).
A regra:
- A tarefa tem módulo —
tasks.module,textnulo (migração0068_modulo_da_tarefa). Quem o atribui é o Dev Lead, no plano de execução:propose_execution_planganhoutarefas: [{ taskId, modulo }], e o kickoff dele lista as tarefas pendentes do backlog do PROJETO com otask_ide os módulos da história. - O conjunto válido é o
module_mapvigente. A api lê o plano (lerPlanoDeExecucao, função pura) na PROPOSTA e recusa com 400plano_de_execucao_invalido, sem criar a ação, quando: o plano não temtarefas; uma tarefa não temtaskIdou está sem módulo; o módulo (da tarefa ou do plano) está fora domodule_map; a tarefa aparece duas vezes; ou não existe neste projeto. A frase nomeia a tarefa, o módulo e os módulos válidos, e volta ao Dev Lead como erro da ferramenta (entrada do laço, RN-163), para ele corrigir. - A aprovação relê o plano contra o
module_mapdaquele momento (ele pode ter mudado): recusa virafailedsem gravar nada e sem ativar. Passando, os módulos são gravados ANTES da ativação, para nenhum dev agent recém-subido procurar tarefa sem dono. - Só o dev daquele módulo pega.
claimNextecountClaimableByModuleusam o MESMO predicado (daTarefaDoModulo): tarefa com módulo é de quem tem aquele módulo, e de mais ninguém — osmodule_idsda história deixam de decidir. Tarefa sem módulo só é pegável quando a história tem UM módulo só, e ele é o do dev: é o único caso em que "o dev daquele módulo" não é ambíguo, e é o caminho de quem ativou pela Visão Geral sem plano. Tarefa sem módulo de história com vários espera o próximo plano.
Fica de fora, declarado: tarefa criada DEPOIS do plano nasce sem módulo (o PO
não atribui); o aviso task.became_claimable segue roteado pelos módulos da
HISTÓRIA (acordar um dev a mais não pega nada, só gasta uma consulta); e o
agentes por módulo do plano segue informativo — a ativação sobe um dev por
módulo do module_map, como antes, e o paralelismo extra continua pelo
parallelize (RN-083).
- Onde:
src/domain/execution/plano-de-execucao.ts:39(lerPlanoDeExecucao);src/infrastructure/persistence/drizzle/backlog.repository.ts:720(daTarefaDoModulo),:403(claimNext),:391(assignModules);src/application/use-cases/actions/propose-action.use-case.ts:141(recusaNaProposta);src/application/use-cases/execution/execute-execution-plan.use-case.ts:67(recusaNaProposta);src/db/schema/backlog.ts:187(module);apps/engine/lib/engine/agents/dev_lead_tools.ex:460(tarefas_do_backlog);apps/engine/lib/engine/agents/dev_lead_tools.ex:259(erro_da_proposta) - Teste:
test/domain/execution/plano-de-execucao.spec.ts:22(caminho feliz),:41(tarefa sem módulo — caso de falha),:49(módulo fora domodule_map),:80(repetida e de outro projeto);test/application/use-cases/execution/claim-next-task.use-case.spec.ts:127(história com dois módulos: só o dev do módulo da tarefa),:152(sem módulo e vários módulos: ninguém),:169(o módulo da tarefa vence);test/application/use-cases/actions/propose-action.use-case.spec.ts:289(400 nomeado, sem proposta);test/application/use-cases/execution/execute-execution-plan.use-case.spec.ts:135(módulo fora do mapa na aprovação: nada gravado, nada ativado);apps/engine/test/engine/agents/dev_lead_tools_test.exs:110,:118;apps/engine/test/engine/agents/dev_lead_server_test.exs:131 - Decisão arquitetural: ADR 0194
- Origem: AT-274 (item A28 / extra E5 da análise do uso real de 29/09)
RN-679 — A curadoria recusa o alias de roteamento livre do OpenRouter
Decisão do dono (01/10): o alias ~ não entra na curadoria — só modelo
com upstream fixo. O alias de roteamento livre do OpenRouter (o id que começa
com ~, como ~deepseek/deepseek-flash-latest) "sempre redireciona para o
último da família": o catálogo publica um preço de VITRINE (no uso real de
29/09, o do endpoint mais barato da família), a lista de endpoints vem vazia,
e quem cobra é o upstream que atendeu. Não há upstream contra o qual o preço
congelado do ADR 0042
signifique alguma coisa. A alternativa — preçar por upstream — foi recusada.
- A régua é do provider.
ehAliasDeRoteamentoLivreéprovider = 'openrouter'E nome começando com~; o mesmo prefixo noutro provider não é alias e não recusa. - Ativar recusa, com código.
POST .../models/activatecomisActive: truee algum alias no lote é 422 comcode: "alias_de_roteamento_livre", osmodelIdsrecusados e uma frase que os nomeia. O lote INTEIRO é recusado, como o 404 do id inexistente (RN-043): nem o modelo de upstream fixo do mesmo lote é ligado. A tela de catálogo mostra a frase da api num toast de título próprio, e casa pelocode, nunca pelo texto. - O sync NÃO filtra o alias. Ele continua no catálogo — sumir dali faria
o sync marcar
unavailableo que já estava curado, e a cascata pularia os bindings dele em silêncio. A leitura da curadoria o MARCA:freeRoutingAlias, derivado de provider e nome na leitura, nunca gravado. A linha mostra o selo e o motivo em TEXTO antes de alguém tentar ativar. - O que já estava curado segue funcionando. É mensurável (o nome com
~na linha do OpenRouter), e a saída escolhida é a menos surpreendente: o alias ativo antes da regra continua ativo — os bindings dele seguem resolvendo, o seletor segue mostrando —, sai marcado no catálogo, e pode ser DESLIGADO. Desligado, não volta. Nada é apagado e nenhum binding é reescrito. Binding NOVO para um alias ainda ativo não é recusado aqui: a regra é da curadoria, e o binding segue a RN-043.
- Onde:
src/domain/llm/alias-de-roteamento-livre.ts:23(ehAliasDeRoteamentoLivre),:41(AliasDeRoteamentoLivreError),src/application/use-cases/llm/set-models-active.use-case.ts:59(AliasDeRoteamentoLivreError),src/infrastructure/persistence/drizzle/workspace-model.repository.ts:67(freeRoutingAlias),src/interfaces/http/shared/llm-binding-error.filter.ts:60(code),apps/web/src/components/ModelCatalogSection.tsx:606(recusaDeAliasLivre) - Teste:
test/application/use-cases/llm/set-models-active.use-case.spec.ts(lote recusado inteiro com código e ids; upstream fixo ativa;~fora do OpenRouter ativa; alias curado antes da regra segue ativo, marcado, desliga e não volta),test/interfaces/http/shared/llm-binding-error.filter.spec.ts,apps/web/src/components/ModelCatalogSection.test.tsx(selo e motivo; toast da recusa; toast genérico para outro erro) - Origem: AT-271 (item A25 da análise do uso real de 29/09); complementa a RN-665
RN-681 — História e regra parecidas com uma existente geram AVISO por embedding, nunca recusa; o gasto da checagem é linha própria do metering
Depois de gravar uma história (create_story) ou uma regra de negócio
(emit_artifact de business_rule), a api compara o TÍTULO dela com os das
existentes do mesmo tipo no PROJETO, por cosseno entre vetores do modelo de
embedding do RAG, e AVISA quando a mais próxima está a partir do limiar.
- Aviso, nunca recusa. O item já existe quando a checagem roda. O aviso
volta ao agente no resultado da ferramenta — a frase nomeia o parecido, o
id e o número — e fica no log como
backlog.semantic_duplicate_warned. A duplicata EXATA continua recusada antes, pela RN-080 e pela RN-081, e por isso título normalizado igual sai da comparação. - O limiar é 0,80 e é PONTO DE PARTIDA, não calibrado (ADR 0198, como os
pesos do ADR 0080):
os vetores reais do par do achado R e dos pares que não são duplicata não
puderam ser gravados no ambiente em que a regra nasceu. A prova roda sozinha
quando
vetores.jsonfor gravado; até lá ela é PULADA com aviso. - Sem provider de embedding a checagem é PULADA e DITA — capability
ausente, daemon fora, timeout:
backlog.semantic_duplicate_check_skippedcom o motivo no log e na frase ao agente. Nunca falha a emissão. - O gasto entra no metering como linha própria: ator
system/duplicata-semantica, provider e modelo do RAG, saída zero, preço do catálogo quando o modelo está nele. É a ÚNICA exceção ao corte do ADR 0075 — a indexação e a busca do RAG seguem fora —, e cabe porque a emissão acontece dentro de sessão. - Tetos: as 100 existentes mais recentes (o resultado diz quantas de quantas quando corta) e 10 s de relógio para a checagem inteira.
- Onde:
src/domain/backlog/duplicata-semantica.ts:35(LIMIAR_DE_DUPLICATA_SEMANTICA),:44(TETO_DE_COMPARACOES_DE_DUPLICATA),:53(TETO_DE_TEMPO_DA_CHECAGEM_MS),:107(ehDuplicataSemantica),src/application/use-cases/backlog/verificar-duplicata-semantica.use-case.ts:112(VerificarDuplicataSemanticaUseCase),:247(medir),:291(narrar),:355(fraseParaOAgente),src/application/use-cases/backlog/create-story.use-case.ts:199(semanticDuplicate),src/interfaces/http/internal/internal-sessions.controller.ts:678(semanticDuplicateCheck),src/application/use-cases/rag/rag-embedding.service.ts:106(uso),apps/engine/lib/engine/harness/tools/emit_artifact.ex(aviso_semantico),apps/engine/lib/engine/harness/tools/create_story.ex(aviso_semantico) - Teste:
test/application/use-cases/backlog/verificar-duplicata-semantica.use-case.spec.ts(avisa acima do limiar; o gasto vira linha própria; abaixo não narra; sem provider pula dizendo; teto de tempo; teto de comparações; regra pelos eventos; metering e log que falham não derrubam),test/application/use-cases/backlog/create-story.use-case.spec.ts(duplicata semântica (RN-681)),test/domain/backlog/duplicata-semantica.spec.ts,test/domain/backlog/limiar-de-duplicata.calibracao.spec.ts(o NÚMERO — pulada até os vetores serem gravados),apps/engine/test/engine/harness/duplicata_semantica_test.exs,apps/web/src/lib/activity.test.ts(duplicata semântica (RN-681)) - Origem: ADR 0198 (AT-171, achado R)
A Anamnese religada: sujeito elegível e fato do perfil (RN-680)
RN-682 — A consultiva sem agente pede um agente: o composer não envia sem destinatário, e a rota de chat recusa com nome
Até aqui, a mensagem de uma sessão consultiva sem agente ia ao SSE de
POST .../chat (SendChatMessageUseCase), que manda ao modelo vinculado só o
texto atual — sem histórico, sem prompt de sistema, sem ferramenta — e grava a
resposta com o NOME DO MODELO como ator. Medido no uso real de 29/09: a sessão
5d66 respondeu "não tenho acesso a conversas anteriores". A decisão do dono
(01/10) é que, sem agente ativo, a consultiva pede para escolher um agente
— a mesma régua da RN-584 (nenhum destinatário padrão) chegando ao
último caso que ainda tinha um: o modelo cru.
A medição, antes do conserto: a rota de chat tinha UM cliente, a tela
(handleSend, só quando não havia destinatário — o que, desde a
RN-631, só acontecia na consultiva sem agente: a criativa sempre tem
o Criativo como opção). O curl que o seed imprime é dica de desenvolvimento,
numa sessão criativa. O caso de uso tem um SEGUNDO consumidor legítimo: os
smokes de provider (*-provider.smoke.spec.ts) o usam direto como instrumento
de ponta a ponta (credencial → chamada → metering) numa sessão sem agente. Por
isso a recusa da api mora na ROTA, e o caso de uso não muda.
A regra:
- A tela não envia sem destinatário. O "Enviar" e o Enter ficam travados
com zero opções, como já ficavam com duas sem escolha; o ramo do SSE saiu de
handleSend. - A linha do destinatário MOSTRA quem pode ser chamado. Sem opção, o
seletor do handoff manual (RN-440) muda de lugar para dentro
dela — "Para", a lista, e "Chamar" —, e o seletor de cima some para não
haver a mesma escolha em dois lugares. A lista são os agentes que conversam
(
AGENTES_DE_CHAT, os que o engine casa com cláusula própria) e que o handoff manual alcança, menos quem já entrou e menos o Criativo: a consultiva promete no convite que ele não entra, porque abrir a ideação é o que a criativa faz (RN-097). Chamar oferece o handoff; aceitá-lo no fio faz do agente o destinatário (RN-631). - A api recusa o mesmo caso para qualquer cliente.
POST .../chatnuma sessão consultiva em que nenhumagent.activatedfoi gravado é 422{reason: "destinatario_ausente"}, com a frase que aponta.../agents/:agent/message, ANTES de qualquer efeito: nada é gravado e nenhum modelo é chamado. A recusa sai como resposta HTTP e não como quadroerrordentro de um stream 200 — o handler devolvePromise<Observable>, e a rejeição antes do cabeçalho do SSE vira a resposta normal.
O que esta regra NÃO fecha, declarado:
-
A rota segue respondendo pelo modelo cru fora desse caso: numa criativa sem agente (o
curldo seed) e numa consultiva COM agente, para quem a chamar à mão. A tela não a chama em nenhum dos dois; a decisão do dono foi sobre a consultiva sem agente, e fechar a rota inteira é decisão à parte. -
O chat livre com histórico, a outra opção da AT-254, não foi feito.
-
Código:
src/domain/sessions/chat-sem-destinatario.ts:24(chatSemDestinatarioRecusado),:17(MOTIVO_CHAT_SEM_DESTINATARIO);src/application/use-cases/llm/garantir-destinatario-do-chat.use-case.ts:27(GarantirDestinatarioDoChatUseCase);src/interfaces/http/llm/chat.controller.ts:82(chat);apps/web/src/lib/session-destinatario.ts:347(agentesParaChamar);apps/web/src/routes/SessionPage.tsx:690(handleSend);apps/web/src/routes/SessionComposer.tsx:276 -
Teste:
test/application/use-cases/llm/garantir-destinatario-do-chat.use-case.spec.ts:82(consultiva sem agente — caso de falha),:96,:113,:120;test/interfaces/http/llm/chat.controller.spec.ts:56(422 como resposta HTTP),:79(caminho feliz);apps/web/src/routes/SessionPage.destinatario-do-chat.test.tsx:458(não envia — caso de falha),:471(lista e Chamar — caminho feliz);apps/web/src/lib/session-destinatario.test.ts:93;apps/web/src/routes/SessionPage.ideacao-automatica.test.tsx:177 -
Origem: AT-254 (item A8 da análise do uso real de 2026-09-29), decisão do dono de 01/10
O plano de teste nasce depois da entrega do dev (RN-674, ADR 0192)
RN-683 — A Infra sobe o container com o MENOR recurso elegível, derivado do que o Arquiteto declara por módulo no module_map
Decisão do dono em 01/10 (ADR 0199,
AT-261). Até aqui resources omitido virava RECURSOS_PADRAO (2 cpus,
4096 MiB, 512 pids) e a subida do servidor no aceite (RN-671) o
omitia sempre: todo container nascia no padrão, sem noção de mínimo.
- Cada módulo declara o seu. O
module_mapganharesources(cpus,memoryMb,pidsLimit) por módulo — o que AQUELE módulo precisa sozinho no container. A ferramentacreate_module_mapo pede; a api o aceita ausente e recusa com 400, na criação do mapa: declaração pela METADE (os três ou nenhum), valor não positivo ou acima do teto, e SOMA entre módulos acima do teto. O campo mora no JSON do artefato, sem migration. - O mínimo é a SOMA, campo a campo. Um projeto tem UM container
(
project_id UNIQUEemproject_containers) e os módulos rodam nele ao mesmo tempo; o máximo entre módulos subdimensionaria o container sempre que dois estivessem de pé. - Módulo sem declaração: piso no padrão de hoje. Não se inventa número.
Havendo módulo sem declaração, o mínimo é o maior entre a soma dos
declarados e
RECURSOS_PADRAO, campo a campo, e orationaleNOMEIA quem não declarou. Sem declaração nenhuma (todo mapa anterior a esta regra), e semmodule_mapvigente, o mínimo é exatamente o padrão de hoje. - A execução de
container_startresolve campo a campo. Omitido vira o mínimo (a subida do servidor manda tudo omitido); abaixo do mínimo éfailednomeado, sem gravar decisão de imagem nem subir; acima vale até o teto, que não muda. Orationaledoartifact.project_imagegravado ganha "Recursos mínimos (RN-683): …" com os módulos somados e a versão do mapa. - Uma régua só. Quem deriva é a api; o engine não soma nada e a subida do servidor diz no rationale que os recursos são o mínimo derivado.
Fora, declarado: choose_project_image (o Arquiteto decide a imagem e os
recursos dele) e container_start_via_runner (sobe a imagem já decidida) não
mudam; a recusa abaixo do mínimo acontece na execução, não ao propor.
- Código:
src/domain/containers/recursos-minimos.ts:70(validarRecursosDoModulo),:108(derivarRecursosMinimos),:150(validarSomaNoTeto),:171(resolverRecursosDaSubida),:206(explicarRecursosMinimos);src/application/use-cases/architecture/create-module-map.use-case.ts:134(normalizarModulo);src/application/use-cases/actions/execute-container-start.use-case.ts:175(resolverRecursosDaSubida);apps/engine/lib/engine/harness/tools/create_module_map.ex:103(normalize);apps/engine/lib/engine/infra/infra_lead_server.ex:805(rationale_do_servidor) - Teste:
test/domain/containers/recursos-minimos.spec.ts:18(soma — caminho feliz),:35(mapa antigo = padrão),:41(piso e nomeado),:54(soma acima do teto — caso de falha),:94(abaixo do mínimo);test/application/use-cases/architecture/create-module-map.use-case.spec.ts:190(grava),:208(pela metade — caso de falha),:217(soma no teto);test/application/use-cases/actions/execute-container-start.use-case.spec.ts:530(subida do servidor com a soma),:556(sem declaração),:575(acima),:588(abaixo — caso de falha),:608(sem mapa);apps/engine/test/engine/harness/tools/create_module_map_test.exs:94,:109;apps/engine/test/engine/infra/infra_lead_server_test.exs:1307 - Origem: AT-261 (item A15 da análise do uso real de 29/09)
A Anamnese religada: sujeito elegível e fato do perfil (RN-680)
RN-684 — O contrato entre módulos é artefato versionado do Arquiteto, e o dev agent o lê pela ferramenta, nunca no worktree de outro dev
Decisão do dono (01/10), registrada no
ADR 0200. No uso
real de 29/09, dev-board-engine gastou 5 dos seus 24 passos com ferramenta
lendo o worktree de outros módulos para descobrir a interface deles: o
module_map diz quem depende de quem, nunca o que um módulo expõe. Aplica a
RN-164 aos dev agents.
- O artefato é
artifact.module_contracts, sem tabela, como omodule_routinge oc4_diagram: o vigente é o de maiorversion, com desempate porseq. Cada declaração leva a lista INTEIRA e SUBSTITUI a anterior; mudar contrato é versão nova do Arquiteto. - Por módulo, só o que ele EXPÕE:
{modulo, expoe: [{tipo, assinatura, descricao}]},tipoemfuncao | rota | evento | dado,assinaturaobrigatória (até 300 caracteres), de 1 a 40 itens. O que ele CONSOME é odependsOndomodule_mapvigente, derivado na leitura — nunca redigitado. - A api recusa com 400 e o motivo inteiro (que volta ao modelo, RN-061):
lista vazia, módulo repetido, módulo fora do
module_mapvigente (listando os válidos) ou semmodule_map,expoevazio ou acima de 40,tipodesconhecido,assinaturavazia ou acima de 300. Nada é gravado. - Artefato SEPARADO do
module_map, nunca um campo dele: revisar um não reemite o outro. - O dev lê por
listar_contratos_de_modulos,:direct, no registro deEngine.Dev.Tools: escopo do projeto pela rota, NENHUM parâmetro, o módulo do dev vindo dectx.module. Mostra por inteiro o dele e os que ele consome; quem o consome e os demais, só pelo nome; teto de 120 itens com o corte dito. Contrato de módulo que saiu do mapa é DITO, nunca atribuído. - O kickoff do dev diz onde ler a interface de outro módulo, e que não é
no worktree de outro dev agent. Sem contrato para um módulo mostrado, a
ferramenta manda implementar pelo que a story e a task dizem e, se faltar
interface,
report_blockednomeando o módulo — nunca reler o worktree.
- Código:
src/domain/architecture/module-contracts.ts:110(validarContratos);src/application/use-cases/architecture/declare-module-contracts.use-case.ts:55(execute),:80(missingModules);src/application/use-cases/architecture/get-module-contracts.use-case.ts:23(execute);src/application/use-cases/architecture/list-module-contracts.use-case.ts:42(execute);src/interfaces/http/internal/internal-sessions.controller.ts:818(moduleContracts);src/interfaces/http/internal/internal-projects.controller.ts:181(moduleContracts);apps/engine/lib/engine/harness/tools/declare_module_contracts.ex:91(run);apps/engine/lib/engine/harness/tools/listar_contratos_de_modulos.ex:43(run),:66(renderizar);apps/engine/lib/engine/dev/tools.ex:36(ListarContratosDeModulos);apps/engine/lib/engine/dev/dev_agent_server.ex:516(module),:501;apps/engine/lib/engine/agents/arquiteto_server.ex:112,:335,:399 - Teste:
test/application/use-cases/architecture/module-contracts.use-case.spec.ts:173(grava a versão 1 — caminho feliz),:185(a vigente substitui),:210(módulo fora do mapa — caso de falha),:222(semmodule_map),:123(as recusas de domínio),:239e:257(a leitura do dev);apps/engine/test/engine/harness/tools/declare_module_contracts_test.exs:26,:65(recusa da api);apps/engine/test/engine/harness/tools/listar_contratos_de_modulos_test.exs:71,:76,:97,:109,:128;scripts/jev/kickoff.spec.ts:19. Os testes ExUnit não rodaram pelomixneste ambiente (repo.hex.pm403; rodaram num harness avulso com os módulos compilados à mão); o CI os prova. - Origem: AT-276 (item A30 da análise do uso real de 2026-09-29), decisão do dono de 01/10
RN-685 — Revogar uma credencial derruba só as conexões abertas COM ELA: o ticket do socket guarda a credencial, e o alvo deixa de ser {projeto, usuário}
Revisa a RN-520. Até aqui, revogar uma chave de dispositivo derrubava
a conexão viva do runner pelo par {projeto, usuário}, porque a identidade da
credencial morria no PatAuthGuard: um runner do MESMO usuário conectado com
PAT ou com outra chave caía junto, e a chave de MÁQUINA
(RN-543) multiplicava isso por projeto. Revogar um PAT não derrubava
nada.
O ticket guarda QUAL credencial o pediu. runner_socket_tickets (tabela do
engine) ganha credential_kind (device_key | pat) e credential_id (o
kid, que é o id do registro — RN-475 —, ou o id do PAT), as duas
NULÁVEIS: o ticket de terminal é da aba da web e não tem credencial de
dispositivo, e o de runner emitido por uma api anterior chega sem ela. O
PatAuthGuard anota a credencial DEPOIS de autorizar
(request.credencialDeDispositivo), o ticket de runner a leva ao engine e o
de terminal nunca leva; par incompleto ou espécie desconhecida vira AUSENTE,
nunca uma credencial inventada.
O socket guarda a credencial, e o id dele também. connect/3 põe
assigns.credencial, e o id passa a ser
runner_socket:<kind>:<projectId>:<userId>:<espécie>:<id> quando ela existe —
o disconnect que o canal difunde para o próprio id alcança só aquela
credencial.
A revogação de UMA credencial:
- anula os tickets PENDENTES dela — o pedido feito segundos antes da
revogação não entra depois dela (
consumed_at, o mesmo estado de "já usado", nenhum estado novo); - pergunta a TODO runner do cluster se ele nasceu desta credencial — só o canal sabe —, e só quem nasceu dela cai. A chave de máquina é UM pedido, e cai em todo projeto em que abriu conexão, inclusive num que a api não listaria;
- responde um BALANÇO (
derrubados,legados,intocados,semResposta,ticketsAnulados) — zero derrubados é o caso normal da chave órfã (RN-519).
Vale para as três revogações: a de chave (por projeto e pela Conta,
RN-611) e as duas de PAT (a do dono, RN-426,
e a do maintainer, RN-427).
Revoga PRIMEIRO, derruba depois, e derrubar continua sendo efeito colateral que
só LOGA — o DELETE segue 204 e idempotente.
O que já caía continua caindo (a proibição da AT-013). A conexão LEGADA —
ticket sem credencial — do mesmo dono, num dos projetos que a revogação de
CHAVE manda como alcance (o da linha, ou os candidatos da máquina), cai pelo
par como antes; o PAT não ganha alcance legado, porque não derrubava nada. E se
o engine não conhece a rota por credencial (404 de um engine anterior), a
revogação de chave volta ao par, disconnectRunnerOfUser, projeto a projeto.
O par {projeto, usuário} continua, com outro dono: é o alvo da remoção de
membro (RN-615), que tira a PESSOA, com a credencial que for.
A tela diz a precisão (RN-561): a confirmação de revogar — chaves de dispositivo em Configurações e chaves de máquina na Conta — troca "outro runner seu também cai" por "só cai o que se conectou com ESTA chave". Muda o rótulo, não o comportamento.
- Onde:
src/interfaces/http/auth/pat-auth.guard.ts:151(credencialDeDispositivo, o PAT),:236(credencialDeDispositivo, a chave);src/interfaces/http/runner/runner-tickets.controller.ts:67(runnerTicket);src/application/use-cases/runner/request-runner-ticket.use-case.ts:55(execute);src/application/use-cases/auth/revoke-runner-device-key.use-case.ts:71(execute),:109(derrubarConexoesDaChave);src/application/use-cases/auth/derrubar-conexoes-do-pat.ts:21(derrubarConexoesDoPat);src/infrastructure/http-clients/api-to-engine-client.ts:693(disconnectRunnerCredential);apps/engine/lib/engine/runners/socket_ticket.ex:107(emitir),:207(anular_pendentes_da_credencial);apps/engine/lib/engine/runners/revogacao.ex:131(derrubar_credencial);apps/engine/lib/engine/runners/registry.ex:64(todos);apps/engine/lib/engine_web/channels/runner_socket.ex:81(socket_id);apps/engine/lib/engine_web/channels/terminal_channel.ex:772(handle_info),:879(desfecho_da_revogacao_por_credencial);apps/engine/lib/engine_web/controllers/runner_connection_command_controller.ex:58(disconnect_credential);apps/engine/priv/repo/migrations/20261001120000_add_credential_to_runner_socket_tickets.exs;apps/web/src/routes/settings/RunnerDeviceKeysSection.tsx:346,apps/web/src/routes/MachineDeviceKeysSection.tsx:202 - Teste:
test/application/use-cases/auth/revoke-runner-device-key.use-case.spec.ts(describe "o alvo da queda é a CHAVE"— caminho feliz;describe "CASO DE FALHA: o engine não atende o pedido por chave"— o plano B pelo par; a máquina num pedido só);test/application/use-cases/auth/machine-device-keys.use-case.spec.ts;test/application/use-cases/auth/revoke-personal-access-token.use-case.spec.tserevoke-personal-access-token-as-maintainer.use-case.spec.ts;test/interfaces/pat-auth.guard.spec.ts(a credencial anotada nos dois caminhos, e não anotada na recusa);test/interfaces/http/runner/runner-tickets.guards.integration.spec.ts(o PAT real até o pedido ao engine);test/application/use-cases/runner/request-runner-ticket.use-case.spec.ts(oterminalnunca leva credencial);test/infrastructure/http-clients/api-to-engine-client.spec.ts(describe "o alvo é a CREDENCIAL", inclusive o 404 que LANÇA);apps/engine/test/engine_web/channels/terminal_channel_test.exs(describe "revogação por CREDENCIAL"— o canal real que cai, o PAT do mesmo usuário que fica de pé, a máquina em dois projetos sem lista, o legado pelo par, o ticket pendente anulado);apps/engine/test/engine/runners/socket_ticket_test.exs,apps/engine/test/engine_web/channels/runner_socket_test.exs,apps/engine/test/engine_web/controllers/runner_connection_command_controller_test.exs(200 com balanço, 400 de credencial fora de forma);apps/web/src/routes/settings/chaves-de-dispositivo.test.tsx,apps/web/src/routes/MachineDeviceKeysSection.test.tsx(a frase da precisão, e a do alvo antigo que não volta) - ADR: 0201
- Origem: AT-013 (HS-008, EP-003), decisão do dono em 01/10. Fica
declarado e NÃO feito: o ticket SEM credencial (de api anterior) não é
anulado na revogação — não há o que comparar, e a janela é de 30 s, só no
rollout; e os testes ExUnit só rodam no CI (
repo.hex.pmdá 403 no ambiente em que foram escritos)
O teto da RN-418 vale também pela porta tipada (RN-689)
RN-689 — git_push e pr_open tipados nunca são auto-aprováveis: o teto da RN-418 vale para o comando E para a ação
Revisada pela RN-713 (AT-385, 02/10): o modo automático (a
curinga em auto_approve) passa a liberar git_push e pr_open.
Revisa a RN-418, por decisão do dono (01/10). Até aqui o teto de
efeito externo de decide() só olhava actionType === 'terminal': o
git push DIGITADO virava require_approval, e a mensagem mandava usar a ação
tipada git_push — que, com o papel mínimo (maintainer) cumprido, nascia
auto_approved por QUALQUER um dos três estágios: o curinga "*" do modo
automático (RN-153, o piloto da
RN-670), uma regra específica de agent_autonomy (a ativação da
execução semeava git_push/pr_open em auto_approve para cada
dev-<modulo>) ou um allow em permissions.json. Medido em
decide.spec.ts antes da correção: os seis casos (dois tipos × três estágios)
saíam auto_approve.
A regra: git_push e pr_open entram no bloco de tetos absolutos — se o
veredito até ali é auto_approve, vira require_approval incondicional, com o
motivo nomeado ("teto de efeito externo (RN-418): … nunca é auto-aprovável"),
que vai no proposed_action.created. deny já retornou antes e continua
vencendo; a ORIGEM da autonomia não importa. O "sempre permitir" dos dois já
era recusado na fonte (RN-642, TIPOS_SEM_SEMPRE_PERMITIR), e é essa
metade que impede o teto de ser decorativo.
A semeadura acompanha: DEV_AUTO_GIT_ACTIONS passa a ser só git_commit
— na ativação e no aceite de paralelização. Semear push e PR seria gravar uma
autonomia que decide() nunca honra, a tela prometendo o que não acontece (o
argumento da RN-642). Linhas já gravadas ficam no banco, inertes. O dev agent
segue o caminho da RN-050: commit executado,
push e PR pending, agente em awaiting_approval retendo o worktree até o
task.pr_settled; só então o gate abre.
Ficam FORA, de propósito: git_merge (teto próprio de branch protegida);
git_commit (fica na máquina, RN-670); open_adr_pr e open_infra_pr (abrem
PR real no provider, mas a decisão do dono não os cobre — open_infra_pr é
semeado auto_approve no aceite da Infra, RN-671); as ações de
bootstrap (git_repo_create/git_branch_create/git_branch_protect, que não
passam por decide()); e deploy, que não é tipo de proposed_action — só
existe como destino da mensagem do comando de terminal.
- Onde:
src/domain/actions/decide.ts:429(ehAcaoTipadaComEfeitoExterno);src/domain/actions/external-effect.ts:318(ACOES_TIPADAS_COM_EFEITO_EXTERNO),:326(ehAcaoTipadaComEfeitoExterno),:340(mensagemDoTetoDaAcaoTipada);src/application/use-cases/execution/activate-execution.use-case.ts:71(DEV_AUTO_GIT_ACTIONS) - Teste:
test/domain/actions/decide.spec.ts:1375(os dois tipos com curinga, regra específica eallowsaemrequire_approval;denycontinua vencendo — caso de falha;git_commitegit_mergepara branch não protegida ficam fora);test/application/use-cases/actions/propose-action.use-case.spec.ts:953(com Postgres: nascempendingcom o motivo no evento);test/application/use-cases/execution/activate-execution.use-case.spec.ts:473eaccept-parallelization.use-case.spec.ts:141(semeiam sógit_commit);test/application/use-cases/actions/approve-always-action.use-case.spec.ts:795("sempre permitir" recusado, 400teto_do_sempre_permitir) - Origem: AT-347, decisão do dono em 01/10
RN-696 — O nível de proficiência por competência chega aos agentes que conversam com a pessoa
Medido no código: até aqui o proficiency_profiles que a Anamnese grava não
chegava a agente nenhum — só os FATOS do Psicólogo aceitos pela pessoa
(RN-680) iam no turno. O perfil era só tela.
A regra: a api lê o nível por competência do AUTOR da mensagem neste
projeto (listByUser) e o acrescenta ao MESMO texto perfilDoAutor que o
engine põe como mensagem system efêmera no turno (Engine.Harness.PerfilDoAutor)
— nenhum servidor de agente muda. Vai só competência: nível, com a
orientação de calibrar quantas perguntas fazer; o rationale e as evidências
NÃO vão, e nível fora da escala é descartado. Teto de 12 competências, e o
texto inteiro segue sob o TETO_DO_TEXTO de 2 000 caracteres. Falha na leitura
é log, e o turno segue sem ela, independente da leitura do grafo.
fatosDoPerfil no chat.message continua contando só os fatos do Psicólogo.
Fica FORA, e é decisão de produto pendente: o que a Anamnese observa sobre o comportamento do AGENTE (o laço, o usuário cancelando o turno) virar sinal de melhoria do produto, separado do perfil da pessoa.
TODO(humano): o sinal de melhoria do produto vindo da Anamnese é evento (
anamnese.agent_signal, sem sujeito pessoa) ou tabela própria, quem o lê (Staff? uma tela de melhorias?) e por quanto tempo vive? É fronteira nova de dado e pede ADR (0205 reservado).
- Onde:
src/domain/anamnese/proficiencia-do-autor.ts:22(textoDaProficienciaDoAutor);src/application/use-cases/agents/send-agent-message.use-case.ts:152(resolverPerfil),:203(resolverProficiencia) - Teste:
test/application/use-cases/agents/send-agent-message.use-case.spec.ts:254(o nível vai, o rationale não),:274(leitura falha, a mensagem segue — caso de falha);test/domain/anamnese/proficiencia-do-autor.spec.ts - Origem: AT-356 (metade 1)
RN-703 — Subagente sem modelo herda o modelo resolvido do lead da área
"Aplicar a todos os agentes" (RN-476) grava um binding por agente do catálogo
de Configurações — 17 linhas —, e subagente que o engine roda fora dessa lista
ficava sem modelo em nível nenhum: o appsec terminava com agent.error
origem infra ("Nenhum modelo vinculado para esta sessão"), e o mesmo valia
para todo dev-<modulo> além de dev-backend/dev-frontend e para a
qa-estrategia. Quando a cascata de um SUBAGENTE não acha valor (ou só acha o
default global do workspace, que ninguém escolheu para o projeto), ele usa o
binding RESOLVIDO do lead da área dele: o lead da área do catálogo
(agent-areas.ts), ou — para appsec → secops e qa-estrategia → qa, que
o engine roda por dentro de um lead mas não recebem handoff — uma tabela ao
lado, que só serve a esta herança. A resposta leva herdadoDoLead com o lead,
e origin continua sendo a do binding dele (RN-470: a tela não afirma
configuração própria). O filtro de tool calling (RN-040) vale na resolução do
lead como na do subagente. Se ainda assim não há modelo, a mensagem de falha
diz onde configurar.
- Onde:
src/application/use-cases/llm/resolve-model-binding.use-case.ts:76(leadDoSubagente);src/domain/agents/agent-areas.ts:139(leadDoSubagente);src/domain/llm/binding-resolver.ts:66(MENSAGEM_SEM_MODELO) - Teste:
test/application/use-cases/llm/resolve-model-binding.use-case.spec.ts(appsecherda dosecopsedev-apidodev-lead; lead também sem modelo seguenull— caso de falha) - Origem: AT-367
O instalador oferece os modelos locais, e não confunde o dev com uma instalação (RN-691, RN-692)
RN-704 — O merge do repositório local é merge de verdade, e conflito é recusa nomeada
- Regra:
mergePullRequestdo providerlocalfaz fast-forward só quando o alvo é ancestral da fonte; com os dois divergidos, monta um commit de merge com os dois pais (git merge-tree --write-tree+commit-tree, sem worktree, mensagemMerge pull request #N from <branch>, identidadeBrabo Bot). A ref do alvo é movida porupdate-refcom o valor antigo esperado, então um merge concorrente falha em vez de sobrescrever. Conflito lançaGitMergeConflictErrorcom os arquivos em conflito: a ref fica intacta, a PR continuaopen, a açãogit_mergeterminafailedcom o motivo noaction.failed, e pela HTTP o erro é 409merge_conflict. - Medição (AT-377): duas PRs do mesmo commit base (#3 e #4) mergeadas em
sequência: a
devficou só com a #4, e os commits da #3 deixaram de ser ancestrais dela. Oupdate-refantigo era um reset forçado. - Onde:
src/infrastructure/git/local-git-provider.ts:783(mergeShas),src/domain/git/git-errors.ts:89(GitMergeConflictError),src/interfaces/http/shared/git-provider-error.filter.ts - Teste:
test/infrastructure/git/local-git-provider.contract.spec.ts("duas PRs divergentes sem conflito: os dois commits ficam no alvo, por commit de merge"; caso de falha: "conflito: recusa nomeada com os arquivos, alvo intacto e PR aberta") - Lacuna declarada: GitHub e GitLab recusam o merge com conflito pela
própria API (405/406), sem tradução para
GitMergeConflictError. - Origem: AT-377
RN-705 — A recusa de merge aparece na aba PRs, e a falha grava o tipo da ação
- Regra: a ação git que falha grava
execution_resultcom okindda PRÓPRIA ação (git_merge,pr_open,git_commit,git_push),failed: true, oerrore, nogit_merge, opullRequestId; antes toda falha gravava{kind: 'git_push', branch: ''}. O conflito de merge (GitMergeConflictError, RN-704) grava tambémconflictingFilese um eventobacklog.task_merge_conflictem cada tarefa da PR.GET projects/:projectId/actionsaceitastatus=failed(só comactionType), e a aba PRs lê dali a última recusa de merge de cada PR aberta: o motivo, os arquivos e a frase de que é preciso resolver o conflito antes. É AVISO em texto, como o do gate pendente (RN-663): o botão "Merge" segue ativo e a decisão continua humana. Aprovar o merge invalida a lista de PRs, e o avisoaction.git_merge/action.faileddo canal também; a lista "PRs em revisão" de Aprovações deixa de mostrar tarefadone. O card do merge nomeia o ator humano pela leitura de membros (RN-655) e mostra o NÚMERO da PR (#3, não#pr-3), e o provider local grava o autor da PR (<agente>[bot]). - Medição (AT-383/AT-382, 02/10, loja-teste): a #6 terminou
failedcom o conflito empackage.jsone a aba PRs seguiu mostrando "ABERTA" com o botão "Merge", sem texto nenhum; a ação gravoukind: git_push. Depois de um merge executado, a PR seguia em "Abertas" até recarregar, o card mostrava o UUID do usuário e#pr-3, e toda PR local era "autor desconhecido". - Onde:
src/application/use-cases/actions/execute-git-action.use-case.ts:73(resultadoDaFalha),src/application/use-cases/actions/list-project-pending-actions.use-case.ts:22(execute),apps/web/src/routes/ProjectPrsTab.helpers.ts:9(ultimaRecusaDeMerge),apps/web/src/lib/canal-vivo.ts:134(alvosDoEvento) - Teste:
test/application/use-cases/actions/execute-git-action.use-case.spec.ts("conflito de merge → kind git_merge, arquivos no resultado e evento na tarefa"; caso de falha: "falha que não é conflito não grava arquivos nem evento de conflito"),apps/web/src/routes/ProjectPrsTab.test.tsx("conflito de merge: mostra motivo e arquivos..."; "recusa de OUTRA PR não aparece nesta"),test/infrastructure/git/local-git-provider.contract.spec.ts("grava o autor ao abrir...") - Lacuna declarada (fechada na RN-715): o conflito NÃO acorda o dev agent da tarefa para
rebasear e reenviar. O caminho que o QA usa (
DevAgentServer.correct/3) só age com o agente em:awaiting_gateNA MESMA tarefa, e no merge o gate já terminou (task.gate_resolvedcomdoneleva o agente afinish_task, que zera tarefa, worktree e branch e reivindica outra). Reaproveitá-lo exige decidir como o agente readota uma tarefain_reviewna branch existente e se o gate roda de novo depois do rebase — decisão de desenho, não tomada aqui. A tarefa seguein_review, com obacklog.task_merge_conflictdurável. - Origem: AT-383, AT-382
O modo automático libera push e PR (RN-713)
RN-713 — Com o modo automático do agente, git_push e pr_open são auto-aprovados; só a curinga libera
Revisa a RN-418 e a RN-689, só na metade de push e PR,
por decisão do dono (02/10, AT-385). Com a curinga agent_autonomy
actionType: "*" em auto_approve (o modo automático,
RN-153, o piloto da RN-670),
git_push e pr_open tipados e o mesmo efeito pelo comando de terminal
(git push, git remote add/set-url, gh pr create, glab mr create) saem
auto_approve, com o motivo "modo automático (RN-713)" no
proposed_action.created (RN-567).
Só a curinga libera: regra específica auto_approve sem ela, "Sempre
permitir" (recusado na fonte, RN-642) e allow no permissions.json
seguem no teto da RN-418, e a ativação continua semeando só git_commit.
Seguem teto absoluto mesmo em modo automático: merge (tipado em branch
protegida, e git merge/gh pr merge pelo terminal), deploy, sudo/doas,
container_remove, instruction_patch e paralelismo. O toggle em manual
restaura a aprovação.
- Onde:
src/domain/actions/decide.ts:311(modoAutomaticoDoAgente),:326(decide);src/domain/actions/external-effect.ts:354(mensagemDoModoAutomaticoNoEfeitoExterno);apps/web/src/components/OQueOPilotoLibera.tsx - Teste:
test/domain/actions/decide.spec.ts(o piloto liberagit push/gh pr createe os tipados;git merge,gh pr mergeesudoseguemrequire_approval; específica eallowsem curinga seguem no teto; curinga emrequire_approvalpede aprovação — casos de falha);test/application/use-cases/actions/propose-action.use-case.spec.ts(com Postgres: auto-aprovados e executados, motivo no evento; específica sozinha nascepending);apps/web/src/components/ModoAutomaticoDoTime.test.tsx,AgentCard.test.tsx,ApprovalCard.test.tsx(a tela diz que push e PR são liberados) - Decisão arquitetural: ADR 0204
- Origem: AT-385, decisão do dono em 02/10
O instalador oferece os modelos locais, e não confunde o dev com uma instalação (RN-691, RN-692)
RN-715 — O conflito de merge devolve a tarefa ao dev agent dono, e os gates julgam de novo
- Regra: quando o merge aprovado de uma PR é recusado por conflito com a
dev(GitMergeConflictError, RN-704), cada tarefa da PR que estáin_reviewvolta ain_progresscomgate_statuszerado (os vereditos antigos de QA/SecOps não valem para a branch que vai mudar), gravabacklog.task_status_changedcomcause: merge_conflicte põetask.merge_conflictna outbox, com o agente que propôs opr_open(o DONO), a PR e os arquivos em conflito. O engine entrega ao dono peloEngine.Dev.Wake; livre, ele READOTA a branch da PR (WorktreeManager.adopt/3, a mesmagarantir_base, sem perder os commits) e roda o laço de CORREÇÃO com o motivo no texto: trazerorigin/devporgit fetch/git merge, resolver os arquivos, rodar a suite. Noreport_donepropõe commit e push pelo caminho de sempre (a política de aprovação vigente decide) e o gate RECOMEÇA do QA (open_gate+ QA), sempr_opennovo — a PR é a mesma branch. Nenhum estado novo de tarefa:in_progresscom a causa no evento. - Desfechos nomeados: dono sem linha em
dev_agent_states— a tarefa é BLOQUEADA (conflito de merge sem dev agent para resolvê-lo, origeminfra); dono com o disjuntor aberto (idle_tripped) — bloqueada, origempolitica; a branch não volta (adoptfalha) — bloqueada, origeminfra. Dono ocupado (working/awaiting_gate/awaiting_approval) recebe o conflito de novo em 30 s. A recusa do merge (RN-705) não depende de nada disso: a ação já estáfailed, com o motivo, antes de o engine agir. Tarefa que já não estáin_review(merge repetido) não é reaberta nem acorda ninguém, e PR aberta por humano não tem dono a acordar. - Medição (AT-383, 02/10, loja-teste): a #6 terminou
failedpor conflito empackage.json, e a tarefa ficouin_reviewsem ninguém para resolvê-lo. - Onde:
src/application/use-cases/actions/execute-git-action.use-case.ts:460(devolverAoDono),src/infrastructure/persistence/drizzle/backlog.repository.ts:504(reabrirPorConflitoDeMerge),apps/engine/lib/engine/workers/dev_agent_wake_worker.ex:151(task.merge_conflict),apps/engine/lib/engine/dev/dev_agent_server.ex:350(handle_info),apps/engine/lib/engine/dev/dev_agent_server.ex:851(trigger_gate_recheck),apps/engine/lib/engine/dev/worktree_manager.ex:47(adopt) - Teste:
test/application/use-cases/actions/execute-git-action.use-case.spec.ts("conflito de merge → kind git_merge, arquivos no resultado e evento na tarefa"; falha: "RN-715: tarefa que já não está em revisão não é reaberta nem acorda ninguém"),apps/engine/test/engine/dev/dev_agent_server_test.exs(describe "conflito de merge (RN-715)": readota, motivo no turno, commit+push sem PR nova, gate reabre do QA; falhas: branch que não volta e disjuntor aberto),apps/engine/test/engine/workers/dev_agent_wake_worker_test.exs("dono que não existe: bloqueia a task com motivo nomeado, nada calado"),apps/engine/test/engine/dev/worktree_manager_test.exs("readotar a branch da PR preserva os commits dela") - Lacuna declarada: a reentrega ao dono ocupado mora em memória — restart
do engine nessa janela a perde, e a tarefa fica
in_progressaté intervenção (mesma família da lacuna doEngine.Dev.Wake, ADR 0045). O agente integra adevpor MERGE, não rebase: o push forçado que o rebase exigiria não existe no executor de git. ONoopDevAgentServernão trata o conflito. - Origem: AT-383
RN-716 — A Anamnese não grava nível sobre o que não observou, e aprovar não é dominar
- Regra: um perfil de proficiência só é gravado quando a evidência inclui
ao menos um evento escrito pela PRÓPRIA pessoa perfilada que não seja clique
de aprovação (
proposed_action.approved,handoff.accepted,readiness.confirmed). Evidência só de outros atores é "não observado" — ausência de oportunidade, nunca pouco conhecimento —, e "não observado" é estado próprio: o lote é recusado com motivo nomeado, nada é gravado e nenhumanamnese.profile_updatednasce. Aprovação ao lado de uma interação da pessoa continua aceita. A mensagem da pessoa que repara falha de agente é desvio do AGENTE e não traço dela: o prompt diz isso, e o registro mora na RN-717. - Medição (AT-376): na loja-teste de 02/10, "Git → iniciante" e "Infra → iniciante" sem a pessoa ter interagido com nenhum dos dois, e "aprova 2 ADRs sem hesitar" como arquitetura avançada.
- Onde:
src/domain/anamnese/proficiency-validation.ts:53(ehEvidenciaObservadaDaPessoa),src/domain/anamnese/proficiency-validation.ts:74(validateProficiencyBatch),src/application/use-cases/anamnese/record-proficiency.use-case.ts:189(resolveKnownEventIds), regras no kickoff (prompts/anamnese-kickoff.mdv3 e o fallback inline deanamnese_worker.ex) - Teste:
test/domain/anamnese/proficiency-validation.spec.ts("RN-716: aprovação ao lado de interação da pessoa passa"; falhas: "só evidência de outro ator é "não observado"", "aprovação sem leitura não é evidência"),test/application/use-cases/anamnese/record-proficiency.use-case.spec.ts("evidência só de aprovação não grava perfil nem profile_updated") - Lacuna declarada: reconhecer a mensagem de REPARO no lote de perfil é do modelo (prompt); o código não classifica texto de mensagem.
- Origem: AT-376
Uma chave de cache, uma pergunta (RN-718)
RN-720 — O PO completa a história que já existe, e não oferece o handoff ao Arquiteto com regra descoberta
Contexto: no uso real da loja-teste-2 (02/10) o PO criou doze histórias
sem regra ligada, leu em listar_regras_de_negocio que sete das oito regras
estavam SEM cobertura, escreveu que não tinha ferramenta para editar história
e chamou offer_handoff ao Arquiteto assim mesmo — afirmando no fio que a
cobertura estava completa e que o sistema aceitaria sozinho. O aceite
automático da RN-660 não aconteceu (o critério não fechava), e a
história que ele recriou disparou o aviso de duplicata semântica
(RN-681).
A regra:
- O PO tem
complete_story: completa uma históriadraftEXISTENTE do projeto — osbusiness_rule_idsinformados são SOMADOS aos já ligados (completar nunca desliga regra; id que não éartifact.business_ruleé 400), listas informadas e não vazias substituem as atuais e as omitidas ficam. História que não édrafté 409; de outro projeto, 404. Depois de gravar, a promoção segue o MESMO critério decreate_story(isPromotable;autovai aready,manualligaproposedReady), e a escrita gravabacklog.story_completed. offer_handoffdo PO ao Arquiteto é RECUSADO, sem criar oferta, enquanto houver regra sem história ou históriadraftincompleta (semproposedReady) — e o resultado lista as duas, com id e título, e manda completar comcomplete_story. A cobertura é a da api (computeCoverage, a mesma do aceite automático), nunca uma segunda conta no engine; falha de leitura não vira recusa. Os outros agentes e destinos não passam pela checagem.- Quando o
emit_artifactdebusiness_ruledo PO volta com o aviso de semelhança (status: warned), o resultado acrescenta que a regra parece ALTERAR uma existente e que ele deve perguntar ao usuário (ask_structured_questions) qual vale antes de seguir (AT-389: o usuário disse "taxa fixa de R$ 10", o Criativo registrou "R$ 10 por pedido" e o PO emitiu "R$ 10 por torrefador"). A regra já gravada não é desfeita; o aviso continua aviso.
O kickoff do PO diz as três coisas.
- Onde:
src/application/use-cases/backlog/complete-story.use-case.ts:53(CompleteStoryUseCase);src/interfaces/http/internal/internal-sessions.controller.ts:541(completeStoryRoute);apps/engine/lib/engine/harness/tools/complete_story.ex:47(run);apps/engine/lib/engine/harness/tools/offer_handoff.ex:52(recusa_por_cobertura);apps/engine/lib/engine/harness/tools/emit_artifact.ex:188(pergunte_antes) - Teste:
test/application/use-cases/backlog/complete-story.use-case.spec.ts(liga a regra e propõe a promoção;autovai aready; regra inexistente, outro projeto e históriareadyrecusados);apps/engine/test/engine/harness/tools/offer_handoff_test.exs(describe "cobertura (RN-720)": recusa com a lista, cobertura completa oferta e o aceite automático segue);apps/engine/test/engine/harness/tools/complete_story_test.exs;apps/engine/test/engine/harness/duplicata_semantica_test.exs(o aviso no PO manda perguntar; fora do PO ou sem aviso, não) - Lacuna declarada: o servidor do PO continua oferecendo sozinho no fim do turno pela RN-700, que já exigia cobertura; a recusa aqui é da FERRAMENTA. Se a duplicata semântica avisou no caso real da loja-teste-2 não foi medido nesta mudança (o banco daquela execução não foi lido).
- Origem: AT-388, AT-389