Web — parte 7 (desde RN-978)
A Anamnese não paga duas vezes pela mesma janela, e o resgate de gate não reinicia ciclo que espera alguém (RN-722)
RN-978 — As mensagens na fila de um agente aparecem num painel acima da caixa, e a cancelada some
- Regra: revisa a apresentação da RN-673: o selo sob cada mensagem sai e o
PainelDaFilalista, acima da caixa, TODAS as pendentes (estadosNaFilaemnaFila), uma linha "Agente: texto" por mensagem. O painel só existe com pendente. Cancelar chama o MESMO endpoint (cancelQueuedAgentMessage), só aparece para quem ENVIOU e com a sessão ativa, não é otimista (o item sai quando o log disserchat.message_cancelled) e a recusa é dita no toast. Decisão do dono (02/11): a mensagem CANCELADA (e a entregue) deixa de aparecer em qualquer lugar; o log a guarda. - Onde:
apps/web/src/routes/PainelDaFila.tsx:20(PainelDaFila). - Teste:
apps/web/src/routes/SessionPage.fila-de-mensagens.test.tsx(lista as pendentes e só quem enviou cancela; sem pendente não há painel). - Origem: AT-683 (EP-031).
RN-979 — Decisão, mapa de módulos, C4, imagem e parecer de QA/SecOps ganham cartão no fio
- Regra: os eventos
artifact.decision_record,artifact.module_map,artifact.c4_diagram,artifact.project_image,artifact.qa_verdicteartifact.secops_verdictaparecem no fio com o MESMO cartão do épico/história, com link para a aba (Arquitetura ou PRs). Título e descrição saem dos campos REAIS do payload: decisão (choice,context), mapa de módulos (sem título; descrição =modules[].name), C4 (systemName,systemDescription), imagem (image,rationale), pareceres (vereditotraduzido,resumo). Campo ausente cai no rótulo do tipo; descrição passa de 200 caracteres é cortada. Leitura do payload, sem query nova. - Onde:
apps/web/src/lib/artefato-do-fio.ts:11(camposDoArtefato),apps/web/src/routes/session-timeline-montagem.tsx:138(ARTEFATOS_DO_FIO). - Onde:
apps/web/src/lib/artefato-do-fio.ts:11(camposDoArtefato),apps/web/src/routes/session-timeline-montagem.tsx:135(ARTEFATOS_DO_FIO). - Teste:
apps/web/src/lib/artefato-do-fio.test.ts(um caso por tipo e payload sem campo opcional). - Origem: AT-683 (EP-031).
RN-980 — As Configurações do projeto são uma página por grupo, com os grupos como abas dentro da página; só o grupo aberto monta
- Regra: revisa a RN-645 no item 3. O sumário lateral deixa de existir: cada grupo do registro (
modelos,projeto,pessoas,avancado) é uma PÁGINA, e os quatro são ABAS numa faixa no topo da página de Configurações (setas, Home e End;aria-selected/aria-controls); "Configurações" segue UM item só da barra lateral, sem sub-itens (decisão do dono, 03/11). O endereço é?tab=settings&group=<grupo>; Modelos é a página padrão.?section=continua valendo: abre a página do grupo da seção e rola até ela, e vence umgroupincoerente; chave aposentada (RN-911) ou desconhecida abre Modelos no topo. O grupo é derivado do campogrupodo registro, nunca de outra lista. Só as seções do grupo aberto montam, então a carga busca só o que é da página (abrindo em Modelos, no máximo 21 requisições, contra 30; teto emconfiguracoes.orcamento.test.tsx). Os itens 1 e 2 da RN-645 (frescor de um minuto, sem poll) não mudam, e trocar de página dentro do minuto não refaz busca. Trocar de página descarta rascunho não salvo, como trocar de aba. - Onde:
apps/web/src/routes/settings/sumario.ts:110(grupoDaSecao),apps/web/src/routes/settings/sumario.ts:115(resolverGrupo),apps/web/src/routes/ProjectSettingsTab.tsx:63(ProjectSettingsTab). - Teste:
apps/web/src/routes/settings/paginas-por-grupo.test.tsx(página só com as seções do grupo; todo?section=antigo cai no seu grupo; rola até a seção),apps/web/src/routes/configuracoes.orcamento.test.tsx(carga de Modelos não busca o resto),apps/web/src/routes/settings/paginas-por-grupo.test.tsx(a faixa de abas e o teclado). - Origem: AT-719 (EP-031).
RN-981 — Na Sessão, as caixas de pergunta e de aprovação ficam na borda do fio, e o contexto e o esqueleto de carregamento ganham o tratamento R3
- Regra: revisa a RN-171 e a RN-173 no alinhamento: a caixa de pergunta estruturada e o contêiner da ação no fio deixam de ser centralizados (
margin: auto) e ficam encostados na borda esquerda do fio, mantendo o teto de 560px. Decisão do dono (03/11/2026). O resto da RN-171 (texto livre) e da RN-173 (um sóApprovalCard) fica. Junto: o botão do contexto tem só ícone, com legenda "Recolher contexto"/"Abrir contexto" earia-expanded; o fio mostra "Carregando sessão…" com esqueleto na mesma condição que adia o convite (RN-131); "Arquivos tocados" é umDisclosurecom a contagem; a faixa de sessão encerrada tem ícone, título e explicação, e "Reabrir sessão" segue com a regra de papel da RN-650. - Onde:
apps/web/src/routes/SessionPage.module.css:1261(.acaoNoFio),apps/web/src/routes/SessionFio.tsx:59(SessionFio),apps/web/src/routes/SessionTopbar.tsx:101(SessionTopbar). - Teste:
apps/web/src/routes/SessionPage.layout-movel.test.tsx(legenda earia-expanded),apps/web/src/routes/SessionPage.sessao-encerrada.test.tsx. - Origem: AT-720 (EP-031).
RN-992 — O composer interpreta @ e / só na tela: @ escolhe destinatários entre os agentes da sessão, / dispara a ação do botão ou insere um prompt, e nenhum comando decide por teto
- Regra:
@e/no início de uma palavra abrem a lista de sugestões (Commanddo shadcn dentro de umPopover); o campo segue NÃO controlado (RN-882) e o texto depois do gatilho só filtra a lista. O@lista SÓ os agentes que já estão na sessão e vira chip; um chip usa a rota por agente de hoje, dois ou mais usamPOST .../messagescomdestinatarios. O/tem nome localizado e aceita o id interno:/pronto,/arquitetura,/handoff,/perguntase/pararchamam o handler e o predicado do botão (RN-976);/resumoe/planoinserem um texto fixo traduzido para a pessoa editar;/ajudalista os comandos;/skillslista as ferramentas e a instrução vigente do destinatário (catálogo da RN-995,queryKey['agent-catalog', projectId]da RN-645; engine fora diz "não sei", nunca "sem ferramentas") e/buscarpesquisa no RAG (POST .../rag/search, sem rota nova) o texto do campo, e ambos só LEEM e dizem o motivo quando falham (AT-724)./aprovarcom UMA pendência pede confirmação e chama o endpoint doApprovalCard; com várias, ou com teto (git_merge,instruction_patch,parallelize,raise_max_parallel, deploy,sudo/doas, RN-154/RN-418), só leva ao card. Enter e Tab escolhem, Esc fecha, e com o menu aberto Enter NÃO envia; composição de acento (RN-830) é ignorada. Nenhum comando criaproposed_action. - Onde:
apps/web/src/lib/comandos-do-composer.ts:115(gatilhoNoCursor),apps/web/src/lib/comandos-do-composer.ts:132(destinoDoAprovar),apps/web/src/lib/use-menu-do-composer.ts:25(useMenuDoComposer),apps/web/src/lib/use-menu-do-composer.ts:97(listarSkills),apps/web/src/lib/use-menu-do-composer.ts:125(buscar),apps/web/src/components/ai-elements/menu-do-composer.tsx:15(MenuDoComposer). - Teste:
apps/web/src/routes/SessionComposer.menu.test.tsx(menu, filtro, Enter sem enviar, Esc,/aprovarcomum e de teto,/resumo,@da sessão, chips e rota nova,/skillse/buscarno caminho feliz e na falha),apps/web/src/lib/comandos-do-composer.test.ts. - Origem: AT-717 (EP-031), lane web.
RN-994 — Na Sessão, a pergunta estruturada vem depois da mensagem do agente que a introduz
- Regra: revisa a RN-162 só na posição. O agente pergunta no meio do turno e fecha com o
agent.response, então ochat.structured_questiontemseqmenor que a resposta; o fio põe o cartão (aberto ou já respondido) logo DEPOIS da bolha deagent.responsedo MESMO agente comseqmaior, antes da próximachat.messagedo usuário. Sem resposta depois (turno em curso), o cartão fica onde está. Move-se o item, não o evento, e o item mantém a flagmensagemdo corte da RN-644. Raciocínio e "Passos do turno" seguem antes da resposta. Decisão do dono (03/11/2026). - Onde:
apps/web/src/routes/session-timeline-montagem.tsx:151(montarTimeline). - Teste:
apps/web/src/routes/session-timeline-montagem.perguntas.test.tsx(resposta antes do cartão; turno em curso; duas perguntas na ordem; não pula para a resposta do turno seguinte). - Origem: pedido do dono, 03/11/2026.
A versão do brabo-contracts é uma só (RN-1038)
RN-1038 — No web, a versão do brabo-contracts é a mesma no alias do package.json, no lock e em scripts/baixar-areas.mjs
- Regra:
@brabo/sharedé o alias do pnpmnpm:@viralabs-dev/brabo-shared@X.Y.Z(pacote público no GitHub Packages, ADR 0270), com versão exata. Oareas.jsonbaixado pelopnpm areas:baixarvem da tagvX.Y.Zdo mesmo contracts. As três fontes (alias, lock e a constanteTAG) têm de coincidir; quem troca a versão usascripts/ci/subir-contracts.shdobrabo-ci, e o lock nunca é editado à mão. Diverge, o checkcontracts-alinhadoreprova; difere docomponentes.jsondobrabo, só avisa. O check não é obrigatório. - Onde:
package.json:20(@brabo/shared),scripts/baixar-areas.mjs:3(TAG),.github/workflows/contracts-alinhado.yml:17. - Teste: o reutilizável
contracts-alinhado.ymldobrabo-ci(specscripts/ci/contracts-alinhado.spec.tsnobrabo-ci) roda no PR;apps/web/src/lib/agent-areas.contrato.test.tsconfere oagent-areas.generated.tscontra oareas.jsonda mesma tag. - Origem: AT-770 (EP-033), R2a.
O web cumpre a RN-1039 (o token do GitHub Packages nunca entra em camada, ARG ou ENV, só como secret do BuildKit), definida no brabo-broker em docs/business-rules/broker.md. A prova no web é test/token-do-registry.test.ts:1, junto de Dockerfile:54, .npmrc:1, .github/workflows/ci.yml:61 e .github/workflows/release.yml:56.
Achados do TP-01 27ª (RN-1060 a RN-1063)
RN-1060 — Aberta por um loopback diferente do da api, a web vai para o loopback da api antes de montar
- Regra:
localhoste127.0.0.1são SITES diferentes para o navegador: a página num e a api (apiUrldo/config.js) no outro dão CORS recusado e cookieSameSite=Strict(RN-034) que não viaja, e a tela parece fora do ar ("api sem resposta"). Antes derestaurarSessao()e do primeiro render, se o host da página e o daapiUrlsão AMBOS loopback (localhost,127.0.0.1,[::1]) e diferentes, a página fazlocation.replacepara o mesmo endereço (porta, caminho, query e hash preservados) com o host da api. Domínio, IP de rede, k8s eapiUrlvazia ou inválida não mudam. Nenhuma origem nova entra no CORS da api nem do engine. - Onde:
apps/web/src/lib/runtime-config.ts:114(enderecoNoLoopbackDaApi),apps/web/src/main.tsx:96(noLoopbackDaApi). - Teste:
apps/web/src/lib/runtime-config.test.ts(127.0.0.1 → localhost com caminho/query/hash, o inverso,[::1], mesmo host, domínio, IP de rede,apiUrlinválida). - Origem: AT-801 (EP-030), TP-01 27ª achado 1.
RN-1061 — O aviso de execução parada, no feed, no fio e em Executores, oferece Rearmar e Desbloquear
- Regra: o
execution.stalledque o engine grava (RN-1059: todos os dev agents parados, tarefa pendente atrás de tarefa bloqueada ou de agente travado) aparece em vermelho no feed de atividade, nomeando as tarefas e os agentes que seguram a execução. Na sessão de execução ele vira uma mensagem do fio (data-testid="execucao-parada") e, acima do time, na aba Executores, o mesmo aviso. Sob ele,DestravarExecucaotraz um Rearmar por agente travado (rearmDevAgent, RN-047) e um Desbloquear por tarefa bloqueada (unblockTask, RN-826), com as mesmas rotas dos atalhos do chat. Os botões só existem enquanto a parada é VIGENTE: o último evento entreexecution.stalled,dev.working,dev.rearmedebacklog.task_unblockedé umexecution.stalled; depois do destrave o aviso fica no histórico, sem botões. Papel abaixo dedevelopervê os botões inertes e o motivo em texto; a recusa da api vira toast com a frase dela. A web não decide nada: só lê o payload do engine. - Onde:
apps/web/src/lib/execution.ts:169(execucaoParadaDe),apps/web/src/routes/DestravarExecucao.tsx:25(DestravarExecucao),apps/web/src/lib/activity.ts:218(classifyEvent),apps/web/src/routes/ProjectExecutorsTab.tsx:201(paradaVigente). - Teste:
apps/web/src/routes/DestravarExecucao.test.tsx,apps/web/src/lib/execution.test.ts,apps/web/src/lib/activity.test.ts. - Origem: AT-800 (EP-030), parte web; engine#57.
RN-1062 — O cartão do plano mostra o teto do projeto e das áreas dev/qa, avisa quando a estimativa passa do que resta e ajusta o teto da área dev ali
- Regra: o "custo esperado" do plano do Dev Lead é o teto POR TAREFA × tarefas (RN-802), não o preço. Enquanto o plano está pendente, logo abaixo dele o cartão mostra o teto do projeto (
GET /projects/:id/budget,maintainer; para papel abaixo a linha do projeto some) e o das áreasdeveqa(GET .../agent-areas), cada um com o gasto, ou "sem teto" (RN-443). Quando a estimativa passa do que resta (limite − gasto, nunca negativo) do MENOR teto aplicável, entre o do projeto (só com políticablock;allownão para nada) e o da áreadev, aparece um avisowarningcom o que resta e a frase "a execução para quando o teto for atingido; a estimativa é o teto por tarefa, não o preço". O aviso NÃO trava o Aprovar (RN-677). "Ajustar teto" (ícone e rótulo) abre na própria linha um campo em dólar para o teto da áreadevsó, com Salvar e Cancelar (setAreaBudget; vazio = sem teto, como na RN-980); abaixo demaintainerfica inerte, com o motivo em texto (ADR 0064). "Outros tetos" abre?section=budgetem outra aba: o teto do projeto, que tem política, segue em Configurações. Reusa as chaves de cache['budget', projectId]e['agent-areas', projectId]; semQueryClient(teste isolado do card) nada é lido nem mostrado. - Onde:
apps/web/src/components/TetoDoPlano.tsx:33(TetoDoPlano),apps/web/src/components/TetoDoPlano.helpers.ts:15(menorTetoAplicavel),apps/web/src/components/ApprovalCard.tsx:432(TetoDoPlano). - Teste:
apps/web/src/components/ApprovalCard.teto-do-plano.test.tsx. - Origem: AT-808 (EP-031); decisão do dono, 07/11/2026 (ajuste só da área dev; o aviso não trava).
RN-1063 — PR que entra na fila de prontas avisa na borda do fio e no título da aba, com Mergear e o tempo de espera, e se dispensa
- Regra: estende a RN-825/RN-841. Quando há PR pronta para o dono mergear (a fila
prontas: gates aprovados, ou PR sem tarefa), umAlertaccent fica FIXO na borda do fio, entre o fio e o composer (RN-981), comrole="status": "PR #3 "título" está pronta para você mergear: os gates passaram", com "Pronta há N min" (sobre oupdatedAtda tarefa da PR, quando há) e oMergearNoChatcom a confirmação na linha (AT-753 P1). Com várias: "N PRs prontas para você mergear" e "Ver as N", que abre a abaprontas(RN-975). "Agora não" dispensa as PRs avisadas; o aviso só volta se ENTRAR uma PR nova. Quando a PR sai da fila (mergeada), o aviso some. O título da aba mostra o contador enquanto houver PR pronta ("(1) Brabo"), mesmo com o aviso dispensado, e volta ao normal quando a fila esvazia. PR que ENTRA na fila (na primeira leitura, as que já esperavam) abre sozinha a abaprontasdo painel de pendências quando nenhuma está aberta (exceção declarada à "nenhuma aba nasce aberta" da RN-975). Sem evento novo, sem mudança em api ou engine e sem notificação do navegador: o aviso lê as mesmas prontas do painel (usePrsProntas, cache compartilhado), pela releitura de 15 s do backlog. O merge segue gesto humano (RN-418/RN-663): o aviso só oferece oMergearNoChat. - Onde:
apps/web/src/components/AvisoDePrPronta.tsx:29(AvisoDePrPronta),apps/web/src/lib/prs-prontas.ts:14(usePrsProntas),apps/web/src/lib/saidas-da-fila.ts:30(entradasDaFila),apps/web/src/components/PendenciasDeOutrasSessoes.tsx:172(idsDasProntas),apps/web/src/routes/SessionPage.tsx:825(AvisoDePrPronta). - Teste:
apps/web/src/components/AvisoDePrPronta.test.tsx,apps/web/src/lib/saidas-da-fila.test.ts,apps/web/src/components/PendenciasDeOutrasSessoes.test.tsx. - Origem: AT-810 (EP-031); decisão do dono, 07/11/2026 (aviso fixo na borda do fio; contador no título da aba; sem notificação do navegador).