API — parte 12 (desde RN-1029)
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-1029 — Depois do merge da PR de infra que declara run.app, a api propõe container_recreate com a porta do app, uma vez
- Regra: quando um
git_mergenamain(RN-1022) recebestate: 'merged', oApproveActionUseCasechama oProporRecriacaoDoAppUseCaseFORA da transação do merge e em melhor esforço: uma falha só registra no log o nome do erro (sem mensagem, sem segredo) e o merge continua válido.- Detecção. Existe um
open_infra_prdo projeto,executed, cujopullRequestIdé o mergeado e cujo payload levarun.app, e oprUrldasrun_instructionscorrentes (RN-852) é opullRequestUrldessa ação (uma PR de infra mais nova e ainda aberta levaria um comando que não está namain). - NÃO propõe quando (função pura
motivoDeNaoRecriar): o projeto érunner; não há container, ou ele estáremoved,failedouprovisioning; já há umcontainer_recreatepending; ou o status do container mudou depois que asrun_instructionscorrentes foram declaradas (statusChangedAt > declaredAt: ele já subiu com o app). Atalho: um container parado e reiniciado depois comdocker startpassa como atualizado; a recriação manual na página do container continua. - Porta do host. SEMPRE na faixa 13000+, mesmo quando
app.portseria permitida: o primeiro valor livre a partir de 13000, pulando toda porta de host já declarada em qualquerartifact.container_networkde qualquer projeto (atalho: qualquer versão conta como ocupada, e a recriação pendente de outro projeto não conta). A porta do container érun.app.port. - Proposta.
ProposeActionUseCasecomcontainer_recreate, atoragent/infra, na sessão do merge, payload exatamente{ reason: 'app_declared', pullRequestId, ports: [{ host, container, protocol: 'tcp' }] }. O teto absoluto da RN-934 o mantémpending: o clique continua humano. A web monta a URL do app a partir depayload.ports[0].host(http://127.0.0.1:<host>). - Clique. O
ExecuteContainerRecreateUseCasegravaartifact.container_network(RN-932) com o atoruser/<quem clicou>ANTES doremove, e só quando o projeto não tem rede corrente (nunca sobrescreve); uma rede recusada faz a ação falhar antes de o container antigo ser tocado. Depois,remove+startcomo na RN-934.
- Detecção. Existe um
- Código:
apps/api/src/application/use-cases/actions/propor-recriacao-do-app.use-case.ts(execute),apps/api/src/domain/containers/recriar-com-app.ts(motivoDeNaoRecriar,proximaPortaLivre),apps/api/src/application/use-cases/actions/approve-action.use-case.ts(proporRecriacaoDoApp),apps/api/src/application/use-cases/actions/execute-container-recreate.use-case.ts(obterRede) - Teste:
apps/api/test/domain/containers/recriar-com-app.spec.ts,apps/api/test/application/use-cases/actions/propor-recriacao-do-app.use-case.spec.ts,apps/api/test/application/use-cases/actions/approve-action-recreate-hook.spec.ts(falha do gancho não desfaz o merge),apps/api/test/application/use-cases/actions/execute-container-recreate.use-case.spec.ts(rede gravada só sem uma anterior, com quem clicou como ator, antes doremove) - Origem: AT-758 (EP-030), decisão do dono de 06/11/2026; rodada 209, lane L1.
RN-1030 — Todo nó do grafo gravado por MERGE numa chave natural tem restrição de unicidade, e a subida funde as duplicatas preexistentes antes de criá-la
- Regra: refina a RN-569.
Handoff (sessionId, seq),Evento (sessionId, seq),PerfilAnamnese (userId, dimensao)eHipotese (id)não tinham restrição, então doisMERGEs concorrentes (o projetor vivo e uma reprojeção, ou duas réplicas da api) criavam dois nós: a prova de reprojeção no k3d mediu12|9em vez de9|6. Agora eles têm restrição de unicidade, e os casos de uso continuam fazendoMERGEexatamente na chave restrita. Restrição de unicidade composta é aceita pelo Neo4j 5.26 Community (medido; sóNODE KEYe restrições de existência são Enterprise), então não existe propriedade de chave sintética. - Subida: antes do
CREATE CONSTRAINT, oGraphStorefunde as duplicatas desses quatro rótulos já presentes num grafo instalado (fica o nó mais antigo porelementId, toda aresta dos outros é movida para ele sem duplicar uma existente, e os outros são apagados). É idempotente (sem duplicatas não grava nada), roda numa transação por rótulo e registra no log só o número de nós fundidos, nunca o conteúdo deles. Uma falha mantém o grafo desligado, como qualquer outra falha de bootstrap (RN-960). - Onde:
apps/api/src/infrastructure/graph/graph-store.ts:51(CONSTRAINTS),apps/api/src/infrastructure/graph/graph-store.ts:160(bootstrapConstraints),apps/api/src/infrastructure/graph/graph-dedup.ts:34(deduplicarNos). - Teste:
apps/api/test/infrastructure/graph/neo4j-integration.spec.ts(contra um Neo4j real: escritas concorrentes do mesmo nó dão um nó; as duplicatas são fundidas com as arestas mantidas, as restrições são criadas depois, uma segunda subida não muda nada),apps/api/test/infrastructure/graph/graph-store.spec.ts(a deduplicação roda antes das restrições). - Origem: AT-762 parte B (EP-033), decisão do dono de 06/11/2026; rodada 214, lane L1.
RN-1065 — O provider de LLM dublê do regressivo só carrega com LLM_DUBLE=1 e com a imagem sem a marca prd; o custo dele é 0 e o modelo sai como duble-llm
- Regra: o regressivo do
brabo-qaprecisa de agentes que respondem sem LLM real nem custo. ODubleLLMProviderocupa o lugar do providerollamanoLLMProviderRegistry(o mesmo ponto dos outros; é o provider sem credencial) e responde por roteiro. Ele só existe quando a variávelLLM_DUBLEvale exatamente1E o arquivo/app/AMBIENTE_DA_IMAGEM(a marca gravada pelo Dockerfile, o mesmo mecanismo da AT-722) não dizprd. Na imagemprdele nunca carrega, nem com a variável: a api só registra um aviso no log. Sem a marca (pnpm dev), só a variável decide. A marca vem da tag (AT-850): a imagemsha-da PR e a de tag-dev.Nlevamdev(o dublê pode carregar no E2E); só a de tag finalvX.Y.Z, sem sufixo, levaprd.LLM_DUBLE=1semLLM_DUBLE_ROTEIROderruba a subida. - Custo e identificação: cada chamada devolve
costMicros: 0eresolvedModel: "duble-llm", entãotoken_usagegrava custo 0 eresolved_model_name = duble-llm, e nunca se confunde com uso real. - Roteiro: arquivo JSON apontado por
LLM_DUBLE_ROTEIRO:{"agentes": {"<agentId>"|"*": [passo, ...]}}, compasso = {texto?, tool_calls?: [{name, arguments?}], inputTokens?, outputTokens?}. Um passo por chamada, por sessão e agente (o agente sai desessionKey = brabo:<sessionId>:<agentId>); acabou a lista, repete o último passo. Agente sem entrada cai em"*"; sem"*", o turno termina em erro. Astool_callssaem no mesmo formato que o parser do Ollama produz. Exemplo emdocs/reference/llm-duble-roteiro.example.json. - Limite: sem
embednemlistModels(RAG e sincronização de catálogo seguem sem o dublê). - Onde:
src/infrastructure/security/ambiente.ts(dubleDeLlm),src/infrastructure/llm/duble-llm-provider.ts(dubleSeAutorizado,DubleLLMProvider),src/infrastructure/llm/llm-provider-registry.ts(resolve),src/infrastructure/llm/llm-infrastructure.module.ts(LLM_DUBLE). - Teste:
test/scripts/marca-da-imagem.spec.ts(marca por tag nos workflows) etest/infrastructure/llm/duble-llm-provider.spec.ts(variável sim ou não × marca prd ou não; roteiro por agente e passo; custo 0 e modelo dublê). - Origem: AT-815 (EP-033), decisão do dono de 08/11/2026 (P1); ADR 0279; rodada 251, lane L3.
RN-1068 — O PO e o Dev Lead corrigem o texto de uma tarefa todo, com a régua da história
- Regra:
POST /internal/sessions/:sessionId/tasks/:taskId/update(ferramentaupdate_taskdo PO e do Dev Lead) corrige título e/ou descrição de uma tarefa. Estende a RN-727 e a RN-859 à tarefa. Recusa em 409 nomeado:tarefa_em_execucao(status diferente detodo),historia_arquivadaehistoria_nao_draft(a históriareadysó aceita comquestionIdde pergunta já respondida pelo usuário). 400 sem campo ou com título vazio ou acima de 200; 404 para tarefa de outro projeto. Gravabacklog.task_updatedna sessão da história, comtaskId,storyId,previousTitle,title,descriptionChangede, quando houver,questionId; o ator éagentpo(padrão) oudev-lead(campoagentId). Nada mudou, nenhum evento. - Onde:
src/application/use-cases/backlog/corrigir-historia.use-case.ts:103(editarTarefa),src/domain/backlog/correcao-de-historia.ts:82(recusaDeCorrecaoDeTarefa),src/interfaces/http/internal/internal-sessions.controller.ts:614(updateTaskRoute) - Teste:
test/application/use-cases/backlog/corrigir-historia.use-case.spec.ts(CorrigirHistoriaUseCase.editarTarefa: feliz com evento;in_progress/in_review/donerecusadas; históriareadycom e sem resposta; arquivada; 404 e 400). - Origem: AT-805 (EP-030), achado 5 do TP-01 27ª; rodada 271, lane L2.
RN-1069 — A ADR proposta declara qual ADR substitui (supersedes), e a api confere
- Regra: o payload de
open_adr_praceitasupersedes(slug da ADR substituída;docs/adr/<slug>.mdtambém vale). Ao propor, a api exige umaopen_adr_prDESTE projeto com esse slug e já aceita (executed: aprovada, com a PR aberta ou mergeada); inexistente, de outro projeto, pendente, negada, com falha ou a própria ADR nova é 400adr_supersedes_invalido, sem criar a proposta. A PR da ADR nova ganha a linhaSubstitui: docs/adr/<slug>.mdno corpo. Semsupersedes, nada muda. - Onde:
src/domain/actions/adr-supersedes.ts:22(recusaDeSupersedes),src/application/use-cases/actions/propose-action.use-case.ts:162(chamada),src/application/use-cases/actions/execute-adr-pr.use-case.ts(corpo da PR) - Teste:
test/domain/actions/adr-supersedes.spec.ts,test/application/use-cases/actions/propose-action.use-case.spec.ts(open_adr_pr com supersedes, contra Postgres). - Origem: AT-806 (EP-030), decisão do dono (b) de 08/11/2026; ADR 0283; rodada 271, lane L2.
A api cumpre a RN-1039 (o token do GitHub Packages nunca entra em camada nem em ARG/ENV; só como secret do BuildKit), definida no brabo-broker. Quem prova na api é test/dockerfile-sem-token.spec.ts:13 (todo pnpm install do Dockerfile monta o secret, nenhum ARG/ENV com token, .npmrc só com o registry).