Contratos
Git
RN-028 — Capability decide, não o nome do provider
Operação não suportada (proteção de branch no provider local) é declarada em
capabilities e rejeitada com GitNotSupportedError — nunca falha silenciosa.
São quatro capabilities: protectBranch e pullRequests desde a Fase 2, e
listTree e pullRequestDiff desde a FASE 26 (aba Code, só leitura). A regra
não mudou com elas — mudou o alcance. E o critério de declarar continua sendo
prova: capability só é true quando a suite a exercita naquele provider; sem
prova, declara-se false e degrada (mesmo critério dos ADRs 0041/0042).
- Onde:
packages/shared/src/index.ts(GitProviderCapabilities); os tetos das duas operações de leitura emapps/api/src/domain/git/git-read-limits.ts, fora do shared porque ele é 100% tipo - Teste: suite de contrato única,
test/contract/git-provider.contract.ts, rodada contra os três providers - Origem: ADR 0001
RN-087 — O Dev Lead é o único endereço externo da execução
Existe um agente dev-lead, conversacional, que recebe o handoff do Arquiteto
(desde a RN-672, da Infra, com o container do
projeto running) e propõe o plano de execução: quantos agentes por módulo e por quê. Ele não
escreve código — distribui trabalho e responde por ele.
Antes dele, a frase "quem decide é o lead" da RN-083 não tinha dono. O Arquiteto terminava e a execução subia por um botão, sem ninguém no meio para avaliar quanto trabalho havia.
Os dev-<modulo> deixaram de ser endereçáveis por handoff. Isso não é
exceção nova: é a regra do ADR 0038 —
handoff externo endereça só lead de área ou agente sem área — passando a valer
para o dev como já valia para QA e Infra. Enquanto não havia Dev Lead, eles
eram agentes SEM área e por isso alvos válidos; virando membros, deixam de ser.
A área de dev é a primeira DINÂMICA, e é o que forçou o predicado: os
membros são um por módulo do module_map, decididos pelo Arquiteto e
diferentes em cada projeto, então não há lista a enumerar. dev-lead casa com
o mesmo prefixo dev- dos membros, e quem o exclui é a regra genérica o lead
nunca é membro da própria área — que vale para qualquer área e vive num lugar
só. A primeira versão repetia essa exclusão em três pontos, e a verificação por
mutação mostrou que nenhuma das cópias era alcançável por teste: cada uma
sobrevivia à mutação da outra.
O plano é proposed_action (revisado pelo ADR 0086, RN-284).
Até essa mudança o plano virava EVENTO simples, sem aprovação: o argumento era
que propor não tem efeito externo — o gasto acontece quando os agentes sobem,
e é lá que o teto cobra autorização; transformar a proposta em ação a decidir
faria você decidir duas vezes a mesma coisa. Uma auditoria de
packages/contracts/fluxo.yml × código encontrou que o fluxo já declarava esta saída como
via: proposed_action desde o ADR 0085, e o código nunca foi ajustado para
bater — o dono do produto decidiu que o código errava: o plano é a PRIMEIRA
decisão real de quanto a sessão vai gastar com paralelismo, e o usuário passou
a decidir ativar a execução tendo VISTO uma aprovação de verdade, não só lido
uma linha no fio. A lição antiga não desapareceu — é o motivo pelo qual
propose_execution_plan NÃO entrou no bloco de tetos absolutos de
decide.ts (ver RN-284).
O plano BEM-SUCEDIDO encerra o turno — no caminho SEM suspensão. Na
primeira execução real o Dev Lead registrou dois execution.plan_proposed
na mesma sessão — textos diferentes, mesmo total —, porque o laço voltava ao
modelo e ele propunha de novo. O event log é imutável: ficaram duas propostas
e nada dizendo qual valia. A instrução "use uma vez" no spec da ferramenta é
pedido, não garantia; quem garante é o laço parar. Desde o ADR 0086, quando a
proposta fica pending, quem encerra o turno é a PARADA por suspensão
(RN-284) — o sucesso imediato (auto_approved/executed/approved, ver
RN-284) continua fechando o laço do mesmo jeito de sempre.
Bem-sucedido, e não "chamou a ferramenta": um plano recusado (vazio, ou com zero agente num módulo) deixa o laço seguir, senão a recusa vira fim de turno e o modelo nunca chega a corrigir. A primeira versão desta guarda olhava só o nome da ferramenta e tinha esse defeito — encontrado pelo teste comportamental, não pela leitura.
Um plano vazio, ou com zero agente num módulo, é recusado antes de propor qualquer coisa — a proposta, uma vez criada, é decisão real do usuário, e um plano meio proposto não teria como ser retratado.
- Onde:
apps/engine/lib/engine/agents/dev_lead_server.exedev_lead_tools.ex; a regra de endereçamento emapps/api/src/domain/agents/agent-areas.ts; o handoff emapplication/use-cases/agents/offer-infra-handoff.use-case.ts - Teste:
apps/engine/test/engine/agents/dev_lead_tools_test.exs,dev_lead_server_test.exs(o plano ENCERRA o turno,o plano recusado NÃO encerra o turno),apps/api/test/domain/agents/agent-areas.spec.ts(o dev de módulo DEIXOU de ser endereçável,`dev-lead` É endereçável, apesar do prefixo) etest/application/use-cases/agents/offer-infra-handoff.use-case.spec.ts - Origem: ADR 0053, FASE 14d; o mecanismo de aprovação revisado pelo ADR 0086
RN-110 — blame é a 13ª operação do GitProviderContract, com o mesmo vocabulário de ausência das outras leituras
Fundação da pendência de blame declarada na FASE 26 (nenhuma tela consome
ainda — vem na onda seguinte). blame(ref, path) devolve GitBlame | null,
null significando exatamente o que já significa em getFileContent/
listTree: arquivo ausente naquela ref, ou ref inexistente. Dois vocabulários
de "não existe" para a mesma aba fariam a tela tratar o mesmo caso de duas
formas — a mesma razão que já valia para as duas operações anteriores.
Cada provider computa por meios PRÓPRIOS, porque não há endpoint comum: o
GitHub não tem blame na REST (só GraphQL — a única operação do provider que
fala GraphQL), o GitLab tem repository/files/:path/blame, e o local sai de
git blame --porcelain, o único dos três testado contra um repositório de
verdade nesta sessão (os outros dois só contra os backends fake do teste de
contrato — sem GITHUB_TEST_TOKEN/GITLAB_TEST_TOKEN no ambiente, quem prova
contra a API real é o smoke manual). GIT_BLAME_LINE_LIMIT (2000) corta
arquivo genuinamente enorme — já cortado por bytes na rota de conteúdo, mas
blame lê o arquivo inteiro do provider antes de decidir.
- Onde:
packages/shared/src/index.ts(BlameInput,GitBlame,GitBlameLine, capabilityblame),apps/api/src/infrastructure/git/{github,gitlab,local}-provider.ts,apps/api/src/domain/git/git-read-limits.ts(GIT_BLAME_LINE_LIMIT),apps/api/src/application/use-cases/git/read-project-code.use-case.ts(métodoblame),apps/api/src/interfaces/http/git/code.controller.ts(GET /projects/:id/code/blame) - Teste:
apps/api/test/contract/git-provider.contract.ts(bloco "blame"), exercitado pelos três specs de provider — o dolocalcontra git de verdade — eread-project-code.use-case.spec.ts(bloco "blame") - Origem: FASE 26b (fundação das pendências declaradas da FASE 26/ ADR 0060)
RN-111 — listPullRequests é a 14ª operação do GitProviderContract; a lista navegável abre o mesmo diff por id
CodeDiffPanel.tsx consome listPullRequests(state?) numa lista clicável
(id/número/título/autor/estado/branches, com filtro por estado); clicar num
item reusa o MESMO fluxo de diff por id que já existia — não há caminho novo
de leitura, só como CHEGAR ao id sem precisar saber de cor. Quem já sabe o id
(ex.: veio de Aprovações) continua podendo colar direto.
listPullRequests(state?) devolve um RESUMO por PR
(GitPullRequestSummary: id, número, título, autor, estado, branches,
updatedAt) — não GitPullRequest, que é o tipo de ESCREVER (abrir/mesclar) e
nunca teve título nem autor porque nenhuma das duas operações precisava. Um
tipo próprio evita que a escrita ganhe campos que só a leitura usa.
O local TEM PR — o store sidecar da Fase 4a já é a fonte, e a suposição do
enunciado ("PR não existe no conceito de repositório local puro") não se
sustentou: o self-contained dos dev agents criou PR local desde então. As três
capabilities são true. GIT_PR_LIST_LIMIT (100) é uma página só, sem
paginação de seguimento — navegação humana, não sincronização de histórico.
- Onde:
packages/shared/src/index.ts(ListPullRequestsInput,GitPullRequestSummary,GitPullRequestList, capabilitypullRequestsList),apps/api/src/infrastructure/git/{github,gitlab, local}-provider.ts,apps/api/src/domain/git/git-read-limits.ts(GIT_PR_LIST_LIMIT),read-project-code.use-case.ts(métodopullRequests),code.controller.ts(GET /projects/:id/code/pull-requests),apps/web/src/routes/code/CodeDiffPanel.tsx(lista clicável, filtro por estado, reuso do fluxo de diff por id) - Teste:
git-provider.contract.ts(bloco "listPullRequests"),read-project-code.use-case.spec.ts(bloco "lista de PRs"),apps/web/src/routes/code/CodeDiffPanel.test.tsx - Origem: FASE 26b
RN-112 — listBranchesDetailed é operação PRÓPRIA, separada de listBranches
Fundação do dropdown rico, agora consumida por CodeBranchPicker.tsx
(ahead/behind, badge de PR — a onda seguinte à FASE 26b fechou a
pendência que CodeShell.tsx declarava). A decisão foi NÃO estender listBranches — a
13ª operação original, que o bootstrap de Gitflow chama sem precisar de nada
disso: enriquecer custa uma chamada extra ao provider POR BRANCH (duas no
GitLab, que não tem endpoint que devolva os dois lados de uma comparação numa
chamada só, ao contrário de compareCommitsWithBasehead do GitHub e de git rev-list --left-right --count no local). Encostar esse custo em toda
adoção/criação de branch transformaria o bootstrap numa varredura cara. As
duas operações convivem no contrato: listBranches pro bootstrap,
listBranchesDetailed (a 15ª) pra aba Code — GitBranchDetail estende
GitBranch só na FORMA, nunca no CONTRATO de quem chama.
ahead/behind são sempre relativos à branch DEFAULT do repositório
(ListBranchesDetailedInput.defaultBranch, que o chamador já sabe — pedi-la
de novo ao provider seria uma chamada a mais só pra redescobrir o que já
tinha). null nos dois quando o provider não consegue computar (branch órfã,
histórico não relacionado) é degradação honesta, nunca um número inventado.
GIT_BRANCH_DETAIL_LIMIT (30) corta pelas mesmas razões de tráfego do item 34
da FASE 26 — sem ele, um repositório com centenas de branches viraria centenas
de chamadas por abertura do dropdown.
- Onde:
packages/shared/src/index.ts(ListBranchesDetailedInput,GitBranchDetail,GitBranchDetailList,GitBranchPullRequestRef, capabilitybranchesDetailed),apps/api/src/infrastructure/git/{github, gitlab,local}-provider.ts,git-read-limits.ts(GIT_BRANCH_DETAIL_LIMIT),read-project-code.use-case.ts(métodobranches),code.controller.ts(GET /projects/:id/code/branches); no web,apps/web/src/lib/api-client.ts(getCodeBranches) eapps/web/src/routes/code/CodeBranchPicker.tsx— o dropdown em si, aberto a partir deCodeShell.tsx - Teste:
git-provider.contract.ts(bloco "listBranchesDetailed"),read-project-code.use-case.spec.ts(bloco "branches detalhadas"),apps/web/src/routes/code/CodeBranchPicker.test.tsx - Borda: o método
branches()mora no MESMO caso de uso das outras seis leituras (ReadProjectCodeUseCase), não perto do bootstrap — é uma LEITURA da aba Code, com a mesma resolução de credencial e o mesmo portão de container (RN-105) que as demais; tratá-la como operação de bootstrap duplicaria os dois. Uma ref fora da lista de branches (tag ou sha) segue alcançável — o rodapé do dropdown tem um campo manual, porquelistBranchesDetailednão enumera essas duas coisas. - Origem: FASE 26b (fundação); onda seguinte fechou a UI
O gate implementavel ativa — QA-estratégia como segundo momento do qa-lead (RN-340/341, ADR 0090)
RN-341 — A QA-estratégia é o segundo MOMENTO do qa-lead, e nunca suspende
packages/contracts/fluxo.yml declarava o papel qa-estrategia como proposto, com o
critério de separação escrito no próprio registro: "pode ser o próprio
qa-lead em segundo MOMENTO, não necessariamente agente novo — a separação é
de entregável". O ADR 0090
constrói exatamente isso: Engine.Gates.QaEstrategiaAgent é módulo SEM
ESTADO (não é GenServer), acionado por Engine.Gates.QaLeadServer.run_design/3
— um ponto de entrada NOVO e ADITIVO no MESMO processo qa-lead, sem tocar
run/2 (o caminho de sempre, revisão de PR, amarrado a
DevAgentState.find_by_task_id).
O contexto é LEVE, e é aí que a separação de entregável aparece.
Engine.Gates.QaEstrategiaContext.fetch/3 busca SÓ story (de
EngineApiClient.list_backlog/1, a árvore que o PO já lê — RN-164)
e module_map vigente (de EngineApiClient.get_infra_context/2, o MESMO
GetInfraContextUseCase que o Infra Lead consome, aqui só pelo campo
moduleMap) — SEM dev_state, SEM worktree_path: o gate implementavel
roda PRE-DEV, antes de existir dev agent, worktree ou task_id. Nenhuma
rota nova na api — as duas funções já existiam.
Nunca suspende. O registro de ferramentas (ReadFile, SearchWorkspace,
EmitPlanoDeTeste) não inclui terminal nem write_file — as DUAS únicas
tools que Engine.Harness.Hooks.ActionPipeline intercepta para criar
proposed_action. Sem chamada nenhuma passando pelo pipeline de ações, o
ToolLoop deste agente nunca produz :pending, e run_design/3 roda
SÍNCRONO dentro do próprio handle_cast — sem mecanismo de
suspensão/retomada, ao contrário do resto da área de QA.
O teto de iterações fica em 8 (conversacional), não 60 (gate) — de
propósito, não lacuna. Este agente roda SEM token_budget_micros — não há
task nem budget de task ainda. O critério da RN-085 não é "quem
trabalha muito": é "o que segura o gasto além do teto de iterações". Sem
budget por baixo, subir o teto multiplicaria o pior caso sem nada para
conter — a MESMA razão pela qual infra-workflows fica em 8 mesmo usando
ferramenta. "qa-estrategia" NÃO ganhou cláusula própria em
Engine.Harness.Iteracoes.tipo/1: cair no default é a decisão certa.
O entregável — emit_plano_de_teste (síntese, critérios executáveis
verificáveis, estratégia de automação GENÉRICA e sem framework — decisão de
escopo desta frente, na spec/0 que o modelo lê, não uma validação em
código) — vira o artefato artifact.plano_de_teste
(ArtifactEmitter.emit/5, schema validado, criteriosExecutaveis não pode
ser vazio), no event log da MESMA sessão que chamou run_design/3 — é lá
que assess_implementability (RN-340) o lê depois. Falha
(limite de iterações, orçamento, modelo que para sem emitir) NUNCA é
silenciosa: agent.error durável com origem, mesma régua da
RN-059.
Desde a RN-674 (ADR 0192) o MOMENTO mudou. O segundo momento
continua sendo do qa-lead, sem agente novo, e continua nunca suspendendo;
mas roda DEPOIS da entrega, no começo do ciclo de run/2, com o worktree do
dev e os arquivos do diff — run_design/3 e QaEstrategiaContext saíram, e
o artefato ganhou taskId. O teto continua 8, agora COM o orçamento da task
por baixo.
- Onde:
apps/engine/lib/engine/gates/qa_estrategia_agent.ex,qa_lead_server.ex(run_area/3, desde a RN-674),tools/emit_plano_de_teste.ex,hooks/termination_plano_de_teste.ex;apps/engine/lib/engine/harness/artifact_schemas.ex(plano_de_teste);apps/engine/lib/engine/harness/iteracoes.ex(SEM cláusula nova, ver acima);packages/contracts/fluxo.yml(qa-estrategia,status: active) - Teste:
apps/engine/test/engine/gates/qa_estrategia_agent_test.exs,qa_lead_server_test.exs(describe "plano de teste da entrega (ADR 0192)"),apps/engine/test/engine/harness/artifact_schemas_test.exs(describe "plano_de_teste"),iteracoes_test.exs("qa-estrategia é conversacional DE PROPÓSITO") - Origem: ADR 0090
secops-runtime — relatório de abuso sobre rate_limit_hits (RN-375..377, ADR 0091)
RN-377 — A seção "não medido" é permanente, e cita as três lacunas por nome
montarRelatorio sempre inclui naoMedido: detecção automática de
incidente, resposta a incidente e postmortem de segurança. As três dependem
do mesmo gatilho que packages/contracts/fluxo.yml já declarava para secops-runtime —
tráfego de produção real — e nenhuma delas é simulada com incidente de
exemplo nem número inventado (mesmo princípio dos ADRs 0041/0042/0077: sem o
dado real, a lacuna fica visível em vez de fingida). A seção só sai da lista
quando o produto tiver tráfego real para medir contra ela — não é um TODO a
apagar na próxima limpeza.
- Onde:
apps/api/scripts/relatorio-seguranca-runtime.ts,apps/api/package.json(relatorio:seguranca-runtime) - Teste:
apps/api/test/scripts/relatorio-seguranca-runtime.spec.ts - ADR: 0091
platform ganha uma primeira entrega: relatório de telemetria sob demanda (RN-385/386, ADR 0092)
RN-385 — O papel platform nasce como SCRIPT de leitura pontual, sobre as mesmas fontes do DomainGaugesCollector
packages/contracts/fluxo.yml descreve platform (camada_plataforma) como "SRE / Platform
— dono do loop de retorno", status: planned, com ativação sincronizada a
DEPLOY_ENABLED — que não existe. O dono do produto decidiu antecipar a
metade que já tem dado real por trás: pnpm --filter api relatorio:telemetria [--projeto <uuid>] [--json] lê, sob demanda, as MESMAS perguntas que o
DomainGaugesCollector já responde a cada METRICS_GAUGE_INTERVAL_MS para o
scrape do Prometheus — sessões ativas/closing por projeto, tasks bloqueadas
por projeto, estado do último backup (idade, status, tamanho, sempre GLOBAL,
nunca por projeto, porque o produto tem um backup só). Não é agente LLM nem
GenServer: é leitura avulsa, sem estado, que termina depois de imprimir.
As consultas SQL são REPLICADAS, não importadas do coletor — os métodos dele
(collectSessions/collectBlockedTasks/collectBackup) são privados e
terminam escrevendo num gauge Prometheus (this.metrics.*.set(...)); não há
uma metade pura de "só a query" para reusar sem acoplar um script avulso ao
ciclo de vida de um @Injectable do NestJS. O script também NÃO é um segundo
coletor: não registra métrica nenhuma, não roda em setInterval.
A saída sempre traz "onde ver mais" (os três dashboards versionados em
deploy/k8s/observability/dashboards/*.json, os alertas em
deploy/k8s/observability/alerts/brabo-alerts.yaml, docs/runbook.md #observabilidade e pnpm dev:obs para observabilidade local) — o script
LINKA para o que já existe, nunca duplica.
- Onde:
apps/api/scripts/relatorio-telemetria.ts; espelhaapps/api/src/infrastructure/observability/domain-gauges.collector.ts - Teste:
apps/api/test/scripts/relatorio-telemetria.spec.ts(funções puras —parseArgs,formatarIdade,formatarBytes— mesmo recorte demedir-execucao.spec.ts: a parte que fala com o banco é exercitada rodando o script contra um banco real) - ADR: 0092
RN-386 — O relatório de telemetria declara, sem inventar, o que NÃO mede
O gatilho real do papel platform (DEPLOY_ENABLED) não existe: não há
ambiente de produção com tráfego real, não há SLO numérico definido em lugar
nenhum do produto, e não há postmortem possível sem incidente de verdade.
relatorio-telemetria.ts diz isso na PRÓPRIA saída, numa seção "não medido",
em vez de fingir cobertura que não tem — a mesma disciplina do ADR 0042 para
nota de modelo e do ADR 0077 para qualidade de código: sem o dado real, o
produto DECLARA a lacuna, nunca inventa o número.
Três lacunas, todas explícitas: SLO numérico formal (nenhum está
definido); postmortem (depende de incidente real que não aconteceu); e
telemetria de volta ao produto em loop fechado (o script é leitura
pontual sob demanda — observar, decidir e agir sozinho é o que tornaria
platform active em packages/contracts/fluxo.yml, e por isso o gate_saida: { id: operavel, status: planned } do papel permanece planned, intocado por esta
mudança).
- Onde:
apps/api/scripts/relatorio-telemetria.ts(funçãoimprimir, seção "Não medido");packages/contracts/fluxo.yml(camada_plataforma › platform › saidas_alvo, artefatotelemetria-consolidada, camponota) - Teste: cobertura indireta — a seção é texto fixo verificado por leitura do arquivo; não há verdade condicional a testar aqui (nada a errar entre "medido" e "não medido" além do texto em si)
- ADR: 0092
Auditoria fluxo.yml × código — Onda 2: RN-160 no backend e delegação Dev Lead → dev (RN-404/405, ADR 0094)
RN-405 — A delegação Dev Lead → dev vira dado em delegations, com parecerArtifactId redefinido para "o que justificou a decisão"
O ADR 0053 (FASE 14d) já previa a delegação Dev Lead → dev-<modulo> como
"o mesmo caminho de QA e Infra" (delegations, area = 'dev'), mas
declarou isso fora de escopo. dev_lead_server.ex nunca gravava a tabela —
só QA (qa_lead_server.ex) e Infra (infra_lead_server.ex) gravavam, e os
dois do lado ENGINE, porque é lá que o subagente produz um PARECER (veredito
de rodada única) que justifica parecerArtifactId.
O Dev Lead não tem esse padrão: a ativação de um dev-<modulo> acontece do
lado API, em AcceptParallelizationUseCase.execute — chamada tanto pelo
caminho direto (RequestParallelizationUseCase, abaixo do teto de sessão da
RN-083) quanto pelo aprovado (ExecuteParallelizationUseCase,
depois que o usuário aprova a proposed_action tipo parallelize). A
gravação entrou DENTRO desse método — cobre os dois caminhos de graça,
porque os dois já convergem ali —, com status: 'completed' REDEFINIDO para
esta área: significa "a delegação foi EFETIVADA" (o agente subiu), não "o
subagente terminou e emitiu parecer" como em QA/Infra (decisão registrada no
ADR 0094).
parecerArtifactId aponta para o id do evento artifact.module_map mais
recente e vigente do projeto — obtido de
SessionEventRepository.listByTypeForProject(projectId, 'artifact.module_map')
(método genérico já existente, usado por computeCoverage para
artifact.business_rule; nenhuma consulta nova foi escrita, só uma chamada a
mais dele — o último item da lista é o mais recente, porque a função ordena
por createdAt ASC) — o artefato que justificou a decisão de delegar.
area: 'dev', leadAgent: 'dev-lead', subagent é o id exato do agente
ativado (extraDevAgentId(module), a MESMA função que constrói o id em todo
o resto do use case — nenhum formato novo). Sem artifact.module_map no
projeto (não deveria acontecer — é entrada obrigatória do Dev Lead em
packages/contracts/fluxo.yml), a delegação NÃO é gravada com um id inventado: só loga o
estado inesperado, pela mesma lição da RN-059 (nunca falha silenciosa, mas
também nunca finge uma justificativa que não existe). E falha de
RecordDelegationUseCase.execute (ex.: banco fora do ar) é capturada e
logada, nunca propagada — a ativação do dev agent já é sucesso quando a
tentativa de gravar a delegação acontece, e não pode ser derrubada por uma
gravação auxiliar.
- Onde:
apps/api/src/application/use-cases/execution/accept-parallelization.use-case.ts(recordDevDelegation, chamado ao fim deexecute) - Teste:
apps/api/test/application/use-cases/execution/accept-parallelization.use-case.spec.ts, describe "delegação Dev Lead → dev (área dev, ADR 0094)" — grava comarea: 'dev'apontando pro module_map mais recente; sem module_map não grava (e não lança);RecordDelegationUseCasefalhando não derruba a ativação - ADR: 0094
Auditoria fluxo.yml × código — Onda 6 (última): o gate necessidade-validada (RN-406, ADR 0095)
Fecha o último achado do plano da auditoria (seção D,
docs/explanation/auditoria-fluxo-vs-codigo.md) — B2. As outras cinco
ondas já tinham fechado (a 3, 4 e 5 antecipadas fora de ordem pelos ADRs
0089/0090; a 1 e a 2 nos PRs anteriores). packages/contracts/fluxo.yml (papel
criativo) declarava gate_saida: { id: necessidade-validada, status: proposto } desde o ADR 0085 sem mecanismo nenhum atrás — e
modelo-de-time.md já registrava por que: o Criativo (o modelo) decidir
sozinho que a necessidade que ele mesmo produziu está validada seria
autovalidação, não gate de verdade.
RN-406 — O gate necessidade-validada se fecha com um clique SEPARADO do usuário, nunca com o Criativo se autovalidando
Revista pelo ADR 0185 (RN-657), por decisão do dono em 30/09. O clique deixou de ser SEPARADO: "Estou pronto — a necessidade está validada" grava
readiness.confirmedenecessity.validatedno mesmo POST, com oproduct_briefproduzido DEPOIS do clique (productBriefId: null). O botão "Confirmar necessidade validada" saiu da tela. O que desta regra continua valendo: quem valida é sempre uma PESSOA, nunca o Criativo; o gate seguewarn.POST .../agents/criativo/validate-necessitycontinua na api, sem consumidor na tela — é o caminho de uma sessão cujo "Estou pronto" é anterior ao ADR 0185. O texto abaixo é o desenho do ADR 0095, mantido como histórico.
SessionPage.tsx ganha um terceiro botão de confirmação, no MESMO padrão
interacional de "Estou pronto para produzir" (RN-142) e "Confirmar
arquitetura pronta" (RN-160): "Confirmar necessidade validada"
(handleValidateNecessity) chama POST .../agents/criativo/validate-necessity, que grava necessity.validated
com payload.productBriefId apontando para o artifact.product_brief
mais recente da sessão.
O botão só habilita DEPOIS que confirm_readiness já consolidou o
product_brief (hasProductBrief, events.some(e => e.type === 'artifact.product_brief')) — não faz sentido "validar" um resumo
executivo que ainda não foi produzido, e é essa a leitura mais
consistente com packages/contracts/fluxo.yml: necessidade-validada é gate de SAÍDA
do Criativo, o momento em que o trabalho dele já entregou um artefato
concreto. ValidateNecessityUseCase revalida a mesma pré-condição no
BACKEND (SessionEventRepository.listByTypeInSession(sessionId, 'artifact.product_brief'), pega o último por seq) — a UI desabilitada
sozinha não bastaria, mesma lição da RN-404.
Diferente de OfferInfraHandoffUseCase (RN-160), esta confirmação NÃO
sinaliza o engine: o handoff Criativo→PO já aconteceu dentro do próprio
confirm_readiness
(CriativoServer.executar_confirm_readiness/1), então não há agente
nenhum esperando por este evento — ele é só o registro de que um humano
validou o MÉRITO do que já foi entregue. ValidateNecessityUseCase não
recebe ApiToEngineClient no construtor.
apps/api/config/gates.yml ganha o gate necessidade-validada: status: active,
aprovacao_humana: true (é literalmente um clique humano),
verificacao: script, mas severidade: warn — nada no produto hoje
CONSULTA a passagem deste gate antes de deixar o PO seguir (diferente de
story-promovida/plano-de-adocao, que são block porque uma trava
real de código os impede de serem pulados). packages/contracts/fluxo.yml (papel
criativo) passa status: proposto → status: ativo.
- Onde:
apps/api/src/application/use-cases/agents/validate-necessity.use-case.ts;apps/api/src/interfaces/http/agents/agents.controller.ts(validateNecessityHandoff);apps/web/src/routes/SessionPage.tsx(hasProductBrief,necessidadeJaValidada,handleValidateNecessity) - Teste:
apps/api/test/application/use-cases/agents/validate-necessity.use-case.spec.ts— semproduct_brief, recusa ANTES de gravar qualquer evento; com um ou mais, grava referenciando o MAIS RECENTE;apps/web/src/routes/SessionPage.validar-necessidade.test.tsx— botão desabilitado semproduct_brief, habilitado com ele, some depois denecessity.validated - ADR: 0095
RN-407 — O PO lê o funil de entrega e DORA parcial, terceira leitura escopada ao projeto
O PO ganhou uma TERCEIRA ferramenta de leitura — listar_metricas_de_produto
(:direct, sem parâmetro nenhum) — servida por
GET /internal/projects/:projectId/product-metrics, mesmo desenho das duas
irmãs da RN-164: escopo fechado no projeto, sem termo de busca,
sem paginação.
packages/contracts/fluxo.yml (papel po, entrada metricas-de-produto) declarava
status: lacuna desde o ADR 0089,
que já tinha entregue o DADO — o script pnpm --filter api analise:funil
mede o funil real sessão → commit → PR → merge, o lead time real e a
deployment frequency real a partir de proposed_actions. O que faltava era
só o MECANISMO: o PO nunca tinha como reler esse relatório dentro do turno.
Fecha o item B4 da auditoria fluxo.yml × código
(docs/explanation/auditoria-fluxo-vs-codigo.md) —
a ÚLTIMA pendência da tabela "Backlog do modelo de time"
(docs/explanation/backlog.md), que fica vazia depois desta.
O CÁLCULO é o MESMO do script, nunca duplicado: as funções puras
(calcularFunil/calcularLeadTimes/leadTimeMedioMs/
deploymentFrequencyPorDia) e a query que monta AcaoGit[]
(buscarAcoesGitDoFunil) foram extraídas de apps/api/scripts/analise-funil.ts
para apps/api/src/application/services/funil-metrics.ts — um caso de uso em
src/ não pode importar de scripts/, e o script passou a REEXPORTAR dali
em vez de definir localmente, sem mudar assinatura nem comportamento (o
teste de regressão do script continua verde sem ser tocado).
O corpo JSON que a rota devolve não tem campo nenhum para as três ausências permanentes que o script já declarava em texto ("Não medido, de propósito": funil de produto completo ideação → commit, evidência de adoção por feature, MTTR/change failure rate) — o shape do relatório nunca reservou espaço para elas. A ferramenta do PO cita as três pelo NOME no TEXTO que devolve ao modelo, sempre, e não só quando o número dá zero: sem essa seção o PO leria só os números do funil e concluiria por omissão que não há lacuna nenhuma.
- Onde:
apps/api/src/application/services/funil-metrics.ts;apps/api/scripts/analise-funil.ts(reexporta);apps/api/src/application/use-cases/backlog/list-product-metrics.use-case.ts;apps/api/src/interfaces/http/internal/internal-projects.controller.ts;apps/api/src/interfaces/http/internal/dto/product-metrics.response.dto.ts;apps/engine/lib/engine/harness/tools/listar_metricas_de_produto.ex;apps/engine/lib/engine/agents/po_server.ex;apps/engine/lib/engine/sessions/engine_api_client.ex - Teste:
apps/api/test/application/use-cases/backlog/list-product-metrics.use-case.spec.ts;apps/api/test/scripts/analise-funil.spec.ts(regressão — continua importando descripts/analise-funil.ts, sem alteração);apps/engine/test/engine/harness/tools/listar_metricas_de_produto_test.exs;apps/engine/test/engine/agents/po_server_test.exs - Origem: mesmo padrão da RN-164 (leitura de agente escopada
ao projeto, sem efeito externo, sem
proposed_action); o dado é do ADR 0089 — esta RN só fecha o mecanismo de leitura que faltava.
O repositório nasce no aceite do handoff ao Arquiteto, e a execução não começa sem ele (RN-582)
RN-605 — O pull de imagem é um passo NOMEADO do start, sob o teto de controle, e o estouro diz que imagem grande não sobe
O start da porta de Docker não fazia pull: o docker run fazia, por dentro,
sob o TIMEOUT_DE_CONTROLE_MS (30 s). Reproduzido na AT-234 com um duplo do
daemon (imagem ausente, pull de 45 s, sem dormir e sem rede): o estouro saía
como ComandoDeDockerFalhouError — "docker run … terminou com código -1:
(sem saída de erro)" —, virava 502 com origem: null no broker e chegava à api
sem nome. E matar o CLI CANCELA o pull (medido no Docker 29.8.0, containerd):
a imagem continuou ausente nos 60 s seguintes, então a frase "o pull pode
seguir no daemon" não valia.
Das quatro saídas medidas (teto próprio de ~300 s, start assíncrono, pull
antecipado na decisão de imagem, só dar nome), o mantenedor decidiu a D —
só dar nome (2026-09-27). A regra:
- O pull é explícito. Antes do
run,image inspect; imagem presente não é baixada de novo (o mesmo--pull missingdorun). Ausente,docker pullsob o MESMO teto de controle de 30 s — nenhum teto maior. - O estouro tem nome.
PullExcedeuTetoError, origeminfra, com a imagem, o teto e a consequência inteira: o pull foi cancelado, a imagem continua ausente, imagem grande não sobe por este caminho, e o conserto é baixá-la antes no host. Antes de afirmar isso, o adaptador confere o daemon (docker version): daemon fora continua sendoDockerIndisponivelError. Pull que falha por outro motivo segue o caminho de sempre (ComandoDeDockerFalhouError, sem origem, ADR 0128). - O nome atravessa. O broker responde 504 com
origem: 'infra', e a api o repassa emBrokerRecusouError—origempreenchida, nuncanull. No modorunnero mesmo adaptador roda na máquina do usuário e a mensagem chega nocontainer_start_result. - Os tetos de quem espera o
startcontam o pior caso inteiro: os doispsderesolver,image inspect,pull,rune odocker versionde diagnóstico — seis chamadas de controle (CHAMADAS_DE_CONTROLE_NO_START). A api espera 10 + 6 × 30 + 5 = 195 s (TETO_DE_MUTACAO_MS); o engine, noRunnerRouter, 6 × 30 + 5 = 185 s nostarte 4 × 30 + 5 = 125 s nostop/remove(eram 60 s, menos que oresolvermais a operação). - O
propose_actiondo engine espera a execução que a api faz dentro dele.container_start,container_start_via_runnerecontainer_stopauto-aprovados são EXECUTADOS na mesma requisição (ProposeActionUseCase), e a chamada caía no default de 15 s do Req — umstartlongo voltava como timeout de transporte mesmo sem pull. Agora ela levareceive_timeoutde 225 s (o teto da api + 30 s), espelhado na api comoTETO_DO_PROPOSE_ACTION_DE_CONTAINER_NO_ENGINE_MS; os testes dos dois lados conferem broker < api < engine e runner < engine.container_removefica fora: nunca nasce auto-aprovado.
Nenhuma dependência nova (ADR 0128), nenhum campo
que o chamador escreva (ADR 0130): a imagem
continua vindo da decisão do Arquiteto, e pull não é operação nova do broker
— é um passo do start.
Declarado, não fechado:
-
Imagem grande não sobe na primeira tentativa, pelo broker nem pelo runner — decisão D do mantenedor. O desfecho agora é nomeado; o conserto é baixar a imagem antes no host.
-
No pior caso de falha, a aprovação fica presa por até ~195 s:
ExecuteContainerStartUseCaseroda dentro doPOSTde aprovação, e nem a web nem o servidor HTTP têm teto próprio. -
Não foi medido se as camadas baixadas antes do cancelamento são reaproveitadas na tentativa seguinte.
-
Código:
packages/docker-port/src/docker-cli.ts:206(PullExcedeuTetoError),:754(garantirImagem),:129(CHAMADAS_DE_CONTROLE_NO_START),:116(TIMEOUT_DE_CONTROLE_MS);apps/broker/src/servidor.ts:240(PullExcedeuTetoError);apps/api/src/infrastructure/http-clients/container-broker.client.ts:252(TETO_DE_MUTACAO_MS),:216(CHAMADAS_DE_CONTROLE_NO_START),:275(TETO_DO_PROPOSE_ACTION_DE_CONTAINER_NO_ENGINE_MS);apps/engine/lib/engine/sessions/engine_api_client.ex:1985(opcoes_do_propose_action),apps/engine/lib/engine/sessions/engine_api_client.ex:1985(opcoes_do_propose_action),:1649(@teto_do_propose_action_de_container_ms);apps/engine/lib/engine/runners/runner_router.ex:52(@timeout_do_start_ms) -
Teste:
packages/docker-port/src/docker-cli.spec.ts:425(describe da reprodução, "(AT-234)": o pull de 45 s termina NOMEADO,:457; daemon fora continua daemon fora,:481; o pull que cabe sobe,:493; imagem presente não é baixada,:508; as seis chamadas,:520);apps/broker/src/servidor.spec.ts:317(504 com origeminfra);apps/api/test/infrastructure/http-clients/container-broker.client.spec.ts:144(chega comorigem),:362e:377(a conta e a ordem);apps/runner/src/index-handlers.spec.ts:420(o nome nocontainer_start_result);apps/engine/test/engine/sessions/engine_api_client_propose_action_test.exs:67(HTTP real: a api que demora 16 s é esperada),:82e:95(as ações e a ordem, lendo o espelho da api);apps/engine/test/engine/runners/runner_router_test.exs:183 -
Origem: AT-234
Decidir no chat, onde o dono está (RN-626)
RN-627 — O container do projeto roda com o dono da pasta, medido pela api
Sob --cap-drop ALL o root do container não tem CAP_DAC_OVERRIDE e não
escreve na pasta do projeto, que é do uid do operador: npm install dava
EACCES para todo dev agent (AT-247, ADR 0180).
- A api mede, o broker compõe.
container-specdevolveusuarioDaPasta(statda pasta dos modoscontainer/mounted;nullse a pasta não é alcançada, se o dono é root ou se o projeto érunner). O broker o copia paraspec.usuarioe o adaptador emite--user uid:gide--env HOME=/tmp. - Nunca vem de quem pede.
startnão tem corpo e o corpo deexecignora campo extra;execherda o usuário do container. - O broker revalida. Inteiros em 1..2^31-1;
0é recusado nomeandousuario.uid/usuario.gid(root é o ausente, "como sempre"). - Não afrouxa contenção:
--cap-drop ALL, um bind, rede de dois valores e cinco operações continuam. Container já criado só muda ao ser recriado.
- Onde:
packages/docker-port/src/docker-cli.ts:838(argsDeCriacao),packages/docker-port/src/spec-de-container.ts:136(usuarioValidado),apps/broker/src/operacoes.ts:456(especificacaoDoProjeto),apps/api/src/application/use-cases/containers/obter-spec-de-container.use-case.ts:124(ObterSpecDeContainerUseCase) - Teste:
packages/docker-port/src/docker-cli.spec.ts(orunleva--usere continua sem campo livre),packages/docker-port/src/spec-de-container.spec.ts(usuario),apps/broker/src/operacoes.spec.ts("o usuário do container"),apps/api/test/application/use-cases/containers/spec-e-observacao-de-container.use-case.spec.ts - Origem: AT-247
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-738 — Parar container de projeto é imediato, e a linha da /containers relê ao decidir
- Regra: todo container de projeto sobe com
--init— CONSTANTE da composição dodocker runemDockerViaCli, nunca campo da spec (a contenção é o tipo, ADR 0130); vale para o broker e para o runner, que usam a mesma porta. O comando de espera (sleep infinity) era PID 1 sem handler de SIGTERM, e o kernel não entrega sinal de ação padrão a PID 1:docker stopesperava os 10 s e matava (Exited (137)). Medido em container descartável: 10,25 s sem--init, 0,20 s com. Na página/containers, decidir a ação pendente (aprovar, negar, sempre permitir) invalida['containers-overview']e['container-lifecycle', projectId]nofinally— também quando a chamada falha ou cai —, e devolve a promessa ao card. Nenhum poll novo (RN-579/632). - Onde:
packages/docker-port/src/docker-cli.ts:838(argsDeCriacao),apps/web/src/components/AcoesDoContainer.tsx:55(invalidateContainers),:166(aprovar) - Teste:
packages/docker-port/src/docker-cli.spec.ts(runleva--initantes do comando),apps/broker/src/contencao-do-brabo.spec.ts(os dois modos sobem com--init),apps/web/src/routes/ContainersPage.test.tsx(aprovar relê visão geral e ciclo de vida; aprovação recusada ainda relê) - Origem: AT-422 (instalação local, 03/10: três paradas
executedcom a linha em "rodando" eExited (137))
RN-807 — O exec que estoura o teto volta como estouro, e o comando morre dentro do container
- Regra: o estouro do teto do
execdaDockerPorté decidido pelo RELÓGIO (decorrido ≥ teto) além dokilled, porque o clientedocker execcaptura o SIGTERM dotimeoutdoexecFilee sai com código 0 — medido:sleep 30; echo FIMsob teto de 3s voltavaerro: null,stdout: "", e o adaptador devolviaexitCode: 0. Comando de agente que estourava os 120s voltava "exit 0", e o QA aprovou suíte que trava. O estouro agora devolveexitCode: -1,timedOut: truee a marca de timeout. E matar o cliente não mata o comando: oexecembrulha o comando (que segue inteiro e não interpretado; o canal é-e BRABO_EXEC_CMD, RN-947) numshque grava o próprio PID — líder de grupo e de sessão, porque o runtime fazsetsidem tododocker exec— em/tmpdo container; no estouro o adaptador mata o GRUPO por esse PID (SIGKILL), numa chamada interna, sem operação nova na porta (ADR 0130). Medido e recusado:timeoutdo busybox dentro dosh -c— o vigia morre com o cliente e osleepsobrevive. Se a morte falhar (/tmpsem escrita, por exemplo), a marca diz que o processo pode ter sobrevivido. - Onde:
packages/docker-port/src/docker-cli.ts:283(estourouOTeto),packages/docker-port/src/docker-cli.ts:538(matarExecEstourado) - Teste:
packages/docker-port/src/docker-cli.spec.ts("erro nulo depois do teto É estouro"; "no estouro mata o grupo do comando DENTRO do container"; "o comando que estoura volta timedOut e MORRE dentro do container", contra Docker real, pulado sem daemon) - Origem: AT-482
RN-820 — O container de projeto volta sozinho quando o daemon reinicia
- Regra: o
docker runcomposto peloDockerViaClileva--restart unless-stoppedcomo CONSTANTE, ao lado do--initda RN-738 — a spec não ganha campo, e a contenção do ADR 0130 não muda. Um daemon reiniciado (atualização do sistema) religa o container de projeto; umdocker stopexplícito (ocontainer_stopdo ciclo de vida) NÃO é desfeito, porqueunless-stoppednão religa o que foi parado à mão — nuncaalways. Medido: containerunless-stoppedparado comdocker stopsegueexitedcomRestartCount=0. - Onde:
packages/docker-port/src/docker-cli.ts:838(argsDeCriacao) - Teste:
packages/docker-port/src/docker-cli.spec.ts("cria o container com rótulo gerenciado, UM bind e as capabilities derrubadas") - Origem: AT-494 (o daemon reiniciado deixou todo container de projeto
Exited (143)com o registrorunning)
RN-928 — O comando do app é argumento de um supervisor constante, e sem ele o container é o de sempre
- Regra:
EspecificacaoDeContainer.comandoDoApp(opcional, até 300 caracteres, sem\n,\rnem NUL) troca osleep infinitypelo supervisor CONSTANTESUPERVISOR_DO_APP, que recebe o comando como$1(molde da RN-807) e nunca o concatena: aspas,;e$()do comando não escapam. O app sobe em background (log em/tmp/brabo-app.log, código em/tmp/brabo-app.exit) e o PID 1 seguesleep infinity. Ausente ounull, odocker runé byte a byte o anterior. O broker lê o campo SÓ dacontainer-specda api. (ADR 0228.) - Onde:
packages/docker-port/src/spec-de-container.ts:351(comandoDoAppValidado),packages/docker-port/src/docker-cli.ts:398(SUPERVISOR_DO_APP),packages/docker-port/src/docker-cli.ts:838(argsDeCriacao) - Teste:
packages/docker-port/src/docker-cli.spec.ts:685(NÃO-REGRESSÃO),packages/docker-port/src/docker-cli.spec.ts:704(comando com',;e$()fica como argumento) - Origem: AT-610 (EP-030).
RN-929 — Porta publicada vai só ao loopback, e DNS e extra_hosts são IP literal e nome de lista fechada
- Regra:
portasvira sempre-p 127.0.0.1:H:C(TCP; interface constante, sem opção), host de 1024 a 65535 fora de 3000, 4000, 5173, 5432, 7474, 7687, 8088, 8090 e 11434, no máximo 10 e sem duplicata.dnsé IP literal v4/v6 (até 3).hostsExtrasé{ nome, ip }(até 10) com nome em[a-z0-9.-]de até 253 caracteres, nuncalocalhost,host.docker.internalnem nome de serviço da instalação, e IPv4 literal. Os três são recusados com redenone.inspectdevolveportasPublicadaslidas deNetworkSettings.Ports, separadas do declarado (RN-468). (ADR 0228.) - Onde:
packages/docker-port/src/spec-de-container.ts:384(portasValidadas),packages/docker-port/src/spec-de-container.ts:405(dnsValidado),packages/docker-port/src/spec-de-container.ts:415(hostsExtrasValidados),packages/docker-port/src/docker-cli.ts:365(portasPublicadasDe) - Teste:
packages/docker-port/src/spec-de-container.spec.ts:237(recusas),packages/docker-port/src/docker-cli.spec.ts:714(-p 127.0.0.1,--dns,--add-host),apps/broker/src/operacoes.spec.ts:289(vem da api) - Origem: AT-608 (EP-030), fatia do contrato.
RN-930 — A instância de QA é efêmera, só mexe no que tem brabo.papel=qa e sobe os serviços que a Infra declarou por digest
- Regra: a spec com
qa: { chave, servicos }sobebrabo-qa-<workspace_dir_name>-<chave>sem--restart, combrabo.papel=qaebrabo.qa.instancia, sem app nem porta.stop,remove,inspecteexeccomalvode QA filtrambrabo.papel=qa; homônimo sem o papel é "não gerenciado" e nada é removido. Cada serviço (até 6, imagem por digest@sha256:obrigatório,envde chaves[A-Z_][A-Z0-9_]*) sobe com--network container:<qa>,--cap-drop ALL, sem privilégio nem porta publicada, tetos constantes e--tmpfs; serviço compartilhado vive na rede privada--internaldo projeto. O broker expõe/containers/:projectId/qa/:chave/{start,remove,exec}e lê os serviços da api, conferindo a chave. (ADR 0229.) - Onde:
packages/docker-port/src/docker-cli.ts:625(iniciarQa),packages/docker-port/src/docker-cli.ts:933(argsDeServicoDeQa),packages/docker-port/src/spec-de-container.ts:439(instanciaDeQaValidada),apps/broker/src/operacoes.ts:212(startQa),apps/broker/src/servidor.ts:135(rotaDeContainer) - Teste:
packages/docker-port/src/docker-cli.spec.ts:815(remove só alcança o papelqa),packages/docker-port/src/spec-de-container.spec.ts:256(digest, chave, env),apps/broker/src/operacoes.spec.ts:321(lista da api e chave da rota),apps/broker/src/servidor.spec.ts:405(roteador fechado) - Origem: AT-612 (EP-030).
RN-941 — O serviço de QA roda com usuário e comando declarados, nunca como root, e leva o nome da instância mais o do serviço
- Regra: revisa a RN-930 e a RN-935 depois da prova com Docker real de 29/10/2026 (AT-612), que achou três defeitos. (1) O serviço PRÓPRIO era criado com o nome da própria instância (
Conflict): agora é<instância>-<serviço>, o queremoverServicosDeQae o rótulobrabo.qa.donojá esperavam. (2) Postgres sob--cap-drop ALLsai comchown/chmodnegados: o serviço ganhauser("<uid>:<gid>", inteiros diferentes de 0, sem zero à esquerda, nunca root) que vira--user, e os caminhostmpfsmontam comuid=,gid=emode=0700do mesmo dono. (3) Imagens que não sobem sozinhas: o serviço ganhacommand(1 a 16 argumentos de até 200 caracteres, sem\0,\nou\r), passado como argumentos DEPOIS da imagem, nunca por shell.nullequivale a ausente. A contenção não muda (--cap-drop ALL, sem--privileged/--cap-add, sem porta publicada, tetos constantes). A régua mora nos três lugares (docker-port, api, engine) e nenhum deles aceita0. - Onde:
packages/docker-port/src/spec-de-container.ts:453(usuarioDeServicoValidado),packages/docker-port/src/docker-cli.ts:933(argsDeServicoDeQa),apps/api/src/domain/infra/run-instructions.ts:160(recusaDoQa),apps/engine/lib/engine/infra/instrucoes_de_execucao.ex:133(qa). - Teste:
packages/docker-port/src/docker-cli.spec.ts(nome,--user, dono do tmpfs, comando),packages/docker-port/src/spec-de-container.spec.ts(recusa de0:0,70:0,70, comando grande/com quebra),apps/broker/src/operacoes.spec.ts,apps/api/test/domain/infra/run-qa.spec.ts,apps/engine/test/engine/infra/instrucoes_de_execucao_test.exs; prova com Docker real:postgres:17-alpinecom70:70aceita conexão emlocalhost:5432ebusybox httpdresponde emlocalhost:9000de dentro da instância. - Origem: AT-612 (EP-030), ADR 0229.
RN-947 — O comando do agente no exec não aparece na linha de comando de nenhum processo do container
- Regra: o comando do
execdaDockerPortviaja por-e BRABO_EXEC_CMD=<comando>e não mais no argv: o embrulho da RN-807 o lê, fazunseteset --, e o roda por(eval "$c")em subshell (mesmo grupo, mesmo PID gravado,exiteset -econtidos, exit code preservado). O argv do container fica só/bin/sh -c <embrulho> brabo-exec <marcador>. Medido no uso real: os 36 comandos do QA de automação compkill -fmorriam com 143, porque o padrão casava o argv do própriosh. Fora, declarado: o runner no host (apps/runner/src/exec.ts), que a RN-507 já condiciona a containerrunning. - Onde:
packages/docker-port/src/docker-cli.ts:303(EMBRULHO_DO_EXEC). - Teste:
packages/docker-port/src/docker-cli.spec.ts("o comando que estoura volta timedOut e MORRE dentro do container", contra Docker real: auto-pkill -fsobrevive,exit 3volta 3, o comando não aparece nops). - Origem: AT-622 (EP-030).
RN-1024 — O app é reiniciado no lugar: só o processo dele, nunca o container nem os outros exec
- Regra: a
DockerPortganha a sexta operação,reiniciarApp(workspaceDirName, comandoDoApp), que reinicia SÓ o processo do app supervisionado dentro do container em execução; as lanes dividem esse container, então ele não é parado nem recriado, e nenhum outro processo é tocado. O supervisor (SUPERVISOR_DO_APP) passa a lançar o app por um lançador cujoshinterno grava o próprio PID em/tmp/brabo-app.pide depois fazexecdo comando, então o PID gravado é o líder do grupo e da sessão do app. O script constanteREINICIAR_APPmanda TERM para esse grupo (KILL depois de 5 s), remove o.pide o.exit, relança com o mesmo lançador e espera até 10 s por/tmp/brabo-app.exit: se ele aparece, o app morreu. Sem.pid(o app nunca rodou, por exemplo num container nascido comsleep infinity), ele só lança. O comando do usuário trafega só como$1do script, pelo argv dodocker exec, nunca concatenado (RN-807). O resultado é tipado:{ reiniciou: true }ou{ reiniciou: false, motivo }, commotivoentresem_container,tempo_esgotado,falha_no_execeapp_saiu(este com oexitCode). O resultado NUNCA leva trecho de log, porque o comando do app pode ter segredo em texto puro; o log fica em/tmp/brabo-app.log, dentro do container. Revisa as decisões 1 ("app que morre fica morto", agora só até o próximo reinício) e 5 ("cinco operações") da ADR 0228. O broker, a api e a engine o consomem pela TAG deste repositório. - Onde:
packages/docker-port/src/docker-port.ts:625(reiniciarApp),packages/docker-port/src/docker-cli.ts:604(reiniciarApp),packages/docker-port/src/docker-cli.ts:408(REINICIAR_APP),packages/docker-port/src/docker-cli.ts:398(SUPERVISOR_DO_APP) - Teste:
packages/docker-port/src/docker-cli.spec.ts:862(argv, resultados tipados, sem log),packages/docker-port/src/docker-cli.spec.ts:921(shreal: mata o grupo antigo, KILL quando o TERM é ignorado, grava o novo.pid,app-exitcom app morto) - Origem: AT-617 (EP-030), ADR 0264.
RN-1037 — O dist do shared não exporta nenhum valor
- Regra: o
@viralabs-dev/brabo-sharedé publicado comdist, mas continua 100% tipo: odist/index.jséexport {};e só o.d.tscarrega a superfície. Umexport constno fonte dosharedvira runtime em quem consome e reabre a porta que o invariante antigo fechava (ver a RN-1024 para o que é runtime e mora nodocker-port). Quem precisa de lista em runtime a guarda no consumidor. - Onde:
packages/shared/src/index.ts,packages/shared/tsconfig.build.json. - Teste:
packages/shared/src/dist-so-tipos.spec.ts(otestdo pacote roda o build antes; mutação provada: umexport constno fonte reprova dois casos). - Origem: AT-770, ADR 0270.
RN-1041 — O docker-cli.json é o espelho das constantes de controle do docker-cli.ts
- Regra:
packages/contracts/docker-cli.jsontem exatamenteCHAMADAS_DE_CONTROLE_NO_STARTeTIMEOUT_DE_CONTROLE_MS, com os valores dedocker-cli.ts. O engine (Elixir) o baixa por raw na tag e deriva dele o teto docontainer_start(RN-604/RN-605), no lugar de ler o.tspor expressão regular. Mudou a constante: mude o JSON, e o spec reprova se divergirem ou se sobrar/faltar chave. - Onde:
packages/contracts/docker-cli.json,packages/docker-port/src/docker-cli.ts(TIMEOUT_DE_CONTROLE_MS,CHAMADAS_DE_CONTROLE_NO_START). - Teste:
packages/docker-port/src/docker-cli-json.spec.ts(mutação provada: trocar o valor no JSON reprova). - Origem: AT-770, ADR 0270.