Pular para o conteúdo principal

Histórico de fases

Registro imutável das fases concluídas do Brabo, movido do CLAUDE.md em 2026-08-23 sem reescrita. O estado VIGENTE do produto mora no CLAUDE.md, nas RNs (docs/business-rules.md) e nos ADRs — este arquivo responde "como se chegou aqui", nunca "o que vale hoje". ADR aceito nunca é editado; as correções de ADR registradas neste texto continuam válidas onde estão.

Status​

  • FASE 1 (MVP) — CONCLUÍDA: IAM/RBAC, sessões com event log imutável, chat com roteador de LLM (Ollama + APIs), metering/budget de tokens, pipeline de proposed_actions + permissions.json, motor Elixir/OTP com supervisão e evento de término.
  • FASE 2 — CONCLUÍDA: GitProvider (Local/GitHub/GitLab) com suite de contrato e capabilities, credenciais criptografadas, bootstrap de Gitflow idempotente e retomável via pipeline, wizard com progresso ao vivo.
  • FASE 3 — CONCLUÍDA: Harness (PromptAssembler, ToolLoop, ContextManager, InstructionFiles, Hooks), handoffs explícitos, agentes Criativo, PO e Arquiteto com artefatos e validações no domínio.
  • FASE 4 — CONCLUÍDA: agentes de execução (devs dinâmicos por módulo em worktrees isolados, QA e SecOps como gates de PR, Infra propositivo), trava de merge protegido, painel do time ao vivo; Psicólogo real e Anamnese com loop fechado. Gates validados por execução real (ADR 0020).
  • FASE 5 — CONCLUÍDA: imagens de produção non-root e CI (ADR 0024), Kubernetes com Kustomize, HPA por fila do Oban, NetworkPolicies e ESO (ADR 0025), graceful shutdown + OpenTelemetry + dashboards (ADR 0026), backup com restore testado, runbooks e hardening (ADR 0027).
  • FASE DOC — CONCLUÍDA: docs/ como fonte única com Diátaxis, site Docusaurus em website/ lendo de docs/, referências geradas, docs/.docmap.yml + drift check (docs:check) no CI, camada de comunidade, MIT verificado, mecanismo documentado em docs/explanation/documentation-workflow.md.
  • FASE 6 — CONCLUÍDA: política de branches mecanizada (ADR 0030) — pr-police, approval-ladder com os dois modos, promote/tag-release com versão calculada e âncora por árvore, backmerge gate com retropropagação automática, rulesets versionados. Esteira exercitada de ponta a ponta (v0.1.0 → v0.2.0) e a cadeia de hotfix validada por execução real.
  • FASE 7 — CONCLUÍDA: auth first-party substituindo o Keycloak — argon2id, access Ed25519, rotação de refresh com revogação de família, lockout progressivo, tokens de conta e MailSender log-only (ADR 0031, RN-030..033); corte atômico com emissor próprio, RBAC intocado, /internal/* com service token, cookie httpOnly + CSRF, migração de usuários e remoção total do Keycloak (ADR 0032, RN-034/035); OpenAPI nos 23 controllers com DTOs travados por tipo, teste de tabela exigindo metadados, docs/reference/api/ gerado com manifesto de hashes (ADR 0033).
  • FASE 8 — CONCLUÍDA COM CORTE: hierarquia de agentes — áreas com lead único como contato externo, delegações internas privadas (tabela delegations, area como TEXT), veredito consolidado com rastreabilidade, falha com origem; QA como área (Lead + Automação + Performance/Segurança, contrato externo dos gates inalterado — suite da Fase 4 verde sem modificação); Infra como área com subagente Workflows gerando CI por provider; painel do time por área e Psicólogo/Anamnese mirando subagentes (ADR 0038). CORTE DE ESCOPO da época, REVOGADO pela FASE 14d: agent_areas/agent_area_members existem desde então, por projeto, com max_parallel configurável — foi a área de dev, a primeira DINÂMICA, que forçou. O que segue cortado é só o budget por área (tetos de orçamento reais continuam sendo projeto, sessão e task). A lista de apps/web/src/lib/agents.ts sobreviveu para cor, ícone e a REGRA de endereçamento; os membros de dev vêm da tabela.
  • FASE 9 — CONCLUÍDA: suite de contrato de LLMProvider rodando contra os existentes, base OpenAICompatibleProvider sobre node:http com timeout de INATIVIDADE, erro normalizado por code, capabilities em duas camadas (ADR 0041); sync de catálogo com modelo descoberto entrando desativado, modelo sumido marcado em vez de apagado, preço congelado em token_usage com auditoria append-only (ADR 0042); ModelPicker reagrupado por origem com curadoria. Os seis providers da 9b entraram pela Fase 11 (ADR 0043).
  • FASE 10 — CONCLUÍDA: Bitbucket + GenericGitProvider entregues VIA dogfooding (primeira execução real do Brabo construindo o próprio Brabo, em fork com seed manual, tandas com restart entre tasks, Criativo obrigatório na cadeia). Colheita escrita na Fase 12d, em docs/explanation/primeiro-dogfooding.md — os 17 achados com arquivo:linha são reais; a metade QUANTITATIVA (restarts, intervenções, custo) ficou como não medido, porque a tabela de observação nunca foi preenchida. Os três P1 de operabilidade foram fechados pela Fase 12. Os outros 14 seguem abertos e listados na colheita — não corrija de passagem.
  • FASE 11 — CONCLUÍDA: os seis providers da Fase 9b como config sobre a base, cada um investigado do zero contra a doc oficial (proibido herdar quirk); LLM_PROVIDER_NAMES de 3 para 9; DTO de credencial e testador de conexão derivando de lista única; capability só declarada quando provada, com duas reversões ao vivo (DeepInfra e Vultr); único hook novo na base (parseErrorFrame, +31 linhas) provado necessário pelo OpenRouter (ADR 0043). Pendência dos smokes com credencial real: absorvida pela FASE 13.
  • FASE 12 — CONCLUÍDA: operabilidade pós-dogfooding, os três achados P1 fechados e provados numa execução única (ADR 0047). 12a — adoção de repositório existente: rota própria, getRepo validando acesso antes de gravar, origin (created|adopted) nas duas tabelas, e o PLANO como portão — dry-run que descreve a divergência e não altera nada enquanto a decisão for nula; readotar converge (ADR 0044, RN-045/046). 12b — reagendamento do dev agent por evento: máquina de estados persistida (working|awaiting_gate|idle|idle_tripped) reagindo a dois eventos da outbox existente, awaiting_gate retendo o worktree (que é por AGENTE, não por task) até o gate terminar, e circuit breaker com rearm explícito (ADR 0045, RN-047). 12c — promoção de story com autoridade do usuário: story_promotion por projeto com manual como DEFAULT NOVO (backfill dirigido põe os projetos existentes em auto), proposed_ready como proposta e não estado, promoção reusando o TransitionStoryUseCase (código morto do achado #13) e recusa devolvendo ao PO com o motivo fixado na sessão dele (ADR 0046, RN-048). 12d — o Noop entrou na máquina de estados da 12b; script pnpm --filter api validacao:fase-12 que sai != 0 quando o critério não fecha e extrai a evidência do banco; colheita da Fase 10 escrita; docmap cobrindo engine/dev e engine/agents. Pendências (execução da validação; limites Local/Noop declarados): absorvidas pela FASE 13.
  • Não refatore o que está pronto sem pedido explícito.

FASE 13 — CONCLUÍDA em 2026-08-07 (provar de verdade e triar)​

Nenhuma feature nova. A fase fecha as pendências declaradas, prova o que a validação Local/Noop declaradamente não prova, e transforma os 19 achados abertos em plano priorizado. Lição incorporada: a tabela manual da Fase 10 nunca foi preenchida — nesta fase TODA métrica é extraída por script do event log/token_usage, nunca anotada à mão.

13a — Fechar as pendências declaradas​

  1. CONCLUÍDO em 2026-08-07: pnpm --filter api validacao:fase-12 sai 0 e a tabela de event ids está em docs/explanation/validacao-fase-12.md (o TODO(humano) saiu). Custou quatro correções — três do INSTRUMENTO (a cobaia nascia em /tmp, invisível ao engine; o Noop não abria o gate; o Noop morria em pr_settled) e uma do PRODUTO (achado W: o dev agent morria quando a fila esvaziava).
  2. PARCIAL em 2026-08-07: o smoke do OpenRouter rodou com credencial real e fechou (custo real US$ 0,000005, lido do token_usage). Os outros cinco seguem pulados — não há chave deles no ambiente. O smoke estava apodrecido contra o ADR 0049 e nunca tinha rodado; corrigido junto. Resultado datado em docs/explanation/aceite-providers.md

13b — CONCLUÍDA em 2026-08-07 (v2.4.0)​

A cadeia inteira provada contra GitHub REAL (daneiel/test), em dez execuções: adoção remota → plano com decisão do usuário → promoção manual de UMA story → dev agent real escrevendo código → commit, push e PR REMOTA → gate abre → área delega e dispensa com justificativa → subagente SUSPENDE em aprovação → a recusa do usuário RETOMA o laço → veredito changes_requested por LLM. Zero restart do engine. Merge fora, por desenho (RN-014). 3. O roteiro é pnpm --filter api validacao:real, com fases separadas por CUSTO (--ate adocao|backlog|execucao) — a barata roda sozinha primeiro, e foi assim que erros de configuração apareceram antes de custar dinheiro. Exige execução DE DENTRO do container da api: a política é arquivo em volume compartilhado, e pelo host ela nasce num filesystem que o engine não enxerga. 4. pnpm --filter api medir:execucao é o instrumento, e está PROVADO: reproduz sozinho os números que o dogfooding anterior anotara à mão (18 chamadas, 292.211 tokens, ~US$ 0,03). 5. Resultado em docs/explanation/validacao-real.md, com as dez execuções e o que cada uma ensinou.

SETE P1 fechados no caminho, todos achados por EXECUÇÃO e nenhum por teste: W (dev agent morria com a fila vazia), Y (a busca não distinguia vazio de não-encontrado), AA (credencial de git resolvida por quem clica, não por quem paga — RN-082), AB (gate chamava decisão pendente de falha de infra — ADR 0057), AC (redirecionamento tornava qualquer comando inaprovável), o ReDoS no escopo de caminho (CodeQL HIGH), e a CVE do postgrex.

Dos achados que a fase abriu, X fechou depois (FASE 14d, RN-085 — o teto de iterações virou por TIPO de agente). DOIS seguem abertos, e o argumento deles vale mais que os números: Z e AD (o allowlist de verbos NÃO converge: verbo, forma e invocação são espaços distintos, e as execuções 6/7/8 travaram em um de cada), e AE (o agente de QA tenta consertar o código que julga, contra o próprio prompt — contido por duas barreiras independentes, allowlist e escopo). Nenhum dos dois é bug a corrigir: são decisão de produto, e estão em docs/explanation/achados-execucao-real.md.

A conclusão que a fase entrega, e que importa mais que a PR: o caminho para autonomia NÃO passa por afrouxar política. Passa por o agente ESPERAR a decisão em vez de morrer — o que o ADR 0057 fez para os gates, estendendo o 0052.

13c — CONCLUÍDA: a triagem virou docs/explanation/backlog.md​

Os 19 achados estão fechados (19 de 19). O documento segue vivo e recebeu os achados novos da 13b. 6. Sessão de triagem: ler docs/explanation/primeiro-dogfooding.md e classificar os 19 achados em P1/P2/P3 com proposta de agrupamento em fases coesas (por tema e dependência entre eles), custo relativo estimado (P/M/G) e risco de esperar. A saída é PROPOSTA — a decisão de prioridade é do usuário. 7. Consolidar o backlog completo num documento vivo (docs/explanation/backlog.md ou equivalente): achados triados + itens antigos — budget por área (ADR 0038; o aparato de áreas e o Dev Lead saíram do backlog com o ADR 0053, implementado pela FASE 14d), handoff manual a agente à escolha, MFA/social/OIDC/federação (ADR 0031), SMTP real no MailSender, deploy (DEPLOY_ENABLED + Environments), volta da rc/rcfix (ADR 0030), modo community do approval-ladder, "N agentes online" no dashboard, preferência de moeda com taxa manual.

FASE 14 — CONCLUÍDA em 2026-08-07 (o que a execução real exigiu)​

A execução do hello world não passou do primeiro turno e revelou defeitos que nenhuma suite pegava, porque tudo antes rodou com modelo LOCAL. As correções já entraram; o que sobra é feature decidida pelo usuário durante a execução.

14a — Operabilidade dos agentes (CONCLUÍDA)​

  • Falha de turno virou agent.error DURÁVEL com origem, e o agente FALA no fio; os quatro conversacionais paravam de gravar agent.response vazio (RN-059). O caminho do erro narrado no frame final, que não emitia evento nenhum, também fechou.
  • O chat do Criativo abre com convite em vez de vazio: ele é ativado e espera o usuário falar primeiro, e a tela agora diz isso — incluindo que tecnologia e código NÃO são dele.
  • A chave que o agente gasta é a do OWNER do workspace (RN-058). Antes o turno procurava a credencial pelo SLUG do agente numa coluna UUID: nenhum agente jamais usou provider com credencial, e só ollama funcionava.
  • Relatório de gasto das chaves, só para o owner, com agente separado de pessoa (RN-060).
  • Bootstrap de Gitflow no GitHub: repo recém-criado responde 409 em toda a Git Data API, e o primeiro commit passa pela Contents API. O backend falso dos testes mentia (404 onde o GitHub responde 409) — corrigido junto, é o que fazia a suite ficar verde com o produto quebrado.

14b — Linha do tempo em árvore (CONCLUÍDA)​

Um ramo por agente, do primeiro marco ao que ele está fazendo AGORA, derivado 100% do event log que a tela já busca. O feed cronológico continua, na coluna de atividade: ele responde "o que aconteceu", a árvore responde "quem está fazendo o quê".

Revisitado depois da FASE 26 (sem fase nova, mesmo espírito de correção pontual da 12d/PÓS-FASE 15): o critério de abertura padrão passou de só "ativo/parado" para os 5 agentes de atividade mais RECENTE — ativo continua com prioridade (RN-118); cada ramo colapsado ganhou contador de NOVIDADE próprio, com "último visto" por agente em read-state.ts (mesmo mecanismo do sino, agora granular); e tool.call/tool.result/agent.response passaram a expandir INDIVIDUALMENTE (args, resultado, iteração), agrupados por fronteira de iteração do ToolLoop.

14c — Validação automática de UI (CONCLUÍDA)​

Contraste virou teste sobre design/tokens.css, com a dívida conhecida medida e travada; layout (texto cortado, menu fora da viewport, dropdown recortado por ancestral, alvo < 24px) virou verificador de navegador em scripts/dev/validacao-visual.js. Sem dependência nova.

14d — CONCLUÍDA: paralelismo decidido pelo LEAD, autorizado por você​

Entregue em quatro PRs (#181, #182, #183, #184). O ADR 0053 passou a ACEITO, revogando de vez os três cortes (Dev Lead, áreas dinâmicas via module_map e o aparato genérico de áreas).

  1. Áreas viraram dado: agent_areas/agent_area_members por projeto, com max_parallel default 2. O que forçou foi a área de dev — a primeira DINÂMICA, cujos membros são um por módulo do module_map. O que não é enumerável em código tem de ser dado.
  2. O teto é da SESSÃO, não do módulo (RN-083). Contar por módulo permitiria N módulos × 2 sem autorização nenhuma — o buraco anterior com outro nome. Acima do teto vira proposed_action.
  3. Configurável por área em Configurações, exigindo maintainer pelo mesmo motivo de ativar execução: mudar o teto é decidir quanto o produto gasta sem perguntar.
  4. A Anamnese propõe subir quando autorizar virou rotina — três aprovações e NENHUMA negação (RN-086). Uma negação derruba o sinal: se você recusou alguma vez, o teto está fazendo o trabalho dele.
  5. O Dev Lead existe (RN-087), conversacional, recebendo o handoff do Arquiteto na mesma confirmação que já entrega ao Infra, e propondo o plano: quantos agentes por módulo e por quê. Os dev-<modulo> deixaram de ser endereçáveis por handoff — a regra do ADR 0038 passando a valer para o dev como já valia para QA e Infra.

O achado X foi fechado aqui (RN-085): TOOL_LOOP_MAX_ITERATIONS virou teto POR TIPO — 8 conversacional, 60 execução e gate. O critério de quem sobe NÃO é "quem trabalha muito": é quem tem token_budget_micros por baixo segurando o gasto. infra-workflows usa ferramenta pesada e fica em 8, porque roda sem budget.

Três lacunas da própria fase, achadas por mim ao escrever a fatia seguinte, e todas a mesma falha: a regra do teto entrou com testes verdes e o produto não a exercitava — nenhuma rota chamava o caso de uso, aprovar a ação não executava nada, e o tipo podia ser auto-aprovado por permissions.json (fechado pela RN-086). Testar a peça não é testar o caminho até ela.

Fora do escopo, por decisão declarada (ADR 0053 item 5): o botão "Ativar execução" mudar de dono. A delegação Dev Lead → dev-<modulo> pela tabela delegations com area = "dev", que este mesmo item também declarava, DEIXOU de estar fora de escopo — fechada pelo ADR 0094 (Auditoria fluxo.yml × código, Onda 2, RN-405). O botão continua reversível; a execução continua no caminho atual, e a correção pós-gate continua indo direto ao dev que abriu a PR.

FASE 15 — CONCLUÍDA em 2026-08-07 (gates como dado)​

Nenhum gate NOVO. A fase extrai para docs/gates.yml os gates que JÁ existem implícitos no produto, com verificação por script e severidade, no mesmo espírito do docs/.docmap.yml. Gate de agente que não existe (dev-lead, platform) entra como status: planned referenciando o backlog — nunca ativo. O contrato externo dos gates NÃO muda.

15a — CONCLUÍDA: registro declarativo​

  1. docs/gates.yml com os gates existentes (ver ADR 0054, que estende o ADR 0048 — a decisão no event log é o que torna a passagem de um gate mensurável), schema validado por teste.
  2. Loader na api lendo o registro; endpoint interno de leitura.
  3. Script pnpm --filter api validacao:gates que extrai do event log a última passagem de cada gate ativo e sai != 0 se algum gate block não tem evidência de verificação por script.

Nem todo gate tem prova no event log, e o registro diz onde ela mora: merge-protegida é garantido por TESTE (a trava em decide.ts não emite evento próprio) e backmerge é CI, com estado em .release/gate.json. Por isso cada gate declara evidencia: event_log | teste | ci com o localizador — rebaixá-los a warn seria mentir sobre as travas mais duras do produto.

15b — CONCLUÍDA: consumo​

  1. Painel do time exibe o gate que cada story/PR está aguardando, derivado do registro (não hardcoded).
  2. docs/explanation/gates.md explicando o mecanismo, no docmap.

PÓS-FASE 15 — CONCLUÍDO em 2026-08-08 (v2.5.0 e v2.5.1)​

Não é fase planejada: é o que o USO encontrou depois da 15, e está aqui porque a origem de cada item importa. Nada disto veio de roteiro — veio de navegar a app no Chrome e de ler o painel de segurança.

Defeitos de UI, achados navegando (RN-088..091):

  • 429 virava TELA BRANCA e a app respondia com mais tráfego. A regra virou geral: toda tela distingue TRÊS estados — carregando, erro (com trace_id e botão) e vazio. if (!dado) return null colapsa os três.
  • Dashboard fazia uma requisição por projeto: 3.824 → 12 req/min, com GET /workspaces/:id/projects-summary. O sino era a metade que faltava: 289 → uma requisição por ciclo (RN-090/091).

Endurecimento de segurança, com os dois painéis a ZERO:

  • ADR 0058 — CSP fechado na api (default-src 'none'), revisando o item 7 do ADR 0027; e o projectId contido na raiz (RN-092), que atingia não só o permissions.json como o escopo que AUTORIZA terminal.
  • ADR 0059 — GIT_OAUTH_STATE_SECRET sem default em produção (RN-093). O default era PÚBLICO (.env.example) e o compose de produção o supria como fallback: exigir "não vazia" não teria pego nada, porque no caminho real de erro a variável estava DEFINIDA.
  • Alerta que não se sustentava foi DISPENSADO com justificativa escrita, nunca silenciado — e dispensar é decisão do usuário, não do agente.

Três lições operacionais que custaram ciclo de CI, e valem como regra:

  1. CodeQL verde numa PR não prova que alerta antigo fechou — o check de PR reporta alerta NOVO. Alerta fecha na varredura do branch DEFAULT, isto é, só depois de a correção chegar em main.
  2. Gitleaks varre commits alcançáveis de refs/heads/*, não a árvore. Um valor de alta entropia commitado uma vez reprova o CI para sempre naquele branch; corrigir por cima NÃO limpa. A saída é o commit deixar de ser alcançável (reconstruir a partir de dev e APAGAR o branch antigo ANTES de abrir a PR nova) — nunca allowlist, que cega o scanner naquele caminho para todos os commits. Refs de PR não entram no escopo. Fixture que representa segredo nasce SEM entropia.
  3. Barreira que mora em outra função o CodeQL não enxerga — daí os três js/path-injection sobreviverem à correção. Manteve-se a checagem centralizada (RN-092) e pagou-se o preço no painel: duplicá-la em cada chamador seria checagem que um dia diverge.

PROGRAMA 16–26 — o que a navegação pediu (em execução)​

Onze pedidos nascidos da PRIMEIRA navegação real na app depois do reset do banco. Não é roteiro: é uso. Três descobertas da investigação definiram o tamanho e a ORDEM do programa, e vale registrar cada uma, porque nenhuma estava no pedido original:

  1. Existe um handoff de design COMPLETO em design_handoff_brabo/ — 8 telas de alta fidelidade, tokens, tipografia e marca. A decisão foi adotá-lo INTEIRO e PRIMEIRO: assim os onze pedidos nascem no visual novo em vez de serem feitos duas vezes.
  2. O terminal da aba Code virou decisão de ARQUITETURA. Cada projeto passa a ter container próprio, e é o ARQUITETO quem decide a imagem. Dentro dele o agente é livre; git push, PR e deploy continuam humanos. Isso paga a maior dívida aberta do produto — hoje o agente executa no MESMO container que o monorepo do Brabo, e o ADR 0055 diz de si que é política, não isolamento.
  3. Dois defeitos reais apareceram no caminho, e nenhum foi pedido: agent_areas NUNCA é gravada (upsert sem chamador, a API devolve []), e a gaveta do sino ordena seq ASC no SQL — mostra os mais ANTIGOS.

Teto de execução: no máximo QUATRO subagentes em paralelo. As fases foram cortadas por ARQUIVO DISPUTADO, não por tema — é o que torna o paralelismo possível. O limitador do backend é apps/api/src/db/migrations/meta/_journal.json e os snapshots do drizzle: UMA migration por onda, porque resolver conflito de snapshot à mão é caminho para schema divergente. Faixas de RN e números de ADR são PRÉ-ALOCADOS antes de a onda começar: duas fases paralelas escrevendo RN-094 fazem uma renumerar no merge, e RN renumerada quebra os links #rn-0xx que o pnpm docs:check reprova.

FASE 16 — CONCLUÍDA: fundações (destravar o paralelismo)​

Nenhum dos onze pedidos entrega aqui. A fase existe porque três deles tocam os MESMOS quatro pontos de router.tsx/ProjectPage.tsx, e outros três esbarram na falta de peça comum. Sem ela, as ondas colapsam para execução serial.

  1. design/tokens.css recebe --violet (agentes/IA) e o que mais o handoff define. As fontes CONTINUAM self-hosted — seguir o handoff aí quebraria a app sob o CSP do nginx (ADR 0036). O teste de contraste cobre os pares novos, e a dívida conhecida de 4 pares segue travada.
  2. A aba deixa de ser lista fechada em DOIS lugares: registro único de onde PROJECT_TABS, type TabKey, a régua e o render passam a derivar. Teste que reprova chave num lugar e ausente no outro — é o defeito real.
  3. Disclosure no design system, com a semântica da implementação mais completa que já existe (ModelCatalogSection). COMPONENTE SÓ: nenhuma das seis implementações ad-hoc migra aqui, senão a fase abriria os três arquivos mais disputados do programa.
  4. Rótulo de sessão vira helper e os cinco slice(0, 8) inline migram. É esta migração que impede a FASE 20 de colidir com a 19.
  5. CLAUDE.md entra na definição de pronto (ver a seção de Documentação), com regra warn no docmap. ENTREGUE: a regra é claude-md em docs/.docmap.yml, disparada por package.json, pnpm-workspace.yaml, apps/*/package.json, apps/engine/mix.exs, docs/adr/*.md e docs/gates.yml — a frase "hoje ele tem ZERO cobertura" que estava aqui descrevia o estado ANTES da fase e ficou mentindo depois dela.

FASE 17 — CONCLUÍDA: as 8 telas conforme o handoff​

Fidelidade visual ANTES do comportamento, para não refazer trabalho. Nenhuma regra de negócio muda: se uma tela precisar de dado que não existe, isso vira pendência declarada, não feature de carona. 6. Login e App/lista de projetos. 7. Projeto e Sessão, preservando os três estados da RN-088 (erro antes de vazio). 8. Aprovações e Configurações — Aprovações é a base visual da FASE 19. 9. Prova de que a fidelidade aconteceu: contraste medido sobre os tokens e layout verificado por scripts/dev/validacao-visual.js em TODAS as telas tocadas. O .dc.html é referência, NÃO código para copiar — o próprio README do handoff diz isso.

FASE 18 — CONCLUÍDA: a área existe no banco (defeito, corrigido antes)​

  1. AgentAreaRepository.upsert não tem NENHUM chamador, então quatro casos de uso operam sobre tabela vazia. Provisionar na criação do projeto + backfill, com teste que prova que projeto recém-criado TEM áreas — é a mesma falha da FASE 14d: testar a peça não é testar o caminho até ela.
  2. Colapsar as TRÊS cópias da lista de áreas (api, web, engine) em uma fonte.

FASE 19 — CONCLUÍDA: aprovação que se lê​

  1. Matar o fallback genérico do ApprovalCard, que despeja chave: JSON.stringify(valor) — a causa provável do "difícil de ler". Todo tipo ganha FRASE em pt-BR; tipo sem frase mostra verbo + "ver detalhes" e o payload cru nasce COLAPSADO, nunca despejado.
  2. Colapso nos TRÊS lugares: Aprovações, Insights e o card no chat, com verbo e frase saindo de UM módulo.
  3. Restrição de projeto: o colapso NÃO introduz prop nova obrigatória em ApprovalCard. É isso que mantém SessionPage.tsx intocado e tira a aresta com a FASE 20.

FASE 20 — CONCLUÍDA: a sessão ganha identidade​

  1. sessions ganha kind e name na MESMA migration — duas migrations sobre a mesma tabela colidem no journal e nos snapshots.
  2. Reconciliar com a derivação por evento: kind classifica a INTENÇÃO de criação, execution.activated continua classificando ESTADO de execução, e nenhum reescreve o outro. execution.activated em sessão consultiva é erro explícito, não conversão silenciosa.
  3. Renomear preservando a hashtag; sem nome, degrada para ela sozinha.
  4. Botão de voltar ao dashboard — hoje SessionPage.tsx não importa Link nem useNavigate, e NENHUMA navegação sai da tela. Destino revisto numa rodada posterior: o botão volta ao PROJETO da sessão (/projects/:projectId), não mais ao dashboard raiz — a sessão sempre nasce dentro de um projeto.

FASE 21 — CONCLUÍDA: o volume de eventos (RN-099/100)​

  1. useSessionEvents continua sendo o ESTADO ATUAL (latest, quatro consumidores: roster, árvore, execução, Aprovações). O HISTÓRICO virou useSessionEventHistory, só para as Atividades, com o afterSeq que o endpoint já devolvia. Nenhuma rota nova, nenhum parâmetro novo. A âncora é a CAUDA, e não o começo da sessão: o endpoint pagina para frente e não existe beforeSeq, mas abrir o feed no evento nº 1 de uma sessão de milhares entrega a tela errada. Então a primeira página é a mesma leitura latest (MESMA queryKey, deduplicada) e cada clique desce uma janela fixa para trás.
  2. O sino ordena DESC no SQL. O corte de "lido" NÃO mudou de semântica, e por isso não há ADR: um corte por seq marca um PREFIXO e a gaveta mostra um SUFIXO, então "marcar as 50 exibidas" é inexprimível sem tabela de lidos por evento. O que mudou é a gaveta parar de esconder o que o avanço engole — total por projeto, + N mais antigos, e o botão dizendo quantas marca. O número que falta sai de SUBTRAÇÃO (latestSeq menos o corte), não de requisição.
  3. A economia da RN-090/091 não regrediu: uma requisição por ciclo, e o primeiro "carregar mais antigos" custa ZERO — a leitura latest traz 200 e a janela mostra 100. Página antiga tem staleTime: Infinity e nenhum refetchInterval: janela fechada de seq sobre evento imutável não muda.

FASE 22 — CONCLUÍDA em 2026-08-09 (gasto com duas audiências)​

Entregue como a aba Gastos (ADR 0063, RN-101). 22. As AGREGAÇÕES que faltavam entraram num método só — sumGroupedBy(dimensao, escopo) com model, project, actor, session e day. provider NÃO é dimensão dele, e a ausência é estrutural: quebrar por provider é quebrar por CREDENCIAL, e é isso que impede a visão do membro de ganhar o eixo por descuido — não há argumento a passar. 23. Sem lib de gráficos: barras diárias e ranking em SVG inline. A série diária vem DENSA da api (dia sem gasto entra com zero), senão três chamadas em três semanas viram três barras coladas. 24. A colisão foi resolvida separando as PERGUNTAS, não os filtros. O relatório por CREDENCIAL segue exclusivo do owner (RN-060) e ganhou ao lado a quebra do workspace por modelo/projeto/ator/dia, também owner. A visão do membro é por ATOR, em tokens e custo estimado, sem provider e sem credencial — e o ator sai do TOKEN autenticado, sem parâmetro onde escrever o id de outra pessoa. Agente não entra na conta do membro: token_usage registra quem gastou, não quem mandou gastar.

Sem migration, mas COM medição: a 525 mil linhas as consultas saem em 55 ms e 38 ms por seq scan, e um índice em token_usage(created_at) as levaria a 32 ms e 19 ms. O número está no ADR; o índice entra na onda que tiver o slot.

FASE 23 — CONCLUÍDA em 2026-08-09 (modelo herdável por área)​

  1. Escopo area entrou na cascata (sessão > agente > área > projeto > workspace), ENTRE agente e projeto: é o padrão que lead e subagentes compartilham, e o agente diverge sobrepondo-o (ADR 0064, RN-102).
  2. A incoerência resolvida ANTES de codar: o binding de agente era GLOBAL (scope_id = slug puro, :projectId da rota ignorado desde a Fase 9a) e área é por projeto desde o ADR 0053. Escopo por projeto acima de escopo global faria o mesmo agente resolver modelos diferentes só onde houvesse área. Decisão: o binding de agente passou a ser por projeto também — scope_id de agent/area virou composto, <projectId>:<slug|chave> (RN-103) — e não a área abaixo do agente, que contrariaria "padrão herdável" (quase todo binding hoje é de agente).
  3. A UI mostra quem HERDA e quem DIVERGIU (AreaModelsSection e a coluna Origem de ModelsSection, em ProjectSettingsTab.tsx); "voltar a herdar" é DELETE no binding do agente/área, nunca grava nele o modelo do nível de baixo — copiar viraria cópia que diverge sozinha na próxima mudança da área. Papel maintainer para mudar o modelo da área (mesma régua do teto de paralelismo, RN-083); o do agente continua developer.

FASE 24 — CONCLUÍDA: Chat e Criativo como lugares (RN-104)​

  1. Duas abas na tela de PROJETO, cada uma listando as sessões do seu kind e criando naquele kind sem perguntar de novo. A Sessão continua tela própria — a aba não virou contêiner de chat.
  2. A colisão foi resolvida com "Sessões" SAINDO da régua. O que ficou dela é a CHAVE de deep-link: o Chat carrega key: 'sessions', e é isso que faz um ?tab=sessions antigo abrir no Chat com a aba MARCADA. Alias resolvido só no painel abriria o painel certo e deixaria a régua sem seleção, porque Tabs compara active com key e quem escreve active é o ProjectPage — um dos dois arquivos que a onda manteve fechados.
  3. Veredito sobre "Iniciar ideação": ele FICA. Metade do problema já tinha fechado na FASE 20 (o botão sumiu da sessão consultiva, RN-097), e a outra metade não era redundância: é ele que traz o Criativo, e é daí em diante que a chave do owner é gasta (RN-058). O que estava errado era o LUGAR — o convite gastava um parágrafo apontando para a topbar. Agora a ação mora DENTRO do convite enquanto ele está na tela, e volta à topbar quando não está; conviteVisivel é a única pergunta que os dois compartilham, para que não apareça em dobro nem desapareça.

FASE 25 — CONCLUÍDA COM CORTE: Container por projeto (a fronteira deixa de ser só política)​

A maior mudança arquitetural do programa, e a que paga a dívida que as Fases B e F já apontavam separadamente. Entregue 25a (a decisão do Arquiteto e o portão) e 25c (a fronteira de efeito externo); 25b (o ciclo de vida do container) ficou CORTADA e declarada — não meio-implementada (ADR 0065, RN-105 e RN-106). 30. CONCLUÍDO: o ARQUITETO decide qual imagem sobe para o projeto, como artefato dele (artifact.project_image no event log, versionado, sem tabela) — auditável, não configuração escondida. Enquanto ele não decidir, a aba Code responde 409 e não libera (RN-105). 31. NÃO ENTREGUE À ÉPOCA, declarado: ciclo de vida por projeto (provisionar, reciclar, limpar, teto de recursos aplicado de verdade), com o worktree do agente vivendo dentro do container. Estado de container é MUTÁVEL e pedia tabela própria — improvisá-lo no event log só para não esperar o slot de migration produziria a correção logo depois. PARCIALMENTE REVOGADO pela Onda 4/frente F1 do PROGRAMA 28: a TABELA de estado (project_containers, ADR 0081) chegou. O que continua faltando, sem atenuar, é a metade que fazia a tabela valer alguma coisa: nenhum orquestrador real chama Docker ainda, e o worktree do agente segue fora do container. A metade "dentro o agente é livre" da política de terminal AINDA NÃO mudou — o ADR 0055 continua valendo como está até o container subir de verdade. 32. CONCLUÍDO À ÉPOCA, REVISADO pelo runner local (ADR 0102/0103, RN-418): a fronteira de efeito externo. git push, PR e deploy não saem pelo terminal — nem dentro do escopo do projeto —, e a regra ERA deny (não require_approval, por causa do "sempre permitir"), com a mensagem redirecionando para a ação TIPADA (git_push/git_merge/ pr_open), que nasce proposed_action (RN-106, revisada). O deny virou TETO ABSOLUTO (require_approval incondicional, nunca auto-aprovável) — decisão do dono do produto, "sempre permitir" fechado na fonte para não virar decorativo (ver a seção do runner local). Merge em protegida segue manual (RN-014), intocado. Rede e gasto têm veredito próprio no ARTEFATO da imagem (network: none/egress, teto de cpus/memória/pids que recusa em vez de rebaixar em silêncio) — decidido UMA vez, não comando a comando, pelo mesmo motivo que Z e AD provaram que allowlist de verbo não converge. O fechamento de Z e AD em si depende de 25b (a parede física), que ainda não subiu.

FASE 26 — CONCLUÍDA: Code, só leitura​

  1. CONCLUÍDO (26a): GitProviderContract ganhou listTree e diff de PR (11ª/12ª operação), capability declarada SÓ quando provada pela suite.

  2. CONCLUÍDO (26b): as quatro rotas de code.controller.ts — árvore, arquivo, busca e diff — atrás da checagem CENTRALIZADA da RN-092/095, com teto e cache na busca (que é COMPOSTA, não operação do contrato).

  3. CONCLUÍDO: a tela (ProjectCodeTab/code/*), registrada em project-tabs.ts sem tocar router.tsx/ProjectPage.tsx. Rail (Explorador/Buscar — os únicos com dado real), explorador carregado por DIRETÓRIO, abas de editor com breadcrumb, busca real com filesScanned/truncated, diff de PR por id conhecido (com patch: null tratado como "sem texto", nunca "sem mudança"). Realce de sintaxe é tokenizer PRÓPRIO por regex (code/highlight.ts) — ZERO dependência nova, contra os 15-90 KB de Prism/highlight.js/Shiki para o que a aba precisa; três tokens novos de cor (--syntax-function/ -comment/-operator) calibrados a 4,5:1 contra --code-bg nos DOIS temas. O quarto estado da RN-088 — bloqueada por decisão pendente do Arquiteto (RN-105) — virou RN-107, perguntado ANTES de tentar ler código, com reconsulta própria a cada 15s enquanto bloqueada.

    Pendências DECLARADAS, sem dado real por trás: terminal interativo (FASE 25b, que segue cortada — estado vazio honesto na aba), blame, lista de PRs dentro da aba (o diff só é alcançável por id conhecido, vindo de Aprovações) e painel de Problemas/lint/testes. Virtualização de linha também ficou de fora à época — o próprio handoff chama a aba de código "a mais custosa do programa"; o teto de 512 KB por arquivo (GIT_BLOB_MAX_BYTES) limitava o pior caso até então. FECHADA pela Onda 4/frente E2 do PROGRAMA 28 (RN-239..242): só a janela visível vira nó de DOM, e o minimapa reusa a mesma tokenização sem segundo passe.

FASE 26b — CONCLUÍDA: fundação de blame, PRs navegáveis e branch rica​

A fase começou como SÓ a camada de API, e a frase "nenhuma UI" que ficou aqui descrevia o estado da PRIMEIRA leva — envelheceu e passou a ensinar errado. As TRÊS UIs existem: o dropdown rico de branches (item 38, CodeBranchPicker.tsx), o toggle de blame sob demanda (CodeEditor.tsx, RN-113) e a lista de PRs dentro da aba (CodeDiffPanel.tsx), as duas últimas entregues na v3.0.0. As três pendências declaradas de blame, dropdown rico de branches e lista de PRs tocariam os MESMOS arquivos (contrato, os três providers, o caso de uso, o controller, a suite de contrato) se atacadas separadamente por agentes em paralelo; a decisão foi entregar as três juntas, de uma vez, e deixar a UI de cada uma para três agentes seguintes sem risco de colisão. 36. GitProviderContract ganhou blame, listPullRequests e listBranchesDetailed (13ª, 14ª e 15ª operação — RN-110/111/112), capability declarada só quando provada pela suite de contrato contra os três providers (local contra git de verdade; github/gitlab mockados — os smokes reais seguem pulados sem GITHUB_TEST_TOKEN/GITLAB_TEST_TOKEN no ambiente, mesma situação da FASE 13a). listBranchesDetailed é operação PRÓPRIA, não extensão de listBranches (RN-112) — enriquecer custa uma chamada extra ao provider POR BRANCH, e listBranches continua sendo a do bootstrap de Gitflow, que não paga esse custo. 37. Três rotas novas em code.controller.ts (GET .../code/blame, GET .../code/pull-requests, GET .../code/branches), mesmo role: viewer e a mesma checagem central de caminho (RN-095) das quatro anteriores. apps/web/src/lib/api-client.ts/api-types.ts ganharam as funções e tipos correspondentes (getCodeBlame, getCodePullRequests, getCodeBranches) para a onda seguinte consumir — CodeShell.tsx e CodeDiffPanel.tsx NÃO foram tocados além do comentário que documenta que a fundação já existe. 38. CONCLUÍDO: o dropdown rico de branches (RN-112) — CodeBranchPicker.tsx, aberto a partir de CodeShell.tsx, troca o campo de texto simples por um seletor listando cada branch com ahead/behind relativos à default e a PR associada, quando houver (aberta/mesclada/fechada). Ref fora da lista (tag ou sha) continua alcançável — listBranchesDetailed não enumera essas duas, então o rodapé do dropdown tem um campo manual.

Congelamento do programa: cada fase declara o que não faz, e o mais duro é o da 26 — SÓ LEITURA de código, nenhum salvamento pela aba. A edição é fase seguinte, e quando vier, escrita é efeito externo: nasce proposed_action.

PÓS-PROGRAMA 16–26 — o que o uso pediu depois (RN-148)​

Mesmo espírito da PÓS-FASE 15: não é fase planejada, é o que o USO pediu. Histórias com promoção pendente ao mesmo tempo (RN-048/RN-126) viravam N cards avulsos na timeline da sessão do PO quando ele produzia várias numa leva. Carousel (apps/web/src/components/ui/Carousel.tsx) é o primeiro componente de navegação item-por-item do design system — 2+ pendentes viram UM carrossel navegável (setas, dots, teclado), com "Aprovar todas" promovendo o lote inteiro numa chamada só (promoteStories já era lote, sem mudança de contrato); 1 pendente continua o card simples de sempre. A decisão de quando uma "leva" se forma é reavaliada a cada render — o conjunto das promoções AINDA PENDENTES na sessão, não "criadas em sequência sem interrupção" — para que resolver uma história no meio do carrossel recalcule a leva sozinho, sem estado próprio a sincronizar.

Projeto Local ou Container (RN-169/RN-170, ADR 0072). Pedido do dono do produto: "cruzar a fronteira de apenas escrever código no container e poder escrever código a partir de uma pasta do usuário", com a variante de caminho livre digitado escolhida por ele, ciente de que só funciona montado no container. projects ganha (workspace_mode, workspace_path) na migração 0043, com container de default — nada muda para quem não escolhe. O ADR 0072 REVISA parte do 0065 e mexe no terreno do 0055, que decidiram a direção contrária (a parede de container), e por isso a consequência está escrita sem atenuar: a contenção ESTRUTURAL do join(env, coluna) — nenhuma coluna corrompida saindo da raiz gerenciada — deixa de existir para projeto Local, e o que sobra é a guarda da criação mais a revalidação léxica na leitura. A FASE 25b continua cortada: projeto Local roda no MESMO container de hoje, só a pasta mudou.

Diagrama C4 do Arquiteto (RN-149, ADR 0068). Entregável novo: create_c4_diagram gera Context + Container (Simon Brown) em Mermaid, renderizado na Visão Geral. O Container level é DERIVADO do module_map vigente pelo caso de uso — nunca redigitado pelo modelo — para não abrir uma segunda fonte que diverge da primeira; só o Context (nome do sistema e atores externos) vem do tool call. artifact.c4_diagram é artefato sem tabela, versionado no event log, mesmo desenho do artifact.project_image (ADR 0065). mermaid entrou como dependência de RUNTIME nova do apps/web — a primeira do tipo (o site de docs já usa Mermaid, mas em build-time) — isolada atrás de apps/web/src/lib/mermaid-render.ts com import() dinâmico; vite build confirmou que só o entrypoint carrega eager e os chunks pesados do Mermaid ficam sob demanda, e nenhum deles usa eval/new Function — o CSP fechado do ADR 0058 seguiu intacto, sem mudança de configuração.

RODADA exp003 — CONCLUÍDA (RN-150..162, PRs #278–#293)​

Não é fase: são 17 pontos que o USO encontrou numa sessão de teste ao vivo sobre o projeto exp003, corrigidos em cinco ondas sequenciadas por ARQUIVO DISPUTADO — quatro delas convergindo em SessionPage.tsx, o que obrigou a serializar. Vale a origem: nenhum destes foi achado por teste.

O que a rodada ensinou, e vale mais que a lista: a maior correção (RN-155) era invisível a qualquer suite porque o defeito estava em comparar dois espaços numéricos DIFERENTES que casualmente são ambos number — o event.seq (gapless, por SESSÃO) contra o action.seq (proposed_actions.seq, bigserial global de TODA a tabela, compartilhado por todo projeto e toda sessão do sistema, e documentado no schema como contraste deliberado). A ordem só parecia certa enquanto a tabela era pequena. A correção não inventou campo: usou o evento proposed_action.created que o ProposeActionUseCase já grava na MESMA transação, com payload.actionId apontando de volta.

Engine e leitura contida (RN-150) — search_workspace devolvia TODOS os hits, sem teto: a segunda causa real do 413 em revisão de PR, depois da correção de read_file. Dois tetos INDEPENDENTES, porque a busca estoura de duas formas — quantidade de hits (SEARCH_WORKSPACE_MAX_HITS, 500, sobre um Stream que PARA de escanear em max_hits + 1) e bytes do texto final (SEARCH_WORKSPACE_MAX_BYTES, 32.768). Variáveis próprias, não reaproveitadas das outras duas: mesma classe de estouro, divergir uma não deve exigir tocar as outras. A marca de truncagem é dirigida ao MODELO (manda refinar o termo) e nunca inventa um total, porque contar o total pagaria de novo o I/O que o teto existe para evitar.

Números que mentem (RN-151) — o badge do projeto na sidebar contava QUALQUER evento não lido: exp003 mostrava 392 enquanto a aba Aprovações do mesmo projeto mostrava 8. Um número que não corresponde a nada acionável ao clicar é pior que nenhum. Virou pendingApprovalsCount no read model que já existia (RN-090), numa agregação a mais no Promise.all — nunca N+1.

Rastreabilidade do que o agente produziu (RN-152) — a branch feature/task-XXXXXXXX no dropdown da aba Code passa a dizer de qual dev agent e módulo ela é. O módulo sai do module_map vigente pelas MESMAS funções que o geraram, nunca por regex reversa sobre o nome; sem vínculo, degrada para null em vez de adivinhar.

Auto mode (RN-153/154, ver Convenções) — a curinga actionType: "*". O que importa é onde ela mora: a resolução é do REPOSITÓRIO (findMode), e decide.ts não ganhou uma linha — é isso que torna os três tetos absolutos verdadeiros por CONSTRUÇÃO, e não por mais um if que um dia alguém esquece.

O fio da sessão que se lê (RN-155..159) — ordenação (acima); o indicador de 5s com texto fixo "Reunindo informações..." em vez de interpolar o agente, que já aparece no cabeçalho (RN-156); criação de épico/história do PO virando aviso compacto em vez de bolha com o mesmo peso visual de uma resposta de verdade (RN-157); Markdown leve com highlight no chat por parser PRÓPRIO de regex, zero dependência nova e nenhum dangerouslySetInnerHTML, e só a SAÍDA do agente é renderizada — o que o humano digita fica literal (RN-158); e "Artefatos gerados" agrupado por agente com o Disclosure da RN-138, cada artefato navegando para onde ele vive (RN-159). O module_map/C4 ficou FORA por decisão registrada: é estado VIGENTE do projeto, não artefato datado por sessão.

Ordem de autoridade (RN-160/161, ADR 0069) — "Confirmar arquitetura pronta" só habilita com ao menos 1 história PROMOVIDA (não basta ter regra capturada); e aceitar o handoff pro Dev Lead encadeia a ativação de execução quando o papel efetivo de quem aceita já autoriza. A fusão é só no cliente: o backend continua exigindo maintainer (RN-137), então ela evita um clique redundante sem mover a fronteira de autorização.

Entrada estruturada (RN-162) — ask_structured_questions, do Criativo: o modelo declara as perguntas em schema, o formulário é renderizado por StructuredQuestionCard, e a resposta REUSA SendAgentMessageUseCase em vez de abrir um segundo caminho de mensagem. Responder é ato único — reenvio é 409, não sobrescrita.

RODADA exp001 — o Criativo cumpre a promessa (RN-163)​

Mesmo espírito das outras rodadas: veio do USO, não de roteiro. O relato foi "o Criativo não respondeu depois de dizer que iria corrigir e tentar de novo", e a frase era literal no código — run_turn_capturing/1 chamava o modelo UMA vez, despachava as ferramentas e voltava. O resultado da ferramenta entrava no histórico em memória e ninguém mais o lia, então a correção prometida só acontecia se o usuário mandasse outra mensagem.

O Criativo era o único conversacional sem laço de tool use; ganhou o mesmo do PO, com teto próprio de 12. O que vale além da correção: quem decide o que se anuncia passou a ser o teto, e não um texto fixo — a promessa de retentativa só entra na frase quando ainda há volta para cumpri-la. Teto esgotado virou agent.error narrado (o po_server terminava calado nesse caso, e essa era a próxima dívida óbvia — fechada logo abaixo pela RN-166), e a falha de ferramenta deixou de ser agent.response — no event log ela era indistinguível de uma resposta normal e não dizia origem nenhuma, exatamente o que a RN-059 fechou para a falha de turno.

RODADA exp001 — o PO lê o que já existe e é cobrado (RN-164..166)​

Não é fase: é um defeito achado por USO real, e a origem importa porque nenhuma suite pegaria. O backlog saiu com épico e nenhuma história — logo sem tarefa, logo a execução travada sem erro nenhum. A investigação achou quatro causas empilhadas, e as três primeiras são a mesma: o PO não tinha como saber e não tinha como avisar.

  1. O PO tinha quatro ferramentas e todas de ESCRITA. O contexto era montado UMA vez, no kickoff, a partir dos 200 últimos eventos da SESSÃO — dali em diante ele nunca mais relia nada: não sabia quais regras existiam, quais já cobrira, nem o que ele próprio já criara. Entraram listar_regras_de_negocio e listar_backlog (RN-164), servidas por duas rotas internas escopadas ao PROJETO — e o escopo é o ponto: leitura por sessão era exatamente o que escondia a regra capturada antes. Reusam o encanamento que já existia (listByTypeForProject, ListBacklogUseCase, computeCoverage); o que faltava era a rota, o método no cliente e o tool.
  2. Nada cobrava a história depois do épico e o laço terminava calado. Épico sem história virou desfecho EXPLÍCITO — backlog.epic_without_story, durável, no padrão da RN-059 (RN-165) — e o teto de iterações do PoServer passou a emitir toolloop.limit_reached, o MESMO tipo do ToolLoop, que os agentes conversacionais nunca emitiram por terem laço próprio (RN-166).
  3. A instrução de kickoff não dizia uma palavra sobre o que fazer quando falta informação — e diante de uma lacuna sem instrução um modelo escolhe entre inventar e parar. Parar foi o que ele fez. A terceira saída passou a estar escrita, com a ferramenta para exercê-la: ask_structured_questions, a MESMA do Criativo (RN-162), agora advertida também ao PO.

Duas decisões de desenho que valem além deste caso: só o create_story que deu certo quita a obrigação do épico (uma história recusada pela api não cobriu nada, e tratá-la como se tivesse é trocar um silêncio por outro); e a cobrança é por OCORRÊNCIA, reportada uma vez, nunca alarme que repete a cada turno até alguém aprender a ignorá-lo.

RODADA exp001 — o fio se lê como o turno acontece (RN-172/173)​

Mesma origem das outras: USO, não roteiro. Quatro queixas sobre a tela da Sessão — o handoff do PO aparecendo ACIMA da última fala dele, a aprovação subindo para o meio da resposta do Arquiteto e só descendo quando ele terminava, o card de aprovação "mal diagramado" e o scroll que não seguia a conversa.

A investigação achou que a ordenação estava CERTA, e é essa a lição da rodada. A RN-155 é fiel ao event log; o que o log diz é que po_server.ex#run_turn/2 emite, na MESMA iteração, o agent.response, DEPOIS o tool.call de offer_handoff e SÓ ENTÃO recursa para a fala de fechamento — logo o seq do handoff é honestamente MENOR que o da última fala. O mesmo para proposed_action.created. A correção, portanto, não é de ordenação: é uma regra de APRESENTAÇÃO declarada (RN-172), numa passada separada (afundarDesfechos) DEPOIS do sort por seq — não um comparador com três termos que ninguém saberia justificar um ano depois.

O que impede turnos de se misturarem são três barreiras, e cada uma existe por um caso real: turno (a fronteira entre dois turnos pode não ter entrada VISÍVEL — agent.activated abre turno e não vira item do fio), autor (em sessão de execução vários agentes escrevem sem o usuário falar nenhuma vez, e todos ficam no mesmo turno) e não-desfecho (dois desfechos seguidos preservam a ordem entre si — Infra antes do Dev Lead).

A RN-173 fecha as outras duas queixas, e elas eram a mesma: o efeito de scroll dependia só de [events.length, streamingText], e actions é uma query SEPARADA — um card chegando empurrava a conversa para fora da tela sem rolar nada. Dependência corrigida, mais um ResizeObserver sobre o conteúdo para o que NENHUMA lista de dependências alcança (colapso de Disclosure, Markdown reflowando). A guarda dos 120px do fim continua intacta, de propósito: o fio segue a conversa, não sequestra a leitura de quem subiu.

RODADA exp001 — o fio diz quem fala, com qual modelo e o que pergunta (RN-171/174/175/176)​

Mesma origem: USO. Quatro queixas sobre a tela da Sessão, e três delas compartilham a mesma lição — o produto sabia a informação e não a mostrava.

A pergunta ao centro, e com saída (RN-171). O relato foi literal: "sempre dê a opção de input do usuário quando ele seleciona Escreva". O modelo oferecia uma opção do tipo "Escreva você mesmo" e o formulário não tinha onde escrever — o schema de ask_structured_questions não sabia expressar "além destas, o que você quiser". allowOther entrou em select com default true, e a assimetria é o argumento: lista fechada por ESQUECIMENTO trava a conversa e o usuário não destrava de fora; lista aberta por engano oferece um campo a mais. O sentinela de interface nunca viaja pro backend, e o botão de envio continua exigindo tudo preenchido (o backend recusa com 400 de qualquer forma). A caixa também virou centralizada com o teto de 560px do ApprovalCard e ganhou avatar: era o único item do fio alinhado a nada.

Ação que dispara turno arma o indicador (RN-174). A animação de "pensando" já existia — o que faltava era COBERTURA. Duas ações da tela disparam turno síncrono no engine e não ligavam nada: responder o formulário (AnswerStructuredQuestionUseCase reusa SendAgentMessageUseCase) e devolver história ao PO (ReturnStoryUseCase chama reviseStory, que é handle_call({:revise, …})). O canal Phoenix não cobre o buraco: com o join ainda em curso (RN-108) o agent.status "working" não tem ouvinte e se perde.

O modelo, para TODOS (RN-175). A premissa de que o defeito era do PO não se sustentou: os quatro conversacionais gravam modelName desde a RN-146, com teste verde. Quem nunca gravou foi o ToolLoop — o caminho de todo agente de execução e de gate — e o chat sem agente ativo na api. Nenhuma chamada nova: RunLlmTurnUseCase já devolvia o campo no corpo. A tela deixou de escrever a palavra solta "modelo" (que se lê como se o modelo se chamasse assim) e virou chip legível; sem o dado, ela DIZ que não foi registrado — adivinhar pelo binding atual atribuiria a uma resposta antiga um modelo que talvez nem existisse, o mesmo erro que o preço congelado da RN-044 evita.

Tabela em Markdown vira tabela (RN-176). Duas saídas eram possíveis, e a escolhida foi suportar tabela no Markdown do chat, com o Table do design system. Renderizar artifact.module_map no fio foi RECUSADO por uma decisão já registrada: ele é estado VIGENTE do projeto, não artefato datado por sessão (RN-159), e vive na Visão Geral. Além disso, o pedido foi sobre a tabela dentro da mensagem — e a correção serve a qualquer agente que escreva uma, não só ao Mapa de Módulos. A linha separadora continua obrigatória (GFM), então prosa com | nunca vira tabela por engano.

RODADA exp001 — o painel agrupa, ordena e diz o que não mostra (RN-177..181)​

Mesma origem das outras: USO, não roteiro. Cinco queixas sobre o painel de contexto e o log da Sessão, e a lição da rodada é uma só — o painel mostrava um recorte como se fosse o todo, em três dimensões diferentes ao mesmo tempo: escondia seis tipos de evento sem oferecer alternativa, lia do mais antigo para o mais novo, e cortava a sessão em 200 eventos sem dizer.

Origem como classificação nova (RN-177). ActivityKind responde "de que ASSUNTO o evento fala" e decide ícone e cor; origem responde "de que CAMADA ele veio", e é ela que torna o histórico legível — eventos, sistema, llm, harness, agente, usuario, derivadas do dado que EXISTE (actor.kind e o prefixo do type). A precedência é o que a torna previsível e está na ordem dos if: mecanismo vence ator (um tool.call é do harness seja quem for), e ator vence prefixo de agente (chat.message existe dos dois lados). As 5 mais recentes ficam abertas e o resto se recolhe por origem, nos dois lugares — no painel de log e no fio, onde o eixo é invertido (o fio é crescente, então o histórico fica no TOPO). O filtro de ruído de máquina continua ligado por padrão: a razão dele não mudou (116 de 193 eventos reais), o que mudou é ele ter virado ESCOLHA em vez de fato consumado.

Ordem e paginação (RN-178). As quatro seções passaram a decrescentes, e o botão "Carregar mais antigos" mudou de lado por consequência — o argumento de antes, com o sinal trocado. Regras de negócio acima de 5 paginam, com a página resolvida por clamp e não por efeito de sincronização.

A árvore do PO (RN-179). backlog.task_created nunca entrou no painel; agora épico → história → tarefa formam árvore, pelo VÍNCULO que o evento já carrega (epicId/storyId), nunca por vizinhança no log — nó sem pai carregado sobe para a raiz em vez de ser pendurado por adivinhação.

O teto que passou a aparecer (RN-180), e é o item que mais valia. O painel lia 200 eventos por prop e não tinha como dizer que havia mais. Passou a ler o histórico paginado da RN-099 (mesma queryKey da cauda, ZERO requisição a mais — RN-090/091), ganhou o pager que o ActivityFeed sempre teve e nenhum call site passava, e a nota conta quantos faltam por SUBTRAÇÃO sobre o seq, como o sino da RN-100. As seções derivadas leem baixados (tudo que já veio) e não a janela do feed, senão passariam a mostrar MENOS do que mostravam antes.

Delegação no fio (RN-181). delegation.completed|failed|dispensed só existiam no painel de log: o gate abria e fechava sem sinal nenhum de que houve uma segunda tentativa por baixo. Viraram aviso compacto no formato da RN-157, com a frase saindo de classifyEvent — a mesma do painel. O contrato externo da área não muda (ADR 0038): o fio narra o que o lead registrou, não passa a endereçar subagente.

PROGRAMA 28 — Onda 1, frente A: o tema deixa de ser inalcançável (RN-182..185)​

O tema claro existia em design/tokens.css, tinha teste de paridade e estava descrito no design/README.md como se fosse uma tela — e nada em apps/web escrevia data-theme. O único jeito de vê-lo era digitar o atributo no DevTools. Os dois testes de contraste sabiam e diziam: um afirmava por expect que TRÊS pares do claro reprovam, "registro do que se herda, não garantia do que se renderiza". Enquanto durou, o argumento era honesto — medir uma tela que ninguém pode abrir é medir uma intenção. Com o botão de tema chegando, ele inverte de sinal.

O boot é um ARQUIVO (apps/web/public/theme-boot.js), síncrono no <head>, antes do bundle. O handoff manda usar script INLINE; a imagem serve sob script-src 'self' (ADR 0058), então inline funcionaria em pnpm dev:web e seria bloqueado em produção — a falha do ADR 0036 com outro sujeito. tema.ts é a API que o shell consome; o botão não mora nela. localStorage que lança degrada para "pinta e não persiste", e valor desconhecido cai no default.

Seis tokens do tema claro mudaram de valor (ADR 0074), porque o fundo mais exigente do claro é o --code-bg de papel: accent 3,56 → 4,81, warning 3,15 → 4,98, success 3,89 → 5,12, violet 4,16 → 4,95, text-muted 2,76 → 5,17, mais o accent-hover um degrau abaixo. O ESCURO não mudou nenhum, e a dívida dele segue travada nos mesmos cinco números. Dois efeitos colaterais registrados: os cinco pares que no escuro são dívida PASSAM no claro, e a "exceção conhecida do design system" (--on-accent sobre --accent, 3,20:1) some no claro — o conserto que o comentário do teste descrevia, escurecer até --terracota-500, é o que o claro passou a fazer.

Cinco dos oito valores --syn-* do handoff foram RECUSADOS por medição (o pior, --syn-cm, dá 2,32:1 no claro). Onde o handoff reprova, vale o número medido: é a mesma régua das fontes. Os oito papéis passam a existir com prefixo --syntax-*; os cinco semânticos que ainda pintam vão medidos ao lado deles.

PROGRAMA 28 — Onda 1, frente D0: gasto por provider (RN-186..188)​

A contenção mudou de natureza, e é isso que importa — não o eixo novo. provider voltou a ser dimensão de sumGroupedBy, revisando o ADR 0063 por decisão do dono do produto. Antes, o membro não alcançava a quebra por CREDENCIAL porque ela não existia — o ADR 0063 escreveu "não há argumento a passar". Agora existe, e quem contém é o TIPO: duas sobrecargas, e o escopo que carrega actor (o único da visão do membro) só aceita Exclude<SpendDimension, 'provider'>. A garantia passou de "impossível de expressar" para "impossível de compilar", que é mais fraca — e é por isso que as barreiras são DUAS e independentes: a rota do membro também não tem parâmetro de dimensão nenhum. Nenhum if sobre a combinação, de propósito (mesma lição da RN-153/154), e a prova foi por INVERSÃO: sem o @ts-expect-error o tsc acusa TS2769 na linha exata.

porOwner/porAgente são partição de porAtor por actor_kind, sem consulta a mais; system fica fora dos dois e continua no total. O índice em token_usage(created_at) que o ADR 0063 mediu e adiou entrou na migração 0044. A TELA não mudou — é onda posterior.

PROGRAMA 28 — Onda 2, frente H2: ranking de modelos sem nota inventada (RN-210, ADR 0077)​

O handoff pedia duas coisas que dependiam do MESMO dado que o produto não tem: uma nota de qualidade por capacidade (mock: "código → claude-sonnet-4 / qwen2.5-coder:14b (9.4)") e um badge "ideal" no ModelPicker quando o modelo cobre "todas as capacidades exigidas pelo agente". A investigação confirmou duas vezes que esse segundo dado não existe: o próprio ModelsSection já tinha renomeado a coluna "Agente · capacidades" para só "Agente" por essa razão, e o ModelPicker de binding de agente lê GET /projects/:id/models (papel viewer) — a curadoria (uses, ADR 0051) só existe em ModelComCuradoria, de uma rota maintainer-only. O badge NÃO foi construído; a tabela "Melhores modelos por capacidade" FOI, com dois sinais reais: quantos agentes do projeto já resolvem, pela cascata, para cada modelo curado, e o custo do catálogo como desempate — nunca nota calculada.

PROGRAMA 28 — Onda 2, frente B: sidebar recolhe (RN-195..201)​

264px/62px com trilha de ícones no colapso; projetos expansíveis revelando as abas (lidas de project-tabs.ts, nunca hardcoded); seção Atividades agrupada por agente-base/instância REAL — sufixo -2 de extraDevAgentId, nunca um contador -01/-02 inventado (achado da Onda 1/frente B0) —, escopada ao projeto da rota atual, não a todos, para não abrir N+1 de eventos. Botão de tema no rodapé; os dois itens globais inertes saem, só Projetos e Atividades são globais; auto-collapse da aba Código via AutoCollapseContext (sidebar-state.ts), sem gravar preferência. Persistência centralizada em sidebar-state.ts, as seis chaves brabo.* do handoff.

Duas divergências do handoff documentadas e NÃO resolvidas (RN-197): o badge do projeto continua pendingApprovalsCount (RN-151 vence, é mais recente) e a cor de identidade do handoff só aparece na trilha recolhida — a linha expandida mantém só o dot de status existente (RN-039), dois dots seria ruído.

PROGRAMA 28 — Onda 2, frente C: moldura de tela (RN-202/203, ADR 0078)​

As quatro correções literais do checklist "moldura de tela" do handoff: .headerTop ganhou min-height: var(--header-h) — PISO, não teto, porque o cabeçalho do projeto (identidade + TokenMeter compacto) é mais rico que o cabeçalho genérico das 6 telas internas do handoff, e forçar 60px cortaria o alerta de orçamento, a mesma classe de erro que a RN-088 proíbe; aba ativa com box-shadow: inset 0 -2px 0 var(--accent) em vez de border-bottom; rolagem horizontal na régua; container de conteúdo com max-width: 1040px nas abas em forma de documento (não nas semRespiro). Os valores de espaçamento da régua que viviam como override de CSS de descendente em ProjectPage.module.css desde a FASE 16 migraram para Tabs.module.css, agora com o mesmo dono.

A parte que exigia decisão, não só CSS: o handoff lista 7 abas de projeto e o registro (project-tabs.ts) tem 10. executores (RN-121), backlog (RN-048) e insights (hipóteses do Psicólogo) nasceram DEPOIS do handoff, com dado real e RN própria, e FICAM — o handoff é referência de fidelidade VISUAL, não teto de quantas abas o produto tem (RN-203, ADR 0078). E a chave sessions continua rotulada "Chat", nunca "Chat RAG" como o handoff mais recente pede (RN-202): "Chat RAG" descreveria uma funcionalidade que não existe — o contrato de embeddings está pronto (ADR 0075) mas nada ainda o consome, sem pipeline de indexação e sem UI de citação — e renomear a aba hoje seria o mesmo erro que o ADR 0042 já recusa para modelo de catálogo: anunciar uma capacidade antes dela existir. Nenhuma FORMA de export mudou (AbaDoProjeto, ABAS_DO_PROJETO); só o rótulo de code foi de "Code" para "Código".

PROGRAMA 28 — Onda 2, frente H1: foco visível (ADR 0036)​

Select, Modal (botão de fechar) e ProjectCard ganharam :focus-visible no mesmo tratamento calibrado de Input.module.css, incluindo o bloco de forced-colors: nenhum dos três tinha indicação de foco alcançável só por teclado. O botão de fechar do Modal subiu de 30px para 32px, o piso de alvo de toque em desktop. Table e Badge foram auditados e NÃO precisaram de mudança — nenhum dos dois expõe afordância interativa própria: linha de Table é apresentação pura (quem precisa de linha clicável já usa <button>/<a> dentro da célula, via render) e Badge não é usado com onClick em lugar nenhum do produto.

PROGRAMA 28 — Onda 3, frente H3: a aba Criativo ganha KPIs sem fingir dado que não existe (RN-227..230)​

O handoff pede 4 selos de status para os 5 estados reais da sessão (created/active/closing/closed/closed_abnormally). closed_abnormally→ abortada e created→aguardando são diretos; closing NÃO foi fundido com "fechada" — ganha selo próprio ("encerrando", tom accent, pulsante), porque em closing o desfecho ainda não é conhecido e chamá-la de "fechada" mentiria sobre isso. Os filtros pill (só 4, pelo mesmo motivo) agrupam created e closing por TRAJETÓRIA, não aparência: created cai em "Ativas" (ainda em jogo), closing cai em "Fechadas" (a caminho de fechar sem erro) — o selo da linha nunca é reescrito pelo filtro, só o agrupamento muda. Dos 4 KPIs, dois são honestos por dado ausente: "custo do mês" REAPROVEITA getMySpend (mesma queryKey da aba Gastos, visão do membro/RN-101) e nunca mostra o total do projeto — que é dado do owner (porProjeto), e a aba Criativo é vista por qualquer membro; "taxa ideação → commit" é DECLARADA ausente (mostra "—"), porque não existe vínculo entre sessão e commit no produto hoje — inventar o cálculo seria a mesma classe de erro que o ADR 0042 já recusa para nota de modelo.

PROGRAMA 28 — Onda 4, frente G2: o Chat RAG ganha pipeline de indexação e busca híbrida (RN-231..238, ADR 0080)​

Sobre a tabela chunks da Onda 3 (ADR 0079): indexação MANUAL (sem watcher, POST .../rag/reindex, full rebuild idempotente — apaga o escopo/sessão antes de recriar) dos três escopos honestos — docs/adr via ReadProjectCodeUseCase (mesma credencial/portão/checagem de caminho da aba Code) e session só de chat.message/agent.response. Chunking por heading/parágrafo, 1200 caracteres com 150 de sobreposição — documentado como ponto de partida, não calibração contra dado real (não existe, ainda, corpo de perguntas rodado contra o índice). Busca híbrida é DUAS consultas independentes (vetor via HNSW, léxico via GIN), fundidas por soma ponderada (0.6/0.4) e cortadas num limiar (0.2) — números também ponto de partida. Quando o provider de embedding (fixo: ollama) não responde, o pipeline grava os chunks SEM vetor e declara a lacuna no relatório — nunca finge indexação completa, e a busca degrada para léxico-only avisando. A tela (Chat RAG como aba própria) é da Onda 5; esta frente é só o backend.

PROGRAMA 28 — Onda 4, frente F1: ciclo de vida do container vira tabela, sem orquestrador (RN-243..248, ADR 0081)​

Ver item 31 da FASE 25 (PARCIALMENTE REVOGADO): project_containers (migração 0046) grava o ESTADO mutável — provisioning → running ⇄ stopped, failed alcançável dos três, removed só sai reprovisionando — distinto de artifact.project_image (a DECISÃO imutável do Arquiteto). Confirmado por investigação (grep, zero ocorrências): NENHUM serviço do produto monta /var/run/docker.sock nem roda privileged — os dois casos de uso só GRAVAM e LEEM, nenhuma chamada real a Docker. A primeira transição exige a imagem já decidida (RN-105) e CONGELA versão/recursos na linha; projeto local (ADR 0072) é recusado, porque não sobe container próprio. Teto de recursos (cpus/memory_mb/pids_limit) é DECLARADO, não aplicado — nenhuma tela afirma "o container está limitado a X", só "a intenção registrada era X". Nenhuma rota HTTP nova à época: sem consumidor real ainda, expor uma seria adivinhar contrato — fechado pela Onda 5/frente F2 logo abaixo.

PROGRAMA 28 — Onda 5, frente G3: o Chat RAG ganha aba própria (RN-252..254, ADR 0082)​

A tela que RN-202 (Onda 2/C) tinha adiado — "Chat RAG" descreveria uma capacidade que ainda não existia — chega junto com o backend da Onda 4/G2: key: 'rag', aba NOVA (ordem: 28, entre Código e Backlog), NUNCA renomeando sessions. A distinção continua a mesma, só a razão mudou: sessions é conversa com agente ATIVADO (gasta a chave do owner por turno, RN-058); rag é busca sobre um índice já construído, sem agente nenhum no meio, read-only por natureza. A tela consome os três contratos do ADR 0080 sem adivinhar forma e mostra as duas degradações honestas que o backend já declarava e nenhuma tela lia — vectorAvailable: false vira aviso visível, e a cobertura nunca inventa "reindexado há Xmin". Citação de origem session navega até o EVENTO exato, reusando o mecanismo que os chips de evidência do Psicólogo já usam; origem file mostra caminho/heading como texto, sem link — a aba Código não tem deep-link por caminho hoje, fora de escopo aqui.

PROGRAMA 28 — Onda 5, frente F2: o consumidor real do ciclo de vida do container (não o terminal) (RN-267/268, ADR 0083)​

O plano original desta frente era o terminal interativo completo. A investigação confirmou que a FASE 25b continua cortada: nenhum serviço monta /var/run/docker.sock, e mesmo depois do ADR 0081 nada em produção transiciona project_containers — não existe container real para abrir um terminal DENTRO dele. Implementar um terminal fingido, ou rodando no mesmo container do monorepo do Brabo (a dívida que o ADR 0055 já descreve como política, não isolamento), inventaria capacidade — o mesmo erro que os ADRs 0041/0042 recusam para provider de LLM e modelo de catálogo. A entrega real: GET /projects/:projectId/container/lifecycle (role:viewer), primeira exposição HTTP do ciclo de vida (revisa o ADR 0081, que tinha adiado a rota por falta de consumidor), e a aba Terminal mostrando esse estado — badge traduzido, motivo de falha — SOB o texto explicativo que já existia, nunca no lugar dele, buscado só enquanto a aba está aberta. null (nunca provisionado) é o resultado honesto e mais comum hoje.

PROGRAMA 28 — Onda 5, frente I: login social revoga a proibição do backlog do ADR 0031 (RN-272..283, ADR 0084)​

Pedido explícito do dono do produto, ciente das consequências de segurança — a proibição sai só para GitHub/GitLab; MFA, OIDC provider e federação genérica continuam fora de escopo (ver "O que NÃO fazer"). SocialLoginCallbackUseCase termina no MESMO EmitirSessaoUseCase do login por senha — nenhum segundo formato de sessão. Reusa o cliente OAuth existente, mas o state tem propósito PRÓPRIO (purpose: 'social_login', checado ANTES de qualquer outro campo): nunca aceita o state do fluxo de CONEXÃO de git a um projeto, o que seria escalação de privilégio. Tabela nova social_identities (migração 0047), chave (provider, provider_user_id) — nunca e-mail/login. Decisão em três passos: identidade conhecida → login; e-mail bate com conta existente E verificado pelo provider → vincula (e verifica o e-mail da conta se ainda não estava); e-mail bate mas NÃO verificado → recusa — um e-mail digitado não é prova de posse, aceitar abriria account takeover; sem conta correspondente → provisiona conta nova sem senha, mesmo estado "pendente" da migração do Keycloak. Reusa o MESMO app OAuth da conexão de git — zero variável de ambiente nova, só redirect_uri/scope diferentes por fluxo; a ação do OPERADOR de cadastrar o segundo callback no provider é o que justifica o branch breaking/.

Auditoria fluxo.yml × código — o plano do Dev Lead vira aprovação de verdade (RN-284, ADR 0086)​

Não é fase planejada: fecha a divergência que uma auditoria só-leitura de docs/fluxo.yml × código encontrou (achado A2, docs/explanation/auditoria-fluxo-vs-codigo.md) — o fluxo já declarava a saída plano-de-paralelismo do dev-lead 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.

propose_execution_plan entrou em decide.ts (papel mínimo maintainer), DELIBERADAMENTE fora do bloco de tetos absolutos — pode ser configurado para auto-aprovar, ao contrário de parallelize/raise_max_parallel. O Dev Lead é o PRIMEIRO agente conversacional (todos rodam turno síncrono via GenServer.call de até 180s) a suspender esperando uma decisão humana — o padrão já existia para o dev agent (ADR 0052) e os gates de QA/Infra (ADR 0057), mas os dois são disparados por cast e nunca precisaram lidar com um from síncrono pendente. TurnoAssincrono.tratar_resultado/2 ganhou um ramo: presente a chave :aguardando_aprovacao (checada pelo VALOR, não pela presença — o Dev Lead a carrega nil desde o init/1), responde ao from do mesmo jeito e na mesma hora de sempre, mas emite só agent.status: awaiting_approval em vez de agent.done — o turno não terminou. LiveBroadcast.agent_status/4 ganhou o status novo na guarda. A retomada usa o MESMO Engine.Dev.Wake que QaLeadServer já reusa para os subagentes de QA (a entrega é por AGENTE, não por tipo).

Lacuna aceita, declarada: sem tabela de estado própria, um restart do engine enquanto o Dev Lead está suspenso perde a inscrição — a decisão continua registrada e visível em Aprovações, mas ele não narra o desfecho sozinho. Fechar isto exigiria o mesmo mecanismo de persistência do ADR 0052; fora do escopo desta correção, que só alinha o comportamento ao que docs/fluxo.yml já declarava.

UX Designer — o quinto agente conversacional, antecipado (RN-285..287, ADR 0087)​

Não é gatilho de separação disparado — docs/fluxo.yml sempre declarou "quando o projeto GERENCIADO tiver interface própria a desenhar" como critério, e ele não disparou: o design system continua insumo estático. Decisão CONSCIENTE do dono do produto de antecipar o papel mesmo assim.

Engine.Agents.UxDesignerServer espelha o DevLeadServer — GenServer por sessão, teto de 14 iterações, ativado por handoff accepted endereçado a "ux-designer" pelo mecanismo GENÉRICO já existente (nenhuma linha mudou em ActivateAgentUseCase). SOLO: sem área, sem subagentes. O kickoff lê a artifact.product_brief mais recente — a MESMA "necessidade de negócio" que o Criativo produz, sem artefato novo — e o sistema de design (design/tokens.css, design/COMPONENTS.md) é DESCRITO na identidade do agente (Engine.Harness.Agents), texto estático: os agentes conversacionais não têm ferramenta de leitura de arquivo do repositório.

propose_prototype é a ÚNICA ferramenta (personas, jornadas, prototipo com telas/anotacoes, resumo). Grava artifact.prototipo_navegavel sem caso de uso dedicado na api — diferente de choose_project_image/create_c4_diagram, que precisam de um porque têm conteúdo DERIVADO de outro artefato ou recusa de domínio compartilhada; nenhum dos dois motivos vale aqui, então a validação de forma mora no engine (ArtifactSchemas) e a gravação usa o append_event_returning genérico que a api já expõe, mesmo caminho do artifact.product_brief. Um artefato só, dois handoffs sobre ele — para "po" e para "dev-lead" — nunca um segundo artefato para "spec-visual": o protótipo (telas + anotações) É a spec visual, e duplicá-lo arriscaria as duas cópias divergirem depois. Reusa a metade do desenho do ADR 0086 que sobrevive sem a suspensão dele: propose_prototype bem-sucedido encerra o turno, para o modelo não propor de novo e duplicar o artefato — sem o "aguardando_aprovacao", porque propor um protótipo não tem efeito externo.

apps/web/src/lib/agents.ts ganhou a entrada (color: var(--accent), o token semântico menos reusado do roster; icon: PencilIcon). uxDesignerActive entrou no roster nas DUAS fontes que a RN-090 exige em sincronia — o painel do time (agent-status.ts) e o card do dashboard (projects-summary.repository.ts, mesma consulta de infraActive ampliada, sem query nova).

Fora de alcance, declarado: teste-de-usabilidade exige usuário humano real — nenhum agente substitui isso. metricas-de-uso segue lacuna mesmo com analytics active (ADR 0089): o relatório de funil mede sessão→commit→PR→merge, não adoção de feature pelos usuários FINAIS do projeto que o Brabo constrói — "evidência de adoção por feature" está DECLARADA como métrica sem caminho para existir hoje, não pendência a fechar na próxima rodada.

Staff — código pronto, dormente para disparo automático (RN-305/306, ADR 0088)​

Não é fase planejada: docs/fluxo.yml declara o Staff/Principal Engineer como status: planned desde o ADR 0085 ("contrato pronto, ativação decidida, aguarda gatilho"). Decisão CONSCIENTE do dono do produto: antecipar o CÓDIGO mesmo sabendo que o gatilho AUTOMÁTICO (a Anamnese notando um problema sistêmico RECORRENTE) não vai disparar — a Anamnese está pausada (ANAMNESE_ENABLED=false, decisão de 2026-08-10). Dormente para disparo automático, não para acionamento MANUAL.

Sexto agente conversacional solo (Engine.Agents.StaffServer), ao lado de Criativo/PO/Arquiteto/Dev Lead/UX Designer, espelhando o Arquiteto (laço bounded teto 14) com duas diferenças: SEM kickoff/1 (não há artefato de sessão para resumir — sobe e fica ocioso até a primeira user_message, que é como quem endereçou o handoff explica o problema) e ativado pelo caminho GENÉRICO de canActivateAgent (handoff accepted endereçado a "staff"), sem entrar em USER_STARTED_AGENTS — investigação confirmou que nenhuma mudança de domínio na api era necessária, porque assertHandoffTargetAllowed só recusa subagente de área e o Staff não tem área. A única ferramenta, propose_rfc (problema, opções com trade-offs, recomendação, PoC descartável), grava artifact.rfc_staff DIRETO via append_event_returning — mesmo padrão sem tabela de emit_insight, e não o de artifact.c4_diagram (que deriva o Container level do module_map na api) — e devolve o handoff ao Arquiteto no MESMO tool call, sem proposed_action (registrar um documento não é efeito externo).

staffActive entrou na roster do painel do time e no card do dashboard (ProjectCardSummary.roster), mesmo critério de infraActive, para não abrir a divergência que o comentário de RosterFacts já alertava. Declarado, não escondido: SessionPage.tsx/AGENTES_DE_CHAT não foram tocados — mesmo padrão já aceito para infra (um lead REAL e ATIVO também fora dessa lista). O caminho ponta a ponta de uso hoje é a rota interna (POST .../agent/message, agent: "staff"), não a tela de Sessão; a UI genérica de "handoff manual a agente à escolha" segue no backlog.

O gate implementavel ativo — QA-estratégia como segundo momento do qa-lead (ADR 0090)​

Decisão consciente do dono do produto de antecipar o gatilho que docs/fluxo.yml já previa ("quando o gate implementavel ativar") — o gatilho AQUI é o próprio trabalho de construir o mecanismo, não um sintoma de uso esperando para acontecer. docs/gates.yml, implementavel: status: planned → active (dono dev-lead, severidade: warn intocada — o registro já dizia "nasce warn mesmo quando ativar").

QA-estratégia deixa de ser papel proposto: é o PRÓPRIO qa-lead, num segundo MOMENTO — mesmo processo, entregável separado do veredito de PR, exatamente como docs/fluxo.yml já declarava ("pode ser o próprio qa-lead em segundo MOMENTO, não necessariamente agente novo: a separação é de entregável"). Engine.Gates.QaLeadServer.run_design/3 é um ponto de entrada NOVO e ADITIVO (sem tocar run/2, o caminho de sempre, amarrado a DevAgentState.find_by_task_id), acionado por Engine.Gates.Dispatcher.run_qa_estrategia/3 (mesma indireção trocável em teste que run_qa/2/run_secops/2 já usam).

Engine.Gates.QaEstrategiaAgent é módulo SEM ESTADO (não GenServer), mesma forma de QaPerformanceSegurancaAgent — registro sem Terminal (ReadFile, SearchWorkspace, EmitPlanoDeTeste) —, mas o CONTEXTO é outro: Engine.Gates.QaEstrategiaContext.fetch/3 busca SÓ story (de list_backlog) e module_map vigente (de get_infra_context, reusado só pelo campo moduleMap — zero rota nova), sem dev_state nem worktree_path, porque o gate roda PRE-DEV: não há dev agent, worktree nem task_id ainda. Nenhuma das três ferramentas passa pelo ActionPipeline (só terminal/write_file passam), então este agente NUNCA suspende — run_design/3 roda síncrono dentro do próprio handle_cast. O teto de iterações fica em 8 (conversacional), não 60 (gate) — DE PROPÓSITO: sem token_budget_micros por baixo (não há task, PRE-DEV), a mesma razão pela qual infra-workflows fica em 8 mesmo usando ferramenta (RN-085). O entregável (emit_plano_de_teste: síntese, critérios executáveis, estratégia de automação GENÉRICA e sem framework) vira artifact.plano_de_teste no event log da sessão que chamou.

assess_implementability, ferramenta nova do Dev Lead, lê o artifact.plano_de_teste mais recente da story no histórico da PRÓPRIA sessão: sem plano ainda, dispara a avaliação e devolve erro pedindo retentativa (erro de ferramenta é ENTRADA do laço, não fim de linha — RN-163); com plano, propõe o parecer (implementavel/inviavel + justificativa, plano embutido no payload) como proposed_action, MESMO padrão de três desfechos de propose_execution_plan (ADR 0086) — maintainer, DELIBERADAMENTE fora do bloco de tetos absolutos de decide.ts.

appsec deixa de ser proposto — o segundo momento do SecOps entra na seção seguinte.

O appsec ganha o segundo momento do secops (RN-360/361, ADR 0090)​

docs/fluxo.yml (id: appsec, camada_seguranca) declarava por antecipação "mesmo padrão do QA: dois MOMENTOS, não dois agentes por ora" — decisão consciente do dono do produto de antecipar a ativação, sem esperar o gate implementavel (frente qa-estrategia, mesmo ADR conceitual 0090) que o próprio registro citava como gatilho.

Engine.Gates.SecOpsAgentServer.run_design/2 roda no MESMO processo do secops de PR (mesma chave de Registry), sem Diff/Scanner/ DevAgentState nenhum: busca a story no backlog + o module_map vigente (Engine.Gates.AppSecContextBuilder, sem dev_state/worktree_path) e chama Engine.Gates.AppSecAgent.run/3 — módulo SEM ESTADO (não é GenServer, mesma forma de QaPerformanceSegurancaAgent), registro de ferramentas SEM Terminal, rodando um checklist STRIDE-lite via ToolLoop.run/1 sobre o DESENHO da story, nunca sobre código. Termina emitindo artifact.threat_model e criando handoff para os três leads declarados (arquiteto, dev-lead, infra — o AGENTE endereçável do id area-infra, RN-361).

Lacuna declarada, não bug: run_design/2 é ACIONÁVEL, mas nada aciona sozinho ainda — o gatilho natural é assess_implementability do Dev Lead (frente qa-estrategia), fora do escopo desta entrega, que foi mantida autocontida (decide.ts/docs/gates.yml/dev_lead_tools.ex intocados).

analytics/delivery-metricas viram relatório (RN-320..322, ADR 0089)​

Decisão consciente do dono do produto de ANTECIPAR dois papéis do modelo-alvo (docs/fluxo.yml, status: proposto) sem esperar o gatilho orgânico que cada um já declarava. A forma é a que o próprio fluxo prescrevia: analytics "absorvido por medicao" até métrica de PRODUTO virar entrada obrigatória do PO, delivery-metricas "nunca vira agente — vira RELATÓRIO do medicao". Os dois viram um SCRIPT só — apps/api/scripts/analise-funil.ts (pnpm --filter api analise:funil -- --projeto <uuid> [--json]), no MESMO formato de medir-execucao.ts (Fase 13b): leitura pura via Drizzle, zero escrita, sem GenServer, sem agente de LLM.

Mede DE VERDADE, sobre proposed_actions.execution_result das três ações git do dev agent (git_commit/pr_open/git_merge, só status: 'executed'): funil real (quantas sessões produziram commit / PR aberta / PR mergeada, e a conversão entre etapas — conta SESSÃO, não ação), lead time real (primeiro commit ao primeiro merge da sessão, por updated_at da EXECUÇÃO, não da proposta) e deployment frequency real (merge em branch PROTECTED_BRANCHES, por dia, cruzando por referência com o gate backmerge de docs/gates.yml — cuja evidência é CI, fora do alcance de um script que só lê o banco).

Três métricas ficam numa seção "Não medido, de propósito" — declaração PERMANENTE, não lacuna a fechar na próxima rodada: funil de produto completo ideação → commit (sessions sem storyId, RN-230, exigiria schema novo — nenhuma migration nesta frente); evidência de adoção por feature (não é dado que falta coletar — o Brabo não instrumenta os projetos que ele CONSTRÓI, sem caminho nenhum para essa telemetria existir hoje); MTTR e change failure rate (exigem sinal de INCIDENTE de produção real, a mesma dependência de secops-runtime/platform — outra frente).

secops-runtime como script de relatório (RN-375..377, ADR 0091)​

Antecipação decidida do dono do produto do papel secops-runtime (docs/fluxo.yml, camada_seguranca, antes proposto), sem esperar o gatilho declarado ("produção com tráfego real, pós DEPLOY_ENABLED + platform ativo"). Só o que o gatilho NÃO exige entra: pnpm --filter api relatorio:seguranca-runtime lê rate_limit_hits — o dado que o RateLimitGuard do ADR 0027 já grava, inclusive sob tráfego de dev/CI — e produz ranking de baldes (user:<uuid>/ip:<endereço>, os dois únicos formatos gravados; sem rota nem motivo, que a tabela nunca guardou) e distribuição temporal em fatias fixas. É SCRIPT, não agente LLM nem GenServer: não há decisão a tomar sobre o dado, só agregação.

Detecção automática de incidente, resposta a incidente e postmortem de segurança continuam FORA — dependem do mesmo gatilho que não disparou — e o relatório os lista numa seção "não medido" PERMANENTE, sem simular incidente de exemplo nem inventar número (mesmo princípio dos ADRs 0041/0042/0077). A janela retida é curta por desenho (DomainGaugesCollector.pruneRateLimit apaga hits com mais de 2 × RATE_LIMIT_WINDOW_MS, 240s por padrão), e o relatório declara as duas janelas — CONFIGURADA e OBSERVADA — nunca deixando a segunda passar por um histórico maior do que é.

platform ganha uma primeira entrega: relatório de telemetria sob demanda (RN-385/386, ADR 0092)​

Decisão consciente do dono do produto de antecipar parte do papel platform (docs/fluxo.yml, camada_plataforma) — cujo status continua planned e cuja ativacao continua sincronizada com DEPLOY_ENABLED, que não existe. pnpm --filter api relatorio:telemetria [--projeto <uuid>] [--json] é um SCRIPT (não agente LLM, não GenServer) que lê, sob demanda, as MESMAS perguntas que o DomainGaugesCollector já coleta para o scrape do Prometheus — sessões ativas/closing por projeto, tasks bloqueadas por projeto, estado do último backup (sempre GLOBAL, nunca por projeto, porque o produto faz backup do banco inteiro) — e termina depois de imprimir. As consultas SQL são REPLICADAS, não importadas do coletor: os métodos dele são privados e terminam escrevendo num gauge Prometheus, sem metade pura de "só a query" para reusar sem acoplar um script de CLI ao ciclo de vida de um @Injectable.

A saída sempre traz "onde ver mais" (os dashboards versionados em deploy/k8s/observability/dashboards/*.json, os alertas em deploy/k8s/observability/alerts/brabo-alerts.yaml, docs/runbook.md #observabilidade, pnpm dev:obs para observabilidade local) — link, nunca duplicação — e uma seção "não medido" que DECLARA, sem inventar: nenhum SLO numérico está definido no produto, postmortem depende de incidente real que não aconteceu, e telemetria de volta ao produto em loop fechado é o que tornaria platform active — o gatilho (DEPLOY_ENABLED) segue ausente. O gate_saida: { id: operavel, status: planned } do papel não muda; docs/fluxo.yml só ganha uma nota na saída telemetria-consolidada dizendo que a versão manual/sob-demanda já é real.

dbre vira dois scripts mecânicos (RN-400..403, ADR 0093)​

Do papel dbre do fluxo.yml, só dois dos quatro entregáveis dependem de volume real de dados (plano-de-capacidade, tuning — seguem lacuna, sem prazo). Os outros dois não precisam de carga real: parecer-de- migracao é reconhecimento de padrão em texto SQL (apps/api/scripts/lint-migracao.ts, varre apps/api/src/db/migrations/ *.sql, sinaliza DROP TABLE/TRUNCATE/DROP COLUMN/ALTER COLUMN ... TYPE/ADD COLUMN ... NOT NULL sem DEFAULT, != 0 se achar algo — hoje acha 3 ocorrências em migrations já mergeadas e aceitas, informativo, não corrigidas de passagem); backup-restore-testado já era real desde a Fase 5 (CronJob + backup_runs + make test-restore executado, RTO ~40s) — faltava só um relatório sob demanda (relatorio:backup, mesma leitura do DomainGaugesCollector.collectBackup()). Nenhum dos dois entra em CI ainda: o linter varre o repositório inteiro, não o diff de uma PR, e viraria gate que reprova PR por achado que não é dela — ver o ADR 0093 para a técnica de escopo (pr-police.ts) quando isso mudar. A regra de "uma migration por onda" (meta/_journal.json) deixou de ser chamada de "versão mecanizada" deste papel: ela evita conflito de snapshot entre agentes em paralelo, preocupação ortogonal a "este SQL tem um padrão arriscado".

Auditoria fluxo.yml × código — Onda 1: fluxo.yml/gates.yml em dia​

Primeira das seis ondas do plano da auditoria (seção D, docs/explanation/auditoria-fluxo-vs-codigo.md) a fechar — as Ondas 3, 4 e 5 já tinham sido antecipadas fora de ordem (analytics/DORA, gate implementavel, QA-estratégia/appsec); esta é só metadado, sem ADR, sem linha de código de produção.

docs/gates.yml (paralelismo-autorizado): status: planned → active, com evidencia no MESMO formato do gate implementavel vizinho — event_types: [proposed_action.created/approved/denied], filtro: { actionType: parallelize }, onde: request-parallelization.use-case.ts (achado A1/B5 — o fluxo.yml já dizia ativo desde o ADR 0053/FASE 14d, só o gates.yml ficou para trás). docs/fluxo.yml ganhou mais três correções da auditoria, todas de citação ou rótulo, nenhuma de comportamento: a máquina de estados do dev passou de 4 para 5 estados (awaiting_approval entrou, Fase 12e/ADR 0052 — A3); a citação de RN-160 saiu da entrada backlog-promovido do Arquiteto e foi para onde ela realmente se aplica, a saída handoff-duplo (o botão "Confirmar arquitetura pronta" que ela trava — A4); a citação de RN-161 saiu de handoff-duplo e virou nota na entrada do Dev Lead (é o passo de ACEITAR o handoff que encadeia ativação de execução, não o duplo handoff em si — A5); e a origem de worktree-por-agente do dev deixou de dizer harness (Engine.Dev.WorktreeManager não é um dos 4 componentes do papel harness — A8). docs/explanation/backlog.md também foi atualizado: a linha do gates.yml desatualizado saiu da tabela de pendências (fechada aqui), junto com o gate implementavel (B3, já fechado pelo ADR 0090 antes desta onda, sem ninguém ter tirado da tabela) e a parte de B7 (deployment frequency/lead time) que o analise:funil (ADR 0089) já entrega de verdade — MTTR/change failure rate continuam de fora, mas como lacuna PERMANENTE já declarada em fluxo.yml, não pendência de engenharia.

Verificação: pnpm --filter api validacao:gates -- --sem-banco (registro válido, sem ativo-sem-evidencia) e pnpm docs:check verdes. A fase 3 do script (evidência real no event log) exige --projeto <uuid> contra um banco com dados — fora do alcance desta correção, que é só de metadado.

Pendentes do mesmo plano, não desta onda: Onda 2 (delegação Dev Lead → dev, RN-160 revalidada no backend) e Onda 6 (gate necessidade-validada, que exige ADR e decisão de produto antes de codar).

Auditoria fluxo.yml × código — Onda 2: RN-160 no backend e delegação Dev Lead → dev (RN-404/405, ADR 0094)​

Segunda das seis ondas do plano da auditoria (seção D), os DOIS itens de maior escopo depois da Onda 1 — os outros quatro (3, 4, 5 antecipadas fora de ordem; 6 ainda pendente, exige ADR e decisão de produto). Os dois vivem exclusivamente em apps/api; nenhuma mudança em engine/web foi necessária.

B6 — RN-160 revalidada no backend. A regra ("Confirmar arquitetura pronta" exige ao menos 1 história promovida) só era garantida no CLIENTE (SessionPage.tsx, hasPromotedStory) — uma chamada HTTP direta a POST /agents/arquiteto/handoff-infra ignorava a regra por completo. OfferInfraHandoffUseCase.execute agora consulta StoryRepository.findByProject e recusa com BadRequestException ANTES de gravar o evento ou chamar o engine, quando nenhuma história tem status !== 'draft'.

B1 — a delegação Dev Lead → dev vira dado. O ADR 0053 (item 5) já previa isto e declarou fora de escopo à época — CLAUDE.md listava como corte reversível. Fechado aqui: AcceptParallelizationUseCase.execute (lado API, cobre os caminhos direto E aprovado de graça, porque os dois já convergem nele) grava delegations com area: 'dev', leadAgent: 'dev-lead', subagent = o id do dev-<modulo> recém-ativado. Diferente de QA/Infra (que gravam do lado ENGINE, porque é lá que o subagente produz um parecer), status: 'completed' foi REDEFINIDO para esta área: significa "a delegação foi EFETIVADA" (o agente subiu), não "parecer emitido" — e parecerArtifactId aponta para o artifact.module_map mais recente do projeto (via SessionEventRepository.listByTypeForProject, método já existente, sem consulta nova). Sem module_map no projeto — não deveria acontecer —, a delegação não é gravada com id falso: só loga o estado inesperado e segue, porque a ativação do dev agent já é sucesso quando essa gravação é tentada; falha em RecordDelegationUseCase também não derruba a ativação, pela mesma razão. Decisão registrada no ADR 0094, que também revoga o corte do ADR 0053 item 5.

docs/fluxo.yml (dev-lead, saída delegacao): status: lacuna → status: ativo, regra: RN-405.

Auditoria fluxo.yml × código — Onda 6 (última): o gate necessidade-validada (RN-406, ADR 0095)​

Fecha a ÚLTIMA das seis ondas do plano da auditoria (seção D, docs/explanation/auditoria-fluxo-vs-codigo.md) — as seis fecharam: a 3, 4 e 5 tinham sido antecipadas fora de ordem (analytics/DORA, gate implementavel, QA-estratégia/appsec, ADRs 0089/0090); a 1 e a 2 fecharam nos PRs anteriores (fluxo.yml/gates.yml em dia; RN-160 no backend + delegação Dev Lead → dev). Esta é a 6ª: o gate necessidade-validada (Criativo → PO), declarado proposto desde o ADR 0085 sem mecanismo nenhum atrás.

modelo-de-time.md já registrava por que ele ficou parado: o Criativo (o modelo) decidir sozinho que a necessidade que ele mesmo produziu está validada seria autovalidação, não gate de verdade. A decisão do dono do produto foi confirmação humana SEPARADA — um terceiro botão em SessionPage.tsx, "Confirmar necessidade validada", no MESMO padrão de "Confirmar arquitetura pronta" (RN-160): rota dedicada (POST .../agents/criativo/validate-necessity), caso de uso dedicado (ValidateNecessityUseCase), evento próprio (necessity.validated). Nunca reaproveitou confirm_readiness/RN-142 (que continua sendo só o piso estrutural, "≥1 regra capturada") nem o aceite do handoff pelo PO (AcceptHandoffUseCase, estrutural, sem julgamento de conteúdo).

O encadeamento escolhido: o botão só habilita DEPOIS que confirm_readiness já consolidou o artifact.product_brief (hasProductBrief) — não faz sentido "validar" um resumo que ainda não existe, e é a leitura mais consistente com docs/fluxo.yml (o gate é de SAÍDA do Criativo, o momento em que o trabalho dele já entregou um artefato concreto). Diferente de OfferInfraHandoffUseCase, esta confirmação NÃO sinaliza o engine — o handoff Criativo→PO já aconteceu dentro do próprio confirm_readiness, então o evento novo é só o registro do MÉRITO, sem gatilho nenhum esperando por ele.

docs/gates.yml ganha o gate necessidade-validada: active, aprovacao_humana: true, mas severidade: warn — nada no produto hoje CONSULTA a passagem deste gate antes de deixar o PO seguir, diferente dos block de story-promovida/plano-de-adocao, que têm trava real de código por trás. docs/fluxo.yml (criativo, gate_saida): status: proposto → status: ativo, regra: RN-406.

Auditoria fluxo.yml × código — B4 (a ÚLTIMA pendência): o PO lê métricas de produto (RN-407)​

Fecha o item B4 — a última linha da tabela "Backlog do modelo de time (ADR 0085)" em docs/explanation/backlog.md; a tabela fica VAZIA depois desta correção, e a auditoria fluxo.yml × código termina aqui. Sem ADR novo: segue o mesmo padrão já estabelecido pela RN-164 (leitura de agente escopada ao projeto, sem efeito externo, sem proposed_action), só com uma RN nova. docs/fluxo.yml (papel po, entrada metricas-de-produto): status: lacuna → regra: RN-407.

O DADO já existia desde o ADR 0089 (analise:funil — funil real sessão → commit → PR → merge, lead time real, deployment frequency real); faltava só o PO conseguir LER esse relatório dentro do turno. Terceira ferramenta de leitura do PO, listar_metricas_de_produto (GET .../product-metrics), mesmo desenho exato das duas irmãs da RN-164 — :direct, sem parâmetro, escopo fechado no projeto. As funções de CÁLCULO puras e a query (calcularFunil/calcularLeadTimes/ leadTimeMedioMs/deploymentFrequencyPorDia/buscarAcoesGitDoFunil) migraram de apps/api/scripts/analise-funil.ts para apps/api/src/application/services/funil-metrics.ts — refatoração MECÂNICA e necessária (scripts/ não pode viver em src/, e src/ não importa de scripts/): o script passou a REEXPORTAR dali, sem mudar assinatura nem comportamento, e o teste de regressão do script continua verde sem ser tocado. O corpo JSON não tem campo para as três ausências permanentes que o script já declarava só em texto (funil de produto completo ideação → commit, evidência de adoção por feature, MTTR/change failure rate) — a ferramenta do PO as cita pelo nome no TEXTO que devolve ao modelo, sempre, para ele nunca concluir por omissão dos números que não há lacuna.

GOVERNANCE.md — o critério do modo community, não o mecanismo dele​

Item do backlog anterior ("Modo community do approval-ladder") virou correção de registro, não implementação: investigação confirmou que o modo community já existe em código desde a FASE 6 (scripts/ci/approval-ladder.ts), testado nos dois lados, só desligado por APPROVAL_MODE=solo (default). A frase do backlog ("vira mudança de aprovacao_humana no registro de gates") vinha de uma linha ESPECULATIVA do ADR 0054 — não editada, por regra, mas a imprecisão está registrada em docs/explanation/backlog.md: aprovacao_humana do gate aprovacoes-da-escada já é true fixo nos dois modos, sem campo nenhum no schema do Gate que reaja a APPROVAL_MODE.

O que faltava de verdade era outro TODO(humano), em docs/explanation/branching-policy.md: o critério de quem entra em cada lista de aprovadores (devs/po/gestão) — quem entra, quem sai, com base em quê. GOVERNANCE.md (raiz do repo, novo) fecha isso: MECANISMO continua em branching-policy.md (a escada, pessoas distintas, o passo a passo da troca de variável), CRITÉRIO passa a morar em GOVERNANCE.md. É proposta, não comitê em funcionamento — o projeto continua com um mantenedor só, e o documento é explícito sobre isso: a maior parte dele descreve o que passa a valer QUANDO houver mais gente, não uma estrutura já ativa. Regra warn nova no docmap (governanca) cobra revisão quando approval-ladder.ts/ branching-policy.md mudam — separada da regra block existente (politica-de-branches) pelo mesmo motivo que checks-e-rulesets é separada dela: MECANISMO decorre do código, CRITÉRIO é decisão humana.

SMTP real no MailSender (RN-408, ADR 0096)​

Fecha o item de backlog aberto desde o corte do Keycloak (ADR 0032, "SMTP real continua sendo config futura"). nodemailer sobre transporte SMTP puro (protocolo de LINHA com estado/MIME/STARTTLS/AUTH, diferente das APIs JSON HTTP que o resto do produto integra), atrás de MAIL_TRANSPORT — log (default, INCLUSIVE em produção: enviar e-mail de verdade é opt-in do operador) ou smtp, selecionado por useFactory em AuthUseCasesModule sem tocar nenhum caso de uso (RegisterUseCase, RequestPasswordResetUseCase, LoginUseCase, o script migrate-keycloak-users.ts continuam só injetando MailSender). SMTP_HOST/SMTP_USER/SMTP_PASSWORD/SMTP_FROM seguem o padrão RN-114 em produção quando o modo é smtp — mas SEM o default público que as quatro RN-114 originais têm, então a régua só entra quando o operador optou por smtp; a validação roda no construtor de SmtpMailSender, exercitado pelo useFactory na montagem do grafo de providers (mesmo desenho de CREDENTIALS_MASTER_KEY), não por chamada eager em main.ts. Corpo em TEXTO PURO, nunca HTML — a porta não carrega estrutura para corpo rico, e um template engine seria superfície de injeção sem ganho nenhum.

Investigação achou uma lacuna real antes de codar: email_verification não tinha rota web nenhuma — verificarEmail existia em apps/web/src/lib/auth.ts desde a Fase 7a sem NENHUM chamador, então o link do e-mail de verificação levaria a lugar nenhum assim que SMTP passasse a mandar e-mail de verdade. /verificar-email (VerifyEmailPage.tsx) fecha isso, espelhando SetPasswordPage.tsx — mesmo padrão de validateSearch no router.tsx, mesma resposta única para link inexistente/expirado/já usado — com uma diferença: sem formulário, a confirmação dispara sozinha ao montar, então a tela precisa dos três estados da RN-088 (carregando/erro/sucesso), não só dois.

"N agentes online" no dashboard — status AO VIVO, nunca presença histórica (RN-409, ADR 0097)​

Item do backlog anterior fechado. Investigação prévia confirmou que não existia agregado de liveness nenhum, nem por projeto nem por workspace — só presença HISTÓRICA (RosterFacts, "já apareceu alguma vez") e status ao vivo de verdade só no cliente, derivado do event log, só quando um projeto está ABERTO. ProjectCardSummary.onlineAgentCount soma dois mecanismos pela MESMA régua ("não ocioso, não travado"): dev agents (engine.dev_agent_states.status NOT IN ('idle', 'idle_tripped'), agregado em lote no engine) e agentes conversacionais (último agent.status da sessão mais recente com status !== 'idle', payload que só aceita working/idle/awaiting_approval). QA/SecOps nunca contam — não emitem agent.status, veredito único por invocação, sem noção de "ocioso" entre chamadas.

A decisão de arquitetura que vale registrar: a consulta lê engine.dev_agent_states DIRETO, via SQL cru batelado por workspace, dentro do MESMO Promise.all que já soma onze consultas do read model de RN-090 (DOZE → CATORZE, ainda CONSTANTE) — em vez de abrir uma rota HTTP interna nova no engine. api e engine compartilham o MESMO banco físico, separados só por schema Postgres, e já havia precedente (apps/api/scripts/medir-execucao.ts já lê engine.oban_peers); esta decisão eleva o padrão de script manual para código de produção TESTADO (fixture mínima e declarada da tabela do engine em test/support/global-setup.ts). Consequência aceita e declarada no ADR 0097: a consulta pressupõe que quem opera o produto migra os dois lados juntos (db:migrate E engine:migrate), e falha alto/visível se só a api migrou — sem try/catch escondendo o erro.

Sessão de execução não fecha mais com dev agent pendente (RN-411)​

Correção de defeito achado numa sessão de execução REAL, não item de roteiro: cinco dev agents subiram, ficaram idle_tripped (RN-047, o circuit breaker) travados esperando o usuário desbloquear tarefa a tarefa, e o heartbeat de 30s da sessão (Engine.Sessions.SessionServer) fechou a sessão por baixo enquanto o desbloqueio manual continuava. O terceiro sinal de GetSessionPendingWorkUseCase (RN-064) só lê agent.status, vocabulário dos agentes conversacionais — dev agents falam dev.* (Engine.Dev.AgentIo) e nunca emitem agent.status, então nenhum dev agent jamais segurava a sessão por esse sinal. O quarto sinal lê o último evento dev.* de cada dev-<modulo>/dev-<modulo>-2 na sessão: dev.working/dev.blocked/dev.idle_tripped seguram a sessão (travado esperando decisão humana É trabalho pendente); só dev.idle (drenado de verdade) não segura. dev.awaiting_gate/dev.awaiting_approval ENTRARAM na régua pela RN-412 — ver abaixo; a lacuna residual que este parágrafo descrevia foi fechada.

Workspace pessoal automático no cadastro (RN-410)​

Achado navegando: o botão "Novo projeto" do dashboard não fazia NADA, sem erro. Causa raiz: RegisterUseCase e SocialLoginCallbackUseCase (no ramo que provisiona conta nova) criavam usuário e credencial mas NUNCA um workspace — TODO cadastro novo caía nessa parede, e só não aparecia antes porque seed.ts sempre cria um workspace junto dos dados de demonstração. Os dois pontos agora criam o workspace e adicionam o usuário como owner na MESMA transação que já cria a conta. Nome/slug saem de uma função pura única, nomeESlugDoWorkspacePessoal (domain/auth/personal-workspace.ts), para a regra não divergir em dois arquivos; o slug é sempre sufixado com userId.slice(0, 8) (mesmo padrão de extraDevAgentId) para ser único sem round-trip ao banco (RN-410).

413 nas PRs e Executores "sem execução" — as duas pontas e o encadeamento (RN-412, ADR 0098)​

Não é fase planejada: correção pedida por USO real ("nas PRs sempre está estourando entity too large") mais o defeito irmão da aba Executores aparecer vazia com trabalho real rolando. Investigação achou os dois encadeados: o gate morre com 413 → devs ficam presos em dev.awaiting_gate → esse estado não segurava a sessão (lacuna da RN-411) → o heartbeat de 30s fecha a sessão por baixo → a aba exige sessão ativa e some com o roster inteiro.

A causa do 413 era da API do Brabo, não do provider de LLM — os comentários que atribuíam o erro ao provider (runtime.exs, terminal_executor.ex, docker-compose.yml) estavam errados; FalhaDeTurno.origem({413, _}) já classificava como "codigo", e estava certo. apps/api/src/main.ts nunca configurou limite de body do Express — valia o default de 100 KB, muito abaixo dos 8 MB que o Phoenix aceita, no sentido mais pesado do transporte (engine → api, POST /internal/sessions/:sessionId/llm-turn, que reenvia o histórico INTEIRO a cada iteração do ToolLoop). API_JSON_BODY_LIMIT (default 10 MB) fecha a ponta do transporte.

A segunda ponta, sem a qual subir o limite só adiaria o estouro: o ContextManager do engine (ADR 0098) tinha DOIS defeitos que mantinham a compactação inalcançável antes do corpo estourar — estimate/1 contava só content, então mensagens de assistant cheias de toolCalls custavam ~zero tokens na estimativa; e a janela de compactação usava só context_window (128.000 tokens nos agentes de gate), o que dava ~350 KB antes de compactar — bem depois de qualquer teto de transporte razoável. A janela EFETIVA agora é min(context_window, teto_de_transporte), com o corte sempre em FRONTEIRA DE ITERAÇÃO do ToolLoop (nunca separando um assistant com toolCalls dos role: "tool" que o respondem — quebraria o protocolo de tool-use do provider).

RN-412 estende a RN-411: DEV_PENDING_TYPES ganhou dev.awaiting_gate (o argumento original — gate pode morrer e travar o dev agent nesse estado indefinidamente) e também dev.awaiting_approval, por um argumento novo achado na investigação: a decisão de aprovação grava proposed_actions.status de forma SÍNCRONA, mas a retomada do dev agent via outbox/Oban é ASSÍNCRONA — na janela entre as duas, nada segurava a sessão, o mesmo defeito da RN-411 um nível mais fundo.

A aba Executores/Visão Geral tinham o defeito irmão do lado web: executionActivated era derivado de events.some(...) sobre a janela de só 200 eventos de useSessionEvents — execution.activated é dos PRIMEIROS eventos de uma sessão e saía da janela em qualquer execução real, apagando o roster inteiro. O valor correto já existia, agregado sobre TODOS os eventos, no resumo do workspace (RN-090, projects-summary.repository.ts) — as duas telas passaram a consumir esse valor. Achado de bônus no caminho: ExecutionSection (Visão Geral) recalculava activated de novo, independentemente, sobre a mesma janela — uma execução longa voltava a oferecer "Ativar execução" como se nunca tivesse começado; corrigido junto. gatesEverOpened sofre da MESMA classe de defeito e ficou DECLARADO como limitação conhecida, não corrigido aqui — corrigi-lo exigiria mudar a assinatura de deriveAgentRoster/rosterFactsFromEvents, fora do escopo desta correção pontual.

Neo4j como grafo de conhecimento — a fundação (RN-413/414/415, ADR 0099/0100)​

Pedido do dono do produto: Neo4j + ollama-model-loader (gemma:1b, yi-coder:1.5b, nomic-embed-text), inspirado explicitamente no repositório ErickWendel/neo4j-ai-experiments — agradecimento registrado no ADR 0099 e em prompts/README.md, pela inspiração concreta do padrão "prompt como arquivo versionado, não string presa no código" e do grafo como memória de agentes.

Duas responsabilidades, uma fundação: templates de prompt versionados (PromptTemplate/PromptVersion, idempotente por hash — RN-413) e memória relacional (interações, hipóteses do Psicólogo com evidência, perfis da Anamnese, handoffs — consumo real é onda futura, esta entrega só constrói o mecanismo). O grafo é memória DERIVADA, nunca fonte de verdade; pgvector continua sendo o índice vetorial dos chunks, sem duplicar embedding em dois bancos. Driver neo4j-driver na api (não no engine) — a api já é dona de toda persistência e do RAG, o engine consome por HTTP interno com service token, como sempre.

rag_search (RN-414) fecha o maior vão do RAG existente: nenhum agente o consultava, só a aba web "Chat RAG". Tool nova no engine, tetos próprios (RN-150), degradação sempre visível no início do texto ao modelo. ollama-model-loader (RN-415) fecha um bug real separado: nomic-embed-text nunca era puxado automaticamente e o RAG degradava pra léxico-only em silêncio em qualquer ambiente limpo.

Primeira leva de templates extraída para prompts/*.md (ux-designer, Psicólogo, Anamnese, sumarização do ContextManager).

Consumo do grafo — templates sem tocar GenServers, relevância sem substituir recência (RN-416/417, ADR 0101)​

InstructionFiles ganhou fonte :graph (db > graph > dir > root — o instruction_patch do usuário continua vencendo tudo), e TRÊS agentes passaram a TENTAR resolver template do grafo antes do texto inline, que virou fallback obrigatório: ux-designer (graph_instruction_templates_enabled?), Psicólogo e Anamnese (graph_templates_enabled?, mesma chave pros dois). Duas flags, não uma — colidiriam com defaults CONTRÁRIOS entre frentes paralelas se dividissem a chave; os dois defaults são false. Só context-manager-summarize (dos quatro templates da leva) ficou sem consumidor à época — lacuna que a ADR 0101 declarou aberta e que fechou depois, pelo QUARTO consumidor da MESMA graph_templates_enabled?: o prompt de sumarização do ContextManager (prompt/1, ver a RN-417).

Psychologist.ContextBuilder/Anamnese.ContextBuilder ganharam uma SEGUNDA fonte de contexto — rag_search, com query derivada do gatilho da análise — em COMPOSIÇÃO com a leitura de recentes/janela temporal de sempre, nunca em substituição: sem hit, o comportamento é idêntico ao de antes. Os trechos entram no orçamento EXISTENTE de Triage, e degraded: true do RAG é sempre visível no contexto, nunca escondido.

O grafo se escreve sozinho: GraphProjector (api) drena uma SEGUNDA linha de outbox (aggregateType: 'graph_projection', mesma transação, valor que o filtro do engine — IN ('session', 'task') — nunca casa, evitando a corrida com o Engine.Outbox.Drain que já drena 'session' em ~2s) e chama os casos de uso de gravação já existentes. O engine NUNCA escreve no grafo direto — preserva o event log como única fonte de verdade; o grafo é reconstruível por replay.

Achado real durante a verificação, registrado porque ensina: o teste de body limit da RN-412 (apps/api/test/main.spec.ts) tinha um bug LATENTE — AppModule importado ESTATICAMENTE no topo do arquivo avalia DrizzleModule (que cria seu pool de conexão em escopo de MÓDULO, não numa factory do Nest) ANTES do beforeAll conseguir apontar DATABASE_URL pra base de teste. Ficou mascarado enquanto a base de dev local também estava migrada (mesma tabela, então a query "achava" mesmo apontando pro lugar errado); destravou assim que a base de dev foi recriada vazia (pelo teste de fumaça real desta própria onda contra Neo4j+Postgres). Corrigido com await import(...) DINÂMICO dentro do beforeAll, depois do DATABASE_URL já setado — é o padrão a seguir sempre que um teste precisa redirecionar DATABASE_URL para um AppModule real.

Runner local — execução na máquina do usuário (RN-418/419/420, ADR 0102/0103)​

Pedido do dono do produto: agente rodando na MÁQUINA DO USUÁRIO, na pasta local do projeto, pelo terminal padrão do usuário — mais terminal interativo de verdade na aba Code. Duas mudanças, uma dependendo da outra.

Política (ADR 0102 — decisão GLOBAL, confirmada explicitamente pelo dono do produto depois de um aviso automático de segurança sobre a mudança): git push/PR/deploy (RN-106) e sudo/doas (novos) saem de deny incondicional e viram TETO ABSOLUTO — require_approval incondicional, no MESMO bloco e MESMO padrão dos tetos vizinhos (current.policy === 'auto_approve'), nunca auto-aprovável por agent_autonomy(curinga "*") nem por permissions.json. A fresta que o deny original tapava à força foi fechada na FONTE: ApproveAlwaysActionUseCase/patternForAction recusam gravar padrão em allow pra ação com efeito externo git ou comando privilegiado — "sempre permitir" aprova só a instância, nunca o padrão, pra esses dois casos. Sem essa segunda metade, o teto seria decorativo. sudo/doas casam por VERBO em comandoPrivilegiadoNoComando (external-effect.ts), varrendo todos os segmentos do comando, mesmo princípio de efeitoExternoNoComando.

Runner (apps/runner, novo workspace Node/TS, ADR 0103): CLI (brabo-runner --project <id> --dir <pasta> --token brb_...) que o usuário roda na própria máquina, conecta no ENGINE via canal Phoenix NOVO (/runner, tópico terminal:<projectId>) autenticado por ticket de USO ÚNICO (mesmo padrão RN-108, mas ticket EMITIDO PELO ENGINE — schema "engine", migration própria — e pedido pela api via rota HTTP interna, invertendo o fluxo do ticket de sessão). Executa exec (comando de agente JÁ APROVADO — o roteamento em TerminalExecutor só acontece DEPOIS do pipeline de decide()/proposed_action de sempre, nunca antes) no $SHELL do usuário, e um modo PTY interativo pra aba Terminal (pty_open/pty_data/ pty_input/pty_resize/pty_close — o engine faz RELAY puro, nunca interpreta os bytes). Só UM runner por projeto (Engine.Runners.Registry, :global, exclusividade de :global.register_name/3); modo runner SEM workspace verificado ou SEM runner conectado RECUSA explicitamente — nunca cai no System.cmd/bind-mount, que não existe pra um projeto runner (revisado pelo ADR 0104/RN-423, ver acima; o fallback pro caminho de sempre continua valendo, mas só pra container/mounted). PTY é ação do USUÁRIO autenticado, não passa por proposed_action, mas audita (terminal.session.started/ended no event log, inclusive quando a aba cai sem pty_close explícito).

A fronteira de segurança do runner NÃO é sandboxing — é autenticação (o CLI apresenta um Personal Access Token da CONTA do usuário) + o pipeline de aprovação de sempre + o consentimento de o usuário rodar o CLI na própria máquina, com os privilégios dele. apps/runner/src/guard.ts valida cwd dentro da raiz por resolução léxica, mas é declarado BEST-EFFORT, não a garantia real.

PAT fechou a lacuna de token de automação (ADR 0105/RN-424..426). O achado real da Onda 1 — "o produto não tem mecanismo de token de conta de LONGA DURAÇÃO pra automação, então o runner replica o login do browser (cookie/CSRF persistidos em ~/.brabo/runner-credentials.json)" — foi fechado na Onda 2: personal_access_tokens (brb_…, hash HMAC-SHA256+ pepper via hashDeToken(), nunca argon2 — errado pra segredo de ALTA entropia) é emitido em Configurações do projeto, escopado a UM projeto, revogável, com expiração opcional. apps/runner/src/auth.ts perdeu por completo login interativo, cookies e o arquivo de credenciais — só valida formato e repassa --token/BRABO_ACCOUNT_TOKEN, NUNCA gravado em disco pelo CLI. O PAT nunca autentica fora de POST .../runner-ticket, por CONSTRUÇÃO (IS_PAT_ROUTE_KEY/ @RequirePatAuth() + PatAuthGuard) — nunca um branch no JwtAuthGuard global, que deixaria RolesGuard autorizar o PAT pra qualquer rota do papel do usuário.

maintainer revoga PAT de outro usuário (RN-427), fechando o item que o ADR 0105 tinha declarado fora de escopo por ora (resposta a incidente — dev desligado com token vazando). Extensão do mesmo modelo, sem ADR novo: rotas SEPARADAS (GET .../personal-access-tokens/all, DELETE .../personal-access-tokens/:tokenId/admin), escopo por projectId em vez de userId — a autorevogação de cada usuário (RN-426) não muda.

@xterm/xterm/@xterm/addon-fit (web) e phoenix/node-pty (runner) são as quatro dependências novas — mesma régua de exceção do mermaid (ADR 0068): import() dinâmico, sem eval/new Function confirmado por grep no pacote instalado (não é garantia formal contra ofuscação, só evidência forte — declarado como incerteza, não afirmado como certeza).

ADR 0104 — Onda 1 CONCLUÍDA (RN-421/422/423): os ADRs 0072 e 0103 nunca se falaram — RN-170 exigia bind-mount na criação, e o roteamento pro runner (RN-420) reusava a mesma flag workspace_mode == 'local' sem bind-mount nenhum, então usar o runner de verdade continuava obrigado a passar pela validação de pasta montada. workspace_mode (2 valores) virou execution_mode (container/mounted/runner, migração 0048) — RN-170 (agora RN-422) passa a valer só para mounted; runner nasce com caminho validado só LEXICAMENTE (sem I/O) e workspaceVerifiedAt: null, promovido a verificado quando um runner conecta e confirma o caminho no HOST (POST /internal/projects/:projectId/workspace-verification, RN-423) — o runner é a FONTE DA VERDADE do caminho, sobrescrevendo o que foi digitado no wizard. Engine.Actions.TerminalExecutor.decisao_de_execucao/1 ganhou QUATRO saídas: roteia só com workspace verificado E runner conectado; sem qualquer um dos dois, RECUSA explicitamente — nunca cai no System.cmd/bind-mount de mounted, que não existe pra um projeto runner.

Achado da implementação, corrigindo o ADR (que não é editado — a correção mora aqui e em backlog.md): a frase do ADR 0104 de que a conversão entre os três modos de projeto existente "passa a ser permitida sem recriar o projeto" está INCORRETA. UpdateProjectDto continua excluindo executionMode/workspacePath de propósito — não é PATCH trivial (worktree, permissions.json e cache do engine apontam pro escopo antigo). A Onda 1 entregou só o campo de três valores na CRIAÇÃO; conversão de projeto existente é onda futura, com desenho próprio, ainda não planejada. Backlog do runner: a Onda 2 (PAT, ADR 0105) fechou o item que bloqueava npm publish @brabo/runner, e a Onda 3 (ADR 0106) entregou a distribuição em si — tsup empacota apps/runner num dist/index.cjs único (node-pty como external obrigatório, binding nativo), publicado a cada tag final por um workflow próprio (publish-runner.yml), paralelo a release.yml. Achado real testado empiricamente: a guarda de auto-run de index.ts precisou de realpathSync em process.argv[1] — sem isso, main() nunca rodava quando o CLI era invocado pelo bin instalado via npm install -g (symlink, que process.argv[1] nunca resolve por realpath mas import.meta.url sempre resolve). Pendência operacional declarada: a publicação de verdade exige o dono do produto criar o Automation Token do npm e configurar o secret NPM_TOKEN — sem ele, o workflow avisa e pula, nunca falha. Exclusividade por {project_id, machine_id} continua adiada até segundo dev simultâneo real; guard.ts best-effort é invariante REAFIRMADO, não lacuna. Ver ADR 0104/ ADR 0105/ ADR 0106.

FERRAMENTA DE DESENVOLVIMENTO — pnpm bootstrap​

Menu de terminal em scripts/dev/bootstrap.sh agrupando o que se faz no dia a dia: Docker, K8s, Database e Test. Existe porque esses comandos moram em TRÊS lugares que não conversam — package.json, Makefile e scripts em deploy/k8s//docker/ — e conhecimento decorado apodrece.

Ele não reimplementa nada: cada folha chama o comando que já existe. O modo --print-commands resolve a árvore inteira sem TTY e sem executar, e é sobre ele que roda o teste (scripts/dev/bootstrap.spec.ts) — um TUI não se testa por unidade, mas o mapeamento menu→comando sim, e é ele que erra. Zero dependência nova; as cores saem de design/tokens.css em ANSI 24-bit com degradação para 256 cores e para nenhuma.

A saída de um comando em execução é ROLÁVEL (roda do mouse em SGR, j/k, PageUp/PageDown, G para voltar ao fim). O que faltava não era ler a roda: era DESLOCAMENTO — tail -n só sabe mostrar o fim —, então a janela virou recorte com sed -n 'a,bp', testável sem TTY por --print-window. Duas consequências que o rodapé anuncia: rolar para trás CONGELA a janela (como o less +F, senão o redesenho de 5 Hz desfaz a rolagem) e o rastreio de mouse, ligado só na tela de execução, é desligado em TODA saída — \e[?1006l\e[?1000l em restaurar_terminal, que o trap de EXIT cobre. O parser de escape é CSI genérico (lê até o byte final): ler dois bytes fixos deixava o resto da sequência no buffer, e como o menu trata [1-9] como escolha, um giro de roda disparava itens do menu.

Três decisões registradas: Create provisiona do zero e Deploy publica num ambiente que já existe (por isso só Deploy tem escolha por serviço); no K8s só All funciona, e Api/Engine/Web aparecem DESABILITADOS em vez de sumirem, porque o menu deve dizer o que o produto não faz; e Database › Delete recria a extensão pgvector, já que docker/postgres/init.sql só roda na primeira inicialização do volume — um DROP SCHEMA puro faria a migração seguinte falhar, e como o engine divide o mesmo banco, recuperar exige db:migrate E engine:migrate.

Docker › Reset total (scripts/dev/reset-total.sh) soma essa sequência inteira numa folha só — preflight, up --build --wait (o --wait do Compose v2.17+ é o "até tudo saudável", sem laço de saúde escrito à mão), o mesmo DROP SCHEMA da Delete, db:migrate + engine:migrate e o seed — com tela de confirmação PRÓPRIA (confirmar_reset, exige digitar RESET), distinta da da Delete (que exige o NOME do banco) e sem a precondição "postgres já de pé" da Delete, porque é o próprio comando que sobe o compose do zero. O seed (apps/api/src/db/seed.ts) ganhou um passo que ativa credencial de provider a partir de .env, reaproveitando as MESMAS variáveis <PROVIDER>_TEST_KEY que os smokes de LLM já usam (apps/api/test/infrastructure/llm/*.smoke.spec.ts) — uma convenção de nome só, dois consumidores; provider sem variável definida não entra, sem erro, e a credencial fica no OWNER (RN-058).

Dois achados ao rodar o reset pela primeira vez, e não pelo item em si — nenhum dos dois é hipotético, os dois quebraram a execução real:

  1. gemma:1b não existe no registry da Ollama (manifest unknown — só gemma3:1b existe), então ollama-model-loader sempre falhava e o --wait nunca fechava verde. Corrigido nos seis lugares que carregavam o valor errado como CÓDIGO — docker-compose.yml/.prod.yml, .env.example, docker/ollama/pull-models.sh, scripts/dev/verificar-modelos-ollama.sh, deploy/k8s/base/ollama/job-model-loader.yaml e a RN-415 em docs/business-rules.md — sem tocar os ADRs 0099/0100 (aceitos) nem a narrativa histórica desta mesma seção do CLAUDE.md, que descrevem o que foi pedido/decidido na época, não o valor operacional corrente.
  2. Mais sério, e mais antigo — Database › Delete (item 3.4, existente desde a criação do menu) nunca dropava de verdade: DROP SCHEMA public CASCADE não alcança engine.* (Ecto/Oban vive em schema PRÓPRIO, engine) nem drizzle.__drizzle_migrations (o controle do drizzle-kit vive em schema PRÓPRIO, drizzle). Resultado: mix ecto.migrate batia em duplicate_table nas tabelas do engine que sobreviviam, e pnpm db:migrate — pior, SEM erro nenhum — via o controle do drizzle-kit intacto, concluía que já tinha rodado tudo e não recriava NENHUMA tabela da api. Delete/Reset total agora dropam engine e drizzle também, nesta ordem, antes de public.

Abas agrupadas, PRs project-wide, pasta local via Runner, carrossel do PO (2026-08-20)​

Cinco ondas independentes, rodadas em paralelo por arquivo disputado, vindas de uso real da tela de projeto — nenhuma planejada de antemão. A sexta onda do mesmo programa (tradução completa de produto e documentação, i18n) é maior que as cinco juntas; a FUNDAÇÃO dela (Onda 6a — mecanismo, sem tradução de conteúdo ainda) já fechou: react-i18next+i18next na web (en virou idioma default do app, pt-BR mantido — RN-425), coluna locale em users embutida no payload de login/refresh, AccountPage nova (/account) provando o mecanismo, e Docusaurus com i18n.defaultLocale: 'en'/locales: ['en', 'pt-BR'] — o snapshot pt-BR atual de docs/ já está preservado em website/i18n/pt-BR/docusaurus-plugin-content-docs/current/. A Onda 6b (extração em massa da interface + tradução de docs/) avançou bastante em 2026-08-22, em quatro frentes paralelas — ver CHANGELOG.md (Unreleased/ Documentação) para o detalhe de cada uma — mas AINDA NÃO fechou: faltam docs/business-rules.md (só o front-matter foi traduzido; o corpo, com centenas de RNs, continua 100% em português — é o maior arquivo da doc inteira e cresce mais rápido do que dá pra traduzir de passagem) e uma fatia residual de componentes .tsx menores. Quando fechar de verdade, esta seção ganha o resumo final e o restante do CLAUDE.md (Stack, "Documentação") é revisado por completo para registrar inglês como idioma primário de verdade.

  • Régua de abas agrupada: 11 abas soltas viraram 6 no topo — Visão geral, Agentes ▾ (Executores, Criativo, Chat, Insights), Dev ▾ (Código, PRs, Aprovações), Documentação ▾ (Backlog, Arquitetura), Gastos, Configurações — via GroupedTabs novo (apps/web/src/components/ ui/GroupedTabs.tsx), construído POR CIMA do Tabs genérico, que continua sem grupo pras outras telas. ChaveDeAba continua flat — só a apresentação ficou agrupada, pra não mexer no deep-link ?tab=. Chat e Chat RAG viram UMA aba (ProjectChatShell.tsx) com controle segmentado interno ("Conversar"/"Buscar") — a distinção de negócio entre os dois (RN-202: conversar ativa agente e gasta a chave do owner por turno; RAG é leitura sobre índice, sem agente) não mudou, só o contêiner de UI.
  • Aba PRs, project-wide: ProjectApprovalsTab escopava "PRs em revisão" à sessão mais recente — a revisão de uma PR de sessão anterior sumia da tela assim que uma sessão nova propunha outra. A aba prs nova lista direto do provider de git (nunca por sessão) e cruza com proposed_action pendente via findPendingByProject, novo, ao lado do método já existente escopado por sessão — decisão usa o sessionId da PRÓPRIA ação, nunca latestSession (RN-423). É a primeira produtora real de git_merge pela UI; a trava de branch protegida (RN-154) continua absoluta e intocada — botão "Merge" só PROPÕE, a aprovação humana continua sendo o único jeito de executar.
  • Aba Arquitetura: extraída da Visão Geral (module_map, diagrama C4, ADRs, pendências), que fica com um resumo condensado + link. Primeiro lightbox do design system — Modal ganhou size="full" — pra ampliar o diagrama C4 (SVG, sem perda de qualidade) (RN-424).
  • Navegação de pasta local via o Runner (RN-422, ADR 0104): revisa a ADR 0072 (que tinha recusado seletor de pasta explicitamente) SEM editá-la — a navegação existe, mas pelo Runner, que já roda com o privilégio real do usuário na máquina dele, nunca pela api enumerando o filesystem do container. Dois eventos novos no canal já existente (fs_list_dir/fs_home_dir), relay puro do engine, mesmo padrão do PTY. FolderBrowserModal funciona onde o projeto já existe. Na criação (NewProjectWizard.tsx), o gap ficou declarado até o ADR 0108: no modo mounted o projeto ainda só nasce na confirmação (a validação de caminho toca disco ali), mas no modo runner "Procurar pasta..." passa a criar o projeto ANTECIPADAMENTE — reusado por snapshot de identidade em vez de duplicado a cada clique — pra poder ancorar o ticket do canal antes da confirmação (RN-437, ADR 0108). RunnerOnboardingPanel novo (compartilhado com a aba Terminal) substitui o <code> cru de antes. apps/runner ganhou README — publicar de verdade no npm/empacotar binário assinado continua fora do escopo, declarado no README. O explorador de pasta em si virou três colunas (atalhos, lista com um clique seleciona/duplo clique entra, painel de detalhes — RN-436), seguindo a referência visual do dono do produto.
  • Bug do carrossel do PO corrigido (RN-421): a leva de promoções pendentes dependia de scan sobre os últimos 200 eventos (useSessionEvents) — numa sessão longa, a proposta saía da janela e o carrossel degradava pra card único ou sumia. Mesma classe de bug que a RN-180 já tinha corrigido pra ContextAside; a fonte agora é useBacklog (completo, sem janela), com fallback por história quando o backlog ainda não respondeu.

Handoff manual a agente à escolha (RN-440/441, ADR 0109)​

Fecha o item de backlog aberto desde a FASE 13c. CreateHandoffUseCase continua sendo o ÚNICO lugar que grava toAgent (ADR 0038) — ganhou só um actor?: Actor opcional, default {kind:'agent', id: fromAgent} de sempre. RequestManualHandoffUseCase (rota nova, POST .../sessions/:sessionId/handoffs, papel developer) valida toAgent contra addressableAgents() — catálogo FECHADO (leads de área ∪ SOLO_CONVERSATIONAL_AGENTS), mais estrito que assertHandoffTargetAllowed (que só recusa subagente) — resolve fromAgent pelo agent.activated mais recente da sessão (sentinela "usuario" sem nenhum), e grava actor: {kind:'user'}. O handoff nasce offered igual a um automático: o card de aceite já existente (SessionPage.tsx) o pega sozinho, zero mudança no caminho de aceite.

Investigação ANTES de codar achou ux-designer (ADR 0087) na MESMA situação do Staff (ADR 0088) que motivou o item: cláusula de message/2 pronta em agent_command_controller.ex, nenhum caminho humano até ela. Os dois entraram em AGENTES_DE_CHAT na mesma mudança — infra/qa continuam de fora, mesmo padrão já documentado ali (leads sem kickoff/1 nem cláusula de message). Declarado, não escondido: o mirror de SOLO_CONVERSATIONAL_AGENTS do lado web (apps/web/src/lib/agents.ts) NÃO é cruzado por teste com o da api — divergir produz, no pior caso, uma opção velha no seletor que o backend ainda recusa com 400.

Budget por área — fecha o corte do ADR 0038 (RN-443, ADR 0110)​

Item aprovado do backlog ("Older backlog", docs/explanation/backlog.md). agent_areas ganha budget_micros/spent_micros, espelhando exatamente max_parallel — mesma linha, mesmo dono da decisão (maintainer), sem tabela nova. É um TERCEIRO teto ADITIVO ao lado de projeto e sessão (CheckBudgetGateUseCase checa os três em paralelo, qualquer um bloqueado já recusa), não a cascata de binding de modelo do ADR 0064 (sessão > agente > área > projeto > workspace, "o mais específico vence") — os dois mecanismos compartilham a palavra "área" e mais nada; não confundir os dois é o ponto central do ADR 0110, do mesmo jeito que RN-101/ADR 0063 já separa a visão do owner da visão do membro no relatório de gasto. spent_micros soma SEMPRE que o ator pertence a uma área (lead ou membro, via areaDo — função pura, sem tocar banco pra ator sem área), com ou sem budget_micros configurado (null é o default, sem teto).

Binário standalone do runner (2026-08-23, RN-451/452, ADR 0112)​

Item de backlog do ADR 0104 ("standalone binary, pkg/bun build --compile"), companion do ADR 0106 (distribuição via npm). Decisão explícita do dono do produto: binário ÚNICO de verdade, matriz completa com Windows desde a v1 — a alternativa mais barata ("binário + pasta node-pty-native/ ao lado") foi oferecida e recusada.

bun build --compile embute o .node nativo do node-pty via with { type: 'file' } e o extrai pra um diretório TEMPORÁRIO REAL em runtime (native-pty-loader.ts) — necessário porque node-pty resolve o próprio addon por um require() de caminho COMPUTADO (lib/utils.js#loadNativeModule), que o Bun não consegue embutir sozinho (build passa, runtime lança Cannot find module, testado antes de desenhar a correção). Cinco plataformas (linux-x64/linux-arm64/darwin-x64/darwin-arm64/win32-x64), cada uma construída no seu runner NATIVO (.github/workflows/build-runner-binaries.yml, gatilho de tag final, mesmo padrão de publish-runner.yml), binário anexado à Release já existente.

Validação honesta, por plataforma: só linux-x64 foi executado de verdade neste ambiente (build:bin+smoke:bin verdes, repetidamente — foi rodando o binário real que os dois bugs abaixo apareceram). As outras quatro — Windows em especial, com TRÊS arquivos nativos em vez de um — foram raciocinadas lendo o código-fonte do node-pty, nunca executadas numa máquina real daquele SO; a primeira execução real delas é a PRÓXIMA tag de versão, não esta PR. Nenhum fallback por plataforma foi aplicado preventivamente — nada nesta investigação PROVOU que alguma das cinco não funciona assim; se uma tag real mostrar que sim, isso vira item novo, não suposição de hoje.

Dois defeitos achados só rodando o binário compilado, não lendo a API: --external node-pty precisa estar no comando do bun build mesmo sem import estático nenhum de node-pty no código — o await import(...) de fallback (usado só fora do binário) é descoberto e embutido pelo Bun de qualquer forma, e falha em runtime com erro enganoso; e a guarda de auto-run (ADR 0106) precisou de um segundo ramo — process.argv[1] dentro do binário é um caminho VIRTUAL (/$bunfs/root/...) que realpathSync não alcança, lançando ENOENT fora de qualquer try/catch antes de main() ser tentado.

Converter execution_mode de projeto existente (RN-447..450, ADR 0111)​

Fecha a correção que a Onda 1 do runner (ADR 0104) tinha registrado em docs/explanation/backlog.md: o item 4 daquele ADR dizia que converter execution_mode entre container/mounted/runner num projeto que já existe "passa a ser permitida sem recriar o projeto", e isso era falso até aqui — UpdateProjectDto continua excluindo os dois campos de propósito. PUT projects/:projectId/execution-mode (maintainer, ConvertProjectExecutionModeUseCase) é rota DEDICADA, não um PATCH afrouxado: ela orquestra a migração em vez de só trocar a coluna.

Recusa com 409 enquanto qualquer dev agent do projeto (em QUALQUER sessão, lido direto de engine.dev_agent_states por project_id, mesmo caminho da RN-409) não está idle — idle_tripped CONTA como ativo aqui, ao contrário do critério de "online" da RN-409, porque o agente ainda tem workspace_root capturado em memória esperando desbloqueio humano (RN-447). PermissionsFileStore ganhou move(from, to): o conteúdo do permissions.json nunca muda, só o caminho físico, porque projectScopeRoot deriva do par (modo, caminho) (RN-448). Saindo de container com container provisionado, o ciclo de vida (ADR 0081) é levado a removed — via stopped primeiro se estava running — ANTES da coluna mudar, porque RegistrarTransicaoDeContainerUseCase recusa transição fora do modo container; entrar EM container nunca auto-provisiona (RN-449). workspaceVerifiedAt zera em toda conversão real, inclusive voltando para runner com caminho de aparência igual (RN-450). Mesmo (modo, caminho) de hoje é no-op — nem checa dev agent ativo.

Consequência declarada, não escondida: a conversão NUNCA copia o código já escrito entre a raiz antiga e a nova. Isso é seguro para o que já foi COMMITADO — o engine deriva o worktree do bare repo do zero (Engine.Actions.Workspace.ensure!/4) sempre que a pasta de destino ainda não está marcada pronta, então o próximo agente reconstrói a árvore de trabalho na raiz nova a partir do git de verdade. O que NÃO sobrevive é diff NÃO commitado no worktree abandonado — ele fica órfão no disco antigo, nunca migrado nem apagado. O teto de dev agent ativo (RN-447) reduz o risco (nunca converte com um agente working) mas não elimina: um agente idle entre tasks pode estar sentado sobre um diff sujo, e nada aqui inspeciona limpeza de working tree antes de converter.

Pasta local anexada como referência do Chat RAG (2026-08-23, RN-455..457, ADR 0113)​

Pedido do dono do produto: anexar uma pasta da PRÓPRIA máquina do usuário a um projeto como referência de LEITURA para os agentes — sem o CLI brabo-runner (ADR 0103). Explicitamente NÃO é execução/workspace de projeto: execution_mode: runner (ADR 0104) precisa do CLI porque precisa de um caminho de HOST real pro container rotear comando; isto é mais parecido com "anexar arquivos a uma conversa". O navegador lê o CONTEÚDO (File.text()) e o caminho RELATIVO (File.webkitRelativePath) dos arquivos escolhidos — nunca um caminho absoluto de host (a API de File não expõe um, pra nenhum site), e por isso o ADR 0113 é explícito que NÃO é uma revisão do que os ADRs 0072/0107 recusaram: aqueles recusavam RESOLVER um caminho de host a partir do navegador; aqui não existe caminho nenhum a resolver, só bytes que o navegador já tinha o direito de ler.

chunks.scope (ADR 0079/0080) ganha um QUARTO valor, 'local' (migração 0052, ALTER TYPE ... ADD VALUE, aditivo — os dois CHECK da migração 0045 já são escritos como "é session ou não é", então local cai do mesmo lado de docs/adr sem CHECK novo). IndexLocalFolderUseCase espelha IndexProjectDocsUseCase de perto — chunking Markdown-aware pra .md/.mdx, RagEmbeddingService, chunk sem vetor em vez de falhar quando o embedding cai (RN-233) — mas REJEITA (400) o upload inteiro quando os tetos AGREGADOS estouram (quantidade de arquivos, bytes somados), nunca trunca em silêncio: é um gesto ÚNICO do usuário com um seletor de pasta na tela, não uma varredura em background. Arquivo individual grande demais ou de extensão não reconhecida só é PULADO (filesSkipped), nunca derruba o lote (RN-456). origemDoChunk não precisou de nenhum ramo novo: local carrega sourcePath como docs/adr, cai no kind: 'file' que a citação já sabia renderizar.

Nunca entra no "Reindexar agora" genérico (RN-457): ReindexProjectUseCase reindexa lendo de uma fonte que o SERVIDOR consegue revisitar (repositório do projeto, event log); local não tem fonte nenhuma pra revisitar — o texto só existe no navegador de quem anexou. Reanexar a pasta (novo upload em POST .../rag/local, maintainer) É o mecanismo de resincronizar, um botão deliberadamente separado, com comentário cruzado nos dois casos de uso apontando um pro outro e pro ADR 0113, pra uma "correção" futura não religar os dois e apagar o material do usuário. A cobertura (RagCoverage.local) é forma PRÓPRIA — não há "total no repositório" pra comparar — e lastAttachedAt é a ÚNICA exceção real à regra "nunca Xmin" (RN-237): um MAX(chunks.created_at) de verdade, não chutado (RN-458).

Faixa de atividade do turno — o fio só recebe a resposta no fim (RN-459/460)​

Pedido do dono do produto: a tela de Sessão passa a mostrar, ACIMA do composer, uma faixa que narra em linguagem humana o que um agente conversacional (Criativo, PO, Arquiteto, Dev Lead, UX Designer, Staff) está fazendo DURANTE o turno — referência visual: a linha de status do Claude Code. O fio só recebe a bolha de resposta DEPOIS que o turno termina; a regra é do CHAT (canal Phoenix) e vale só pros seis conversacionais — o chat consultivo sem agente ativo (SSE, streamChatMessage) continua com a bolha de streaming de sempre, intocada.

Mecanismo: os seis servers já emitiam tool.call DURÁVEL; passam a também broadcast(state, "tool.call", %{tool: name, agent: @agent}) — EFÊMERO, sem args (RN-096/RN-412) — pro canal entregar em tempo real. Não há behaviour/macro compartilhado entre os seis dispatch_tool (quatro formas estruturais distintas, confirmadas por leitura completa) — a mudança foi seis edições adaptadas à forma local de cada um, não uma refatoração. Do lado web, um reducer PURO (reduzirAtividadeDoTurno/lib/atividade-do-turno.ts) acumula o agent.delta como texto corrente; ao chegar tool.call, arquiva o corrente não-vazio como linha de narração, zera, e sempre adiciona uma linha de ferramenta com fraseDaFerramenta(tool) (dicionário das 19 ferramentas, lib/narracao-de-ferramentas.ts, sem RN própria). Uma flag PRÓPRIA (turnoViaCanal, nunca derivada de statusAgent/streamingAgent, que passam por janelas legitimamente null no meio do turno) decide se a faixa aparece ou a bolha antiga — nunca as duas.

Regra de apresentação (histórico): agruparNarracoesDoTurno, nova passada pura depois de afundarDesfechos (RN-172) no mesmo useMemo — agent.response consecutivas do mesmo turno+autor viram um Disclosure compacto ("Passos do turno · N"), só a última fica intacta fora dele. turno === 0 (o prólogo sentinela de turnoDoSeq) fica de FORA do agrupamento — fixtures antigas empilham agent.response sem fronteira de turno pra testar outro mecanismo (RN-138/RN-177), e sem a exclusão o agrupamento novo produziria Disclosure dentro de Disclosure ali. afundarDesfechos em si não mudou.

Achado no caminho, escopo estendido por decisão do dono do produto: a investigação achou que QUATRO dos seis servers (Arquiteto, Dev Lead, UX Designer, Staff) terminavam CALADOS no teto de iterações — defp run_turn(state, remaining) when remaining <= 0, do: state, sem evento nenhum, contradizendo a regra permanente de que laço de agente não termina calado. Só o PO já emitia toolloop.limit_reached (RN-166); o Criativo diverge deliberadamente com agent.error (decisão documentada no próprio código). Os quatro corrigidos passam a emitir toolloop.limit_reached, mesmo evento do PO (RN-459).

Migrado do índice do CLAUDE.md em 2026-09-11​

As entradas abaixo vinham da tabela-índice do CLAUDE.md e não tinham seção própria neste arquivo — a narrativa delas existia num lugar só. Foram movidas sem reescrita, na ordem em que estavam, quando a tabela foi reduzida a índice. O que estiver aqui é histórico; o que vale hoje mora nas RNs, nos ADRs e na seção "Estado atual e aberto" do CLAUDE.md.

PROGRAMA 16–26 (ADR 0060–0072)​

Handoff de design adotado; 8 telas; kind de sessão; eventos paginados; Gastos; modelo por área; container por projeto (25b cortada); aba Code

pnpm bootstrap​

Menu de terminal sobre comandos existentes; Reset total

Ollama nativo / pull de Hugging Face (RN-461..463, ADR 0114–0115)​

Bootstrap dev pergunta uma vez e persiste o modo do Ollama em .env; navegador de modelos do Hub com pull em duas etapas e allowlist de publisher oficial

Lockfile próprio do website (ADR 0117)​

website/ sai do workspace pnpm da raiz — lockfile e overrides de segurança isolados, pnpm audit do produto para de reportar a árvore do Docusaurus

Configuração automática do runner pelo navegador (RN-464..466, ADR 0118)​

Chave de dispositivo Ed25519 gerada no navegador, aditiva ao PAT; proxy do binário via GitHub Releases; File System Access API grava a pasta configurada, com fallback de download fora do Chromium; --project/--dir/--token viram opcionais no CLI

Imagens publicadas no GHCR (ADR 0119)​

release.yml publica as quatro imagens a cada tag final e registra os digests em .release/images.json; overlay de produção deixa de apontar para um placeholder

Schema por agregado (ADR 0121)​

db/schema.ts vira barrel; as 51 tabelas e 34 enums viram 16 arquivos sob db/schema/, um por agregado de domain/*

SessionPage.tsx em 5 PRs mecânicos (ADR 0122)​

3 807 → 2 661 linhas; timeline/turno, StorySlide, StructuredQuestionCard, árvore de backlog + ContextAside, hook useSessionReadiness — cluster do canal de turno e ProjectSettingsTab.tsx seguem fora, declarado

Hook do canal de turno do SessionPage.tsx (ADR 0124)​

2 661 → 2 479 linhas; PR mecânica de dedup (iniciarTurnoDoAgente/finalizarTurnoDoAgente/cancelarTurnoOtimista) seguida da extração de estado + efeito do canal Phoenix para o hook useTurnoDoAgente (lib/session-turno.ts) — fecha o item que a ADR 0122 deixou declarado em aberto; ProjectSettingsTab.tsx segue fora, decisão separada

ProjectSettingsTab.tsx por seção (ADR 0125)​

2 532 → 77 linhas; as 17 seções viram um arquivo cada sob routes/settings/ (+ shared.ts com os DOIS helpers de mais de um chamador), e o arquivo antigo fica no caminho como entrada e barrel — caminho e os 11 nomes exportados são contrato de 3 testes. Fecha a ÚLTIMA metade da linha de dívida de docs/architecture.md

Trilho vertical de navegação do projeto (ADR 0126, RN-201)​

A régua horizontal de dois níveis (GroupedTabs) vira um trilho vertical com os TRÊS grupos abertos ao mesmo tempo; geometria do trilho do CodeShell, teclado portado, os 5 contadores seguem separados. Revisa a RN-201: a aba de Código não recolhe mais a sidebar sozinha

Sumário ancorado de Configurações (CHANGELOG)​

As 17 seções da aba ganham id, <section> com nome acessível e um sumário em quatro grupos DENTRO da área de conteúdo — sem faixa vertical nova, moldura segue nos 444px do ADR 0126. Registro em routes/settings/sumario.ts (terceira lista do mesmo desenho de project-tabs.ts), deep-link ?section=, spy por IntersectionObserver; só entra no sumário a seção que MONTOU

Padrão único de valor herdado (CHANGELOG)​

As QUATRO redações de "não tem valor próprio" da aba Configurações viram dois polos e um verbo, de uma fonte só (routes/settings/heranca.tsx): rótulo diz o ESTADO, detalhe diz a CONSEQUÊNCIA. Vocabulário unificado, forma não — a marca só entra onde o controle não mostra o estado sozinho

Salvar por seção em Configurações (RN-469)​

Paralelismo e Teto de gasto trocam N botões de linha por UM da seção (settings/secao-salvavel.tsx), com contagem de linhas pendentes e desfecho POR LINHA — salvar é N chamadas, não uma transação, e a tela nunca afirma o que não obteve. As três seções de autosave e Credenciais ficam de fora, cada uma por um motivo declarado

Painel "precisa de você" (RN-467)​

Chip no topo do projeto abre as CINCO filas de decisão num lugar só — separadas, ordenadas por urgência, sem soma nenhuma (nem no chip). Aprovações e merges decidem ali pelo ApprovalCard; as outras três levam à aba. Pendência de arquitetura empresta a data da história e DIZ que emprestou

Cascata de modelo como cadeia visível (RN-470)​

A coluna Origem para de imprimir o enum do banco e vira workspace › projeto › área › agente com quatro estados por nó (settings/cascata.tsx) — separando os DOIS sentidos de origin: 'agent' sem tocar na api. O aviso de nível descartado entra na cadeia; os três — ganham três textos. Fecha o canvas de melhorias de UI (7 de 7 itens tomados)

Pasta antes do runner (RN-473/474)​

A configuração do runner pelo navegador (ADR 0118) inverte a ordem: showDirectoryPicker é o PRIMEIRO passo e o binário o ÚLTIMO, best-effort — a release sem asset devolvia 502 antes do seletor abrir, e a pasta ficava inalcançável. Falha do binário mantém os dois arquivos gravados e troca a instrução pelo caminho npm install -g @brabo/runner. Depois da instrução, EsperaDoRunner sonda workspaceVerifiedAt com três estados e teto

O kid na chave de dispositivo (RN-475)​

O modo automático do ADR 0118 NUNCA autenticou: o navegador descartava o id que o registro devolvia, e a JWK gravada nascia sem kid — o CLI a recusava sempre. Registro e exportação viram UMA função (registrarChaveEExportarPrivada), nos DOIS caminhos; e a recusa do CLI deixa de ser indistinguível de "não há chave". O teste que deixou passar afirmava que o arquivo fora ABERTO, nunca o conteúdo

Provisionamento que não fica calado (RN-477)​

Duas falhas não viravam estado nenhum — step.check fora do try e a recusa de createRepo ANTES de a linha de bootstrap existir —, e a tela pollava "Iniciando…" para sempre sem botão. Causa raiz de tudo: /data/git-repos e /data/project-workspaces nasciam root nas imagens de DEV, que o Dockerfile.prod já sabia criar antes do USER

O arquivo de política e o escopo do terminal (RN-478)​

projectScopeRoot tinha DOIS consumidores com necessidades opostas, e o modo runner (sem bind-mount) os separou: o escopo segue apontando para o HOST, o permissions.json passa a morar na raiz GERENCIADA. A ativação da execução devolvia 500 (mkdir '/home/<usuario>'), e a LEITURA degradava calada — em projeto runner o arquivo nunca existiu. Junto: erro tipado com 400 em vez de Error cru, a Visão geral parando de engolir a mensagem da api, e a anotação de OpenAPI que prometia 409 para dois casos que nunca foram 409

Aviso do passo humano antes do clique (RN-473/477)​

O passo de terminal do runner era anunciado só no estado de sucesso; passa a ser dito também no inicial, e o comando final ganha cd <caminho> quando dá para afirmar que a pasta escolhida é a do projeto. Rótulo do botão NÃO muda — ele fala da pasta, que é automática mesmo

Um modelo para todos os agentes (RN-476)​

A tabela Modelos por agente ganha uma barra que aplica UM modelo aos 17 de uma vez, gravando no nível do AGENTE (pelo projeto seria maintainer, e mexeria no default de sessão). Primeira EXCEÇÃO à régua "valor nomeado salva no onChange" da RN-469 — o seletor é argumento de ação, não configuração —, com o desfecho em três estados da própria RN-469

Tetos de rebaixamento em project_members (ADR 0127, RN-472)​

A sobreposição projectRole ?? workspaceRole FICA nos dois sentidos (é capacidade, não bug); o que entra são dois tetos de 403 no caso de uso — ninguém rebaixa o owner do workspace, ninguém rebaixa a si mesmo. As três descrições de OpenAPI que a RN-471 declarou falsas passam a descrever o código; o gate do Select é PR à parte, por a tela não ter como calcular o primeiro teto

Telemetria de busca do RAG (RN-479..481)​

A busca híbrida deixa rastro: rag_searches (com os pesos CONGELADOS na linha) e rag_feedback (o voto útil/irrelevante, o único sinal de verdade). TABELA e não só evento, porque session_events.session_id é NOT NULL e a busca da aba não tem sessão — o evento rag.search/rag.feedback é NARRAÇÃO, só quando há sessão. Ferramenta rag_feedback (:direct) nos seis agentes que já tinham rag_search, e medir:rag para ler. NADA calibrado — esta etapa só instrumenta

Broker de container (ADR 0130, RN-485/486)​

Nasce apps/broker, o ÚNICO processo que fala com um daemon Docker no servidor — e ele NÃO aceita especificação: recebe projectId + operação, LÊ a decisão do Arquiteto da api e COMPÕE imagem, rede, recursos e o único mount. A porta do ADR 0128 MOVE de apps/runner/src/ para packages/docker-port (os dois consumidores a empacotam; a api não pode consumi-la). A rota de ciclo de vida passa a devolver observado ao lado de registrado, sem fundir

Roteamento de módulos para infra (PR 1.4) (ADR 0131, RN-487)​

Sétima ferramenta do Arquiteto, route_modules_to_infra: um item {modulo, imagemCandidata, porque} por módulo do module_map vigente, gravado como artifact.module_routing (sem tabela, mesmo desenho de artifact.project_image/artifact.c4_diagram). Ele CANDIDATA — a imagem de cada item passa pela mesma validarDecisaoDeImagem de choose_project_image —, e quem ELEGE entre as candidatas é a Infra, num PR à parte (1.5, fechado logo abaixo)

Golden-set de acerto do RAG (ADR 0132, RN-490)​

Molde do golden-set do QA (ADR 0123) aplicado à busca híbrida: como o julgamento não mora no engine (é HybridSearchUseCase, na api), seed-golden-set-rag.ts provisiona UM projeto com corpus real curado (22 arquivos de docs/) E roda a busca para 17 perguntas compostas de RNs/ADRs reais; rag_golden_test.exs só invoca o script e aplica o piso. Critério de acerto — caminho de arquivo, top-5, nunca chunk exato/rank 1 — é função pura testada. Gate novo rag-acertivo, warn (sem CI com LLM). Medido de verdade, duas vezes, deterministicamente: 17/17 no top-5

Dev agents executam DENTRO do container real (PR 1.6) (ADR 0134, RN-492/493)​

Fecha o que a RN-491 declarou pendente: Engine.Actions.TerminalExecutor ganha a QUINTA saída, :executar_no_container — projeto execution_mode: container com uma linha REGISTRADA running (Engine.Containers.ProjectContainerLifecycle, leitura direta, mesmo padrão de Engine.Projects.Project) — e o comando atravessa engine → api (POST internal/projects/:projectId/container-exec, ExecutarComandoNoContainerUseCase) → broker (ContainerBrokerPort.exec, o PRIMEIRO chamador real da última das cinco operações do ADR 0128/0130), rodando via docker exec em vez de System.cmd local. cwd é traduzido do caminho de HOST pra dentro de /work no engine, ANTES da chamada — confirmado em código que é o MESMO diretório físico que Workspace.ensure!/4 já escreve (o worktree do dev agent não muda de lugar nem de mecanismo). BrokerRecusouError/BrokerIndisponivelError/falha de transporte viram failed_result normal, nunca crash nem fallback silencioso pra fora do container (RN-486: running registrado nunca garante container de pé). Terminal DENTRO do container ganha PISO de auto-aprovação (ProposeActionUseCase/decide.ts, containerExecutionActive) — os cinco tetos absolutos e o escopo léxico de sempre continuam rodando por cima, byte a byte

O runner sobe o container do projeto (PR 1.3, Parte 1 fora de ordem — fecha a metade que a RN-494/PR 1.7 tinha deixado declarada) (ADR 0137, RN-497)​

container_start/container_stop/container_remove ganham SEGUNDO caminho de execução, ramificado por executionMode: container segue pelo broker (inalterado); mounted/runner passam a pedir ao RUNNER conectado, via TRÊS pares novos no canal Phoenix (container_start/_result, container_stop/_result, container_remove/_result, mesmo molde de exec/exec_result) — EngineWeb.ContainerCommandController repassa pro RunnerRouter, que o runner atende com DockerViaCli (o Docker DELE, @brabo/docker-port, que ganha uso real além de --self-test-docker). A imagem é LIDA (ObterSpecDeContainerUseCase, o mesmo caso de uso do broker), nunca reeleita. "Sem runner"/"timeout" (RunnerNaoConectadoError) e "runner recusou" (RunnerRecusouContainerError) viram failed nomeado, nunca exceção. Engine.Actions.TerminalExecutor não ganhou saída nova — ele já roteava todo comando de projeto runner conectado pro canal incondicionalmente; a escolha host-vs-container é INTERNA ao runner (EstadoDoRunner.containerAtivo, tratarExec roteia via docker exec com cwd traduzido por troca de prefixo, cwdParaContainer). guard.ts/RN-434 passa a cobrir o bind-mount sem NENHUMA validação nova — o mount É a raiz já confirmada no startup da CLI

O handoff da Infra podia ser aceito por tela nenhuma (D0) (RN-499)​

offeredHandoff filtra o card do fio por AGENTES_DE_CHAT, que exclui infra — e o filtro está CERTO (o engine não tem cláusula de message pro Infra Lead; alargá-lo faria a tela oferecer um fio que não existe e o composer mandar mensagem pro Criativo). O defeito era a consequência, que o comentário do próprio código registrava como aceita: acceptHandoff tinha UM consumidor só, atrás desse filtro, então o handoff de OfferInfraHandoffUseCase ficava offered para sempre, o Infra Lead nunca era ativado, propose_container_start nunca era chamado e NENHUM projeto de nenhum modo chegava a ter container de pé. Correção: card acionável PRÓPRIO, fora do fio, na faixa fixa entre a área que rola e o composer — a mesma que já hospeda o handoff manual e se declara o lugar das ações de handoff que não são conversa —, chamando o acceptHandoff que já existia, fechado pelo mesmo activeFor. O handoff.offered da Infra continua NARRADO no fio como divisor mudo

A base única dos projetos montados (PR 1, BREAKING) (ADR 0141, RN-500)​

Criar projeto mounted exigia uma linha de bind-mount escrita À MÃO em api E engine mais um restart dos dois — que mata todo turno de agente, socket de terminal e chamada de LLM em voo da instalação, para onboardar UM projeto. Nasce BRABO_PROJECTS_BASE: UMA base, configurada uma vez pelo operador, montada por IDENTIDADE ($X:$X) nos dois serviços, com todos os projetos montados dentro dela e NADA editado por projeto. Identidade e não mountpoint fixo porque workspace_path é digitado pelo usuário e mostrado de volta a ele — e é o que faz projectScopeRoot/workspace_dir/2 continuarem certos sem código novo. Variável PRÓPRIA (colisão de namespace com workspace_dir_name UNIQUE faria git init na pasta de outro projeto; dono oposto; a base é navegável e a raiz gerenciada não deve ser). AUSENTE é normal: projectsBase: null (GET workspaces/:id/projects-base, maintainer) e o modo não é oferecido. E pnpm dev RECUSA subir com a base sobreposta ao checkout — a única checagem possível, porque a api compara contra /workspace. Dois custos declarados: symlink sob a base e uma base só

Container de projeto montado (PR 3) (ADR 0144, RN-503)​

Projeto mounted não conseguia container NENHUM, por dois bloqueios independentes — a api mandava todo modo não-container pro RUNNER (que exige brabo-runner conectado) e o broker recusava não-container na fonte. Os dois eram sobre GEOMETRIA, não sobre o nome do modo, e a base única do ADR 0141 mudou a geometria. A ramificação vira por DESTINO: container E mounted → broker, só runner → runner (e container_stop/_remove mudam JUNTAS com o _start, senão sobe no servidor e para na máquina do usuário). O invariante do ADR 0130 não se mexe — nenhum caminho absoluto atravessa a rede: o broker ganha uma SEGUNDA raiz (BRABO_PROJECTS_HOST_BASE) e a api manda um localizador DISCRIMINADO (gerenciada |montada |indisponivel) dizendo contra qual raiz o segmento relativo vale. TRÊS variantes porque indisponivel tem dois consertos diferentes (runner × mounted legado fora da base), e raiz que falta NUNCA é suprida pela outra. mounted ELEGE a imagem como container — o broker compõe de artifact.project_image, então eleição não gravada é eleição inerte (ADR 0133 num segundo modo)

Agentes de dev só depois do container (PR 7) (ADR 0143, RN-502)​

Numa execução real do exp001, DEZ tasks de dev travaram de uma vez — e o achado não foi a pasta que falhou, foi que nada ordenava container antes de dev agent. Engine.Dev.AgentIo.try_claim/2 — o ponto ÚNICO de claim — passa a consultar ProjectContainerLifecycle.running?/1 (o predicado que o ADR 0134 já tinha) ANTES de chamar a api: sem linha REGISTRADA running, o agente cai em :idle, persiste e emite dev.blocked_by_container. No ENGINE e não só no activate-execution da api porque a REIDRATAÇÃO chega ao try_claim/2 sem passar por rota nenhuma (init/1 → finish_restart_recovery/1). :idle e NÃO status novo, pela razão que o próprio docblock do try_claim/2 já registrava desde a Fase 12b — é o único estado do qual um wake ainda resgata, e todos os guards de handle_info/2 se apoiam nisso; quem distingue "fila vazia" de "sem container" é o EVENTO, nunca o status. O wake vem pelo outbox: RegistrarTransicaoDeContainerUseCase publica container.running na MESMA transação que grava a chegada em running, num aggregate_type NOVO (container, o terceiro que Engine.Outbox.Drain lê) em vez de pegar task emprestado, com o PROJETO como aggregateId; a mensagem entregue é a {:wake, :became_claimable} que já existia. Segunda metade: TerminalExecutor para de degradar calado — container sem running caía em System.cmd DENTRO do processo do engine, e agora RECUSA, com mounted no mesmo ramo

Alarme de merge de esteira com destinatário (ADR 0139)​

A regra "promoção é --no-ff" foi quebrada TRÊS vezes (#367, #394, #464 — a terceira era o PR que consertava as duas primeiras). O achado: a detecção JÁ existia e JÁ funcionou (o tag-release reprovou as duas primeiras com a mensagem certa) — faltava para QUEM tocar, porque workflow de push que falha numa permanente não tem PR onde ficar vermelho e o repo tinha zero issues. tag-release ganha o job avisar, que abre issue (e comenta na já aberta, nunca duplica) com github.token e nunca com o PAT — PAT inválido é uma das falhas que ele reporta — e isso deixou de ser hipotético: o BRABO_BOT_TOKEN expirou em 2026-09-03 à noite, reprovou duas runs do tag-release, e foi rotacionado em 2026-09-04; a v4.0.0 carimbou e DISPAROU a Release, o que só acontece com PAT válido (tag criada com GITHUB_TOKEN não dispara workflow). A regra passa a cobrir dev de forma ESTREITA: só é defeito o PR que trazia ARESTA NOVA (head era merge cujo segundo pai não estava na base), nunca o squash de PR de trabalho, que é a convenção. Desligar squash no repo foi MEDIDO e recusado (taxaria todo PR e mexeria no changelog.mjs, que roda --no-merges)

O artefato de decisão dos conversacionais (Frente 3, fatia genérica) (RN-505)​

decision_record reusa o padrão GENÉRICO de emit_artifact/ArtifactSchemas (o mesmo de note/business_rule) em vez do dedicado de project_image/c4_diagram — uma decisão é log append-only, nunca um "vigente" que se substitui. PO, Arquiteto, Dev Lead, UX Designer e Staff ganham a ferramenta (alias + tool_specs + run_tool/3 + a mesma frase no system_prompt/1); o Criativo já tinha emit_artifact desde a Fase 3b e só ganhou o tipo novo, sem mudança nele. Distinto de open_adr_pr (só o Arquiteto, commit real + PR + aprovação humana): os dois COEXISTEM para escalas diferentes de decisão. Fora de escopo, declarado: agentes de execução (ToolLoop, sem harness comum) e a amarração com o Infra Lead — emitir decision_record ao propor container_start_via_runner — que depende de uma tool de outra frente do mesmo plano, em branch separada

"Sempre permitir" separado por Dev Agent de módulo (RN-509)​

Clicar "sempre permitir" numa ação de um dev-<modulo> gravava em permissions.json, escopo de PROJETO INTEIRO — liberava o mesmo comando pra qualquer outro agente. Passa a gravar em agent_autonomy(projeto, agente, tipo), o MESMO mecanismo já usado pras 3 ações git seedadas na ativação da execução. dev-lead (lidera a área, não é membro dela) e qualquer ator não-modular continuam indo pro permissions.json de sempre; os dois tetos absolutos (terminal com efeito externo/comando privilegiado, container_remove) continuam recusando o clique inteiro ANTES do branch novo. Sem migração: entradas antigas continuam em permissions.json, decisão consciente

Docker vira pré-requisito real do modo Runner (RN-507/508, ADR 0145)​

runner roteava todo terminal aprovado ao CLI assim que workspace verificado

  • runner conectado batiam, sem checar container running — sem Docker de pé, o runner caía sozinho no HOST puro, o MESMO fallback silencioso que o ADR 0143 já tinha fechado para container/mounted. Engine.Runners.RunnerReadiness unifica as TRÊS pré-condições (verificado, conectado, container running) com dois consumidores: TerminalExecutor e a materialização do worktree do dev agent, que deixa de tentar File.mkdir_p!/git init LOCAL contra o caminho do HOST (a lacuna aberta desde a RN-478) e passa a rodar DENTRO do container real, na máquina do usuário, pelo MESMO canal exec/exec_result (Engine.Actions.Workspace.RunnerGit, novo) — WorktreeManager bifurca pelo mesmo critério, e o job periódico de limpeza PULA em silêncio um projeto runner sem runner pronto agora. O protocolo exec ganha env opcional para a credencial de git (ADR 0056) viajar no ambiente do processo filho do HOST — mesclado sobre process.env, nunca repassado ao docker exec nem logado. container_start deixa de atender runner (o payload dela, com eleição de imagem, nunca fazia sentido lá); nasce container_start_via_runner — schema só com rationale, sobe a imagem já decidida —, com a tool nova do Infra Lead recusando LOCALMENTE (sem HTTP) antes de propor às cegas

RN-505 duplicada em dev, corrigida​

O "artefato de decisão" e o "sempre permitir por Dev Agent" mergearam com 2 minutos de diferença, cada um alocando RN-505 sem ver o outro; "sempre permitir por Dev Agent" foi renumerado pra RN-509 (docs/business-rules.md, CHANGELOG, o comentário do approve-always-action.use-case.ts e do ApprovalCard.test.tsx), liberando o anchor {#rn-505} pro decision_record. Achado junto: a renumeração 505→507/506→508 feita durante a resolução de conflito de outro PR (Docker/runner, ADR 0145) tinha ficado INCOMPLETA — sobravam três menções a "RN-505" neste próprio arquivo, também corrigidas pra "RN-507"

FASE 28 (sessão 5) — o join do agente local deixa de ser mudo (RN-514, ADR 0147 ponto 1)​

O join do canal terminal:<projectId> era mudo dos DOIS lados — o runner mandava {} e o servidor fazia _params —, então o servidor não tinha como saber se o binário do outro lado entende uma mensagem nova, e o defeito era o pior possível: a mensagem chega, o handler não existe e NADA acontece, sem erro e sem log. Agora o runner DECLARA nos params (Engine.Runners.Capacidades, vocabulário exec/pty/espelho) e o servidor CONCEDE a interseção com o que CONHECE, em socket.assigns e NUNCA em tabela (capacidade é propriedade daquela CONEXÃO). O runner declara só exec e pty — declarar espelho sem implementá-lo é exatamente o defeito que a negociação impede; ele entra como NOME do vocabulário e nada mais, e a sessão 6 o implementa. Params ausentes/vazios são o binário LEGADO e concedem {exec, pty} — FATO, não benevolência: são as duas funções com que o runner nasceu, e recusar legado seria breaking/ e MAJOR. Desconhecido é IGNORADO (runner mais NOVO que o engine conecta); exigida-e-não-declarada recusa o join NOMEANDO a que falta, ANTES do Registry — mecanismo implementado e testado que hoje NÃO dispara (runner exige exec, que todo binário tem; nada exige espelho ainda), e isso é o desenho. A metade que entrega valor HOJE: mensagem sem a capacidade é RECUSADA com resposta nomeada — exec responde no formato de exec_result (126 + causa) em vez de o RunnerRouter esperar o timeout, e pty_* vira pty_error na aba em vez de sumir. RunnerReadiness byte a byte como está

FASE 28 (sessão 6, metade B) — o espelho passa a existir (RN-516, ADR 0147 pontos 2/3/4/8)​

A capacidade espelho sai de nome do vocabulário e vira código nos DOIS lados. O DESTINO viaja na CONCESSÃO do join (a resposta deixa de ser vazia para o :runner: %{espelho: %{destino: ...}}), vive só naquela conexão e o runner RECUSA mirror_sync para destino não concedido — nunca configuração global nem variável, que faria o artefato do projeto B aterrissar na pasta do A. mirror_path não-nulo passa a EXIGIR a capacidade, e esse é o PRIMEIRO disparo real do mecanismo de recusa da RN-514: binário velho num projeto com destino DEIXA de conectar, nomeado e fatal — opt-in, custo declarado no ADR. espelho-guard.ts nasce IRMÃO de guard.ts (reusa os três helpers e a dupla passada, herda a ressalva de TOCTOU por escrito) e recusa as três coisas do ADR; achado implementando: realpathMaisProximo cai no ancestral, e como o destino quase nunca existe, colapsar os dois lados faria /base-outra e /base virarem / — a segunda passada resolve o ancestral e RECOLOCA o sufixo. UMA direção e NUNCA apaga: arquivo apagado na origem PERMANECE no destino (a pasta do usuário é acúmulo, não réplica). O que se copia é a LISTA DO GIT (rastreados + não-rastreados-não-ignorados), sem lista de exclusão própria; git que falha vira erro NOMEADO, nunca cp -r de plano B; repo git aninhado (o worktree do dev agent) volta como UMA entrada e não é descido, e symlink da origem é pulado. Predicado PRÓPRIO (Engine.Runners.Espelho), DUAS pré-condições e NUNCA RunnerReadiness com flag — runner_readiness.ex fica byte a byte. Gatilho: o COMMIT, momento nomeado, fire-and-forget e nunca watcher; "fim de turno de agente" fica DECLARADO e não ligado, por não haver enganche não-ambíguo

O broker que nunca subiu (correção da RN-512) (RN-512, ADR 0130/0146)​

A RN-512 tirou o broker do profiles para ele estar de pé em dev, e ele NUNCA subiu: corepack prepare rodava como ROOT no build (cache em /root/.cache), o container roda como uid 1000, e sem cache o corepack tenta BAIXAR o pnpm num serviço cuja única rede é internal: true — EAI_AGAIN registry.npmjs.org, Exited (1) cinco segundos depois. Sem healthcheck, up --wait dizia Healthy e o reset total anunciava "reset completo" com o processo morto; enquanto houve profile, quem subia o broker era quem já estava olhando. A correção tira o registry do RUNTIME e NÃO abre a rede (primeira das cinco camadas do ADR 0130): COREPACK_HOME preparado no build com o dono do uid de runtime, pnpm install como passo de BUILD, e — porque os três volumes de node_modules cobrem o que a imagem instalou e o Docker só semeia volume VAZIO — um entrypoint que os reconcilia contra uma cópia fora de /workspace, carimbada pelo sha256 do lockfile do build (o mesmo carimbo AVISA que a imagem ficou para trás, sem recusar subir). Healthcheck no serviço, de dentro do container. Produção não sofria disto e não muda

FASE 28 (sessão 7) — o agente local vira serviço de usuário (RN-518, ADR 0147 ponto 5)​

brabo-runner service install|uninstall|status, e o precedente era ZERO (nenhuma menção a systemd/launchd em apps/runner/; o modelo era foreground morto por SIGINT). Nível de USUÁRIO sempre — systemd --user/LaunchAgent —, e root é RECUSA e não aviso: o docblock de guard.ts declara "roda com os privilégios DELE" como a premissa das três fronteiras reais, e um serviço root deixaria a guarda best-effort de cwd como única coisa entre um comando aprovado e o disco. Windows é recusa NOMEADA (terceiro mecanismo, não variação dos dois). Uma unit por PROJETO porque o servidor já impõe um runner por projeto — nomear pela pasta criaria duas units que nunca sobem juntas, e a segunda apareceria como falha de conexão em vez de erro de instalação; no Linux o arquivo vai para onde o systemd PROCURA ($XDG_CONFIG_HOME/systemd/user, senão ~/.config). Restart=on-abnormal e NUNCA on-failure (no plist, KeepAlive/Crashed): exit 1 do runner é recusa fatal de join (RN-514) ou teto de tentativas esgotado, e reiniciar seria o laço que o CLI recusa fazer sozinho. Serviço autentica por CHAVE DE DISPOSITIVO e nunca por token — gravá-lo numa unit o deixaria em disco, e este CLI nunca gravou credencial em disco. uninstall remove os TRÊS lendo a pasta da UNIT e nunca do cwd (sem unit, só com --dir — adivinhar apagaria a chave de outro projeto), e DIZ que a chave saiu do disco e NÃO foi revogada no servidor. status responde QUATRO estados com frase e código próprios (0/3/4/5), e a primeira resposta vem do DISCO — por isso continua certa numa máquina sem gerenciador; spawn que falhou nunca vira "o gerenciador disse não". BRB-031 NÃO fecha aqui

A spec do runner ia sem projectId, e o caminho NUNCA subiu container (RN-508, ADR 0145)​

container_start_via_runner (RN-508, ADR 0145) terminava failed em 100% das vezes — "especificação de container recusada em projectId: esperava texto não vazio, recebi undefined" —, e numa execução real do exp004 o que se via era o EFEITO: dez tasks de dev sem nenhum agente reivindicando. Esse bloqueio estava CERTO (RN-507 e RN-502/ADR 0143 recusam sem container running REGISTRADO); o errado era o passo anterior. EspecificacaoDeContainerParaRunner nasceu com NOVE campos e especificacaoValidada (packages/docker-port) sempre exigiu DEZ — o dado estava à mão em SpecDeContainer.projectId e só não era copiado; o engine repassa o mapa OPACAMENTE, então nada se perdia no meio. Por contraste o BROKER sempre funcionou porque ele BUSCA a spec na api e recebe o SpecDeContainer inteiro — era por isso que só o modo runner quebrava. Três suítes verdes não pegaram porque cada elo montava o próprio fixture: ninguém testava a CORRENTE. Entra ela — o payload que o caso de uso REAL compõe atravessando o validador REAL, e o CONJUNTO de campos travado (raizDoProjeto é o único que a api não manda) — na suíte da api, por import RELATIVO, o único lugar onde os dois lados existem vivos; o invariante api-nao-consome-docker-port segue verde e foi ESTENDIDO para reprovar import relativo a partir de src/. Irmãos conferidos e CERTOS: container_stop/container_remove mandam só workspaceDirName, que é tudo que nomeDeWorkspaceValidado pede

A pausa que não podia ser desfeita (RN-540)​

Anamnese e Psicólogo estavam PAUSADOS desde 2026-08-10 e três lugares chamavam a pausa de reversível — os docblocks de AnamneseSchedulerWorker.enabled?/0/PsychologistWorker.enabled?/0 ("Ligar de volta é X=true e reiniciar o engine") e docs/reference/configuration.md. Não era reversível em ambiente NENHUM: o environment: do serviço engine mapeava as TREZE variáveis de Anamnese/Psicólogo — todas TETOS de custo — e nenhuma das duas de PRODUTO, e docker compose NÃO repassa o ambiente do host, então ANAMNESE_ENABLED=true no .env era inerte e runtime.exs caía no default "false" em SILÊNCIO. Medido no container de dev: [true] [] []. Origem provável: quando o default virou false, ninguém precisou da variável para DESLIGAR — só quem tenta LIGAR descobre que o fio não existe, meses depois. As OITO flags booleanas (System.get_env("X", "true"|"false") == "true") entram nos DOIS composes com o MESMO default do código — as quatro extras (START_MODEL_SYNC, START_GATE_RESCUE, os dois GRAPH_*) porque são o mesmo defeito, achado pelo próprio mecanismo, e excetuá-las exigiria quatro desculpas. Zero mudança de comportamento: a decisão de produto de manter os dois desligados fica byte a byte, o que passa a existir é o INTERRUPTOR. O mecanismo é scripts/ci/flags-do-engine-no-compose.spec.ts, que DERIVA a lista do runtime.exs (as outras 50 variáveis são tetos com default bom e coisas de release, legitimamente ausentes) e cobra default igual só no compose de DEV — o de produção desliga os agentes de fundo de propósito. deploy/k8s/ NÃO muda, por coerência: lá não existe a camada que quebra

FASE 29 (sessão 1) — instalação de uma linha, só planejamento (ADR 0149-0152, fase-29-instalacao-de-uma-linha.md)​

Instalar o Brabo são TRÊS gestos desconexos (compose com cinco segredos à mão, runner pelo navegador com chmod +x, pnpm bootstrap que só existe num checkout), e a FASE 28 recusou por escrito o instalador de uma linha enquanto os artefatos não forem assinados (BRB-005) — por isso a fase assina ANTES de instalar. Quatro ADRs Proposed: assinatura cosign keyless por DIGEST mais UM checksums.txt assinado, nunca cinco assinaturas (0149); o instalador invocado por sh -c "$(curl …)" e NUNCA curl|sh, porque com o pipe o script É o stdin e o consentimento morre (0150); a base consentida do runner, reusando dentroDoEscopo e a recusa de laço por SEGMENTO de espelho-guard.ts em vez de uma quarta cópia da régua (0151); e backup/restore contra compose (0152). A medição corrigiu CINCO pontos do recorte original: workspace.verified NÃO existe (o que existe é workspace_confirm, e ele é UNIDIRECIONAL), o ponto 5 do ADR 0146 versiona consentir-base.mjs e não um install.sh, docker-compose.prod.yml não consome GHCR e se declara compose de VALIDAÇÃO, make test-restore é caminho de Kubernetes com backup só de Postgres, e "um runner por projeto" está imposto em CINCO lugares independentes — o que tirou o runner por máquina desta fase, para a FASE 30. Achado próprio: git_local_repos não está em backup nenhum, e perdê-lo é perder o código dos projetos local. Nasce docs/reference/brb.md — e o achado é que o registro de BRB não é do repositório: vive no vault do mantenedor, com 33 itens, e aqui só dois ids apareciam em prosa. Medindo: BRB-032 e BRB-033 descrevem defeitos que já não reproduzem, e BRB-004 não é o que o id sugere — pede imagem de terceiro por DIGEST, não a tag que o #419 fixou. O arquivo daqui guarda a EVIDÊNCIA (estado medido, arquivo:linha); o vault segue dono da finding. Faixa RN-524..538 e ADRs 0149-0152, pulando o que está EM VOO: três colisões observadas no mesmo instante (ADR 0148 ×2, RN-522 ×2, RN-523 ×2), resolvidas depois pelo critério da DATA — quem alocou primeiro ficou com o número, e as branches de 08/09 viraram ADR 0153, RN-539 e RN-540

FASE 29 (sessão 2) — a esteira assina o que publica (RN-524, ADR 0149)​

O produto assinava nada: cosign/sigstore/attestation não existiam em .github/, scripts/ ou docker/, e a única procedência era o --provenance do pacote npm do runner. release.yml publicava as quatro imagens sem id-token: write; build-runner-binaries.yml subia os cinco binários com gh release upload e nada mais — enquanto os sha256sum -c que já existiam verificam binário de terceiro, o padrão que o repositório sabia aplicar e nunca aplicou a si mesmo. Agora as imagens são assinadas por DIGEST (nunca por tag, ponteiro móvel) e os binários ganham UM checksums.txt assinado, num job checksums que espera a matriz — não cinco assinaturas, porque quem verifica quatro e esquece o quinto não sabe que esqueceu. O job é if: always() de propósito (fail-fast: false na matriz) e nomeia o que o manifesto não cobre. Os dois workflows verificam antes de publicar. Keyless pelo histórico da casa: chave privada seria mais um segredo de CI, e um PAT já expirou calado (ADR 0139). Procedimento de verificação no runbook, com as duas flags --certificate-* obrigatórias — sem elas o cosign aceita assinatura válida de QUALQUER UM. Fecha BRB-005 para imagens e binários; ficam fora, declarados, o code-signing de SO e o proxy de download

FASE 29 (sessão 3) — o instalador que ainda não instala (RN-526, ADR 0150)​

Nasce install.sh na RAIZ, versionado e servido pela Release. A forma é sh -c "$(curl …)" e nunca curl|sh, por motivo MECÂNICO: com o pipe, o stdin do processo É o download, e todo read lê bytes do próprio script ou acha EOF — um instalador que não pergunta escolheria sozinho onde criar pasta na máquina de alguém. Ele VERIFICA A PRÓPRIA ORIGEM (a assinatura do checksums.txt da RN-524, e o hash dele DENTRO do manifesto verificado), com o cosign pinado e conferido por sha256sum contra o hash escrito no script; a DETECÇÃO nomeia o que achou antes de perguntar; sem TTY relata e sai 0; e nunca apaga pasta de usuário — base de projetos e pasta de espelho ficam fora de qualquer confirmação, pela régua do espelho (RN-516). Marcador em $XDG_STATE_HOME/brabo/, nunca em ~/.brabo/, que é POR PROJETO. Não instala nada nesta versão, e DIZ isso em vez de terminar calado. --print-plan/--print-state expõem a decisão sem TTY, sem rede e sem efeito — molde do --print-commands do bootstrap.sh, testado igual. Dois bugs de bash achados rodando, os dois do mesmo gênero: sob set -e+pipefail, [ cond ] && atribuição que dá falso MATA o script, e um ls que não casa derruba a substituição inteira — numa função cujo caso normal é "não achei nada". A RAIZ do repositório não era coberta por docmap nenhum (os globs de raiz são nomes literais); nasce a regra instalador

FASE 29 (sessão 4) — a instalação sobe de um compose próprio (RN-527, ADR 0150)​

Nasce docker/docker-compose.install.yml, GERADO a partir do de produção e diferente em três pontos: as quatro imagens vêm de variável obrigatória (${BRABO_API_IMAGE:?…} — com default, faltar a variável subiria metade da stack com imagem que ninguém escolheu, e o erro viraria comportamento estranho em vez de recusa), nenhum build: sobrevive, e o serviço broker NÃO existe porque a imagem dele não é publicada (o bake tem quatro alvos) — com --source=ghcr, mounted não sobe container, e o compose DIZ isso no comentário da BROKER_URL. O docker-compose.prod.yml fica intacto: ele se declara compose de VALIDAÇÃO e é o que o smoke.sh usa construindo local. O custo dos dois arquivos é declarado e o teto é MECÂNICO — composes-em-conformidade.spec.ts reprova serviço/volume/porta a mais, build: sobrevivente e imagem de terceiro afrouxada. Duas coisas do plano morreram no contato com o disco, por motivos opostos: migrate não é passo (o compose encadeia api→migrate-api com service_completed_successfully, e dois lugares mandando migrar seriam duas fontes da mesma verdade) e o smoke.sh não é reusado (ele CONSTRÓI o que a instalação baixou por digest, e derruba a stack no fim) — no lugar, o instalador PERGUNTA antes de afirmar, batendo no /health dos dois

FASE 29 (sessão 6) — migrar exige backup PROVADO (RN-530, ADR 0150/0152)​

Instalar por cima de instalação existente não é oferecido — ou migra, ou para: up sobre volumes de outra versão é estrago que não avisa. A ordem é backup → PROVAR → perguntar → apagar → instalar → restaurar, e o passo do meio é o que dá ao instalador o direito de apagar: um backup que ninguém tentou restaurar é um arquivo, e a hora de descobrir isso não é depois do down -v. A prova roda test-restore-compose.sh (as MESMAS três validações do ADR 0152); backup ou prova que falham param a migração sem apagar nada. O destino é pasta do HOST — o default do compose é o volume backup_local, que o down -v apagaria JUNTO com o que se quer preservar. O dump do Postgres NÃO é restaurado sozinho sobre banco populado (é destrutivo, e brabo-restore nunca toca a origem); os repos git locais sim, porque nasceriam vazios

FASE 29 (sessão 7) — uma base para os dois lados, e o BRB-031 fecha (RN-531, ADR 0150/0151)​

O instalador pergunta a base UMA vez e grava nos DOIS lugares: BRABO_PROJECTS_BASE no .env (servidor, ADR 0141) e o campo base do runner.json (agente local, RN-529) — o que difere entre mounted e runner é QUEM executa, não onde o código mora, e duas bases para a mesma pasta seriam a colisão que o ADR 0141 recusou. Recusa nos DOIS sentidos contra o checkout, e ~ não é expandido de propósito (o valor vai para arquivo de configuração, onde ~ é lido literalmente). O binário do runner é conferido contra o mesmo checksums.txt que o script já verificou para conferir a si mesmo — baixá-lo de novo seriam duas chances de pegar manifestos diferentes, e a segunda não seria verificada — e instalado com install -m 0755. BRB-031 fecha por aqui: ele sobreviveu à FASE 28 porque a instalação não vinha de artefato versionado, e quem chegava ao service install já precisara do chmod. Release sem o binário da plataforma NÃO interrompe a instalação

FASE 29 (sessões 8-10) — o protocolo, o picker e o E2E (RN-532..534, ADR 0149/0150/0151)​

8 (RN-532): nasce o par workspace_create/_result no molde de exec/exec_result, e a confirmação REUSA o workspace_confirm que já existe — nenhuma rota de gravação nova, engine continua sem escrever a tabela. O predicado é PRÓPRIO por uma razão mais forte que a do espelho: ali a terceira pré-condição seria circular — projeto runner só tem container running depois de o runner o subir, e o container sobe SOBRE a pasta que esta mensagem cria. A capacidade workspace é a única cuja declaração depende do ESTADO (base consentida), não da versão do binário. 9 (RN-533): o picker volta a ler o disco do usuário por fs_list_dir, e a decisão de NÃO esconder o modo sem runner é simétrica à RN-513, não contrária — lá a api TEM sinal e sem base a criação termina em 400; aqui não há sinal de presença (a RN-521 já declara), e esconder seria decidir "não tem" a partir de "não sei". Achado: a tela decidia casando a FRASE 'Nenhum runner conectado' que mora num .ex do engine — virou motivo discriminado. 10 (RN-534): o install.sh entra no checksums.txt assinado (sem isso a verificação que ele faz de SI MESMO era letra morta) e é exercitado em máquina limpa, com TTY simulado por script -qec e nunca por pipe — o pipe é o que mata o consentimento. NÃO roda em PR, pelo mesmo motivo da assinatura

FASE 29 (sessão 8) — a base do agente local ganha um consumidor (RN-532, ADR 0151 pontos 3–6)​

A RN-529 construiu a BASE e declarou por escrito que nada a consumia. Entra o par workspace_create/workspace_create_result, no molde de PEDIDO COM RESPOSTA de exec/exec_result — e NUNCA no de workspace_confirm, que é UNIDIRECIONAL ({:noreply, socket}, não empurra nada de volta), precisão que a fase já tinha registrado junto com "workspace.verified NÃO existe". Só o SEGMENTO relativo atravessa a rede (invariante do ADR 0130/0144: quem tem a raiz é quem executa), e segmento ABSOLUTO é recusado por léxico. A confirmação REUSA o workspace_confirm que já existia, e é o que economiza uma frente inteira: nenhuma rota nova de gravação nasce, o engine continua não escrevendo a tabela, e o único caminho que carimba workspace_verified_at continua sendo um só — com a ORDEM como mecanismo (confirm ANTES do resultado, porque os dois chegam ao mesmo processo de canal em ordem e o handler do confirm é síncrono). A capacidade workspace entra no vocabulário NA SESSÃO EM QUE O CÓDIGO ENTRA, dos dois lados — a RN-514 fez a aposta oposta com espelho e ela só se pagou porque alguém a cobrou —, e ela é a ÚNICA das quatro cuja declaração depende do ESTADO daquela execução: o runner só a declara com base consentida, e é por essa linha, e por mais nenhuma, que o servidor SABE que existe base (o engine não lê o disco do usuário). NINGUÉM a exige no join, de propósito: exigi-la em runner seria breaking/ e MAJOR por uma função que aquele projeto talvez nunca use. Predicado PRÓPRIO (Engine.Runners.PastaDoProjeto), DUAS pré-condições, RunnerReadiness byte a byte — e aqui a terceira seria pior que desnecessária, seria CIRCULAR: um projeto runner só chega a ter container running depois de o runner o subir, e o container sobe sobre a pasta que esta mensagem existe para criar. Ele também não pergunta workspace_verified_at (aqui é o RESULTADO) nem o modo. As DUAS pré-condições são respondidas em lugares diferentes porque "base consentida" é propriedade da CONEXÃO (socket.assigns, nunca tabela): o canal recusa ANTES de empurrar, com motivo: "sem-base" pelo mesmo ref, no formato que o módulo espera — nunca timeout. Cinco motivos nomeados que não colapsam, idempotência com desfecho PRÓPRIO (ja-era-repositorio é sucesso), e estado.dir NÃO muda. Não é proposed_action e nenhum teto ganha exceção

FASE 29 (sessão 5) — o backup passa a cobrir o que perder dói (ADR 0152, RN-528)​

O backup cobria UM banco e rodava NUM lugar: make test-restore é caminho de Kubernetes (exige kubectl, cluster e o CronJob) e docker-compose.prod.yml não tinha serviço backup NENHUM — migrar uma instalação por compose não tinha caminho provado. A decisão começa RECUSANDO o movimento fácil ("fazer backup de todos os volumes") e CLASSIFICANDO os sete: neo4j_data e project_workspaces são derivados, ollama_data é re-obtenível, brabo_projects_base é do usuário — copiá-los dá impressão de cobertura sem acrescentar recuperação. Fonte de verdade são DOIS, e o achado é o segundo: git_local_repos não estava em backup nenhum, e um projeto com provider local guarda o bare ali — não é reconstruível do Postgres (o event log guarda a NARRATIVA, não os objetos do git), então perder o volume é perder o código dos agentes. A prosa do runbook afirmava o contrário ("the real repositories live in GitHub/GitLab") e é ela que explica os anos sem cobertura: vale para github/gitlab, e para local não há upstream. A garantia do arquivo é DECLARADA e menor que "snapshot" — consistência por REFERÊNCIA (git escreve objetos antes de mover a ref, e troca a ref por rename atômico), sem instante global e sem git fsck (a imagem não tem git, de propósito); gc concorrente vira FALHA e não arquivo parcial, com o status do tar capturado num ARQUIVO porque em POSIX sh o status de um cano é o do último comando. As cinco variáveis de S3 deixam de ser obrigatórias e viram a configuração de UM destino (BACKUP_DIR escolhe disco; ausente, S3 — a inferência é o que mantém o CronJob INTACTO). make test-restore-compose dispara backup REAL e chama o MESMO brabo-restore com as MESMAS três validações: muda o INVÓLUCRO, nunca o julgamento, e deploy/k8s/test-restore.sh fica INTACTO (unificar faria o de compose depender de kubectl); restore.sh segue sem tocar a database de ORIGEM. Restaurar os repos é comando IRMÃO (brabo-restore-git, verifica por default, escreve só com --restaurar) e não fase do outro — dois artefatos, dois julgamentos, e juntá-los faria falha de git reprovar a validação do event log. Achado RODANDO, que a leitura não daria: o volume é compartilhado por três imagens com uids diferentes e carrega o dono de quem o montou primeiro (medido 1000:1000, modo 0755), enquanto a imagem roda como uid 70 — a extração morria no meio deixando o volume pela metade; agora recusa ANTES do tar nomeando --user 0:0, e o extraído como root volta ao dono do DIRETÓRIO, senão o volume voltaria íntegro e INÚTIL (api e engine, non-root, sem conseguir escrever no que se acabou de restaurar). Neo4j recebe REPROJEÇÃO e não backup — não construída aqui (BRB-018): instalação migrada nasce com o grafo VAZIO, degradação nomeada e não perda de dado (o RAG vive no pgvector, dentro do dump)

FASE 30 — o agente local da máquina (ADRs 0154/0155, RN-543..552, fecha na sessão 8 com a RN-549)​

A FASE 29 entregou instalar numa linha e PAROU em dois pontos que o próprio produto já declarava por escrito: o agente local continuava pareado por PROJETO (quem quisesse usar o binário instalado voltava ao navegador para colar um comando no terminal), e ninguém conseguia entrar — o .env gerado não tem variável de e-mail, MAIL_TRANSPORT cai em log e o registro exige verificar e-mail, então a saída era pescar o link em docker compose logs api. Os dois se resolviam no mesmo lugar, e é isso que fez uma fase só. O achado que definiu o TAMANHO dela veio de remedir com arquivo:linha a afirmação da FASE 29 de que "um runner por projeto está imposto em cinco lugares": três dos cinco não precisavam mudar, porque tópico, socket id e ticket descrevem uma CONEXÃO, e um processo que abra N conexões — uma por projeto — os satisfaz byte a byte, preservando a recusa de segundo runner no mesmo projeto, que é garantia real e não acidente. Nada no engine mudou na fase inteira, e foi o que manteve RunnerReadiness, o espelho e workspace_create intactos. O que sobrou foi o que é do PROCESSO: a credencial (runner_device_keys.project_id vira nullable, RN-543 — e a listagem passa a devolver as DUAS espécies marcadas, senão uma chave de máquina seria invisível e permanente, o defeito que a RN-519 fechou renascido) e a unit (RN-545, service install --machine, uma por máquina, CONVIVENDO com as por projeto). Duas decisões contrariaram a letra dos ADRs e as duas foram MEDIDAS: o discriminador da unit é a flag --machine e não a ausência de --project (resolverProjeto tem duas fontes, e o caminho normal é rodar install sem flag de dentro da pasta que o navegador configurou — ao pé da letra, o ADR converteria em silêncio a instalação de quem já usa o produto), e install RECUSA quando a outra espécie já está instalada, sem --force, porque as duas juntas seriam dois processos disputando o mesmo projeto. A sessão 6 tocaria três workspaces de uma vez e foi DIVIDIDA em três, com as duas peças disjuntas saindo antes e o install.sh depois delas, como orquestrador e nada mais. Dessa divisão saiu a revisão mais interessante da fase: com ZERO conexões o agente deixou de SAIR e passou a ESPERAR, reconsultando a 15s/30s/60s (RN-550) — o argumento da RN-544 contra repesquisa continua inteiro onde ele vale (lista que volta MENOR é ambígua, e derrubar conexão VIVA por ambiguidade troca estado certo por palpite), e com zero conexões não há nada a derrubar; o que caiu foi o argumento do exit 0 ("um serviço ativo que não faz nada"), porque o processo passou a fazer algo e a DIZER que faz. Medido: Restart=on-abnormal não olha código de saída, então nenhuma unit mudou e o efeito é a de máquina ficar active (running) de verdade. A outra peça (RN-551) fecha o "ninguém CRIA chave de máquina" pelo lado do agente, e a decisão ali é o CORTE: o CLI não fala com a api, porque quem registra é o instalador com o BRABO_SERVICE_TOKEN — cada lado guarda um segredo e nenhum vê o do outro. Disso decorre o desenho de dois passos, e dele decorre o invariante: o kid É o id do registro e só existe depois dele, então o create grava um .parcial que o leitor ignora e o finish grava o nome de verdade — o arquivo que o runner lê nunca existe sem kid, o defeito da RN-475 tornado impossível por construção. A metade de api (RN-552) declarou o custo em vez de escondê-lo: a credencial é o service token, e um token vazado passa a poder FABRICAR credencial duradoura — por isso três contenções vivem na ROTA e não no texto (não há userId no corpo; registrar SUBSTITUI as chaves de máquina ativas do dono na mesma transação, então máquina reinstalada passa e mil chaves são impossíveis por cláusula, sem número que envelheça; e JWK com d é recusada por nome). A sessão 6 (RN-547) encadeou os cinco comandos e fixou três regras: elo do meio que falha não desfaz nada, vira linha nomeada num bloco final e o script SEMPRE sai 0 (desfazer exigiria apagar conta e revogar chave, e não há rota para isso nem deveria haver uma que o instalador chame sozinho); idempotência é o CÓDIGO HTTP, e 409 é desfecho ESPERADO e não falha, com 000 tratado à parte para "a api não subiu" não ser lido como "a api recusou"; e a senha viaja pelo STDIN do curl, nunca por argv, porque /proc/<pid>/cmdline é legível por qualquer usuário da máquina. Achado dessa sessão: o --help MENTIA havia meses ("ESTA VERSÃO NÃO INSTALA NADA") porque ele imprime o cabeçalho, que é comentário, e a regra que proíbe falar de "sessões de fase" só lia linhas de código. A sessão 7 (RN-548) deu consumidor à marca de espécie, e a decisão foi de HONESTIDADE: com chave e sem conexão viva — o caso mais comum — a tela diz as duas coisas que sabe e nomeia as duas que não sabe (chave registrada não é agente rodando, a lista é da sua CONTA e não deste navegador), tom accent e nunca success. A sessão 8 (RN-549) é a única que podia provar o conjunto, e ela ESTENDEU o workflow que a FASE 29 já tinha em vez de criar um segundo. A decisão central dela é o SINAL: perguntarEstado mapeia activating para rodando — com razão —, então systemctl is-active e service status --machine dariam verde a um processo que vai morrer em dois segundos; o sinal escolhido é a LINHA que o agente escreve ao entrar na espera, que só existe depois de ele ter lido a chave, tirado ticket, falado com a api e recebido lista VAZIA, e que é um FATO GRAVADO e não uma amostragem de estado. O desfecho se prova pela API e não pelo navegador, porque o sujeito da afirmação é o AGENTE e o que ele observa é GET /runner/projects — um navegador criaria o projeto pela mesma rota com uma camada a mais que falha por motivos alheios, e a asserção continuaria sendo a linha do journal. E o E2E segue FORA de pull_request, com a consequência declarada em vez de escondida (o PR que o escreveu não o executou): abrir essa porta é dar ao instalador um jeito de pular a verificação de origem, que é o que o ADR 0150 recusa — daí a metade que roda em PR ser um TESTE (scripts/dev/install-e2e.spec.ts), guardando as duas formas de o workflow apodrecer calado, o gate afrouxado e a frase do install.sh reescrita (que faz as asserções não falharem, e sim SUMIREM). Dois defeitos apareceram, os dois invisíveis porque o workflow nunca rodou: as respostas do TTY começavam por nao-migrar, que numa máquina limpa cai na pergunta do MARCADOR e fazia o script sair 0 dizendo "Nada foi gravado"; e o ACHADO que ficou como achado — o install.sh publicado não sobe nada sozinho numa máquina limpa, porque docker/docker-compose.install.yml (com ./postgres/init.sql junto) não é asset da Release, não entra no checksums.txt assinado e ele não o baixa em lugar nenhum, então quem segue o curl … install.sh do runbook morre em "no such file or directory" DEPOIS de já ter verificado assinatura e gravado o .env. Consertar é decidir entre asset assinado e clone, e é entrega própria. O que a fase deixou DECLARADO e não feito: a revogação continua alcançando {projeto, usuário} e nunca {chave}, e agora custa mais (derruba o agente em TODOS os projetos daquela máquina); não há tela onde listar e revogar chave de dispositivo, e a fase acrescentou uma espécie a listar sem construir a tela — a lacuna ficou MAIOR, de propósito; um processo por máquina é ponto único de falha, preço do desenho; e install --machine continua sem saber se a chave daquela pasta é mesmo de máquina, porque em disco as duas espécies são o mesmo arquivo e quem sabe é o servidor

O golden-set do QA volta a medir e roda agendado, semanal (AT-076/AT-149, ADR 0168)​

Duas entregas, uma decisão humana entre elas. A AT-067 tinha medido que o relógio cabia num ubuntu-latest sem GPU e que o instrumento não media: desde a RN-502 o npm test de um projeto container sem container running é recusado, e o seed nunca subia um — 0/6 por construção. O mantenedor escolheu o "caminho 1" e a AT-076 o entregou: o seed sobe o container de cada caso pelo caminho de PRODUÇÃO (module_map → roteamento → container_start proposto pela Infra e aprovado pelo dono → ExecuteContainerStartUseCase → broker → running pela máquina de estados), com node:24.11.1-bookworm-slim preso por digest, a RN-502 intacta, e container que não sobe virando erro NOMEADO que reprova o ExUnit (antes o {:skip, _} do corpo passava verde calado). Duas rodadas em CI deram 3/6 e 3/6 contra o piso de 1/6, com casos diferentes, em 52 e 86 min de job. A AT-149 fechou a cadência: SEMANAL (sábado 01:17 UTC, mais workflow_dispatch, nunca pull_request), em golden-set-qa.yml, no molde do golden-set-rag.yml. Semanal e não noturno porque a rodada custa até 86 min e VARIA — o mesmo total por casos diferentes —, e uma por semana basta para o piso acusar uma queda. timeout-minutes: 150 pelo pior caso medido. O veredito é o do próprio teste; o workflow acrescenta só duas guardas de INSTRUMENTO (a linha de placar tem de existir, e o event log não pode ter recusa da RN-502) e, em falha, imprime os logs como o do RAG — sem issue. A imagem a puxar antes do seed é LIDA da constante do seed, nunca de um segundo literal, porque o docker run do broker roda sob o teto de controle de 30 s (RN-604). A rodada de prova, com gatilho temporário de push removido antes do PR, foi a 36327281774: 3/6 de novo, zero recusas da RN-502, 10 min de mix golden_set.qa e 13 min 41 s de job. Declarado: o broker do CI não tem as cinco camadas do compose, o heartbeat fecha as sessões do seed enquanto o QA ainda trabalha nelas (sem efeito visível), e PR que mexe no workflow não o executa.

O Infra Lead conversa pelo composer (AT-141, ADR 0175, RN-617)​

A RN-584 (AT-098) tirou o destinatário padrão do chat e deixou a pergunta de produto por escrito: o Infra Lead conversa? O mantenedor disse sim em 2026-09-27, com a condição que a própria RN-584 tinha posto — o turno primeiro. O turno do Infra Lead rodava INTEIRO dentro do handle_call/handle_cast: sem aceite, sem "Parar", fora do TurnoOrfao, com o 14º passo do laço terminando calado e um teto de 180 s menor que os 225 s do propose_action de container (RN-605). O kickoff, sendo cast, não prendia clique, mas prendia o processo — 8,4 min medidos na AT-089 —, e uma mensagem nesse meio tempo esperaria na fila. A etapa 1 pôs os três turnos (kickoff, correção de gate, mensagem) no TurnoAssincrono, com a consolidação do Workflows dentro da Task; a correção de gate que chega com turno em curso ganhou uma fila, porque o TurnoAssincrono sem from a descartaria. A etapa 2 deu ao infra a cláusula de message/2 (saiu a recusa agente_sem_conversa), o via_for do "Parar" e o lugar em AGENTES_DE_CHAT; o aceite do handoff dele ficou no card próprio da RN-499, e o card do fio o exclui por nome. SOLO_CONVERSATIONAL_AGENTS não mudou: o Infra Lead é lead de área. Declarado: ele não ganhou perguntas estruturadas nem leitura de backlog.

O git credenciado roda no host do runner, o código no container (AT-116, ADR 0193, RN-676)​

2026-10-01, rodada 36. A RN-558 tinha fechado a metade do SILÊNCIO — o git fetch autenticado em modo runner, com o container de pé, deixou de rodar com as variáveis vazias e passou a ser recusado com a marca credencial-nao-atravessa-o-container — e declarado a outra metade como ADR pendente, porque toda opção conhecida mexia na porta de contenção do ADR 0130. A decisão do dono (01/10) escolheu a que não mexe: operação de git credenciada no HOST, código no container. Medido antes: o único exec do engine com env era o fetch de RunnerGit.fetch!/3; não havia push nem clone credenciado por exec (o clone da criação de pasta já rodava no host, RN-532); e a pasta é a mesma dos dois lados (estado.dir montada em /work). O discriminador escolhido foi uma MARCA explícita do engine (gitCredenciado: true, posta só por RunnerRouter.exec_git_credenciado/5), e não o env que já existia — com o env como chave, qualquer comando com env escaparia do container. A recusa da RN-558 sobreviveu encolhida, para env sem a marca, e na prática passou a significar "runner anterior ao ADR 0193". A prova é a AT-111, dos dois lados: no runner, um git credential fill e um git fetch origin reais com o helper do GitAuth sucedem no host com container ativo; no engine, a corrente pelo TerminalChannel real termina em {:ok, _}. Os ExUnit não rodaram no ambiente da entrega (repo.hex.pm 403) e ficaram para o CI.

A consultiva sem agente pede um agente (AT-254, RN-682)​

2026-10-01, rodada 36. No uso real de 29/09 uma sessão consultiva sem agente respondeu "não tenho acesso a conversas anteriores": a mensagem sem destinatário ia ao SSE de POST .../chat, que manda ao modelo vinculado só o texto atual — sem histórico, sem prompt de sistema — e grava a resposta com o nome do modelo como ator. A decisão do dono (01/10) foi pedir um agente em vez de dar histórico ao chat livre. Medido antes: a tela era o único cliente da rota (o curl impresso pelo seed é dica de desenvolvimento, numa sessão criativa), e o caso de uso SendChatMessageUseCase tinha um segundo consumidor legítimo, os smokes de provider, que o usam como instrumento de ponta a ponta numa sessão sem agente. Por isso a recusa da api mora na ROTA, numa guarda própria, e o caso de uso ficou intacto. Na tela, a linha do destinatário passou a carregar o próprio seletor do handoff manual quando não há agente — o mesmo gesto de sempre, agora no lugar onde ele é a única forma de a mensagem ter destino —, e o Criativo ficou fora da lista porque a consultiva promete que ele não entra.

A revogação mira a chave, e não o par {projeto, usuário} (AT-013, ADR 0201, RN-685)​

2026-10-01, rodada 36. A RN-520 tinha feito a revogação alcançar a conexão viva, com o alvo {projeto, usuário} porque a identidade da credencial morria no PatAuthGuard; o ADR 0154 pôs "revogação por chave" entre o que não fazia, e o PR #534 deu à chave de máquina o mesmo par aplicado projeto a projeto. A AT-013 ficou bloqueada por recorte e por decisão até o dono escolher (01/10): por CHAVE, com ADR, mexendo no engine. Medido antes: o ticket nascia de {userId, kind}, o socket guardava só projeto/usuário/kind, e o canal comparava o usuário — o runner do mesmo usuário com PAT ou outra chave caía junto; a conexão da chave de máquina num projeto fora da lista da api ficava de pé; o ticket emitido antes da revogação entrava depois dela; e revogar PAT não derrubava nada. Entregou-se: duas colunas nuláveis no ticket (migration Ecto, a tabela é do engine — o diário do Drizzle ficou na 0068), o guard anotando a credencial depois de autorizar, o id do socket ganhando espécie e id da credencial, e uma rota runner/disconnect-credential que anula os tickets pendentes e pergunta a todo runner do cluster se nasceu dela. A proibição da AT-013 (não deixar de derrubar o que caía) virou duas peças de transição: a conexão legada cai pelo par nos projetos da linha, e um engine sem a rota faz a api voltar ao par. O par ficou como alvo da remoção de membro. A tela trocou só o texto da confirmação (RN-561). O runner não mudou. Os ExUnit não rodaram no ambiente da entrega (repo.hex.pm 403) e ficaram para o CI.