API — parte 11 (desde RN-916)
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-916 — Resposta cortada pelo raciocínio é retentada UMA vez com o raciocínio desligado
Revisa a RN-782 e a RN-783 (o desligado deixa de ser só dos dev agents) e convive com a RN-915.
- Gatilho. A resposta veio
truncated, sem texto e sem tool call, e o raciocínio comeu o orçamento:reasoningTokens >= (reasoningBudget do modelo ?? 4096) − 256(folgaFOLGA_DO_ORCAMENTO_DE_RACIOCINIO, a mesma ordem de grandeza do erro dousagee da contagem caracteres/4), ou o stream foi abortado pela RN-915 (semusage, o aborto já é a prova do estouro). - Uma vez. A api repete a chamada com
reasoningOff, nos três caminhos (run-llm-turn,stream-llm-turn,send-chat-message). Se a segunda também trunca, o desfecho é o de hoje (truncated); nunca há terceira. Nada foi emitido ao cliente na primeira, então a retentativa é invisível. - Metering. As DUAS chamadas gravam linha em
token_usage; a primeira, ao preço/custo que o provider informou (ou a estimativa abortada da RN-915, com reconciliação). - Não retenta sem raciocínio pedido, com o raciocínio já desligado (dev agents, RN-783) ou com provider que não sabe desligá-lo (
aceitaRaciocinioDesligado, OpenRouter e Anthropic; ver a RN-1001, que corrige o decorator que escondia a flag): ali o desfecho segue o de antes.
- Código:
src/domain/llm/raciocinio-do-agente.ts:66(devoRetentarSemRaciocinio);src/application/use-cases/llm/run-llm-turn.use-case.ts:359(devoRetentarSemRaciocinio) - Teste:
test/domain/llm/raciocinio-do-agente.spec.ts(folga, aborto e as recusas);test/application/use-cases/llm/run-llm-turn.use-case.spec.ts(com a capability lida doAnthropicProviderreal, AT-757: retenta e grava duas linhas; segunda truncada não tem terceira; truncada com texto não retenta);test/application/use-cases/llm/stream-llm-turn.use-case.spec.ts(idem no stream) - Origem: AT-587, AT-757 (o provider Anthropic declara a capability, o que passa a ser coberto por um spec de caso de uso)
RN-917 — O OpenRouter recebe um teto de preço por chamada, e sem upstream no teto a chamada repete UMA vez sem ele
Revisa a RN-583 (o sort deixa de ser o único campo de provider que vai ao fio) e convive com a RN-907 (o session_id) e a RN-916 (a mesma volta de retentativa).
- Teto.
provider.max_price = { prompt, completion }= preço de catálogo do modelo (inputPricePerMillionMicros/outputPricePerMillionMicros) ×FATOR_DO_TETO_DE_PRECO(1,5, decisão do dono de 27/10), em US$ por milhão. Fixo no código e ligado por padrão no provider que declarapriceCeiling(só o OpenRouter, provado por smoke); preço 0 em qualquer ponta, ou provider sem a capability, não manda teto. Mudar o fator é ADR novo (0225). - Mescla. O objeto
providerdo teto é mesclado com o dosortdo binding (RN-583): os dois vão juntos no mesmo corpo, nunca um apaga o outro. - Sem upstream no teto. O 404 de uma chamada que levava teto é o erro
LLMPriceCeilingError(code: price_ceiling), nuncaLLMModelNotFoundError. Os três caminhos de turno (run-llm-turn,stream-llm-turn,send-chat-message) repetem a chamada UMA vez sem teto, avisam no log e não falham o turno; nunca há terceira por este motivo. - Metering.
token_usage.price_ceiling_factorcongela o fator que FOI AO FIO (null= nada enviado, inclusive na repetição sem teto), no molde dorouting_preference.
- Código:
src/domain/llm/routing-preference.ts:108(tetoDePreco);src/domain/llm/routing-preference.ts:132(devoRetentarSemTeto);src/infrastructure/llm/openrouter-provider.ts:221(campoDeTetoDePrecoOpenRouter);src/infrastructure/llm/openai-compatible-provider.ts:604(camposDoHub);src/application/use-cases/llm/stream-llm-turn.use-case.ts:372(devoRetentarSemTeto);src/db/schema/llm.ts:414(priceCeilingFactor) - Teste:
test/domain/llm/routing-preference.spec.ts(fator, preço 0, só o OpenRouter declara);test/infrastructure/llm/openrouter-provider.contract.spec.ts(sorte teto mesclados no corpo, sem capability nada vai, 404 com teto éprice_ceilinge sem teto seguemodel_not_found);test/application/use-cases/llm/stream-llm-turn.use-case.spec.ts(repete uma vez sem teto, fator[1.5, null]no metering);test/infrastructure/llm/openrouter-provider.teto.smoke.spec.ts(manual, API real) - Origem: AT-606; ADR 0225
RN-921 — O botão "Retomar" do fio é gesto humano, sem teto, e só vale para o turno interrompido
- Regra: o
agent.errorcomreason: turno_interrompido_por_reinicioganha no fio o cartãoRetomarTurnoInterrompido(objetivo e passos do último plano, "Parou em", botão). O clique chamaPOST /projects/:projectId/sessions/:sessionId/agents/:agent/resume(developer; 200{ok:true}; 404 sessão alheia; 409turno_nao_interrompidoquando o último desfecho do agente não é a interrupção ou há turno em curso; 409agente_sem_retomada), que pede ao enginePOST /internal/sessions/:sessionId/agent/resume: ele gravaagent.turn_resumedcomautomatica: falsee manda:retomar_turno_interrompidoà MESMATurnoAssincrono. Sem teto: cada clique é uma retomada. O cartão some quando algo segue o erro, e a retomada automática vira uma linha discreta. O "sem desfecho" do diário NÃO aparece na tela (o evento não o traz). - Onde:
src/interfaces/http/sessions/agent-resume.controller.ts:23(AgentResumeController),src/application/use-cases/sessions/resume-agent-turn.use-case.ts:29(execute),apps/engine/lib/engine_web/controllers/agent_command_controller.ex:638(resume),apps/web/src/routes/RetomarTurnoInterrompido.helpers.ts:10(ehTurnoInterrompido) - Teste:
test/interfaces/http/sessions/agent-resume.controller.spec.ts(feliz, duas recusas 409, 404, papel);apps/web/src/routes/RetomarTurnoInterrompido.test.tsx; ExUnit doagent_command_controller - Origem: AT-582 (EP-031); ADR 0226.
RN-926 — A rota de providers com chave devolve só os nomes, e a chave é a do TITULAR
- Regra:
GET workspaces/:workspaceId/llm-providers/credentialed(viewer, o mínimo da leitura de membros, RN-655) devolve{ providers: LLMProviderName[] }com os providers de LLM em que o TITULAR do workspace (workspaces.created_by, a pessoa cuja credencial os agentes gastam, RN-058) tem credencial cadastrada. Só nomes, na ordem da lista canônica: nunca segredo, id, data nemkey_id.ollamanão tem credencial e nunca aparece; token de git (github/gitlab) também não. Credencial de outro membro não conta. É a fonte de "tem chave" das telas de providers e do catálogo (AT-607); a tela vem depois. - Onde:
src/application/use-cases/llm/list-credentialed-providers.use-case.ts:20(execute),src/interfaces/http/llm/models.controller.ts:80(credentialed) - Teste:
test/application/use-cases/llm/list-credentialed-providers.use-case.spec.ts:44(titular com duas chaves: só os dois nomes; a chave de outro membro não conta) - Origem: AT-607 (EP-026).
RN-927 — O runner grava a chave de API de LLM do usuário por POST /runner/llm-credentials, só com chave de máquina, sem sobrescrever sem confirmação
- Regra:
POST /runner/llm-credentials(@RequirePatAuth(),PatAuthGuard) recebe{ provider, apiKey, origem, substituir? }e delega ao mesmoUpsertUserCredentialUseCase(envelope comkey_id, ADR 0158) para o usuário dono da credencial. Rota sem:projectId: o guard recusa com 403 credencial amarrada a projeto, e todo PAT é de projeto, então só a chave de dispositivo de MÁQUINA entra (a primeira rota em que ela escreve segredo de usuário). Provider fora deLLM_PROVIDER_NAMES_COM_CREDENCIALé 400; credencial existente do provider semsubstituir: trueé 409credencial_existente, sem tocar a existente. A resposta é{ provider, substituida }. O valor nunca vai a log (a auditoria leva provider eorigem), URL, evento, exceção nem resposta, e a rota não testa a conexão (ADR 0050). Só chave de API é importável; token de login por assinatura não (ADR 0230). - Onde:
src/application/use-cases/llm/import-runner-llm-credential.use-case.ts:22(execute),src/interfaces/http/runner/runner-llm-credentials.controller.ts:33(RunnerLlmCredentialsController) - Teste:
test/application/use-cases/llm/import-runner-llm-credential.use-case.spec.ts(grava cifrado comkey_idsem o valor em log/resposta; 409 sem substituir; substituir; provider inválido); 401/403 do guard emtest/interfaces/pat-auth.guard.spec.ts;route-surface.spec.ts - Origem: AT-611 (EP-026), ADR 0230.
RN-932 — A rede do container é artefato declarado por humano e só vale no container recriado
- Regra:
artifact.container_network({ ports[{host,container,protocol:'tcp'}], dns[], extraHosts[{name,ip}] }) é versionado, sem tabela, e o vigente é o de maiorversion. Só um humanomaintainero escreve (PUT /projects/:projectId/container/network;GETéviewer), com a MESMA régua do docker-port (host 1024-65535 fora das portas da instalação, até 10 portas sem duplicata, até 3 DNS por IP literal, até 10 hosts com IPv4 e nome fora dos reservados); recusa é 400rede_invalidanomeando o campo. Escrever NÃO é efeito externo. Acontainer-specdevolveports,dnseextraHosts(ounull;runnersemprenull, runner depois) e a mudança só vale em container recriado (RN-934). A api não importa o docker-port, então a régua é espelhada e a paridade é provada por teste. - Onde:
src/domain/containers/rede-do-container.ts:86(validarRedeDoContainer),src/interfaces/http/containers/containers.controller.ts:91(declarar),src/application/use-cases/containers/obter-spec-de-container.use-case.ts:135(execute). - Teste:
test/contract/rede-do-container-paridade.spec.ts(paridade com o docker-port),test/application/use-cases/containers/spec-e-observacao-de-container.use-case.spec.ts(spec devolve a rede;runnertudonull). - Origem: AT-608 (EP-030), ADR 0228.
RN-933 — run.app declara o comando do app e só o piloto automático dispensa a aprovação da PR de infra
- Regra:
rundeopen_infra_praceitaapp: { command, port }opcional (comando de até 300 code points, uma linha, sem NUL; porta 1024-65535;nullequivale a ausente, como no engine) validado na proposta e na execução; semapp, tudo é byte a byte o de antes. Comrun.app, a regra ESPECÍFICAopen_infra_pr: auto_approve(a semente do aceite) NÃO basta e a decisão virarequire_approval; só a CURINGAauto_approvedo agente (piloto automático, RN-603/RN-670) auto-aprova. Acontainer-specdevolvecomandoDoAppdorun.app.commandvigente, ounull. - Onde:
src/domain/infra/run-instructions.ts:77(recusaDoRun),src/domain/actions/decide.ts:626(comAppDeclarado),src/application/use-cases/actions/propose-action.use-case.ts:236(comAppDeclarado),src/application/use-cases/containers/obter-spec-de-container.use-case.ts:171(comandoDoApp). - Teste:
test/domain/infra/run-app.spec.ts,test/domain/actions/decide.spec.ts(sem app + específica = auto; com app + específica = aprovação; com app + curinga = auto). - Origem: AT-610 (EP-030), ADR 0228.
RN-934 — container_recreate remove e sobe de novo, no teto absoluto de container_remove
- Regra:
container_recreate(maintainer) éproposed_actionnunca auto-aprovável, nem com a curinga, e "sempre permitir" é recusado na fonte (mesma lista decontainer_remove). A execução fazremoveestartpelo broker passando porrunning -> stopped -> removed -> provisioning -> running; recusa ou indisponibilidade do broker virafailednomeado (se a subida falha, o motivo diz que o container já foi removido).runneré recusado nomeando "runner depois". A frase emaprovacoes.tsexiste e o teste do web reprova tipo sem frase. - Onde:
src/domain/actions/decide.ts:609(container_recreate),src/application/use-cases/actions/execute-container-recreate.use-case.ts:48(execute),apps/web/src/lib/aprovacoes.ts:117(container_recreate). - Teste:
test/application/use-cases/actions/execute-container-recreate.use-case.spec.ts,test/domain/actions/decide.spec.ts(teto),apps/web/src/lib/aprovacoes.test.ts. - Origem: AT-608 (EP-030), ADR 0228.
RN-935 — A Infra declara os serviços da instância de QA (run.qa), e a api guarda uma instância por tarefa com teto de 1 por projeto
- Regra:
open_infra_praceitarun.qa = { services: [...] }(opcional,null= ausente; até 6 serviços de nome único[a-z][a-z0-9-]{0,31};imageCOM digest@sha256:<64 hex>;envaté 20 variáveis[A-Z_][A-Z0-9_]*de uma linha;tmpfsaté 3 caminhos absolutos;sharedbooleano). A mesma régua vale na proposta (400run_invalido) e na execução, e quem declara é a Infra, não um catálogo do produto (decisão do dono, 29/10). A instância de uma tarefa é uma linha deproject_qa_environments(provisioning|running|failed|removed, único(projeto, tarefa)), e um índice parcial único garante no banco UMA ativa por projeto: subir a de outra tarefa com uma ativa é 409instancia_de_qa_ocupada.runneré recusado (409qa_nao_suporta_runner, "runner depois"). A chave do container é ochaveDeQa(taskId)(8 primeiros[a-z0-9]do id, sem hífen), calculada só pela api. - Onde:
src/domain/infra/run-instructions.ts:160(recusaDoQa),src/domain/containers/instancia-de-qa.ts:15(chaveDeQa),src/db/schema/containers.ts:87(projectQaEnvironments),src/application/use-cases/containers/instancia-de-qa.use-case.ts:80(start). - Teste:
test/domain/infra/run-qa.spec.ts,test/application/use-cases/containers/instancia-de-qa.use-case.spec.ts. - Origem: AT-612 (EP-030), ADR 0229.
RN-936 — Subir a instância de QA é a ação qa_environment_start, semeada auto_approve para o QA; remover é automático e fora do teto do container_remove
- Regra:
qa_environment_start(developer, payload{ taskId }) éproposed_actionsemeadaauto_approveparaqa-automacaona ativação da execução; ao ser auto-aprovada ou aprovada, a api sobe a instância pelo broker (failednomeado se o teto está ocupado, se o projeto érunnerou se o broker recusa). A REMOÇÃO não é proposta:POST /internal/projects/:projectId/qa-environments/:taskId/removeé idempotente e só alcança o que o broker rotulabrabo.papel=qa, então não entra no teto absoluto decontainer_remove/container_recreate. Ocontainer-execaceitaalvo: { qa: true, taskId }vindo do engine (nunca do modelo) para rodar na instância; sem instânciarunningdevolvesucesso: false. A frase do tipo existe emaprovacoes.tse o teste do web reprova tipo sem frase. - Onde:
src/domain/actions/decide.ts:198(qa_environment_start),src/application/use-cases/actions/execute-qa-environment-start.use-case.ts:25(execute),src/application/use-cases/containers/instancia-de-qa.use-case.ts:120(remove),apps/web/src/lib/aprovacoes.ts:473(qa_environment_start). - Teste:
test/application/use-cases/containers/instancia-de-qa.use-case.spec.ts,test/domain/actions/decide.spec.ts,apps/web/src/lib/aprovacoes.test.ts. - Origem: AT-612 (EP-030), ADR 0229.
RN-937 — A container-spec de QA e a limpeza de órfãs
- Regra:
GET /internal/projects/:id/container-spec?papel=qa&task=<ref>devolve o contexto normal comcomandoDoApp: null,ports: nulleqa: { chave, services }(os derun.qa,[]se ausentes).taské o id completo OU a chave de 8 (o broker dadevmanda a chave), resolvido DENTRO do projeto: tarefa inexistente ou de outro projeto é 404, chave ambígua é 409tarefa_ambigua.POST /internal/qa-environments/sweeprecebe do engine ostaskIdsEmVooe remove toda instância ativa oufailedfora dessa lista. Lacuna declarada: o broker ainda não tem operação para remover o serviço compartilhado nem a redebrabo-qa-<wd>, que ficam até essa operação existir. - Onde:
src/application/use-cases/containers/obter-spec-de-container.use-case.ts:135(execute),src/application/use-cases/containers/instancia-de-qa.use-case.ts:140(limparOrfas),src/interfaces/http/internal/internal-qa-environments.controller.ts:90(sweep). - Teste:
test/application/use-cases/containers/instancia-de-qa.use-case.spec.ts. - Origem: AT-612 (EP-030), ADR 0229.
RN-938 — O terminal só vai para a instância de QA quando o ator é o qa-automacao e a tarefa é do projeto
- Regra: a api repassa
payload.alvo = { qa: true, taskId }de umaproposed_actionterminalao/internal/actions/executedo engine SOMENTE se o ator da ação éqa-automacaoe otaskIdpertence ao projeto da ação. Em qualquer outro caso (outro agente,alvomalformado, tarefa de outro projeto) oalvoé descartado com o avisoterminal.alvo_de_qa_descartadono log e o comando segue pelo caminho de sempre: nenhum outro agente desvia comando para a instância de QA. - Onde:
src/application/use-cases/actions/execute-terminal-action.use-case.ts:40(alvoDeQa),src/infrastructure/http-clients/api-to-engine-client.ts:166(executeTerminalAction). - Teste:
test/application/use-cases/actions/alvo-de-qa-no-terminal.spec.ts. - Origem: AT-612 (EP-030), ADR 0229.
RN-942 — O recorte do Jev nunca tira do PO a ferramenta de declarar dúvida
- Regra: estende a RN-758. O piso do PO ganha
declare_open_question, porque a regra 3 das obrigações do PO manda declarar a dúvida em vez de inventar ou omitir, e o recorte do Jev (RN-625) a tirava de todo passo depois do primeiro: o PO punha a lacuna na borda da história e nenhumaartifact.open_questionnascia. Continua valendo que o piso só devolve o que está no catálogo do passo. - Onde:
src/domain/llm/tool-router.ts:315(PISO_DO_MENU). - Teste:
test/application/use-cases/llm/decidir-ferramenta-do-passo.use-case.spec.ts(PO recortado mantémdeclare_open_question; catálogo sem ela não a ganha). - Origem: AT-615 (EP-030).
RN-948 — Higiene do entregável: banco SQLite fora do commit, pasta dos comandos dita, rotas do README preservadas
- Regra: estende a RN-761, a RN-900 e as RN-706/RN-850. Três partes, sem gate novo. (1) Segunda lista,
ARQUIVOS_DE_DADOS(*.db,*.db-*,*.sqlite,*.sqlite-*,*.sqlite3,*.sqlite3-*), nas MESMAS três camadas da RN-761: o.gitignorebase do bootstrap, a exclusão:(exclude,glob)**/<padrão>nogit adddo dev agent e um achado próprio do SecOps ("arquivo de dados em tempo de execução commitado", que nomeia o caminho). O teste que cruza as listas da api e do engine cruza as duas. (2) A mensagem de ambiente do agente com módulo diz quenpm/pip/mixrodam na pasta do módulo na MESMA chamada, nuncacd … 2>/dev/null;(ocdfalho caía na raiz e deixou umpackage-lock.jsonvazio), dentro do teto de 1.500 caracteres. (3) O kickoff do dev nomeia oREADME.mdda RAIZ, sem repetir a seção em README de pasta, ewrite_filerecusa reescrever um README que perde cabeçalho de rota (### GET /links) presente no antigo, no molde da RN-900. Declarado: repositório já bootstrapado não ganha o.gitignorenovo; fixture.dbintencional fica fora do commit (renomeie para versioná-la); a guarda não cobre escrita peloterminal. Fora desta RN:expires_at×expiresAt. - Onde:
src/application/use-cases/git/bootstrap-templates.ts:29(ARQUIVOS_DE_DADOS),apps/engine/lib/engine/actions/diretorios_de_dependencia.ex:37(argumentos_do_add),apps/engine/lib/engine/gates/secops_agent_server.ex:162(dependencias_commitadas),apps/engine/lib/engine/harness/ambiente_do_agente.ex:178(modulo),apps/engine/lib/engine/harness/tools/write_file.ex:62(recusa_de_secao_perdida),apps/engine/lib/engine/dev/dev_agent_server.ex:666(initial_message). - Teste:
apps/engine/test/engine/actions/diretorios_de_dependencia_test.exs(lista cruzada com a api, add real que estagia sósrc/a.js, SecOps nomeia o.db),test/application/use-cases/git/bootstrap-plan.spec.ts(os seis padrões no.gitignore),apps/engine/test/engine/harness/ambiente_do_agente_test.exs,apps/engine/test/engine/harness/tools/write_file_test.exs,apps/engine/test/engine/dev/report_done_instrucao_test.exs. - Origem: AT-623 (EP-031), entregável
entregavel3(commits 956fa5c, 8e593b0).
RN-949 — Chamada de ferramenta escrita como texto não vira resposta no fio
- Regra: quando a chamada ao LLM leva
tools, vem semtoolCallse o texto traz a chamada escrita como marcação (<function_calls>,<invoke name="X">com X ferramenta do passo,<tool_call>{json}comnamede ferramenta; com ou sem prefixoantml:), a api repete a MESMA chamada UMA vez (flag própria, independente das RN-916/917; as duas chamadas entram no metering). Se repetir, o desfecho é falha NOMEADA:finalcomcontent: '',errornomeando o formato e o modelo eerrorCode: 'tool_call_as_text'(erro do MODELO, RN-059/163), nunca resposta. No stream, a api para de repassar deltas a partir do primeiro marcador e retém a cauda que pode ser início dele: o texto cru não sai da api. Bloco cercado, código inline, prosa, nome que não é ferramenta e chamada semtoolsnão contam; o JSON solto segue com oToolCallRecovery(ADR 0020). Decisão do dono: NÃO se parseia nem se executa o XML (RN-040: resgate não é licença). Limite declarado: o prefixo legítimo da 1ª tentativa fica na bolha em curso até o turno fechar. - Onde:
src/domain/llm/chamada-em-texto.ts:10(marcacaoDeChamadaEmTexto),src/application/use-cases/llm/run-llm-turn.use-case.ts:347(semChamadaEmTexto),src/application/use-cases/llm/stream-llm-turn.use-case.ts:386(semChamadaEmTexto) - Teste:
test/domain/llm/chamada-em-texto.spec.ts,test/application/use-cases/llm/run-llm-turn.use-case.spec.tsestream-llm-turn.use-case.spec.ts(chamada escrita como texto: duas vezes falha nomeada com 2 linhas de uso; segunda limpa responde) - Origem: AT-633 (sessão
2b5161d0, seq 4:<invoke name="emit_artifact">apareceu como resposta no fio)
RN-950 — O 9Router recebe as ferramentas, o vínculo confere a capability do provider e ferramenta sem capability lança
- Regra: revisa a RN-909 e completa a RN-040 com a SEGUNDA camada do ADR 0041. (1)
nineRouterConfigdeclaratoolCalling: true, provado por smoke com chave real em 31/10: 3/3 chamadas emcc/claude-haiku-5-5voltaram comtool_callsestruturado, 10/10 consistente a temperatura 0 e laço de 3 voltas íntegro. Antes, a capabilityfalsefazia obuildBodydescartartoolsem silêncio e o Criativo escrevia a chamada como XML no fio (AT-637). (2)assertModelFitsBindingScopeconfere também o PROVIDER nos escoposagentearea: vínculo cujo provider declaratoolCalling: falseé recusado com 422 (ModelNotFitForAgentScopeError, causaprovider), com frase própria.workspace,projectesessionseguem livres. (3)buildBodycom ferramentas pedidas e capabilityfalseLANÇA erro nomeado em vez de descartar. Providers afetados: só oopencode-zentemtoolCalling: falsealém do 9Router de antes; vínculos novos de agente/área a ele passam a ser 422 e o chat sem ferramenta segue igual. Vínculo antigo a ele continua gravado e o turno do agente falha nomeado. - Onde:
src/infrastructure/llm/nine-router-provider.ts:171(nineRouterConfig),src/domain/llm/model-capabilities.ts:95(assertModelFitsBindingScope),src/application/use-cases/llm/set-model-binding.use-case.ts:56(toolCalling),src/infrastructure/llm/openai-compatible-provider.ts:633(buildBody) - Teste:
test/domain/llm/model-capabilities.spec.ts(recusa por provider emagent/area; escopos livres),test/infrastructure/llm/nine-router-provider.contract.spec.ts(capabilitytrue),test/contract/llm-provider.contract.ts(provider sem capability lança nomeado); smoke manual emnine-router-provider.smoke.spec.ts. - Origem: AT-637 (EP-026), TP-01 de 31/10 e medição da AT-631.
RN-952 — O relatório do owner recorta por projeto e por sessão, e o membro vê o total do projeto sem credencial
Revisa a RN-101 (o membro deixa de ver SÓ o próprio consumo) e convive com a RN-060, a RN-186 e a RN-187.
- Recorte.
GET workspaces/:workspaceId/spend-report?projectId=(owner) limita o relatório a UM projeto e devolve tambémporSessao. Projeto de outro workspace é 404, igual a inexistente: o filtro nunca vira leitura entre workspaces. - Membro.
GET projects/:projectId/spend/team(viewer) devolve o total do projeto e o custo por agente (porAgente) e por área do catálogo (porArea,sem-areaquando não há). Nunca provider nem credencial: a rota não tem parâmetro de dimensão e o caso de uso só pedeactor(ADR 0233, que revisa o 0063).
- Código:
src/application/use-cases/llm/get-workspace-spend-report.use-case.ts:98(NotFoundException);src/application/use-cases/llm/get-project-spend-team.use-case.ts:29(GetProjectSpendTeamUseCase);src/interfaces/http/llm/spend.controller.ts:147(getProjectSpendTeam) - Teste:
test/application/use-cases/llm/spend-audiencias.use-case.spec.ts(recorte por projeto com sessão, 404 entre workspaces, membro sem provider);test/interfaces/spend.controller.spec.ts(papéis das rotas) - Origem: AT-632
RN-953 — O não-owner vê o PRÓPRIO consumo no workspace, somado por projeto
Convive com a RN-101: o ator vem do token, nunca da rota.
GET workspaces/:workspaceId/spend/me (viewer) devolve só as linhas do chamador, por projeto e por dia. Escopo de ator: a dimensão provider não compila (RN-187), e a rota não tem parâmetro de dimensão.
- Código:
src/application/use-cases/llm/get-my-workspace-spend.use-case.ts:32(GetMyWorkspaceSpendUseCase);src/interfaces/http/llm/spend.controller.ts:120(getMyWorkspaceSpend) - Teste:
test/application/use-cases/llm/spend-audiencias.use-case.spec.ts(soma por projeto, nunca a linha de outro ator) - Origem: AT-632
RN-954 — A cobrança do modelo é uma marca CURADA, e a assinatura é separada do US$ pago
Convive com a RN-665 (custo real do OpenRouter) e o preço congelado do ADR 0042.
- Marca.
models.billingéapi(padrão),subscriptionoufree, gravada pela curadoria emPATCH workspaces/:workspaceId/models/:modelId/billing(owner, o nível de…/pricing). O sync nunca a toca e a linha detoken_usagenão muda. - Relatório. Todo bucket e o total ganham
assinaturaMicros= soma do custo dos modelossubscription, contida emcostMicros; o pago em US$ écostMicros − assinaturaMicros.
- Código:
src/db/schema/llm.ts:175(modelBillingEnum);src/infrastructure/persistence/drizzle/token-usage.repository.ts:394(assinaturaMicros);src/interfaces/http/llm/models.controller.ts:302(updateBilling) - Teste:
test/application/use-cases/llm/spend-audiencias.use-case.spec.ts(assinatura separada do pago) - Origem: AT-632
RN-958 — O usage do 9Router é corrigido no provider: −2 000 só na 0.5.112, e o cache do topo conta
Completa a RN-666 (item 4) para o 9Router. A 0.5.112 SOMA 2 000 a prompt_tokens no frame final, inclusive sobre o usage que ele mesmo estima; gravar como veio inflava o input_tokens e, pela RN-665, o custo.
- Só na versão conhecida. O provider lê
currentVersiondeGET <origem>/api/version(público, teto de 2 s, cache de 5 min no sucesso e 1 min na falha) antes do stream. Só a versão da tabela (0.5.112) temprompt_tokensreduzido em 2 000, com piso em 0; subtrair numa versão que consertou o bug gravaria MENOS do que foi cobrado. - Versão outra ou ilegível (404, lento, corpo inválido): grava como veio e loga UM aviso por versão. O turno nunca falha por causa da leitura.
- Cache.
cached_tokensdo TOPO do usage viraprompt_tokens_details.cached_tokens(e entãocachedInputTokens) quando este não veio; ausente continua "não disse" (RN-666 item 2). - Declarado, não remendado. Em
cc/*o 9Router descartacache_read_input_tokensao filtrar o usage no stream, então o cache desse upstream NÃO chega ao Brabo;cache_creationnão tem coluna; o caminho sem stream não existe na base. - Hook opcional
normalizarUsageemOpenAICompatibleConfig: só o 9Router o configura; os demais providers leem o usage como sempre.
- Código:
src/infrastructure/llm/nine-router-provider.ts:118(normalizarUsage9Router);:99(versaoDo9Router);src/infrastructure/llm/openai-compatible-provider.ts:188(normalizarUsage) - Teste:
test/infrastructure/llm/nine-router-provider.contract.spec.ts(versão 0.5.112, outra, 404, lenta, corpo inválido, piso, cache do topo, outros providers sem o hook) - Origem: AT-638
RN-959 — O 503 do 9Router que embrulha um 4xx do upstream é classificado pelo status de dentro
- Regra: completa a RN-864 e a RN-909. Quando todas as contas do 9Router estão em cooldown por um 4xx do upstream, ele responde 503 com
[<status>]: <msg> (reset after Ns). O provider lê o status de dentro: 404 viramodel_not_found, 401/403auth, 402insufficient_credit(RN-730), 429rate_limit. 5xx de dentro, ausente ou corpo semreset afterseguemupstreampassageiro. A forma do corpo vem da leitura do bundle do 9Router 0.5.112; o smoke com chave a confirma. - Onde:
src/infrastructure/llm/nine-router-provider.ts:153(classificarErro9Router), aplicado porsrc/infrastructure/llm/openai-compatible-provider.ts:305(classificarErroHttp). - Teste:
test/infrastructure/llm/nine-router-provider.contract.spec.ts(503 com cada status interno e os três casos que seguemupstream);apps/engine/test/engine/agents/falha_de_turno_test.exs(model_not_foundnão é passageira). - Origem: AT-639 (EP-026).
RN-960 — O GraphStore reconecta depois de boot sem Neo4j, e o aviso diz a causa
- Regra: refina o ADR 0099. Boot em que o Neo4j ainda não respondia deixava o driver
nullpara a vida do processo. Agora, com config presente, a operação seguinte tenta conectar de novo, no máximo uma vez a cada 30 s; sem config nunca tenta, eonModuleInitsegue sem lançar. O aviso doGraphProjectorinclui a causa do erro do driver. - Onde:
src/infrastructure/graph/graph-store.ts:114(reconectarSeNecessario),src/application/graph-projection/graph-projector.ts:106(drainOnce). - Teste:
test/infrastructure/graph/graph-store.spec.ts("RN-960"),test/application/graph-projection/graph-projector.spec.ts("RN-960"). - Origem: AT-642 (EP-030).
RN-961 — O aviso de recorte diz que as ferramentas fora dele podem ser pedidas neste passo
- Regra: revisa a RN-759 (decisão do dono de 31/10, AT-643). O aviso
systemefêmero do passo recortado não diz mais que as ferramentas fora do recorte "voltam nas próximas voltas" nem que ela "volta no seguinte": o recorte é do PASSO, e a RN-759 mandava o agente esperar por um passo que, para o modelo, pode não vir. O texto novo diz a verdade do ADR 0179: as demais seguem disponíveis e podem ser pedidas neste mesmo passo, e o agente não deve concluir que não tem a ferramenta nem encerrar o turno esperando por ela; se precisar de uma delas e responder sem ferramenta, o passo é refeito com o catálogo completo (a repetição da regra 10 do ADR 0179, pelo mecanismo que a fachada já tem). Não muda o piso, a política do Jev nem a montagem do menu: só o texto do aviso. - Onde:
src/domain/llm/tool-router.ts:420(avisoDeRecorte). - Teste:
test/domain/llm/aviso-de-recorte.spec.ts(fixa o texto novo e reprova o antigo, "voltam nas próximas voltas" e "volta no seguinte"). - Origem: AT-643 (EP-030): o
write_filee oterminalsaíam do menu do dev no Jev, e o aviso mandava esperar por uma volta que o modelo não tinha como pedir.
RN-964 — O preço e a cobrança são curados na tela de Providers, e a edição manual protege o número do sync
Completa a RN-044 (o caminho manual passa a marcar manual_pricing), a RN-954 (a marca ganha tela) e a RN-963 (a mesma frase para a assinatura sem preço).
- Edição manual marca. Mudar o preço com origem
manualgravamanual_pricing = truena mesma escrita do preço. Origemsyncnão mexe na marca. - O sync respeita a marca. Linha
manual_pricingmantém o preço gravado mesmo quando o catálogo remoto traz outro; só o catálogo sem preço e a linha não-manual seguem o remoto. - Preço igual ao vigente segue no-op (RN-044): não grava marca nova nem linha de auditoria.
- Escopo.
modelsé GLOBAL da instalação: o preço vale para todos os workspaces. A tela diz isso em texto.
- Código:
src/application/use-cases/llm/update-model-pricing.use-case.ts:51(manualPricing);src/application/use-cases/llm/sync-model-catalog.use-case.ts:286(resolverPreco) - Teste:
test/application/use-cases/llm/update-model-pricing.use-case.spec.ts(edição manual marcamanualPricing; origem sync não marca) - Origem: AT-634
RN-965 — O catálogo sugere o preço do FABRICANTE do modelo e a faixa de cobrança do 9Router, e quem aplica é o humano
Completa a RN-964 (curadoria de preço na tela) e a RN-954 (marca billing), por decisão do dono de 01/11 (AT-640): o preço de um modelo é o que o fabricante dele publica, qualquer que seja o provider que o serve.
- Tabela curada no domínio, sem raspar página. Cada linha de
PRECOS_DO_FABRICANTEcobre ids exatos e guarda o preço em micros, o link da fonte (domínio do fabricante, conferido por teste) e a data em que foi conferida. A primeira entrega traz só a Anthropic; fabricante novo entra por PR, com a fonte conferida. - Casamento por igualdade exata. No
9routersai só o primeiro segmento (o alias) antes de casar; nos demais providers o nome fica como está. Variante que não casa fica "sem sugestão", nunca adivinhada. Oopenrouterfica fora de propósito: o preço dele vem do sync e o custo real dousage.cost(RN-665). - Faixa pelo alias do 9Router 0.5.112, calculada na leitura e sem gravar:
cc/cx→subscription; aliasollamaou id terminado em:free,/freeou-free→free; o resto →api. Sem credencial do painel. - Sugestão, não escrita. A rota de curadoria devolve
sugestaopor modelo; nada é gravado sozinho (ADR 0042). Quem aplica é oowner, pela tela da RN-964.
- Código:
src/domain/llm/sugestao-do-catalogo.ts:18(sugestaoDoModelo);src/domain/llm/precos-do-fabricante.ts:96(precoDoFabricante) - Teste:
test/domain/llm/sugestao-do-catalogo.spec.ts(alias, grátis, assinatura, openrouter sem sugestão, igualdade exata);test/domain/llm/precos-do-fabricante.spec.ts(id repetido, data futura, preço inválido, link fora do domínio) - Origem: AT-640
RN-966 — O 9Router 0.5.112 faz três coisas por trás: off desliga RTK/Headroom/Caveman/Ponytail e não o Custom Prompt; o OAuth do Claude redige o raciocínio; e camufla as ferramentas
- Regra: completa a RN-909 com o que o build 0.5.112 do 9Router faz por trás do gateway, medido no bundle e declarado sem código novo. (1) O cabeçalho
x-9router-token-savercom valoroff(sem caixa) desliga, juntos, o RTK, o Headroom, o Caveman e o Ponytail; o Custom Prompt NÃO passa por ele e segue como o painel o deixou. O provider manda o cabeçalho em toda chamada, e o efeito é confirmado pela medição da AT-631 (oksem ele = 1 262 tokens). (2) Para a conta OAuth do Claude (cc/*) o 9Router mandaredact-thinking-2026-02-12emAnthropic-Beta: o pensamento volta redigido e semreasoning_tokens, então a contagem de raciocínio do stream (RN-915) e a retentativa sem raciocínio (RN-916) ficam cegas nesse caminho, ereasoning_tokensgrava ausente ("não disse", RN-666). (3) Com tokensk-ant-oato 9Router renomeia as ferramentas, acrescenta cerca de 20 ferramentas falsas do Claude Code e envia cabeçalhos de Claude Code; não há configuração que desligue isso. O Brabo não corrige nem desliga a camuflagem: o risco é o modelo chamar uma ferramenta falsa e os termos de uso, e por isso o TP-01 voltou ao OpenRouter. - Onde:
src/infrastructure/llm/nine-router-provider.ts:38(NINE_ROUTER_TOKEN_SAVER_HEADER),src/infrastructure/llm/nine-router-provider.ts:172(nineRouterConfig) - Teste:
test/infrastructure/llm/nine-router-provider.contract.spec.ts:140(cabeçalhoofffixado no contrato). Os efeitos (2) e (3) vivem no bundle do 9Router, não no repositório: não há teste deles, e o smoke com chave é o único que os exercitaria. - Origem: AT-641 (EP-026), levantamento de 31/10 sobre o 9Router 0.5.112 (
chunks/8895.js,chunks/8499.js,chunks/2380.js,chunks/2598.js), derivado da AT-631.
RN-969 — A ocupação da janela do agente na sessão é o input_tokens da última chamada dele, fora o Jev
Completa a RN-101 (consumo por agente) sem rota nova: GET projects/:projectId/sessions/:sessionId/token-usage passa a trazer lastCall por agente.
- Última chamada.
lastCall = { inputTokens, modelName, contextWindow }é a linha mais recente detoken_usagedo agente na sessão; oinput_tokensdela é o que ela pôs na janela. - Jev fora. As linhas com
model_name = MODELO_DO_JEVsão ignoradas (ele grava em nome do agente, ADR 0179); agente só com Jev temlastCall: null. - Janela do modelo daquela chamada (LEFT JOIN
models), não a do binding vigente.context_windownulo viracontextWindow: null: "não sei", nunca 0%. - As somas não mudam, e nenhuma requisição nova nasce (RN-632).
- Onde a tela mostra (decisão do dono, 02/11). Na topbar da Sessão, ao lado do medidor de orçamento, do agente DESTINATÁRIO do composer: gatilho em porcentagem (
—com janela nula, nunca 0%) e cartão "N de M tokens na última chamada · modelo". Lê a MESMAqueryKey['session-token-usage', …]desession-destinatario.ts, sem poll próprio; sem chamada, sem destinatário ou 403, nada aparece.
- Código:
src/infrastructure/persistence/drizzle/token-usage.repository.ts:155(sumBySessionGroupedByActor);apps/web/src/routes/JanelaDoAgente.tsx:12(JanelaDoAgente);apps/web/src/lib/session-destinatario.ts:223(oactorIdé o mesmo id que alimenta as opções do destinatário) - Teste:
test/infrastructure/persistence/token-usage.repository.spec.ts(lastCall: última chamada, Jev ignorado, janela nula, ator só com Jev);apps/web/src/routes/SessionTopbar.test.tsx(porcentagem, janela nula, sem chamada) - Origem: AT-654
RN-983 — O clique numa fonte consultada é gravado em tabela própria e nunca é voto
- Regra: o clique num link do bloco "Consultado neste turno" (RN-982) vira uma linha em
source_clicks, de escopo de PROJETO comorag_searches(RN-479):projectId,sessionId(anulável),eventId(oagent.response),tipo,ref/searchId/chunkIde o usuário que clicou, congelados sem FK de sessão/evento. NÃO é voto: o único sinal de verdade do RAG segue sendo orag_feedback(RN-480), que esta tabela nunca alimenta, porque clique mede curiosidade.POST /projects/:projectId/source-clickséviewer(quem lê pode clicar). Gravar a medição nunca derruba o que ela mede: a falha vira log de origeminfraenullexplícito, e o clique navega do mesmo jeito. No web, o clique dispara o POST sem esperar a resposta (um POST por clique, sem poll nem query nova, RN-579); fonte que é só texto não grava. - Onde:
src/application/use-cases/rag/record-source-click.use-case.ts:18(execute),src/db/schema/rag.ts:299(sourceClicks),src/interfaces/http/rag/source-clicks.controller.ts:36(registrar),apps/web/src/lib/api-client.ts:1456(sendSourceClick). - Teste:
test/application/use-cases/rag/record-source-click.use-case.spec.ts,apps/web/src/components/ai-elements/sources.test.tsx("RN-983"),test/interfaces/route-surface.spec.ts. - Origem: AT-716 fatia 4 (EP-031).
RN-984 — O acesso rápido por papel só existe com BRABO_AMBIENTE=local
GET/POST /auth/dev-login listam os papéis e emitem a sessão de uma conta
<papel>@brabo.dev (owner, maintainer, developer, viewer) sem
credencial. Só existem quando BRABO_AMBIENTE=local E a imagem não traz marca
ou traz local (ADR 0245): fora disso o módulo não é registrado e a rota dá
404. A imagem das Actions sai marcada dev ou prd no build, então nem
BRABO_AMBIENTE=local no .env a liga. A rota resolve só o e-mail fixo do
papel, emite pelo mesmo EmitirSessaoUseCase do login, audita dev_login e,
na instalação, garante as contas com senha ALEATÓRIA descartada num workspace
próprio (a RN-546 fica intacta: não há senha conhecida). Sem nenhum usuário
na instalação responde 409.
- Onde:
src/infrastructure/security/ambiente.ts:13(acessoRapidoLigado),src/interfaces/http/acesso-rapido/acesso-rapido.controller.ts:34(AcessoRapidoController),src/application/use-cases/acesso-rapido/acesso-rapido.use-case.ts:27(AcessoRapidoUseCase) - Teste:
test/infrastructure/security/ambiente.spec.ts,test/interfaces/http/acesso-rapido/acesso-rapido.http.spec.ts,test/application/use-cases/acesso-rapido/acesso-rapido.use-case.spec.ts,scripts/ci/ambiente-nas-actions.spec.ts,e2e/testes/autenticacao.spec.ts
RN-986 — O anexo do chat aceita texto/código e imagem, com tipo pelos bytes e tetos
- Regra:
POST /projects/:projectId/sessions/:sessionId/attachments(developer, JSON{nome, base64}) guarda o arquivo emchat_attachments. O MIME sai dos BYTES, nunca do nome nem do cabeçalho: png, jpeg, gif e webp pela assinatura; texto/código se for UTF-8 válido e sem byte NUL (text/plain); o resto é 415. Tetos: 5 MB por imagem e 512 KB por texto, acima disso 413 (rejeita, nunca trunca). Sessão encerrada recusa com o 409sessao_encerradada RN-581. JSON com base64 em vez de multipart porque 5 MB viram ~6,7 MB e cabem no corpo de 10 MB (main.ts), sem parser novo. Sem antivírus, lacuna declarada emdocs/security-surface.mde no ADR 0246. - Onde:
src/domain/sessions/anexos.ts:32(classificarAnexo),src/application/use-cases/sessions/anexos-do-chat.use-case.ts:37(enviar),src/interfaces/http/sessions/attachments.controller.ts:54(enviar),src/db/schema/sessions.ts:303(chatAttachments). - Teste:
test/application/use-cases/sessions/anexos-do-chat.use-case.spec.ts("RN-986"),test/interfaces/route-surface.spec.ts. - Origem: AT-715 fatia 1, lane L1 (EP-031), ADR 0246.
RN-987 — O chat.message guarda só a referência do anexo, nunca os bytes
- Regra:
POST .../agents/:agent/messageaceitaanexos?: string[](ids), no máximo 4, todos da MESMA sessão e do MESMO autor — senão 400. O evento gravaanexos: [{id, nome, mime, tamanho}]; o log é append-only e não incha com bytes. A mesma lista de referências viaja ao engine emanexos(omitida quando vazia). Quem precisa dos bytes (L2, a api na chamada ao provider) os lê peloChatAttachmentRepository. - Onde:
src/application/use-cases/sessions/anexos-do-chat.use-case.ts:91(referencias),src/application/use-cases/agents/send-agent-message.use-case.ts:67(execute),src/infrastructure/http-clients/api-to-engine-client.ts:210(sendAgentMessage),src/interfaces/http/agents/dto/send-agent-message.dto.ts:34(anexos). - Teste:
test/application/use-cases/sessions/anexos-do-chat.use-case.spec.ts("RN-987"). - Origem: AT-715 fatia 1, lane L1 (EP-031), ADR 0246.
RN-988 — Todo viewer do projeto lê o anexo, escopado a projeto e sessão
- Regra:
GET .../attachments/:attachmentId(viewer) devolve os bytes com oContent-Typegravado,X-Content-Type-Options: nosniffeContent-Disposition: attachmentpara o que não é imagem. A consulta casa id, projeto E sessão: anexo de outra sessão ou projeto é 404, igual ao inexistente. Quem vê o evento vê o anexo (decisão do dono, 02/11). Os anexos morrem com a sessão (cascade). - Onde:
src/application/use-cases/sessions/anexos-do-chat.use-case.ts:80(ler),src/interfaces/http/sessions/attachments.controller.ts:87(ler). - Teste:
test/application/use-cases/sessions/anexos-do-chat.use-case.spec.ts("RN-988"),test/interfaces/route-surface.spec.ts. - Origem: AT-715 fatia 1, lane L1 (EP-031), ADR 0246.
RN-989 — Imagem só vai a modelo com visão provada; sem ela, o agente é avisado
- Regra: o
llm-turnaceita nocontentde uma mensagem uma lista de partes{type:'text',text}e{type:'anexo',id}(o engine manda o id, nunca bytes). A api troca o id pelo anexo da MESMA sessão (ChatAttachmentRepository.find): imagem vira parteimage(data URL no formato OpenAI) SÓ quando o provider declaracapabilities.visionE o modelo temsupports_vision— as duas camadas do ADR 0041;visionnascefalseem todos e só viratruecom smoke real: hoje étrueSÓ no OpenRouter, PROVADO em 2026-11-03 portest/infrastructure/llm/openrouter-provider.visao.smoke.spec.ts(AT-715) comgoogle/gemini-2.5-flashe um PNG 8x8 vermelho — o modelo respondeu "red". Os outros dez seguemfalse. Sem as duas, a imagem não sai da api e entra no lugar o texto[imagem <nome> não enviada: o modelo atual não lê imagem], que é o aviso ao agente. Anexo de texto vai delimitado ([anexo: <nome>]…[fim do anexo: <nome>]) com ou sem visão; id que não resolve vira[anexo <id> indisponível]. Texto puro continuastring. Os tokens da imagem entram no metering pelousagedo provider (ADR 0188), sem código novo. - Onde:
src/domain/llm/anexos-no-turno.ts:23(resolverAnexos),src/application/use-cases/llm/run-llm-turn.use-case.ts:173(resolverAnexos),src/application/use-cases/llm/stream-llm-turn.use-case.ts:186(resolverAnexos). - Teste:
test/domain/llm/anexos-no-turno.spec.ts,test/infrastructure/llm/openai-compatible-provider.visao.spec.ts; prova da capability:test/infrastructure/llm/openrouter-provider.visao.smoke.spec.ts(manual, com credencial). - Origem: AT-715 fatia 1, lane L2 (EP-031), ADR 0246.
RN-991 — Uma mensagem pode ter vários destinatários, e o log diz quais
- Regra:
POST /projects/:projectId/sessions/:sessionId/messages(developer) recebedestinatarios(1 a 7) e grava UMchat.messagecompayload.destinatarios; a rota de um agente só grava[agent]. Cada agente recebe a entrega com o MESMOmensagemId, pelo caminho de sempre (turno, fila RN-673, recusa nomeada do engine). Quem o engine recusa vem comorecusadacommotivo, sem derrubar os outros; sessão encerrada é 409sessao_encerrada(RN-581). Com omensagemIdrepartido,chat.message_deliveredechat.message_cancelledsó fecham a cópia do agente a que se referem. Revisa a RN-584/631. - Onde:
src/application/use-cases/agents/send-agent-message.use-case.ts:96(executeVarios),src/interfaces/http/agents/agents.controller.ts:115(messages),apps/engine/lib/engine/agents/fila_de_mensagens.ex:76(pendentes). - Teste:
test/application/use-cases/agents/send-message-to-agents.use-case.spec.ts,apps/engine/test/engine/agents/fila_de_mensagens_test.exs. - Origem: AT-717 (EP-031), ADR 0247.
RN-993 — O raciocínio do modelo é gravado inteiro fora do content, aparece ao vivo e só developer ou mais o lê; nunca volta ao modelo
- Regra: o provider devolve o TEXTO do raciocínio como chunk
reasoning(delta.reasoningdo OpenRouter,delta.reasoning_contentdo dialeto DeepSeek/vLLM,thinking_deltada Anthropic) e nunca nocontent. Ostream-llm-turnrepassa cada pedaço como frameraciocinioe o framefinallevaraciociniocom o texto INTEIRO do passo, sem corte; ollm-turnsem stream devolve o mesmo campo. A Anthropic recebethinkingquando o modelo raciocina e o raciocínio não foi desligado:adaptivecomdisplay: "summarized"e sembudget_tokensnos modelos atuais (4.6 em diante e a família 5; odisplaypadrão devolveria texto vazio, e o texto que chega é um RESUMO, nunca o bruto),enabledcombudget_tokensnos antigos (pensamentoAdaptativo,src/infrastructure/llm/anthropic-provider.ts:559);reasoningOffnão mandathinking. Os blocosthinkingassinados voltam emmessage.thinkingBlockspara o engine devolvê-los intactos no turnoassistantque pediu ferramentas. O engine grava o texto emagent.response.payload.raciocinio(lane do engine) e ele não entra na reidratação, no RAG nem no grafo. As leituras de evento da sessão (GET .../events,GET .../events/:eventIde o evento por id da Anamnese) tirampayload.raciociniodeagent.responsepara papel efetivo abaixo dedeveloper(projectRole ?? workspaceRole, RN-471), por UM filtro de saída. A capabilityreasoningTextéfalseem todos os providers até haver smoke com credencial (ADR 0041). - Capability:
reasoningTextétruesó na Anthropic e no OpenRouter, PROVADA com credencial real em 2026-11-03:anthropic-provider.raciocinio.smoke.spec.ts(claude-haiku-5-5eclaude-sonnet-5-5, saída 0 nos dois; o texto é o RESUMO do raciocínio,display: summarized, nunca o bruto) eopenrouter-provider.raciocinio.smoke.spec.ts(deepseek/deepseek-r1-0528, odeepseek/deepseek-r1saiu do OpenRouter e dá 404). Os outros nove providers seguemfalse; ligar outro exige o smoke equivalente com credencial. - Onde:
src/infrastructure/llm/openai-compatible-provider.ts:381(textoDoRaciocinio),src/infrastructure/llm/anthropic-provider.ts:536(toThinkingBlocks),src/domain/sessions/raciocinio-do-evento.ts:16(filtrarRaciocinio),src/interfaces/http/sessions/sessions.controller.ts:260(listEvents),:333(getEvent),src/application/use-cases/sessions/create-socket-ticket.use-case.ts:69(podeVerRaciociniono ticket do socket, colunasession_socket_tickets.pode_ver_raciocinio, que o engine lê ao consumi-lo, RN-108). Engine (AT-676): oRaciocinioDoTurnoacumula o raciocínio do turno eTextoDoTurno.payload_do_turnoo grava comoraciocinio/raciocinioTotalfora docontent; devolve osthinkingBlocksna mensagemassistantcomtoolCalls(apps/engine/lib/engine/agents/raciocinio_do_turno.ex:47(anexar_blocos)); o frameraciociniodo stream vira o evento efêmeroagent.reasoning_delta(apps/engine/lib/engine/sessions/engine_api_client.ex:1848(consume_sse)); e ointerceptdo canalsession:<id>só entrega o delta, e só deixaraciocinionoagent.response, ao socket cujo ticket tempode_ver_raciocinio(apps/engine/lib/engine_web/channels/session_channel.ex:42(handle_out)). Coluna ausente ou nula conta como falso. - Teste:
test/infrastructure/llm/raciocinio-texto.spec.ts(os dois campos,thinking_delta, adaptive/summarized × enabled por modelo, blocos devolvidos no corpo seguinte,reasoningTextfalso),test/domain/sessions/raciocinio-do-evento.spec.ts(viewernão recebe,developer+ recebe),test/application/use-cases/sessions/create-socket-ticket.use-case.spec.ts(tickettrueparadeveloper,falseparaviewer),test/application/use-cases/llm/stream-llm-turn.use-case.spec.tserun-llm-turn.use-case.spec.ts(frameraciocinio,finalcom o texto inteiro,contentintacto); smoke manualopenrouter-provider.raciocinio.smoke.spec.ts. Engine:apps/engine/test/engine/agents/raciocinio_do_turno_test.exs(frame acumula efinalsubstitui, payload comcontentintacto, reidratação sem raciocínio,thinkingBlocksna 2ª chamada) eapps/engine/test/engine_web/channels/session_channel_test.exs(interceptcom e sem a flag). - Origem: AT-676 (EP-031), lane api; ADR 0249.
RN-995 — O catálogo do agente vem do engine e é só leitura: identidade e ferramentas do engine, instrução vigente do banco, sem prompt composto e sem schema, e engine fora do ar é "não sei", nunca "sem ferramentas"
- Regra:
GET /projects/:projectId/agent-catalog(viewer, RN-102) devolve, por agente, a identidade do código, a instrução VIGENTE do projeto (agent_instructions, só a versão atual) e as ferramentas (nomeedescricao, o texto em pt-BR que o modelo lê; os outros idiomas caem nele, ADR 0216). A fonte das ferramentas e da identidade é o engine, porGET /internal/agents/catalog(service token) — uma fonte só, sem cópia na api. Não expõeparameters(o JSON schema), o prompt composto do turno, o template do grafo nem credencial ou modelo. É o catálogo COMPLETO: o recorte por passo do Jev (RN-625) pode mostrar menos ao modelo. Leitura contida (ADR 0060): uma chamada ao engine e uma consulta ao banco, sem parâmetro livre;conteudocorta em 8 000 caracteres comconteudoTotal(molde da RN-589) edescricaoem 1 000. Engine fora do ar:identidadeeferramentasvêmnullcommotivoSemCatalogo: 'engine_indisponivel'e as instruções do banco continuam vindo (RN-470). Os dev agents por módulo (dev-api) herdam a entrada únicadev-*do engine. No engine:GET /internal/agents/catalog(service token, sem parâmetro) devolve, por agente, a identidade do código e as ferramentas (nome,descricaoaté 1 000 caracteres). A fonte é otool_specs/0de cada um dos sete conversacionais e os registros de execução e de gate; nada é copiado. Não devolveparameters, o prompt composto nem o template do grafo. É o catálogo COMPLETO: o Jev (RN-625) recorta o menu por passo. Psicólogo e Anamnese ficam fora. - Onde:
src/application/use-cases/agents/get-agent-catalog.use-case.ts:39(execute),src/infrastructure/http-clients/api-to-engine-client.ts:280(getAgentCatalog),src/interfaces/http/agents/agent-catalog.controller.ts:32(get); lado do engine: lane E1 da AT-686 (Engine.Agents.CatalogoDeFerramentas); engine:apps/engine/lib/engine/agents/catalogo_de_ferramentas.ex:38(listar),apps/engine/lib/engine_web/controllers/agent_catalog_controller.ex:9(index),apps/engine/lib/engine_web/router.ex:79,apps/engine/lib/engine/agents/po_server.ex:845(tool_specs). - Teste:
test/application/use-cases/agents/get-agent-catalog.use-case.spec.ts(junção, tetos,dev-*, engine fora),test/interfaces/route-surface.spec.ts(a rota e o papel); engine:apps/engine/test/engine/agents/catalogo_de_ferramentas_test.exs(bate com cada fonte, teto da descrição, 401 sem token e 200 com). - Origem: AT-686 (EP-031), lanes A1 (api) e E1 (engine).
RN-997 — O catálogo da Anthropic nasce com preço de lista datado e supportsReasoning do SDK; preço manual nunca é sobrescrito
- Regra: a
/v1/modelsda Anthropic não informa preço, e o modelo sincronizado nascia com preço 0 (gasto de custo zero, sem aviso). OlistModelsdevolve o preço da tabela ÚNICA do fabricante da RN-965 (precoDoFabricante, micro-USD por milhão), casando pelo ID e, se não achar, pelo ID sem o sufixo de data (-AAAAMMDD); ID fora da tabela segue sem preço (RN-042).supportsReasoningvem decapabilities.thinking.supporteddo SDK; comcapabilitiesnulo vale a família (todoclaude-que não seja 3.x raciocina). O sync trata o preço como já tratava (RN-044): linhamanual_pricingvence sempre, e modelo novo COM preço de lista nascemanual_pricing = false. O preço congelado no metering não muda. Linha antiga jámanual_pricing = true(nascida sem preço) segue como está: corrigi-la é a curadoria doowner. - Onde:
src/infrastructure/llm/anthropic-provider.ts:544(precoDeLista),src/domain/llm/precos-do-fabricante.ts(PRECOS_DO_FABRICANTE,precoDoFabricante),src/application/use-cases/llm/sync-model-catalog.use-case.ts(resolverPreco). - Teste:
test/infrastructure/llm/anthropic-provider.catalogo.spec.ts(preço com e sem sufixo de data, ID desconhecido sem preço,supportsReasoning);test/application/use-cases/llm/sync-model-catalog.use-case.spec.ts(preço manual não é sobrescrito). - Origem: pedido do dono de 03/11; ADR 0251 (referencia o 0042).
RN-1000 — Na Anthropic direta o raciocínio desligado é POR MODELO, o adaptativo ganha a folga e o thinking_tokens chega ao metering
- Regra: revisa a RN-741 e a RN-782/RN-783 para o provider
anthropic. A frase da RN-741 "oanthropic-providerdireto não enviathinking, então o modelo não raciocina" ficou falsa: nos modelos de pensamento ADAPTATIVO (os 5.5) omitir othinkingé o adaptativo no esforço padrão, e o Haiku 5.5 pensava até omax_tokenscom o raciocínio "desligado" (TP-01 de 05/11: 10 de 10 tarefas bloqueadas com "resposta só de raciocínio"). (1) ComreasoningOffo corpo é POR MODELO (raciocinioDesligado, decisão do dono de 05/11): Haiku 5.5 →thinking: { type: 'disabled' }(desligado de verdade, sem folga); Sonnet 5.5 →thinking: { type: 'between_tools' }(pensa só entre chamadas de ferramenta, com folga); Opus 5.5 →output_config: { effort: 'low' }com folga, DECLARADO como REDUZIDO, não desligado — odisabledé 400 no Opus e no Sonnet 5.5; modelo adaptativo desconhecido → nada (o padrão do modelo) com a folga, porque reduzir exige prova; modelo antigo (3.x, 4.0..4.5) semthinkingnão pensa, e omitir já é desligar. (2) Com o raciocínio ligado no adaptativo, omax_tokensé a saída visível +reasoningBudget ?? 4096— a mesma folga da RN-741, porque ali o teto é TOTAL (antes ficava 4096 TOTAL e a Infra era cortada). (3) O provider declaraaceitaRaciocinioDesligado: true(RN-916). (4)reasoningTokensvem dousage.output_tokens_details.thinking_tokensdo últimomessage_delta(o SDK 0.113 não o copia para a mensagem final), só quando é inteiro >= 0; senão é omitido e a coluna fica NULL (RN-666). - Prova (smoke pago de 05/11,
ANTHROPIC_TEST_KEY): Haiku/Sonnet/Opus 5.5 comreasoningOffe uma ferramenta: aceitos, chamaram a ferramenta, sem truncar,thinking_tokens0;thinking: disabledno Opus 5.5 → 400 ("thinking.type.disabled" is not supported for this model); Haiku 5.5 ligado: 303 de 308 tokens de saída eram raciocínio; trocar osystementre duas voltas reenviando o bloco de raciocínio foi ACEITO. Custo: ~US$ 0,009. - Onde:
apps/api/src/infrastructure/llm/anthropic-provider.ts:573(corpoDeRaciocinio),:614(raciocinioDesligado),:635(tokensDeRaciocinio),:81(aceitaRaciocinioDesligado) - Teste:
apps/api/test/infrastructure/llm/raciocinio-texto.spec.ts(corpo por modelo comreasoningOff, folga no adaptativo e com orçamento,thinking_tokensdomessage_deltae omitido quando não é inteiro >= 0, a capability),apps/api/test/infrastructure/llm/anthropic-provider.raciocinio.smoke.spec.ts(manual, pula sem a chave) - Origem: AT-739 (EP-030), achados 2 e 8 do TP-01 de 05/11; ADR 0257 (revisa o item 4 do ADR 0223 para a Anthropic).
RN-1001 — O decorator de tracing repassa aceitaRaciocinioDesligado, então a retentativa sem raciocínio roda de verdade
- Regra: revisa a RN-916, cujo texto dizia que a capability
aceitaRaciocinioDesligadodo provider era "hoje só do OpenRouter". Agora vale para OpenRouter e Anthropic (RN-1000). Defeito declarado: oTracedLLMProviderrepassava sóname,capabilities,chat,listModelseembed; como o registry envolve TODO provider,provider.aceitaRaciocinioDesligadoeraundefinedem produção e a retentativa da RN-916 nunca rodava, para provider nenhum. O decorator agora copia a flag do provider envolvido no construtor (undefinedcontinuaundefined: provider que não a declara não a expõe). Os outros membros opcionais da portaLLMProvider(listModels,embed) já eram repassados. - Código:
apps/api/src/infrastructure/llm/traced-llm-provider.ts:78(aceitaRaciocinioDesligado) - Teste:
apps/api/test/infrastructure/llm/traced-llm-provider.spec.ts(mantémtrue, continuaundefined, e oLLMProviderRegistryImpl.get('anthropic')real devolvetrue) - Origem: AT-743 (EP-030)
RN-1002 — O agente fecha a pergunta em aberto que o usuário respondeu no chat; a resposta humana e a do agente dividem um núcleo só
- Regra: estende a RN-858/RN-860 (ADR 0214, decisão do dono de 05/11: o agente fecha).
POST /internal/projects/:projectId/open-questions/:questionId/answer(token de serviço) recebe{ sessionId, agentId, resposta?, messageEventId? }e grava o MESMO parartifact.decision_record+open_question.answereddo clique, por um núcleo compartilhado (fechar), e depois retoma o gate de QA estacionado de cada tarefa afetada. O fechamento roda dentro derunComoEfeitoDeAcao(RN-848), então o canal da sessão é notificado deopen_question.answeredmesmo com a escrita vindo por/internal/*(o contador da web atualiza ao vivo).respondidaPoré o autor da sessão de chat (sessionId); o agente vai emviaAgente(emessageEventIdquando enviado). Semresposta, a sugestão da pergunta é a resposta; sem nenhuma das duas, 400. Pergunta desconhecida, pergunta de outro projeto ou sessão fora do projeto: 404. Já fechada:200 { status: 'already_answered' }, nunca 409 (idempotente para o agente). Oresponderhumano não muda (so_o_autor_da_sessao, 409). - Onde:
apps/api/src/application/use-cases/backlog/perguntas-em-aberto.use-case.ts(responderPeloAgente,fechar),apps/api/src/interfaces/http/internal/internal-open-questions.controller.ts. - Teste:
apps/api/test/application/use-cases/backlog/perguntas-em-aberto.use-case.spec.ts(fecha comdecision_record, 404 para outro projeto, a segunda chamada dáalready_answered) eapps/api/test/interfaces/http/internal/internal-open-questions.controller.spec.ts(o fechamento roda marcado como efeito de ação). - Origem: AT-741 (EP-030), achado 4 do TP-01 de 05/11; o lado do engine é a RN-1003.
RN-1004 — O plano de execução do Dev Lead é recusado quando um módulo sem tarefa é dependência de um módulo que tem tarefa
- Regra: estende a RN-678.
lerPlanoDeExecucaoagora recebe omodule_mapcorrente COM odependsOnde cada módulo, não só os nomes. Depois das checagens por tarefa, se um módulo SEM tarefa no plano aparece nodependsOnde um módulo que TEM tarefa, o plano é recusado com 400plano_de_execucao_invalido, nomeando o par: "deploy depende de api, que ficou sem tarefa — crie a tarefa de fundação de api". Módulo folha sem tarefa continua legítimo (RN-709); a checagem roda na proposta e na aprovação, como o resto da leitura. - Código:
apps/api/src/domain/execution/plano-de-execucao.ts(lerPlanoDeExecucao),apps/api/src/application/use-cases/execution/execute-execution-plan.use-case.ts(lerPlano) - Teste:
apps/api/test/domain/execution/plano-de-execucao.spec.ts(fundação com dono: o plano semdb/apié recusado nomeandodeploy->api; com tarefa emdb,apiedeployele passa) - Origem: AT-742 (EP-030), achado 9 do TP-01 de 05/11; ADR 0259.
RN-1005 — O claim não entrega a tarefa de um módulo enquanto houver tarefa aberta de um módulo do dependsOn direto dele
- Regra: estende a RN-678.
claimNextpula a filatododo módulo M enquanto qualquer tarefa que NÃO estádonepertencer a um módulo listado nodependsOnde M nomodule_mapcorrente do projeto (a mesma fonte da RN-1004). Só as dependências DIRETAS contam (sem caminhada transitiva); tarefa semmoduleconta para o módulo de uma história de módulo único, como no próprio claim. Semmodule_map, ou quando M não está nele, o claim se comporta como antes. A tarefain_progressórfã do próprio agente (RN-856) não é afetada. - Código:
apps/api/src/infrastructure/persistence/drizzle/backlog.repository.ts(claimNext,semDependenciaAberta) - Teste:
apps/api/test/infrastructure/persistence/drizzle/backlog-claim-depends-on.repository.spec.ts(tarefa aberta emapi->deploynão recebe nada;apiconcluída -> recebe a tarefa; só a direta; sem mapa) - Origem: AT-742 (EP-030); ADR 0259.
RN-1007 — O Claude Haiku 5.5 na Anthropic direta tem uma folga de raciocínio curada de 12288 tokens
- Regra: revisa a RN-1000, cuja folga adaptativa cai no padrão de 4096 quando
models.reasoning_budgeté NULL. No padrão, um gate que raciocina noclaude-haiku-5-5recebiamax_tokens4096 + 4096 = 8192 no total e era cortado ainda pensando (TP-01 rodada 25,qa-estrategia: 8192 de 8192 tokens de saída gastos raciocinando), inclusive um corte COM texto que a RN-916 não retenta. Dado curado pelo ADR 0223: a migration0083gravareasoning_budget = 12288na linha do catálogoanthropic/claude-haiku-5-5, então omax_tokensadaptativo = saída visível + 12288 (16384 no padrão de 4096). Sem mudança de código:corpoDeRaciociniojá lêreasoningBudget. A migration é idempotente e não toca outro modelo (nem o mesmo nome sob outro provider, nem outros modelos da Anthropic). Com NULL a folga volta a 4096 (8192 no total). Não adotado:effort: lownos gates (a documentação avisa que o agente pula checagens) e transformar o orçamento emeffort. - Código:
apps/api/src/db/migrations/0083_folga_do_haiku_5_5.sql,apps/api/src/infrastructure/llm/anthropic-provider.ts(corpoDeRaciocinio) - Teste:
apps/api/test/db/folga-do-haiku-migration.spec.ts(só aquela linha, idempotente),apps/api/test/infrastructure/llm/raciocinio-texto.spec.ts(12288 dá 16384 eadaptive; NULL dá 8192),apps/api/test/infrastructure/llm/anthropic-provider.folga.smoke.spec.ts(manual, pago, pulado sem chave) - Origem: AT-743 (EP-030), decisão do dono de 05/11 (folga 12288, teto 16384).
RN-1008 — O provider Anthropic direto usa prompt caching, soma as parcelas de cache na entrada e precifica a chamada pela tabela do fabricante
- Regra: revisa o item 1 da RN-892 (
promptCachingera "só do OpenRouter") e completa a RN-666 para o provider Anthropic direto. Medido no TP-01 (25º, 05/11): 463 chamadas, 10,12 M tokens de entrada,cached_input_tokens= 0, US$ 2,28 dos US$ 2,62 do total foram entrada. (1) Osystemsai como UM bloco, o PREFIXO (as mensagenssystemantes da primeira que não ésystem), levandocache_control: {type: "ephemeral"}(TTL de 5 min); a CAUDA (mensagenssystemtardias) não sobe mais para o topo (RN-1015). (2) Mais um ponto vai no último bloco do último turnouser(resultados de ferramenta são turnosuser), espelhando a RN-892; texto vazio não ganha ponto (a RN-1014 acrescenta um no penúltimo turnouser). (3)usage: oinput_tokensda Anthropic é só a parte NÃO cacheada, entãoinputTokens = input_tokens + cache_read_input_tokens + cache_creation_input_tokensecachedInputTokens = cache_read_input_tokens; sem os campos,cachedInputTokensfica ausente ("não disse", RN-666). (4) Custo (opção A do dono, ADR 0188): quando alguma parcela de cache não é zero, o provider emitecostMicrosdas quatro parcelas vezesprecoDoFabricante(model): entrada 1x, leitura de cache 0,1x (0,05x no Opus 5.5), escrita de cache 1,25x, saída 1x; a api o grava compriceImplicitecatalogCostMicros. Modelo fora da tabela não emite custo (RN-042); sem parcela de cache, o caminho do catálogo já dá o mesmo número. (5)capabilities.promptCachingétrue, provado poranthropic-provider.cache.smoke.spec.tsantes do merge. - Código:
apps/api/src/infrastructure/llm/anthropic-provider.ts(blocosDeSistema,marcarCacheDaConversa,usageDeEntrada) - Teste:
apps/api/test/infrastructure/llm/anthropic-provider.contract.spec.ts("prompt caching (RN-1008)": blocos e pontos, ponto na última user, soma do usage, custo pelas quatro parcelas, modelo fora da tabela); smokeanthropic-provider.cache.smoke.spec.ts(manual) - Origem: AT-744 (EP-030)
RN-1009 — O merge que fecha uma tarefa acorda os módulos que dependem dela
- Regra: estende a RN-1005 e a RN-628, reaproveitando o evento
task.became_claimableda RN-047 / ADR 0045. A RN-1005 tornou uma tarefa pegável quando a última tarefa aberta de um módulo dodependsOndireto dela viradone, mas nada anunciava esse momento, então dev agents ociosos nunca acordavam (TP-01 26º, 06/11). OsettleMergeagora, na MESMA transação e só quando pelo menos uma tarefa de fato mudou paradone(merge repetido não grava nada), anexa UMA linha de outbox:aggregateType: 'task',eventType: 'task.became_claimable',aggregateId= a primeira tarefa fechada,payload: { projectId, sessionId, taskId, modules, cause: 'dependency_done' }.modulessão os nomes dos módulos domodule_mapcorrente cujodependsOnnão é vazio. Isso é de propósito mais largo que "os dependentes do módulo desta tarefa" (não precisa ler a história de uma tarefa semmodule): o acordar a mais custa um claim vazio num agente ocioso, e um agente ocupado o ignora. Semmodule_map, ou sem nenhum módulo comdependsOn, nada é gravado (a RN-1005 também não filtra). Fora do escopo: arquivar a história da dependência (RN-727) também libera os dependentes sem acordá-los. - Código:
apps/api/src/application/use-cases/actions/execute-git-action.use-case.ts(settleMerge,acordarDependentes) - Teste:
apps/api/test/application/use-cases/actions/execute-git-action.use-case.spec.ts("acorda os dependentes (RN-1009)": uma linha comcause: dependency_done, merge repetido não grava nada, sem mapa não grava nada),apps/api/test/infrastructure/persistence/drizzle/dependente-acorda-no-merge.spec.ts(Postgres real:authnão pega nada antes do merge, a linha existe depois dele eauthpega a tarefa) - Origem: AT-746 (EP-030); o lado do engine (outbox -> drain -> acordar -> agente ocioso faz o claim) já existe e é provado por
dev_agent_chain_test.exs.
RN-1018 — O agente retira uma regra de negócio obsoleta, citando a substituta ou a resposta do usuário; todo leitor a ignora
- Regra: uma regra de negócio é um evento
artifact.business_rulee o log é append-only, então uma regra obsoleta ficava para sempre (TP-01 26º, 06/11: o Criativo pediu ao condutor que a "removesse à mão").POST /internal/projects/:projectId/business-rules/:ruleId/retire(token de serviço) recebe{ sessionId, agentRole, reason, replacedBy?, questionId? }e anexaartifact.business_rule_retired { ruleId, reason, storyIds, replacedBy?, questionId? }, sem cartão de aprovação (decisão do dono de 06/11/2026). (1) Sócriativoeporetiram: qualquer outro papel é 403papel_nao_retira_regra. (2) A chamada precisa citarreplacedBy(uma regra existente, não retirada e diferente dela mesma, senão 400substituta_invalida) e/ou umquestionIdque o usuário já respondeu (o padrão da RN-859, senão 409pergunta_nao_respondida); sem nenhum dos dois é 400retirada_sem_citacao. (3) Regra desconhecida é 404, regra já retirada é 409regra_ja_retirada. (4) Cada história (não arquivada) que cita a regra deixa de citá-la e passa a citarreplacedBy(inclusive históriaready, sem nova promoção); se uma história ficaria SEM regra (só possível semreplacedBy), a retirada inteira é recusada com 409historia_ficaria_sem_regra, e uma história com tarefain_progress/in_reviewrecusa com 409historia_com_tarefa_em_execucao; nada é gravado numa recusa. (5) A regra retirada fica no log mas sai de todo leitor que decide: a lista de regras e a cobertura (RN-164), a checagem de duplicata semântica (RN-681), os contextos do dev e do psicólogo, a contagem de cobertura do aceite automático e a resolução deafeta.regrasdas perguntas em aberto, e o resumo de contexto por sessão da RN-844 (contagem eitensDeRegras, SQL cru);create_story/complete_storyrecusam ligá-la (400). - Onde:
apps/api/src/application/use-cases/backlog/retire-business-rule.use-case.ts(execute),apps/api/src/application/use-cases/backlog/regras-retiradas.ts(idsDeRegrasRetiradas),apps/api/src/interfaces/http/internal/internal-business-rules.controller.ts. - Teste:
apps/api/test/application/use-cases/backlog/retire-business-rule.use-case.spec.ts(papel, citação faltando, troca, recusa por história sem regra, tarefa em execução, leitores). - Origem: AT-755 (EP-030), parte B da AT-752; a ferramenta do engine e o painel da web vêm depois.
RN-1014 — O provider Anthropic direto também põe um ponto de cache no penúltimo turno user
- Regra: estende o item 2 da RN-1008. Além do último turno
user,marcarCacheDaConversamarca o último bloco do PENÚLTIMO turnouser(resultados de ferramenta são turnosuser), sob as mesmas regras (sem ponto em texto vazio, emtool_useou em bloco de raciocínio). O penúltimouseré onde a chamada anterior do mesmo loop gravou a entrada de cache dela, então a leitura vira um ponto explícito e não depende mais do lookback de uns 20 blocos da Anthropic, que um turno com 7 a 10 chamadas paralelas mais os resultados delas ultrapassa (TP-01 26º, 06/11: PO, Arquiteto e Criativo com 19% a 35% de cache). Conversa com um único turnouserganha um ponto. A requisição leva no máximo 3 dos 4 pontoscache_controlque a Anthropic permite (o prefixo dosystemmais estes dois); um ponto em todo turnouserpassaria do limite e está errado. - Código:
apps/api/src/infrastructure/llm/anthropic-provider.ts(marcarCacheDaConversa,marcarTurno) - Teste:
apps/api/test/infrastructure/llm/anthropic-provider.contract.spec.ts("(b)" último e penúltimouser; "(b2)" o antepenúltimouserfica sem marca e a requisição tem no máximo 4 pontos) - Origem: AT-749 (EP-030), parte da api; a parte do engine (blocos de raciocínio por
assistant) é a RN-1013.
RN-1015 — Na Anthropic direta, as mensagens system tardias vão para o fim do último turno user, depois do ponto de cache, nunca para o system do topo
- Regra: revisa o item 1 da RN-1008 e torna a RN-622 / RN-878 verdadeiras neste provider. O engine anexa a orientação efêmera (lembrete de idioma, lista de citações, decisões) como mensagens
systemDEPOIS da conversa. Este dialeto só aceitasystemno topo, e o provider as içava para lá, à frente de dezenas de blocostool_resultem inglês, então num loop longo o modelo seguia o idioma do contexto recente (TP-01 26º, 06/11: respostas dosdev-*em inglês em 38% a 60% a partir da 3ª posição). Agora as mensagenssystemdepois da primeira que não ésystem(a CAUDA) viram o ÚLTIMO bloco de texto do ÚLTIMO turnouser, DEPOIS do bloco que leva o ponto de cache da RN-1008 / RN-1014, então o prefixo cacheado não muda e a cauda é lida junto do contexto recente. Osystemdo topo fica só com o PREFIXO. Se a conversa não termina num turnouser, a cauda fica como um segundo blocosystemsem ponto (comportamento anterior). Texto e ordem das mensagens não mudam. O OpenRouter NÃO muda (decisão do dono): isto cobre só o provider Anthropic direto. - Código:
apps/api/src/infrastructure/llm/anthropic-provider.ts(blocosDeSistema,caudaDeSistema,comCaudaDeSistema) - Teste:
apps/api/test/infrastructure/llm/anthropic-provider.contract.spec.ts("(a)"systemsó com o prefixo e a cauda depois do ponto; "(a2)" cauda depois detool_result, recurso semuserfinal),apps/api/test/contract/llm-provider.contract.ts(fim_do_ultimo_user,systemtardio depois deusere depois detool) - Origem: AT-751 (EP-030). O aceite (comportamento) é o próximo TP-01:
agent.responsedosdev-*em inglês na posição >= 3 caindo para 5% ou menos; nenhum smoke pago antes dele.
RN-1017 — git push de terminal para branch protegida, com force, --all ou --mirror é decisão humana, mesmo no modo automático
- Regra: estreita a RN-713 só para a porta do terminal. No modo automático (o curinga
"*"emauto_approve), umgit pushdigitado no terminal continua auto-aprovado SÓ quando não consegue alcançar uma branch protegida nem reescrever histórico. Ele virarequire_approval(umproposed_actionpendente, com motivo começando porRN-1017) quando qualquer segmento do comando (a) empurra para uma branch protegida (dev,qa,rc,main, a lista deisProtectedBranch), pelo nome ou pelo destino do refspec (HEAD:dev,+x:refs/heads/main), (b) usa--force,-f(também agrupado, como em-fu) ou--force-with-lease, ou (c) usa--allou--mirror. Sem isso, a RN-014 era contornável: com ogit mergelocal permitido,checkout dev,mergeepush origin devrodavam sem humano e sem gates. O push da própria branch da tarefa não muda, e nenhum modo não automático muda (já sãorequire_approval, RN-418). Limites conhecidos: um push sem refspec (git push origin) empurra a branch corrente, que odecide()não enxerga, e a branch padrão do repositório só é coberta quando está na lista fixa de protegidas. - Código:
apps/api/src/domain/actions/external-effect.ts:260(pushProibidoNoModoAutomatico),apps/api/src/domain/actions/decide.ts:443(o bloco de terminal dos tetos) - Teste:
apps/api/test/domain/actions/decide.spec.ts("é recusado em modo automático (RN-1017)":dev,main,HEAD:dev,+HEAD:refs/heads/main,--force,-f,--force-with-lease,--all,--mirror, composto; a branch da tarefa continua passando) - Origem: AT-756 (EP-030), achada no levantamento da AT-754 (Rodada 204).
RN-1019 — No modo automático, um git merge de terminal dentro do escopo da tarefa é auto-aprovado
- Regra: estreita a porta do terminal da RN-014 e a linha
git_mergeda RN-713. No modo automático (o curinga"*"emauto_approve), um comando de terminal cujos segmentosgit merge(inclusivegit merge --abort) rodam dentro do worktree ou da pasta da tarefa (terminalNoEscopoétrue) deixa de contar como efeito externo: esses segmentos são removidos e o resto do comando é julgado como antes. O merge nunca sai da máquina; só o push sai, e um composto comogit merge origin/dev && git push origin devcontinua virandorequire_approvalpela RN-1017. Não liberados: qualquer outro modo (regra específica, "sempre permitir",permissions.json, manual), comando fora do escopo,gh pr merge/glab mr merge, a ação tipadagit_mergee comando que também rodagit checkout/git switchpara uma branch protegida. Revisa a RN-706 (doc do engine) do lado do engine (o texto do agente agora permitegit mergeegit fetchno worktree da tarefa). Limite conhecido: a imagem do container do projeto pode não tergit, então no modo container o merge é aprovado mas ainda falha comgit: not found. - Código:
apps/api/src/domain/actions/external-effect.ts:238(semMergeLocal),apps/api/src/domain/actions/decide.ts:443(o bloco de terminal dos tetos) - Teste:
apps/api/test/domain/actions/decide.spec.ts("git merge local (RN-1019)": merge simples permitido; merge mais push para branch protegida recusado pela RN-1017; fora do escopo e fora do modo automático como antes) - Origem: AT-754 (EP-030), decisão do dono de 06/11/2026.
RN-1026 — A api reinicia só o processo do app no container do projeto, com resposta tipada
- Regra: o engine chama
POST /internal/projects/:projectId/app/restart(engine-service, corpo vazio) depois que a pasta do projeto avançou. A api repassa ao broker (POST /containers/:projectId/app/restart) porContainerBrokerPort.reiniciarApp; o comando do app NUNCA é enviado, o broker lêrun.appdo container-spec. A resposta espelhaResultadoDoReinicio(RN-1024 na doc do contracts):{ reiniciou: true }, ou{ reiniciou: false, motivo }comsem_container,tempo_esgotado,falha_no_exec, ouapp_saiumaisexitCode(e osem_app, só da api). Não há campo de texto livre: o comando do app pode conter um segredo, então nenhum trecho de log e nenhuma mensagem do broker sai da api. Broker respondendo 404 dásem_container; um 409 (semrun.appno container-spec, ou projetorunner) dásem_app, um no-op nomeado; qualquer outra recusa ou broker inalcançável dáfalha_no_exec, nunca um erro HTTP. Um reinício que falhou só é relatado: a pasta continua namainnova. - Código:
apps/api/src/application/use-cases/containers/reiniciar-app-do-projeto.use-case.ts:25(execute),apps/api/src/interfaces/http/internal/internal-projects.controller.ts(reiniciarOApp),apps/api/src/infrastructure/http-clients/container-broker.client.ts(reiniciarApp) - Teste:
apps/api/test/application/use-cases/containers/reiniciar-app-do-projeto.use-case.spec.ts(resultado repassado intacto; 409 dásem_app; só oprojectIdé enviado; 404 dásem_container; a mensagem do broker nunca vaza; a rota é guardada peloEngineServiceGuard) - Origem: AT-617 (EP-030), decisão do dono de 06/11/2026.
RN-1022 — O repositório do projeto é trunk: o bootstrap cria e protege só a main, merges nela avisam o engine, e projetos antigos migram por script
- Regra: o repositório que o Brabo cria para o projeto do usuário é trunk (ADR 0263); o desenvolvimento do próprio Brabo continua
dev→main.- Branch de trabalho.
BRANCH_DE_TRABALHOémain: a PR de tarefa, a PR de infra (RN-729) e a PR de ADR abrem a partir dela e miram nela. O engine guarda o mesmo valor (ProjectRepository.branch_de_trabalho/0); os dois mudam juntos. - Bootstrap. A sequência não cria mais
devnemqa, eprotect_branchesprotege só amain. Obranching-policy.mdcommitado no repositório do usuário descreve trunk.create_dev_branchecreate_qa_branchcontinuam no enumbootstrap_step(bootstraps antigos têm linhas com eles) e são listados como passos aposentados. - A proteção não muda.
isProtectedBranchmantémdev,qa,rcemain(RN-1017, RN-1019): projetos antigos ainda têm as três primeiras, e desproteger uma branch que existe custa mais que proteger uma que não existe. - Gancho. Depois de um merge com
state: 'merged'namain,settleMergeanexa o evento de outboxproject.trunk_merged(aggregateType: 'project') com payload{ projectId, sessionId, mergedSha }.mergedShaé a ponta damaindepois do merge, em melhor esforço (''quando o provider não consegue listar branches). Merge que não émerged, que falha ou que mira outra branch não grava nada. O engine o consome para avançar a pasta do projeto. - Migração de projetos antigos.
pnpm migrar-projeto-para-trunk [--dry-run] [--root <dir>] [<bare>...](src/scripts/migrar-projeto-para-trunk.ts) faz fast-forward damainpara adevem cada repositório bare. Ele recusa, sem mudar nada e saindo com 1, quando amainnão é ancestral dadev;--dry-runsó relata; ele nunca apagadevnemqa. Não redireciona PRs abertas que miramdev. É rodado pelo dono, porque escrever namainé escrever numa branch protegida (RN-418).
- Branch de trabalho.
- Código:
apps/api/src/domain/actions/protected-branches.ts:26(BRANCH_DE_TRABALHO),apps/api/src/application/use-cases/git/bootstrap-steps.ts:161(BOOTSTRAP_STEP_SEQUENCE),apps/api/src/application/use-cases/git/bootstrap-templates.ts:66(branchingPolicyContent),apps/api/src/application/use-cases/actions/execute-git-action.use-case.ts:374(settleMerge),apps/api/src/scripts/migrar-projeto-para-trunk.ts(migrarParaTrunk) - Teste:
apps/api/test/application/use-cases/actions/execute-git-action.use-case.spec.ts("project.trunk_merged (RN-1022)"),apps/api/test/application/use-cases/git/provision-repository.use-case.spec.ts,apps/api/test/application/use-cases/git/bootstrap-plan.spec.ts,apps/api/test/scripts/migrar-projeto-para-trunk.spec.ts - Origem: AT-617 (EP-030), decisão do dono de 06/11/2026; rodada 208, lane L2.