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,areacomo 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_membersexistem desde então, por projeto, commax_parallelconfigurá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 deapps/web/src/lib/agents.tssobreviveu 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 emtoken_usagecom 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,
getRepovalidando 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_gateretendo 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_promotionpor projeto commanualcomo DEFAULT NOVO (backfill dirigido põe os projetos existentes emauto),proposed_readycomo 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; scriptpnpm --filter api validacao:fase-12que 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
- CONCLUÍDO em 2026-08-07:
pnpm --filter api validacao:fase-12sai0e 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 empr_settled) e uma do PRODUTO (achado W: o dev agent morria quando a fila esvaziava). - 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.errorDURÁVEL com origem, e o agente FALA no fio; os quatro conversacionais paravam de gravaragent.responsevazio (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ó
ollamafuncionava. - 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).
- Áreas viraram dado:
agent_areas/agent_area_memberspor projeto, commax_paralleldefault 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. - 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. - Configurável por área em Configurações, exigindo
maintainerpelo mesmo motivo de ativar execução: mudar o teto é decidir quanto o produto gasta sem perguntar. - 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.
- 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
- 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.
- Loader na api lendo o registro; endpoint interno de leitura.
- Script
pnpm --filter api validacao:gatesque extrai do event log a última passagem de cada gate ativo e sai != 0 se algum gateblocknã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
- Painel do time exibe o gate que cada story/PR está aguardando, derivado do registro (não hardcoded).
- 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):
429virava TELA BRANCA e a app respondia com mais tráfego. A regra virou geral: toda tela distingue TRÊS estados — carregando, erro (comtrace_ide botão) e vazio.if (!dado) return nullcolapsa 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 oprojectIdcontido na raiz (RN-092), que atingia não só opermissions.jsoncomo o escopo que AUTORIZA terminal. - ADR 0059 —
GIT_OAUTH_STATE_SECRETsem 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:
- 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. - 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 dedeve 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. - Barreira que mora em outra função o CodeQL não enxerga — daí os
três
js/path-injectionsobreviverem à 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:
- 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. - 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. - Dois defeitos reais apareceram no caminho, e nenhum foi pedido:
agent_areasNUNCA é gravada (upsertsem chamador, a API devolve[]), e a gaveta do sino ordenaseq ASCno 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.
design/tokens.cssrecebe--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.- 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. Disclosureno 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.- 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. CLAUDE.mdentra na definição de pronto (ver a seção de Documentação), com regrawarnno docmap. ENTREGUE: a regra éclaude-mdem docs/.docmap.yml, disparada porpackage.json,pnpm-workspace.yaml,apps/*/package.json,apps/engine/mix.exs,docs/adr/*.mdedocs/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)
AgentAreaRepository.upsertnã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.- 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ê
- Matar o fallback genérico do
ApprovalCard, que despejachave: 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. - Colapso nos TRÊS lugares: Aprovações, Insights e o card no chat, com verbo e frase saindo de UM módulo.
- Restrição de projeto: o colapso NÃO introduz prop nova obrigatória em
ApprovalCard. É isso que mantémSessionPage.tsxintocado e tira a aresta com a FASE 20.
FASE 20 — CONCLUÍDA: a sessão ganha identidade
sessionsganhakindenamena MESMA migration — duas migrations sobre a mesma tabela colidem no journal e nos snapshots.- Reconciliar com a derivação por evento:
kindclassifica a INTENÇÃO de criação,execution.activatedcontinua classificando ESTADO de execução, e nenhum reescreve o outro.execution.activatedem sessão consultiva é erro explícito, não conversão silenciosa. - Renomear preservando a hashtag; sem nome, degrada para ela sozinha.
- Botão de voltar ao dashboard — hoje
SessionPage.tsxnão importaLinknemuseNavigate, 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)
useSessionEventscontinua sendo o ESTADO ATUAL (latest, quatro consumidores: roster, árvore, execução, Aprovações). O HISTÓRICO virouuseSessionEventHistory, só para as Atividades, com oafterSeqque 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 existebeforeSeq, 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 leituralatest(MESMAqueryKey, deduplicada) e cada clique desce uma janela fixa para trás.- O sino ordena
DESCno SQL. O corte de "lido" NÃO mudou de semântica, e por isso não há ADR: um corte porseqmarca 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 (latestSeqmenos o corte), não de requisição. - A economia da RN-090/091 não regrediu: uma requisição por ciclo, e o
primeiro "carregar mais antigos" custa ZERO — a leitura
latesttraz 200 e a janela mostra 100. Página antiga temstaleTime: Infinitye nenhumrefetchInterval: janela fechada deseqsobre 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)
- Escopo
areaentrou 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). - A incoerência resolvida ANTES de codar: o binding de agente era GLOBAL
(
scope_id= slug puro,:projectIdda 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_iddeagent/areavirou 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). - A UI mostra quem HERDA e quem DIVERGIU (
AreaModelsSectione a coluna Origem deModelsSection, emProjectSettingsTab.tsx); "voltar a herdar" éDELETEno 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. Papelmaintainerpara mudar o modelo da área (mesma régua do teto de paralelismo, RN-083); o do agente continuadeveloper.
FASE 24 — CONCLUÍDA: Chat e Criativo como lugares (RN-104)
- Duas abas na tela de PROJETO, cada uma listando as sessões do seu
kinde criando naquelekindsem perguntar de novo. A Sessão continua tela própria — a aba não virou contêiner de chat. - 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=sessionsantigo abrir no Chat com a aba MARCADA. Alias resolvido só no painel abriria o painel certo e deixaria a régua sem seleção, porqueTabscomparaactivecomkeye quem escreveactiveé oProjectPage— um dos dois arquivos que a onda manteve fechados. - 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
-
CONCLUÍDO (26a):
GitProviderContractganhoulistTreee diff de PR (11ª/12ª operação), capability declarada SÓ quando provada pela suite. -
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). -
CONCLUÍDO: a tela (
ProjectCodeTab/code/*), registrada emproject-tabs.tssem tocarrouter.tsx/ProjectPage.tsx. Rail (Explorador/Buscar — os únicos com dado real), explorador carregado por DIRETÓRIO, abas de editor com breadcrumb, busca real comfilesScanned/truncated, diff de PR por id conhecido (compatch: nulltratado 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-bgnos 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.
- 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_negocioelistar_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. - 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 doPoServerpassou a emitirtoolloop.limit_reached, o MESMO tipo doToolLoop, que os agentes conversacionais nunca emitiram por terem laço próprio (RN-166). - 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:
gemma:1bnão existe no registry da Ollama (manifest unknown— sógemma3:1bexiste), entãoollama-model-loadersempre falhava e o--waitnunca 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.yamle a RN-415 emdocs/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.- 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 CASCADEnão alcançaengine.*(Ecto/Oban vive em schema PRÓPRIO,engine) nemdrizzle.__drizzle_migrations(o controle do drizzle-kit vive em schema PRÓPRIO,drizzle). Resultado:mix ecto.migratebatia emduplicate_tablenas tabelas do engine que sobreviviam, epnpm 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 totalagora dropamengineedrizzletambém, nesta ordem, antes depublic.
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
GroupedTabsnovo (apps/web/src/components/ ui/GroupedTabs.tsx), construído POR CIMA doTabsgenérico, que continua sem grupo pras outras telas.ChaveDeAbacontinua 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:
ProjectApprovalsTabescopava "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 abaprsnova lista direto do provider de git (nunca por sessão) e cruza comproposed_actionpendente viafindPendingByProject, novo, ao lado do método já existente escopado por sessão — decisão usa osessionIdda PRÓPRIA ação, nuncalatestSession(RN-423). É a primeira produtora real degit_mergepela 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 —
Modalganhousize="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.FolderBrowserModalfunciona onde o projeto já existe. Na criação (NewProjectWizard.tsx), o gap ficou declarado até o ADR 0108: no modomountedo projeto ainda só nasce na confirmação (a validação de caminho toca disco ali), mas no modorunner"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).RunnerOnboardingPanelnovo (compartilhado com a aba Terminal) substitui o<code>cru de antes.apps/runnerganhou 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 praContextAside; 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 paracontainer/mounted.Engine.Runners.RunnerReadinessunifica as TRÊS pré-condições (verificado, conectado, containerrunning) com dois consumidores:TerminalExecutore a materialização do worktree do dev agent, que deixa de tentarFile.mkdir_p!/git initLOCAL 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 canalexec/exec_result(Engine.Actions.Workspace.RunnerGit, novo) —WorktreeManagerbifurca pelo mesmo critério, e o job periódico de limpeza PULA em silêncio um projetorunnersem runner pronto agora. O protocoloexecganhaenvopcional para a credencial de git (ADR 0056) viajar no ambiente do processo filho do HOST — mesclado sobreprocess.env, nunca repassado aodocker execnem logado.container_startdeixa de atenderrunner(o payload dela, com eleição de imagem, nunca fazia sentido lá); nascecontainer_start_via_runner— schema só comrationale, 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.