API — parte 8 (desde RN-618)
O idioma das respostas dos agentes (RN-618..620, RN-622..624, ADR 0177)
O épico EP-028 nasceu de uma conversa em português respondida em espanhol. As decisões do mantenedor de 2026-09-28 (AT-168, AT-169) fixaram onde o idioma das RESPOSTAS mora — conta, sessão e projeto — e a precedência entre as fontes. Nenhuma destas regras toca o idioma da INTERFACE (RN-432).
RN-618 — O idioma das respostas é da PESSOA: escolha na conta, override por sessão, e a precedência que termina sempre num idioma
- Lista aberta, forma canônica. O idioma das respostas é qualquer código
BCP-47 cuja subtag de idioma o ICU da api reconheça, gravado canônico
(
pt-brvirapt-BR) — nunca o enumpt-BR/enda interface, que continua fechado.und, uso privado e texto livre são recusados com 400 que nomeia a regra. - Conta nova nasce no AUTOMÁTICO.
users.response_languageéNULL("automático") até a pessoa escolher; a api mostra e aceitaautomaticocomo o nome desse estado. - Interface e respostas são independentes.
PATCH /users/me/preferencesaceitalocaleeresponseLanguagecomo OPCIONAIS; mandar um não toca o outro, um corpo sem nenhum é 400, e umresponseLanguageinválido recusa o corpo INTEIRO antes de gravar qualquer campo. - O detectado só vale confirmado. O idioma detectado pelas mensagens da
pessoa (a detecção é a RN-624) só entra em
users.detected_languagejunto comdetected_language_confirmed_at— um CHECK recusa um sem o outro. A detecção sozinha não troca nada. - Override por sessão é só de quem o fixou.
PUT projects/:projectId/sessions/:sessionId/response-language(developer, o papel de mandar mensagem a um agente — o override só muda as respostas às mensagens da própria pessoa) fixa um idioma naquela sessão para QUEM CHAMA;language: null(chave obrigatória) solta e volta a herdar da conta. OGET(viewer) devolve o efetivo de quem chama. A chave é o par sessão×usuário: outro participante da mesma sessão nunca é afetado, e não há rota para ler ou mexer no de outra pessoa. - A precedência, resolvida por pessoa e nunca por sessão: override da
sessão > escolha da conta > detectado confirmado >
users.locale. Comousers.localeéNOT NULL, a cadeia sempre termina num idioma, e a origem (sessao/conta/detectado/interface) viaja com ele. O pedido pontual dentro de uma mensagem ("traduza para o inglês") não é degrau: quem o atende é o modelo.
Quem consome: desde a RN-622 o efetivo chega ao modelo — a
api chama ResolverIdiomaDaRespostaUseCase com o AUTOR de cada mensagem a um
agente e a sessão, e o engine põe a orientação no fim de cada chamada de LLM
daquele turno. A Conta deixou de dizer que o valor "ainda não chega".
- Código:
src/domain/iam/idioma-de-resposta.ts:56(normalizarIdiomaBcp47),:118(resolverIdiomaDaResposta);src/db/schema/iam.ts:104(responseLanguage),:111(detectedLanguage),:128(users_idioma_detectado_so_confirmado);src/db/schema/sessions.ts:124(sessionLanguageOverrides);src/application/use-cases/iam/resolver-idioma-da-resposta.use-case.ts:44(ResolverIdiomaDaRespostaUseCase);src/application/use-cases/iam/update-user-preferences.use-case.ts:25(idiomaDaRespostaOuRecusa),:47(UpdateUserPreferencesUseCase);src/application/use-cases/iam/idioma-da-resposta-na-sessao.use-case.ts:19(IdiomaDaRespostaNaSessaoUseCase);apps/web/src/routes/ResponseLanguageSection.tsx - Teste:
test/domain/iam/idioma-de-resposta.spec.ts:12(a régua, e o que ela recusa — caso de falha),:40(a precedência);test/application/use-cases/iam/idioma-da-resposta.use-case.spec.ts:96(conta nova no automático),:124(grava canônico, não toca a interface),:158(inválido não grava nada — caso de falha),:188(o override não alcança o outro participante),:209(404 de sessão de outro projeto);test/infrastructure/persistence/session-language-override.repository.spec.ts:73(o CHECK no banco),:93(upsert pelo par);apps/web/src/routes/ResponseLanguageSection.test.tsx:76(efetivo com origem, sem o aviso antigo de "ainda não chega"),:115(código digitado só salva no botão) - Origem: AT-162, sobre as decisões das AT-168 (respostas 1, 2, 3, 4 e 6) e AT-169 (resposta 2); especificação da AT-079
RN-619 — O projeto tem um idioma, o de tudo que não tem autor humano, e ele nasce do idioma de quem cria
- O que ele governa (decisões do mantenedor: AT-168 resposta 8, AT-169
resposta 1): o idioma dos artefatos COMPARTILHADOS (product brief, regras
de negócio, ADRs) e dos turnos SEM autor humano (abertura por
kickoff, dev agents, gates, mensagem de commit, corpo de PR). O que um agente responde a uma PESSOA continua sendo o idioma das respostas dela (RN-618). - Sempre um idioma concreto.
projects.languageéNOT NULL, código BCP-47 canônico pela MESMA régua da RN-618 — mas sem "automático", que é resolver pelas mensagens de uma pessoa e não cabe no que não tem pessoa.automaticoe código não reconhecido são 400, e o projeto não nasce (ou não muda). - Projeto novo: o
languagepedido na criação, canonicalizado; sem pedido, o idioma EFETIVO das respostas de QUEM CRIA (ResolverIdiomaDaRespostaUseCase, sem sessão: escolha da conta > detectado confirmado > idioma da interface). A proposta da nota era ousers.localede quem cria; a cadeia inteira foi escolhida porque quem já disse em que idioma quer trabalhar não deveria ver o projeto nascer em outro — e, sem escolha nem detecção, ela É ousers.locale. - Projetos que já existiam: a migração
0062_idioma_do_projetograva o idioma do TITULAR do workspace (workspaces.created_by, de quem é a credencial que os agentes gastam — RN-058/RN-616), pela mesma cadeia. Na migração ninguém tinha escolha nem detecção ainda, então o valor é ousers.localedele. O titular, e não "um owner" deworkspace_members, porque ele é UM só e a migração não pode escolher entre dois. - Quem troca:
PATCH /projects/:projectIdcomlanguage(maintainer, o mínimo da rota), e a seção "Idioma do projeto" em Configurações, com o controle inerte e o motivo em texto para quem não alcança (RN-102). O defaultpt-BRda coluna é só a rede de quem insere por fora do caso de uso (seed, script, fixture).
Quem consome, e o que sobra: desde a RN-622 o engine lê a
coluna (Engine.Projects.Project, direto do Postgres) e a manda ao modelo em
toda chamada de LLM de turno SEM autor humano, e desde a RN-623
também como idioma do artefato compartilhado que um agente grava DURANTE um
turno com autor humano — a divergência que esta regra declarava fechou.
- Código:
src/db/schema/iam.ts:262(language);src/db/migrations/0062_idioma_do_projeto.sql(o backfill);src/application/use-cases/iam/idioma-do-projeto.ts:13(idiomaDoProjetoOuRecusa);src/application/use-cases/iam/create-project.use-case.ts:72(language);src/application/use-cases/iam/update-project.use-case.ts:18(idiomaDoProjetoOuRecusa);src/interfaces/http/iam/dto/iam.response.dto.ts:233(language);apps/web/src/routes/settings/ProjectLanguageSection.tsx - Teste:
test/application/use-cases/iam/idioma-do-projeto.use-case.spec.ts:63(nasce com o idioma de quem cria),:74(a escolha da conta vence a interface),:100(inválido eautomatico— caso de falha, o projeto não nasce),:132(PATCH inválido não muda nada),:152(a migração grava o do titular);apps/web/src/routes/settings/idioma-do-projeto.test.tsx:84(vigente e a declaração do que ainda não segue ele),:125(abaixo demaintainer: o valor fica, o controle fica inerte) - Origem: AT-243, sobre as decisões das AT-168 (resposta 8) e AT-169 (resposta 1)
RN-624 — A api detecta o idioma do autor pelas próprias mensagens e PERGUNTA; só o confirmado vira preferência
A RN-618 deixou o degrau "detectado confirmado" pronto e vazio. Decisão do mantenedor (AT-168 respostas 4, 6 e 7): detectar, PERGUNTAR se o idioma está certo, e o confirmado vira o padrão — por pessoa, global, com a pergunta na tela onde o idioma já é mostrado e corrigido.
- A régua é UMA, e mora na api. A heurística da AT-080, medida pela
AT-160 (marcadores contrastivos de
pt/es/en, a limpeza de código, log, citação, URL e identificador), saiu descripts/idioma/parasrc/domain/iam/heuristica-de-idioma.ts, e o instrumento a REEXPORTA de lá — o que o produto roda é o que o instrumento mede. A regra de O QUE é evidência (só atoruser; no formulário, só as respostas, e o eco com os rótulos do agente pulado) também é uma só,evidenciasDoAutor, usada pela extração do corpus real e pela detecção. Local, sem LLM, sem dependência nova. - A amostra é do event log, a histerese é refeita. A detecção lê as 11
evidências mais recentes da pessoa em TODAS as sessões (a janela de 10 mais
a que a histerese refaz) e decide quando as DUAS últimas avaliações da
amostra apontam o mesmo idioma (
idiomaConcordante). Não há estado de detecção guardado: a avaliação da mensagem anterior é refeita com a janela que terminava nela. A leitura é servida por um índice PARCIAL (session_events_evidencia_de_idioma_idx, atorusere os dois tipos de evidência): medido com 200 mil eventos, 0,056 ms pelo índice contra 13,5 ms da varredura sem ele. - Os limiares são PROVISÓRIOS.
PARAMETROS_PROVISORIOSsão os candidatos da AT-080 com UMA diferença, a evidência mínima de 10 palavras e não 20 — no corpus SINTÉTICO a 10 a cobertura dos decidíveis sobe de 42,2% para 58,9% com os mesmos três erros (todos em língua fora das três). Listaat080; limiar 0,8, margem 0,3, amostra 10 mensagens / 2.000 caracteres, histerese 2. O corpus sintético foi escrito pela mesma mão que escreveu a heurística: nenhum destes números está calibrado, e o dono os recalibra com o corpus real (pnpm --filter @brabo/scripts idioma:medir --real), mudando o objeto, que é o único lugar de onde a api os lê. - Quando pergunta.
GET .../sessions/:sessionId/response-languagedevolvedetectionQuestioncom o idioma a perguntar, ounull. Não pergunta quando: a detecção não concorda (texto curto, "ok", código, log, misto); o efetivo vem de escolha EXPLÍCITA — override da sessão ou Conta —, porque confirmar gravaria um degrau ABAIXO delas e a resposta não mudaria; o detectado já é o efetivo, comparado pela SUBTAG (ptnão diverge dept-BR— a heurística só sabe a língua); ou a pessoa já recusou esse idioma. - A detecção nunca troca nada sozinha. Só
POST /users/me/preferences/detected-languagegrava:confirmescreve o pardetected_language+detected_language_confirmed_at(o CHECK da RN-618) e apaga a recusa antiga do mesmo idioma — e é 409deteccao_mudou, sem gravar, quando as mensagens já não apontam o idioma (a coluna diz "detectado" e não pode guardar o que a heurística não detectou);declinegrava a recusa emdetected_language_declines(uma linha por pessoa e idioma, com a data), e ESSE idioma não é perguntado de novo. O que se guarda é o idioma e a decisão da pessoa — nunca país, nacionalidade ou proficiência. - Onde a pergunta aparece. Num cartão pequeno ancorado logo abaixo do indicador de idioma da barra da sessão (RN-620): "Detectamos que você escreve em espanhol (es) — usar espanhol (es) nas respostas?", com "Usar", "Não" e "Agora não". Sem modal, sem cobrir o composer; "Agora não" só esconde nesta tela, sem gravar, e a pergunta volta na próxima leitura. Enviar mensagem (ou responder formulário) relê o indicador, porque a mensagem nova é a evidência que pode fazer a pergunta aparecer.
- Falha nunca bloqueia nada. A detecção roda na LEITURA do idioma, e não
dentro de
SendAgentMessageUseCase: a evidência já está no event log, a pergunta sobrevive a recarregar a página e aparece em qualquer sessão, e o envio da mensagem não passa por ela — não ganha latência e não tem como falhar por ela. Na leitura, qualquer falha (banco, heurística) virawarningedetectionQuestion: null; o idioma continua sendo mostrado. - Custo: CPU medido no corpus sintético, nesta máquina — 11,6 µs para limpar e classificar uma mensagem, 110 µs por detecção (11 mensagens, duas avaliações), mais a leitura indexada do item 2. Zero token.
O que esta regra NÃO fecha, declarado: a heurística conhece três línguas,
e a lista de idiomas é aberta — galego e francês podem virar es/pt (os
erros do sintético), e sob esta regra o custo é uma pergunta errada, que a
pessoa recusa. Uma mensagem em espanhol numa janela portuguesa deixa a
amostra indeterminada enquanto estiver nela (sem pergunta, sem troca). E o
arquivo da heurística não pode ter import: o instrumento o executa por type
stripping, e scripts/idioma/ tem tsconfig próprio sem verbatimModuleSyntax
para aceitar os export de um módulo CommonJS.
- Código:
src/domain/iam/heuristica-de-idioma.ts:175(PARAMETROS_PROVISORIOS),:457(evidenciasDoAutor),:500(mensagensNecessarias),:520(idiomaConcordante);src/domain/iam/deteccao-de-idioma.ts:43(idiomaAPerguntar);src/application/use-cases/iam/detectar-idioma-do-autor.use-case.ts:57(DetectarIdiomaDoAutorUseCase),:66(detectar),:88(pergunta),:123(responder);src/infrastructure/persistence/drizzle/deteccao-de-idioma.repository.ts:27(ultimasEvidencias),:75(confirmar);src/db/schema/iam.ts:144(detectedLanguageDeclines);src/db/schema/sessions.ts:168(session_events_evidencia_de_idioma_idx);src/interfaces/http/iam/session-response-language.controller.ts:66(comPergunta);src/interfaces/http/iam/user-preferences.controller.ts:94(answerDetected);apps/web/src/routes/SessionLanguageIndicator.tsx;scripts/idioma/heuristica.ts(a reexportação) - Teste:
test/domain/iam/deteccao-de-idioma.spec.ts:29(os provisórios),:42(duas avaliações concordam),:48(uma só não basta),:52(indeterminado não aponta — caso de falha),:77(o eco do formulário pulado),:105..:158(quando pergunta);test/application/use-cases/iam/detectar-idioma-do-autor.use-case.spec.ts:109(pergunta sem gravar),:116(confirmar),:131(recusar não pergunta de novo),:142(indeterminado não pergunta — caso de falha),:148(Conta explícita),:159(erro na detecção vira "sem pergunta" — caso de falha),:169(409deteccao_mudou);test/infrastructure/persistence/deteccao-de-idioma.repository.spec.ts:87(só o autor, todas as sessões),:166(o índice parcial),:200(o par e a recusa apagada);apps/web/src/routes/SessionLanguageIndicator.test.tsx:117("Usar"),:148("Não"),:162("Agora não" não grava),:172(409 vira toast) - Origem: AT-163, sobre as decisões da AT-168 (respostas 4, 6 e 7), a especificação da AT-080 e a medição da AT-160
Decidir no chat, onde o dono está (RN-626)
RN-628 — O merge executado fecha a tarefa da PR: done, uma vez só
O gate de QA/SecOps termina em awaiting_user e nada mais mexia no status da
tarefa: no uso real de 29/09 o pr-6 foi mergeado três vezes e as tarefas
seguiram em in_review (AT-275).
- Depois do merge, nunca no lugar dele.
git_mergecontinua sendorequire_approvalincondicional (RN-418); a regra roda DENTRO deExecuteGitActionUseCase, na transação que grava o resultado do merge, e só quando o provider devolveustate: 'merged'(PR aberta, ou merge que falhou, não toca a tarefa). - A tarefa é achada pela PR que a abriu. Toda
pr_opendo projeto cujopullRequestIdé o da PR mergeada e que carregastoryTaskIdno payload. PR sem tarefa (infra, ADR) não muda nada. - Idempotente.
TaskRepository.markDoneIfNotDoneé umUPDATE … WHERE status <> 'done': merge repetido da mesma PR devolvenull, não move de novo e NÃO grava o evento. Quando move, grava UM evento imutávelbacklog.task_status_changed(cause: 'pr_merged',pullRequestId,actionId), append-only, sem UPDATE em tabela de eventos. Sem migration. - Fronteira declarada (AT-249). Esta regra NÃO recusa merge de PR já
mergeada nem consulta
gate_status. A recusa da PR já mergeada (e da proposta repetida) veio depois, na RN-663, antes de o merge chegar aqui; o gate pendente, por decisão do dono, é só aviso na tela. - O merge na branch de trabalho também avisa o engine. Com
state: 'merged'etargetBranch = main,settleMergeanexa o evento de outboxproject.trunk_merged(RN-1022), para PRs de tarefa, de infra e de ADR igualmente.
- Onde:
src/application/use-cases/actions/execute-git-action.use-case.ts:374(settleMerge),src/infrastructure/persistence/drizzle/backlog.repository.ts:494(markDoneIfNotDone) - Teste:
test/application/use-cases/actions/execute-git-action.use-case.spec.ts("git_merge marca a tarefa como done": feliz, repetido, PR aberta/merge falho, PR sem tarefa) - Origem: AT-275
RN-630 — Quem está ativo na sessão se lê da sessão INTEIRA, não dos últimos 200 eventos
"Quem está ativo" e "quem recebe o composer" saíam do agent.activated visto
na janela de 200 eventos (latest: true). Numa sessão longa as ativações do
Arquiteto e da Infra saíam da janela; as ofertas de handoff endereçadas a eles
voltavam a parecer aceitáveis e o "oi" ia para o último agente que ainda
aparecia. É a classe de defeito da RN-180 (teto silencioso faz a
tela afirmar sobre o que não leu), já fechada para a presença de QA/SecOps na
RN-568.
A regra: o resumo do projeto (RN-090) devolve
roster.activatedAgents — os agentes com ao menos um agent.activated na
sessão mais recente, um por agente, o de ativação mais recente (por seq)
primeiro. Quatro decisões:
- A fonte continua sendo o event log. Nenhuma coluna derivada, nenhum
índice e nenhuma migration: o campo entra na MESMA varredura por sessão que
já calcula
executionActivated/gatesEverOpened(array_agg ... filter), então o número de idas ao banco do resumo não muda (o spec de contagem constante segue valendo). A migration 0066 reservada não foi usada. - Sem HTTP novo por poll. O campo viaja no resumo que as telas já leem (RN-579).
- O cliente SOMA à janela e só confia com o resumo da MESMA sessão
(
latestSessionId === sessionId), como na RN-568: ativação é monótona (agente ativado não se desativa), então a janela só dá falso negativo. Resumo de outra sessão não vale; sem resumo, a janela decide sozinha. - A api diz o que aconteceu, não quem recebe. O destinatário explícito e
visível no chat (AT-251) e o aceite por
handoffId(AT-253) consomem este campo na web e NÃO fazem parte desta regra.
Lacuna declarada: esta rodada só entrega o CONTRATO da api. apps/web
(session-readiness.ts, session-handoffs.ts) segue lendo a janela até a
AT-251/AT-253 passarem a consumir roster.activatedAgents.
Fechada na RN-631 (AT-251): o destinatário do composer e as ofertas de handoff da tela de Sessão SOMAM
roster.activatedAgentsà janela, com a guarda da mesma sessão.criativoActive/arquitetoActive(session-readiness.ts) seguem lendo só a janela.
- Onde vive:
src/infrastructure/persistence/drizzle/projects-summary.repository.ts:212(activatedAgents);src/application/ports/projects-summary-repository.port.ts:32(activatedAgents) - Testes:
test/infrastructure/persistence/drizzle/projects-summary.repository.spec.ts("activatedAgents cobre ativações anteriores à janela de 200 eventos" e "activatedAgents é da sessão MAIS RECENTE: ativação de sessão antiga não vale") - Origem: AT-252
RN-635 — Uma oferta de handoff pendente por destino no projeto; a que deixa de valer vira superseded
handoffs só gravava offered/accepted, sem expiração, supersessão nem
dedupe: duplo clique em "arquitetura pronta" dava 2× Infra e 2× Dev Lead, e
oferta a agente já ativo ficava acionável para sempre (AT-291,
ADR 0182).
- No máximo UMA
offeredpor (projeto, destino).decidirOfertadecide: sem pendente, cria; pendente na MESMA sessão e sem artefato novo (o novo énullou igual), devolve a existente (desfecho: ja_oferecido, sem linha nem evento); qualquer outro caso cria a nova e substitui a pendente (substituiu_oferta) — devolver a antiga perderia o artefato novo, ou deixaria quem pediu sem nada para aceitar na sessão em que está. - Quem substitui grava. A linha vira
supersedede nasce um eventohandoff.superseded(handoffId,toAgent,motivo,substitutaId) na sessão da oferta VELHA, atorsystem, mesmo com a sessão encerrada. Ohandoff.offeredoriginal não é tocado. - Serializado por destino.
CreateHandoffUseCaseroda numa transação compg_advisory_xact_lockdo par (projeto, destino): duas abas chegando juntas produzem UMA linha. Duplicatas de antes convergem na próxima oferta ou ativação, com evento; a migration não reescreve linha nenhuma. - Agente já ativo não recebe oferta: 409
reason: agente_ja_ativo, sem linha nem evento. Ativo =agent.activatednuma sessão NÃO terminal do projeto. Amessageé o texto que o modelo lê:offer_handoffa repassa literal, eFalhaDeTurno.origem/1a classificapolitica. - Ativar substitui.
ActivateAgentUseCase(aceite e ativação direta) eActivateExecutionUseCase(pordev-<modulo>) substituem, depois de ativar, toda oferta pendente ao agente em qualquer sessão do projeto; a aceita não é tocada, e ativação recusada não substitui nada. - "Arquitetura pronta" é idempotente.
OfferInfraHandoffUseCasenão aciona o destino (infra,dev-lead) que já tem oferta pendente ou está ativo; com os dois assim, não grava nem chama o engine (desfecho: ja_oferecido,jaAtendidoscom o motivo de cada um).
- Onde:
src/domain/sessions/ciclo-de-vida-do-handoff.ts:71(decidirOferta),:104(mensagemDeAgenteJaAtivo);src/application/use-cases/agents/ciclo-de-vida-do-handoff.service.ts:43(sessaoOndeEstaAtivo),:71(substituir),:97(substituirOfertasAoAtivar);src/application/use-cases/agents/create-handoff.use-case.ts:109(travarOfertasDoDestino);src/infrastructure/persistence/drizzle/handoff.repository.ts:94(travarOfertasDoDestino);src/application/use-cases/agents/activate-agent.use-case.ts:88(substituirOfertasAoAtivar);src/application/use-cases/execution/activate-execution.use-case.ts:327(substituirOfertasAoAtivar);src/application/use-cases/agents/offer-infra-handoff.use-case.ts:96(jaAtendido);apps/engine/lib/engine/harness/tools/offer_handoff.ex:132(agente_ja_ativo);apps/engine/lib/engine/agents/falha_de_turno.ex:67(agente_ja_ativo);src/db/migrations/0064_ciclo_de_vida_do_handoff.sql - Teste:
test/application/use-cases/agents/ciclo-de-vida-do-handoff.spec.ts(contra Postgres: repetida na mesma sessão, duplo clique concorrente, artefato novo, outra sessão encerrada, dado de antes do ADR, agente ativo e ativo em sessão encerrada, ativação que substitui e ativação recusada);test/application/use-cases/agents/offer-infra-handoff.use-case.spec.ts("ADR 0182: segundo clique…", "ADR 0182: só aciona o destino que falta…");test/application/use-cases/agents/activate-agent.use-case.spec.ts;test/application/use-cases/execution/activate-execution.use-case.spec.ts("ciclo de vida do handoff");apps/engine/test/engine/harness/tools/offer_handoff_test.exs;apps/engine/test/engine/agents/falha_de_turno_test.exs;apps/web/src/lib/session-handoffs.test.ts(a tela:supersedednão vira card acionável no fio, na faixa fora da janela nem no card da Infra) - Origem: AT-291
RN-637 — A tela lê a CAUDA das ações da sessão, e a pendente antiga volta junto
GET /projects/:projectId/sessions/:sessionId/actions ordenava por seq
crescente com teto de 200, e usePendingActions nunca paginava: a partir da
ação 201 de uma sessão — o que uma execução real alcança fácil, um dev agent
propõe dezenas de comandos por tarefa — a pendente NOVA ficava fora da única
página lida e sumia do fio, dos Executores e de Aprovações (AT-296), justamente
quando havia alguém esperando por ela.
A regra:
- A rota aceita
latest=trueestatus=pending.latesttraz aslimitações deseqmais alto, devolvidas em ordem CRESCENTE e semnextCursor, ignorandoafterSeq— o mesmo contrato dolatestdos eventos (ADR 0021).statussó aceitapending(outro valor é 400, sem consultar); combinado comlatest, traz as pendentes mais novas sem que as decididas ocupem a janela. Sem os dois parâmetros o contrato é o de antes. - A tela faz duas perguntas, e a segunda só quando precisa.
buscarAcoesDaSessaolê a cauda (200); só se ela vier CHEIA pede também as pendentes e une porid, emseqcrescente. Uma pendente antiga que a cauda empurrou para fora continua sendo decisão a tomar e não some por ter esperado demais. A sessão curta — o caso comum — continua custando UMA requisição, e a leitura de pendentes que falha derruba a query (a tela diz que falhou, nunca finge fila vazia — RN-088).
O que esta regra NÃO fecha: com mais de 200 PENDENTES numa sessão, as mais
antigas além delas ficam fora (o teto é o mesmo limit); e o que o fio mostra
de DECIDIDO continua sendo a cauda — ação decidida antiga, fora das 200 mais
novas, não aparece no fio. Pendentes do PROJETO inteiro são outra leitura
(RN-638).
- Código:
src/infrastructure/persistence/drizzle/proposed-action.repository.ts:96(listPaginated),:113(o ramolatest);src/interfaces/http/actions/actions.controller.ts:105(list),:114(a recusa destatus);src/application/ports/proposed-action-repository.port.ts(ListProposedActionsOptions);apps/web/src/lib/acoes-da-sessao.ts:48(buscarAcoesDaSessao),:30(juntarCaudaEPendentes);apps/web/src/lib/hooks.ts:432(usePendingActions) - Teste:
test/infrastructure/persistence/proposed-action-latest.repository.spec.ts(semlatesta pendente nova fica de fora — o defeito; comlatestela está na cauda;status=pendingdevolve a antiga; e não traz outra sessão);test/interfaces/http/actions/actions-list.controller.spec.ts(repasse dos parâmetros e o 400 — caso de falha);apps/web/src/lib/acoes-da-sessao.test.ts(sessão curta numa requisição; cauda cheia com 250 ações devolve a pendente 3 e a 250; leitura de pendentes que falha derruba a query) - Origem: AT-296 (HS-076)
RN-642 — "Sempre permitir" aprova e grava o padrão na MESMA transação; clique em ação já aprovada é sucesso nomeado
No uso real de 29/09, 45 de 173 cliques em "Sempre permitir" devolveram 409
(AT-310): o padrão era gravado ANTES de aprovar, e uma ação que já tinha saído
de pending (clique duplo, a mesma pendência em dois painéis, outra aba)
lançava 409 com o padrão JÁ gravado e SEM o evento permission.granted.
- Mesma transação, depois da transição.
ApproveActionUseCaseaceita umaoAprovarque roda DENTRO da transação da decisão, depois deassertTransitionpassar e antes da execução; é por ele que o padrão (permissions.json/allowouagent_autonomy, RN-509) e opermission.grantedsão gravados. "Aprovar antes, gravar depois" em transações separadas foi recusado: trocaria o defeito por ação aprovada (e até executada) com o padrão falhando depois.agent_autonomye o evento são atômicos com a decisão; opermissions.jsoné arquivo e não entra em transação — ele só é escrito depois de a transição ser válida, e falhar ao escrevê-lo desfaz a aprovação. Sobra, declarada, a janela de o COMMIT falhar depois do arquivo escrito. - O evento acompanha a gravação, e só ela. Padrão que já existe não é
regravado nem narrado de novo (
padraoGravado: false). - Ação já aprovada é sucesso nomeado. Se a transição é recusada porque a
ação já está
approved/auto_approved/executed/failed, a resposta é 201 comdesfecho: 'ja_aprovada'(o caminho normal devolve'aprovada'), sem executar de novo, gravando o padrão só se ele faltar — com o evento. Transição inválida vinda da EXECUÇÃO, depois de a decisão ter sido confirmada, NÃO viraja_aprovada: propaga como sempre. - Ação RECUSADA continua 409, nomeado (
reason: 'acao_ja_recusada'), sem gravar padrão nem evento: liberar para sempre o que alguém acabou de recusar não é idempotência. - Tetos intactos.
git push/PR/deploy,sudo/doas(motivoDeRecusaSempreAprovar, RN-418) econtainer_removecontinuam recusados com 400 ANTES de qualquer leitura de estado — inclusive com a ação já aprovada. O padrão continua o EXATO da ação (a generalização por verbo é a AT-257, fora daqui). - Os TIPOS do teto também (AT-320). Medido na auditoria de 30/09: a api
só recusava o
git pushDIGITADO no terminal; ogit_pushTIPADO passava e gravavaGitPush()emallow(ou autonomia do módulo), e oApprovalCardoferecia o botão.TIPOS_SEM_SEMPRE_PERMITIRé a lista ÚNICA —git_push,pr_open,git_merge(inteiro: o padrãoGitMerge()valeria para todo destino),container_remove,instruction_patch,parallelize,raise_max_parallel— emotivoDeRecusaDoSempreAprovarjulga terminal pelo comando e o resto pelo tipo. A recusa é 400 comreason: 'teto_do_sempre_permitir'eactionType. A web esconde o botão pela cópia emapps/web/src/lib/sempre-permitir.ts, conferida contra a da api por teste; comando de terminal segue com o botão (repetir o parser na tela seria segunda régua) e recebe a recusa da api. Quando nasceu, NÃO mudavadecide()nem a semeadura; desde a RN-689 mudam os dois —git_push/pr_openestão no teto dedecide()e a ativação semeia sógit_commit—, e o padrão que o clique gravaria para eles seria inerte.
- Onde:
src/application/use-cases/actions/approve-always-action.use-case.ts:109(execute),:190(cliqueSobreAcaoJaDecidida),:240(gravarPadraoSeFaltar),src/application/use-cases/actions/approve-action.use-case.ts:300(approve),src/domain/actions/sempre-permitir.ts:30(TIPOS_SEM_SEMPRE_PERMITIR),:65(motivoDeRecusaDoSempreAprovar),apps/web/src/lib/sempre-permitir.ts:15(TIPOS_SEM_SEMPRE_PERMITIR) - Teste:
test/application/use-cases/actions/approve-always-action.use-case.spec.ts("ordem e idempotência (RN-642)": clique duplo, cliques concorrentes, ação aprovada por outro caminho, recusada → 409 nomeado sem padrão, padrão que falha desfaz a aprovação, tetos com a ação já aprovada; "tipos do teto (AT-320)"),test/domain/actions/sempre-permitir.spec.ts,apps/web/src/lib/sempre-permitir.test.ts(a cópia do web igual à da api),apps/web/src/components/ApprovalCard.test.tsx(git tipado sem o botão) - Origem: AT-310, AT-320
A sessão encerrada pode ser reaberta (RN-649/RN-650)
RN-649 — Sessão closed/closed_abnormally reabre para active com o log intacto, e o fechamento anterior vira evento novo
A RN-001 dizia que os estados terminais não têm saída. Desde o
ADR 0183 há UMA, e só uma: a
reabertura, de closed ou closed_abnormally para active, por caminho
próprio. A transição GENÉRICA continua recusando closed → active com 409, e
closing → active segue proibido por qualquer caminho.
- O que volta é a sessão inteira. O event log não é tocado — a conversa,
as regras de negócio, as perguntas respondidas, as ofertas e aceites de
handoff. Um agente conversacional parado no fechamento volta pelo
start_agentidempotente na primeira mensagem e reconstrói o histórico (RN-580). A conversa volta a entrar porque a recusa da RN-581 lê o estado da sessão, que agora éactive. - O fechamento anterior não se apaga. A linha de
sessionsperdeclosed_atetermination_reason; o evento NOVOsession.reopened, no mesmoseq(RN-002) e com o ator humano que reabriu, carrega{from, to, closedAt, terminationReason}. Nenhum evento é editado nem renumerado, e okindnão muda (RN-097). - Sessão com execução não reabre — 409
sessao_com_execucao. Reabrir uma sessão comexecution.activateda devolveria afindActiveExecutionSession(RN-139). Decisão do dono, confirmada no ADR 0184. - Sem prazo. Toda sessão encerrada é reabrível (decisão do dono, ADR 0184).
- O engine é chamado antes da transação, como na ativação: falha dele
deixa a sessão encerrada, sem evento. Outro estado que não terminal é 409
sessao_nao_encerrada. - O fechamento que a reabertura desfez não para a sessão viva. O
SessionLifecycleWorkerlê ostatusda sessão e ignora, com log, o job desession.closed/session.closed_abnormallyde uma sessão que a api já diz não-terminal. - A segunda análise automática do Psicólogo fica bloqueada pelo
alreadyAnalyzedque já existia; o reprocessamento manual segue. A segunda consolidação do grafo estende a janela da mesmaInteracao.
- Onde:
src/domain/sessions/session-state-machine.ts:66(canReopen),:70(assertReopen);src/domain/sessions/reabertura-de-sessao.ts:24(SessaoComExecucaoNaoReabreError),:44(garantirQueSessaoSemExecucaoReabre),:58(payloadDaReabertura);src/application/use-cases/sessions/reopen-session.use-case.ts:53(execute),:96(garantirQuePodeReabrir);apps/engine/lib/engine/workers/session_lifecycle_worker.ex:59(encerrar_se_ainda_encerrada);apps/engine/lib/engine/sessions/project_session.ex:48(status) - Teste:
test/domain/sessions/session-state-machine.spec.ts:56(só terminais reabrem;closingnunca volta);test/application/use-cases/sessions/reopen-session.use-case.spec.ts:121(closedreabre, log preservado,session.reopenedcom o fechamento anterior — caminho feliz),:151(closed_abnormally),:163(a conversa volta a entrar),:186(closingé 409 sem engine nem evento — caso de falha),:208(sessão com execução é 409sessao_com_execucao),:239(engine fora do ar: nada muda);apps/engine/test/engine/workers/session_lifecycle_worker_test.exs:66(fechamento de sessão jáactivenão para o processo),:82(fechamento de sessãoclosedpara, como sempre) - Origem: AT-071 (HS-048), sobre o
exp001
RN-650 — Reabrir é developer, por rota própria, e a tela só oferece o botão a quem alcança o papel
POST projects/:projectId/sessions/:sessionId/reopen exige developer — o
MESMO papel da transição genérica que encerra a sessão: quem pode dá-la por
terminada pode trazê-la de volta. Responde 200 com a sessão. viewer é 403.
O ADR 0183 tinha fixado maintainer
como padrão provisório; o dono decidiu developer em 2026-09-30
(ADR 0184), confirmando junto
as outras duas decisões — sem prazo, e sessão com execution.activated
recusada.
Na tela de Sessão, a faixa da sessão encerrada mostra "Reabrir sessão" com uma
frase do que volta. Abaixo de developer (roleAtLeast, papel de WORKSPACE —
a lacuna da RN-471 já declarada na tela) o botão fica inerte e o
motivo é dito em TEXTO. A recusa da api (409 de sessão com execução, 403) vira
toast com a frase dela. O fio narra session.reopened como "sessão reaberta",
com a causa do fechamento anterior quando ela foi gravada.
- Onde:
src/interfaces/http/sessions/sessions.controller.ts:214(reopen);apps/web/src/lib/api-client.ts:634(reopenSession);apps/web/src/routes/SessionPage.tsx:147(podeReabrir);apps/web/src/lib/session-acoes-da-sessao.ts:77(handleReopen);apps/web/src/routes/SessionComposer.tsx:383(o botão);apps/web/src/lib/activity.ts:633(a frase do fio) - Teste:
test/interfaces/http/sessions/sessions-reopen.controller.spec.ts:22(developer, o mesmo da transição genérica),:36(viewer não alcança o papel da rota; developer, maintainer e owner alcançam),:49(200);apps/web/src/routes/SessionPage.reabrir-sessao.test.tsx:143(developer reabre pela rota própria — caminho feliz),:167(maintainer e owner também),:178(viewer/sem papel: inerte com o motivo em texto — caso de falha),:191(recusa da api vira toast),:213(sessão ativa não oferece);apps/web/src/lib/activity.test.ts("reabertura de sessão") - Origem: AT-071; papel decidido na AT-337
Acompanhar ao vivo sem martelar (RN-639)
RN-654 — Os bindings resolvidos de todos os agentes e áreas vêm numa leitura só
A RN-645 mediu 49 requisições na carga da aba Configurações, e 20 delas eram o binding RESOLVIDO de cada agente (17) e de cada área (3), uma rota por chave. Três seções liam essas chaves — modelos por agente, modelos por área e melhores modelos por capacidade —, e o teto de 300 req/min é do USUÁRIO (RN-579).
A regra:
GET projects/:projectId/model-bindings/resolved?agents=…&areas=…devolve{ agents: [{ key, binding }], areas: [{ key, binding }] }, na ordem pedida e sem repetição. Cadabindingé EXATAMENTE o que a rota individual responderia para aquela chave (GET .../agent-bindings/:slug,GET .../area-bindings/:key): a mesma cascataworkspace › projeto › área › agentesem sessão, as mesmas origens que a tela transforma em cadeia (RN-470), a herança do Criativo, enullquando nenhum nível tem modelo. Não há cascata nova: o caso de uso chamaResolveModelBindingUseCasechave a chave, com a entrada que a rota individual passaria.- O papel mínimo é o das rotas individuais de leitura,
viewer. Quem não alcança o projeto recebe o MESMO 403. - A leitura é contida
(ADR 0060): as chaves vêm em
lista separada por vírgula, com o alfabeto dos slugs (minúsculas, dígitos,
-,_), e são no máximo 64 somadas (TETO_DE_CHAVES_NO_LOTE). Chave malformada ou lote acima do teto é 400 nomeando o motivo, nunca chave descartada calada — a resposta afirmaria sobre menos chaves do que a tela pediu. - A web lê o lote por UMA
queryKey(['model-bindings-resolved', projectId]), servida por uma requisição às três seções de Configurações e, desde a AT-339, também à Visão geral e à aba Executores (useBindingsDosAgentes): os agentes do roster que o catálogo conhece saem dessa MESMA chave — o cache que uma aba aqueceu serve a outra sem requisição —, e os que o catálogo não conhece (osdev-<modulo>) saem de UMA leitura em lote a mais, sob o mesmo prefixo e em fatias do mesmo teto. Toda escrita de binding de agente ou de área relê o lote inteiro por esse PREFIXO, que alcança as duas leituras; não sobra chave por agente para invalidar. A falha dessas leituras deixa o cartão do agente sem modelo — "não sei", nunca um modelo afirmado — e não cai de volta na rota por agente. - A falha do lote não vira "sem modelo". Com uma leitura só, a falha é de
todas as chaves de uma vez: a seção diz o motivo UMA vez, com a frase da api
e a ação de reler, e cada linha diz "não lido" no lugar da cadeia. Enquanto
o lote não responde, a linha diz "lendo…". "Sem modelo em nenhum nível" fica
reservado ao
nullque a api afirmou.
Números: a carga da aba caiu de 49 para 30 requisições
(configuracoes.orcamento.test.tsx, sobre a dev em 94e5dc721d); o minuto
parado segue em 64, todo da moldura. Na AT-339, a carga da aba Executores caiu
de 29 para 27 e a da Visão geral de 33 para 31 (duas-abas.orcamento.test.tsx,
com o roster base de três agentes — com time e módulos, a queda é de um por
cartão), e ir de Configurações para a Visão geral deixou de buscar binding
nenhum (antes, 3).
O que esta regra NÃO fecha: as rotas por chave continuam existindo na api
(nenhuma tela as lê mais). Um dev-<modulo> cuja chave passe de 64 caracteres
seria recusado pelo formato do lote, e a leitura dos extras falharia inteira —
não medido em projeto real, e declarado.
- Código:
src/application/use-cases/llm/resolve-model-bindings-em-lote.use-case.ts:12(TETO_DE_CHAVES_NO_LOTE),:37(lerListaDeChaves),:77(ResolveModelBindingsEmLoteUseCase);src/interfaces/http/llm/model-bindings.controller.ts:233(getResolvedBindings);apps/web/src/lib/api-client.ts:1217(getResolvedModelBindings);apps/web/src/lib/bindings-resolvidos.ts:43(invalidarBindingsResolvidos),:69(useBindingsResolvidos),:111(useBindingsDosAgentes);apps/web/src/routes/ProjectOverviewTab.tsx:144eapps/web/src/routes/ProjectExecutorsTab.tsx:155(quem o chama);apps/web/src/components/AgentTeamGrid.tsx:46(bindingDoAgente);apps/web/src/routes/settings/LeituraDosBindings.tsx:19(AvisoDeBindingsNaoLidos),:40(MarcaDeBindingNaoLido) - Teste:
test/interfaces/http/llm/model-bindings-em-lote.integration.spec.ts:182(caminho feliz contra Postgres: viewer lê o lote, e cada chave é igual à rota individual),:256(recusa por papel, o mesmo 403 da rota individual),:275(chave malformada),:287(lote acima do teto);apps/web/src/routes/settings/bindings-em-lote.test.tsx:150(as duas seções renderizam de UMA chamada, sem rota por chave),:182(a falha do lote é dita e nenhuma linha vira "sem modelo"),:217(lendo, não "sem modelo");apps/web/src/routes/configuracoes.orcamento.test.tsx:233(a carga lê o lote uma vez e cabe em 30),:279(ir para a Visão geral não busca binding);apps/web/src/routes/ProjectOverviewTab.test.tsx:472(o modelo do cartão sai de UMA leitura em lote, sem rota por agente),:493(o lote recusado não inventa modelo nem cai na rota por agente);apps/web/src/routes/ProjectExecutorsTab.test.tsx:524(odev-<modulo>fora do catálogo vem da leitura dos extras),:547(os extras recusados deixam o cartão sem modelo);apps/web/src/routes/duas-abas.orcamento.test.tsx:334(a carga das duas abas lê o lote uma vez e cabe em 27 e 31) - Origem: AT-334 (decisão do dono sobre a lacuna declarada na RN-645); AT-339 (a Visão geral e a aba Executores no lote)
A leitura dos membros do workspace (RN-655)
RN-655 — GET workspaces/:id/members é viewer, e devolve só id, nome, e-mail e papel
O fio da sessão (RN-652) nomeava só quem tinha linha em
project_members; quem entra no projeto só pelo papel de WORKSPACE virava
"Outro membro", porque não havia rota que lesse os membros do workspace
(decisão do dono, AT-335). A rota nasce ao lado das três de ESCRITA de membro
(POST/DELETE de membro e PUT owner-of-record, todas owner), e o mínimo
dela NÃO é o delas: o mínimo é do ENDPOINT (RN-102), e ler a lista
não é mantê-la. É viewer, o das leituras vizinhas (GET workspaces/:id,
.../projects) e o de GET projects/:id/members, que ela completa — quem
abre uma sessão precisa saber quem falou nela, e um viewer de workspace já
enxerga todo projeto do workspace.
A resposta tem a MESMA forma da leitura de projeto — userId, name,
email, role (o papel de WORKSPACE) — e nada além: sem createdAt, sem
estado de conta. Workspace inexistente é 404, e quem não é membro é recusado
pelo RolesGuard (403). Continua não havendo TELA de membros de workspace: a
leitura existe para nomear, e as rotas de escrita seguem sendo de API.
- Código:
src/interfaces/http/iam/workspaces.controller.ts:216(listMembers);src/application/use-cases/iam/list-workspace-members.use-case.ts:16(execute);src/infrastructure/persistence/drizzle/workspace.repository.ts:127(listMembers) - Teste:
test/application/use-cases/iam/list-workspace-members.use-case.spec.ts:75(a forma da resposta, contra Postgres — caminho feliz),:96(workspace inexistente é 404),:104(o mínimo éviewer),:112(viewerpassa pelo guard),:125(quem não é membro recebe 403) - Origem: AT-335
O motivo da política em cada cartão de Aprovações (RN-656)
RN-656 — A leitura de eventos da sessão filtra por actionId, e cada cartão da aba Aprovações lê o próprio motivo por ela
A ação não guarda o motivo da política: ele mora no proposed_action.created
(RN-567, RN-609). A aba Aprovações lê a fila do
PROJETO (RN-638) e o log de UMA sessão, então desde a AT-333 ela
dizia numa nota única quantas ações ficavam sem motivo (RN-614).
Decisão do dono (AT-336): cada cartão mostra o PRÓPRIO.
O que se mediu. As duas saídas do card eram um filtro actionId na
leitura de eventos ou uma rota que lesse o evento pela ação. O filtro é o
menor: reusa a rota (GET .../sessions/:sessionId/events, viewer), o DTO e o
caso de uso, e a ação já carrega o sessionId da sessão que a propôs. Não
nasce índice nem migration: o predicado sobre payload->>'actionId' roda
DENTRO da sessão, que o índice único (session_id, seq) já recorta. Uma
leitura de PROJETO por actionId exigiria rota nova para achar a sessão que o
cartão já conhece.
A regra na api. actionId devolve só os eventos cujo payload.actionId é
aquela ação, na sessão pedida — nunca atravessa sessões. Combina com os outros
parâmetros e o limit conta só os que casam; o seq deixa de ser contíguo na
página, então quem filtra NÃO deriva omitidos por subtração
(RN-180). Vazio é 400: um filtro que some com a string errada
devolveria o log inteiro a quem pediu o motivo de uma ação.
A regra na tela. useDecisoesDaPolitica: a ação cujo evento está no log
que a aba JÁ carregou usa esse, sem requisição; as outras leem pela ação, na
sessão dela, com staleTime: Infinity — evento é imutável, então cada ação
custa UMA leitura enquanto o cache viver e a fila polla sem repetir nenhuma.
Nada é pedido enquanto a aba ainda carrega o próprio log. A leitura que
responde SEM o evento dá a frase de "motivo não registrado" (nunca cala); só a
leitura que FALHA deixa o cartão calado, e é essa, e só essa, que a nota do
topo conta ("de N das M ações abaixo"). Desde a AT-340 os cartões das
pendências de OUTRAS sessões no chat (RN-626,
PendenciasDeOutrasSessoes, também na aba Executores) passam pelo MESMO hook e
pelo MESMO cache: toda ação ali é de outra sessão, então nenhuma é coberta
pelo log da tela e cada uma lê pela ação — só as DESENHADAS, dentro do teto de
cards do bloco, e a que a aba Aprovações já leu não custa nada ali (e
vice-versa). A leitura que falha também cala o cartão e é dita uma vez, no topo
do bloco. O painel "precisa de você" NÃO usa este caminho: ele não lê log
nenhum e abrir não dispara consulta, então o cartão ali segue calado, como a
RN-614 já dizia — decisão, não lacuna. Nenhum teto muda.
-
Código:
src/interfaces/http/sessions/sessions.controller.ts:260(listEvents);src/infrastructure/persistence/drizzle/session-event.repository.ts:61(o filtro);apps/web/src/lib/decisao-da-politica-queries.ts:35(useDecisoesDaPolitica);apps/web/src/routes/ProjectApprovalsTab.tsx:307(quem o chama),:481(a nota da falha),:499(o motivo em cada cartão);apps/web/src/components/PendenciasDeOutrasSessoes.tsx:74(o chat chama o mesmo hook),:145(o motivo em cada cartão),:181(a nota da falha) -
Teste:
test/application/use-cases/sessions/list-session-events-por-acao.use-case.spec.ts:80(só os eventos da ação, com o motivo — caminho feliz, contra Postgres),:109(não atravessa sessões),:123(sessão de outro projeto é 404),:134(actionIdvazio é 400);apps/web/src/routes/ProjectApprovalsTab.test.tsx:267(ação de outra sessão lê o motivo pela ação, e a do log carregado não vira requisição),:294(evento ausente diz "não registrado"),:304(a leitura que falha é dita uma vez, contando só ela);apps/web/src/components/PendenciasDeOutrasSessoes.test.tsx:204(o cartão do chat lê o motivo pela ação, na sessão dela, e remontar não relê),:228(a leitura que falha cala o cartão e é dita uma vez) -
Origem: AT-336; AT-340 (as pendências de outras sessões no chat)
-
Origem: AT-329 (achado N2 de
docs/explanation/auditoria-visual-rodada-29.md)
Um clique fecha a prontidão, a necessidade e o aceite do PO (RN-657/RN-658)
RN-657 — "Estou pronto — a necessidade está validada" fecha os DOIS gates num POST, com o product_brief produzido depois do clique
Revisa a RN-406 por decisão do dono (30/09, AT-311): o botão do
Criativo passou a se chamar "Estou pronto — a necessidade está validada"
(en: "I'm ready — the need is validated"), e é o rótulo que faz do clique o
julgamento de MÉRITO que o ADR 0095
exigia separado — a pessoa declara as duas coisas, com as palavras das duas.
POST .../sessions/:id/readiness grava, nesta ordem:
readiness.confirmed, ator a pessoa, com a marca{ necessidadeValidada: true, aceiteImplicitoDoPo: true }(a marca é o que a RN-658 lê; evento antigo, com payload{}, não aceita nada);- o comando ao engine (o turno que consolida o
product_brief, RN-142); - só se o engine ACEITOU o turno,
necessity.validated, ator a mesma pessoa, comproductBriefId: null(o brief ainda não existe — nasce do turno que o clique dispara),via: 'readiness.confirmed'e oreadinessEventIddo passo 1.
A recusa do engine (409 turno em curso, 422 sem regra de negócio) sobe ANTES
do passo 3: sem turno aceito não há brief a caminho, e a necessidade NÃO fica
validada. O gate segue warn (config/gates.yml) — nada a jusante o consulta.
A tela não tem mais o botão "Confirmar necessidade validada"; o título do
botão habilitado diz o que o clique fecha, e o aviso de sucesso diz que o PO
entra quando o resumo ficar pronto.
- Código:
src/application/use-cases/agents/confirm-readiness.use-case.ts:26(ConfirmReadinessUseCase),:42(necessity.validated);src/domain/sessions/estou-pronto.ts:27(MARCA_DO_ESTOU_PRONTO);apps/web/src/routes/SessionComposer.tsx:315(o título do botão);apps/web/src/lib/session-acoes-da-sessao.ts:143(handleReadiness) - Teste:
test/application/use-cases/agents/confirm-readiness.use-case.spec.ts:49(os dois eventos, com a marca e o brief por vir — caminho feliz),:72(422 do engine: só a prontidão fica gravada — caso de falha);apps/web/src/routes/SessionPage.estou-pronto.test.tsx:150(um clique, aviso de necessidade validada, sem botão separado),:169(recusa: nada é dito como validado) - ADR: 0185, que revisa o 0095
- Origem: AT-311 (levantamento da AT-309, achado 1)
RN-658 — O aceite do handoff Criativo→PO é implícito no "Estou pronto": os MESMOS eventos, com a pessoa como ator e a marca implicito
Decisão do dono (30/09, AT-312): o clique separado de aceitar o PO some. O
handoff Criativo→PO nasce DEPOIS do clique (é o turno que ele dispara que o
oferece, CriativoServer.executar_confirm_readiness/1), então o aceite não
cabe no POST do clique: ele acontece na rota interna que CRIA a oferta
(POST /internal/sessions/:id/handoffs), logo depois de
CreateHandoffUseCase, e passa pelo MESMO AcceptHandoffUseCase do card.
Medido antes de mudar, o aceite pelo card grava: a transição da linha para
accepted, handoff.accepted (ator a pessoa, {handoffId, toAgent}),
agent.activated (ator a pessoa, {agent}) e, pela RN-635, o
handoff.superseded das outras ofertas ao PO no projeto. O aceite implícito
grava exatamente esses, com o MESMO ator humano — quem clicou, lido do
readiness.confirmed marcado —, e acrescenta a marca
implicito: { via: 'readiness.confirmed', readinessEventId } no payload de
handoff.accepted e de agent.activated. Nenhum evento é editado, nenhum ator
de sistema entra no lugar da pessoa, e o aceite pelo card segue sem a marca.
Aceita-se só quando TODAS valem (decidirAceiteImplicitoDoPo): a oferta é
criativo → po, offered, DESTA sessão, e leva como artefato o
product_brief nascido DEPOIS do readiness.confirmed marcado mais recente.
Fora disso a oferta fica offered e o card aparece como sempre: sessão
anterior ao ADR 0185 (sem marca), brief anterior ao clique, e o handoff
MANUAL, que nunca leva artefato (RN-633 — o "Estou pronto" segue
na tela depois dele). A falha do aceite NÃO sobe para o engine (a oferta foi
criada, e o Criativo diria "não consegui oferecer" — falso): vira
agent.error durável, ator system/aceite-implicito, com origem (infra
ou politica) e reason: aceite_implicito_falhou. O 400 de "não está
offered" não é falha: alguém decidiu a oferta por outro caminho no intervalo,
e isso já está no log. A tela passa o destinatário do composer ao PO no clique
(o gesto de chamá-lo, RN-631); enquanto ele não entra, a escolha não
vale e o Criativo segue recebendo. Nenhum teto se move: aceitar o PO não
provisiona repositório (só o Arquiteto e o Dev Lead, RN-582), não
faz merge nem push.
- Código:
src/domain/sessions/estou-pronto.ts:62(decidirAceiteImplicitoDoPo);src/application/use-cases/agents/aceite-implicito-do-po.use-case.ts:37(AceiteImplicitoDoPoUseCase),:45(seCouber);src/interfaces/http/internal/internal-sessions.controller.ts:439(handoff);src/application/use-cases/agents/accept-handoff.use-case.ts:135(implicitonohandoff.accepted);src/application/use-cases/agents/activate-agent.use-case.ts:80(implicitonoagent.activated) - Teste:
test/application/use-cases/agents/aceite-implicito-do-po.use-case.spec.ts:105(aceita em nome de quem clicou — caminho feliz),:129(falha do aceite viraagent.errore não sobe — caso de falha),:120,:146;test/domain/sessions/estou-pronto.spec.ts:58,:72,:83,:94;test/application/use-cases/agents/accept-handoff.use-case.spec.ts:318(mesmos eventos, ator humano e marca),:338(sem marca pelo card) - ADR: 0185
- Origem: AT-312 (levantamento da AT-309, passo 4)
O motivo da política em cada cartão de Aprovações (RN-656)
RN-659 — Projeto novo nasce com promoção de histórias automática; projeto existente fica no modo que tinha
O default de projects.story_promotion passa de manual para auto (decisão
do dono em 30/09, AT-313): cada história completa do PO nasce ready, sem o
clique de promoção por história que o levantamento da AT-309 contou como +N
passos. A validação é a MESMA nos dois modos (RN-048); muda só quem dispara.
A mudança é do DEFAULT da coluna, pela migration 0065
(ALTER COLUMN … SET DEFAULT 'auto'), e nada mais: nenhuma linha é reescrita,
então projeto que nasceu manual continua manual até alguém trocar em
Configurações. A criação NÃO manda o campo — o valor é o default da coluna,
uma fonte só —, e as duas telas DIZEM o valor: o passo Confirmar da criação
mostra "Promoção de histórias: automática — o PO promove", com onde mudar, e a
seção de Configurações diz que projeto novo nasce em Automática e que mudar ali
vale só para aquele projeto. Os dois scripts de validação que exercitam o fluxo
manual (validacao-fase-12.ts, validacao-real.ts) passam a pedir manual
explícito.
- Código:
src/db/schema/iam.ts:287(storyPromotion);src/db/migrations/0065_default_de_promocao_auto.sql;apps/web/src/routes/NewProjectWizard.tsx:978(a linha do Confirmar);apps/web/src/routes/settings/PromotionSection.tsx:75(a nota do default) - Teste:
test/db/story-promotion-migration.spec.ts:29(projeto novo nasceauto— caminho feliz),:40(projeto existente emmanualnão muda — caso de falha);apps/web/src/routes/NewProjectWizard.test.tsx:254(o Confirmar diz o valor);apps/web/src/routes/ProjectSettingsTab.test.tsx:625(projeto antigo seguemanuale a tela diz o default),:646(a api recusa a troca e a tela diz que não salvou) - Origem: AT-313 (levantamento da AT-309, passo 5); revisa o default da RN-048
RN-660 — O handoff do PO ao Arquiteto é aceito pelo SISTEMA quando o backlog está coberto e o repositório é local e sem credencial
A oferta po → arquiteto que o engine cria (POST /internal/sessions/:id/handoffs) é aceita sem clique, na mesma requisição e
DEPOIS da transação da oferta, quando as quatro condições valem
(decidirAceiteAutomatico, puro):
- a oferta está
offered; - o backlog está coberto: o projeto tem ao menos UMA regra de negócio
(
artifact.business_rule) e NENHUMA sem história que a cite embusinessRuleIds— a MESMAcomputeCoverageda aba Backlog e da leitura do PO (RN-164). Zero regras não é coberto; - o repositório é local e sem credencial: o projeto não tem repositório (o
aceite provisiona
local) ou tem umlocal, e não tem conexão de git (OAuth) emproject_git_connections; - quem abriu a sessão ainda tem
developerno projeto (papel efetivo, RN-471) — o mínimo da rota humana de aceite.
Quem aceita continua sendo AcceptHandoffUseCase: o repositório nasce ali,
antes de activateAgent (RN-582, ADR 0165), em nome de quem abriu a sessão. O
aceite é auditável: handoff.accepted e agent.activated saem com o ator de
SISTEMA handoff-auto-accept, e o handoff.accepted leva automatico: true,
emNomeDe e o criterio (regras, cobertas, repositorio). Condição que
falha não grava nada — a oferta fica com o botão de sempre, e a resposta ao
engine diz o motivo (regras_sem_historia, repositorio_nao_local,
credencial_de_git_no_projeto…). Falha DEPOIS de decidir "sim" não sobe: vira
o evento handoff.auto_accept_failed (origem infra) e o fio o mostra. Com o
aceite feito, a ferramenta offer_handoff diz ao PO que o Arquiteto já foi
ativado. Repositório remoto ou com credencial continua pedindo o clique, e o
handoff manual (ADR 0109) também.
- Código:
src/domain/sessions/aceite-automatico-do-handoff.ts:81(decidirAceiteAutomatico),:74(backlogCoberto);src/application/use-cases/agents/aceitar-handoff-automaticamente.use-case.ts:45(AceitarHandoffAutomaticamenteUseCase);src/application/use-cases/agents/accept-handoff.use-case.ts:91(AceitePeloSistema);src/interfaces/http/internal/internal-sessions.controller.ts:439(handoff);apps/engine/lib/engine/harness/tools/offer_handoff.ex:59;apps/web/src/routes/session-timeline-montagem.tsx:604(o aviso no fio) - Teste:
test/application/use-cases/agents/aceitar-handoff-automaticamente.use-case.spec.ts:73(backlog coberto e sem repositório: aceita pelo sistema — caminho feliz),:89(regra sem história: não aceita e não grava nada — caso de falha),:99(repositório remoto),:109(conexão de git),:128(falha virahandoff.auto_accept_failed);test/domain/sessions/aceite-automatico-do-handoff.spec.ts:37,:49(cada motivo de recusa);test/application/use-cases/agents/accept-handoff.use-case.spec.ts:323(ator de sistema, critério e provisionamento antes de ativar);apps/web/src/routes/SessionPage.handoff-aceite-automatico.test.tsx:174,:196;apps/engine/test/engine/harness/tools/offer_handoff_test.exs:38(não rodado nesta sessão: sem Elixir no ambiente) - Origem: AT-314, ADR 0186
O Infra Lead não anuncia subida que não fez (RN-668)
RN-663 — O merge de PR recusa a PR já mergeada e a proposta repetida; gate pendente é só aviso
O uso real de 29/09 mediu os dois buracos: a pr-6 teve TRÊS git_merge
executed, e a pr-4 foi mergeada com a tarefa em awaiting_qa. A api não
deduplicava git_merge por PR (só a tela, e só na mesma lista), e o
LocalGitProvider devolvia a PR já mergeada sem erro — o executor gravava um
segundo executed para o mesmo merge.
- Proposta.
git_mergede uma PR que uma execução anterior já mergeou (executedcomstate: 'merged') é 409pr_ja_mergeado; com uma proposta VIVA da mesma PR (pending,approvedouauto_approved), 409merge_ja_proposto. Nos dois, nada é criado. Negada ou falha não contam: quem negou ou viu falhar pode tentar de novo. A comparação é pelopullRequestIddo payload, no projeto inteiro (qualquer sessão). - Aprovação. Aprovar um
git_mergependente cuja PR OUTRA proposta já mergeou é 409pr_ja_mergeado, antes de qualquer efeito, e a ação continuapendingpara quem a vê negar. A irmã apenas VIVA não impede decidir esta. - Provider. O
LocalGitProviderrecusa mergear PR já mergeada comGitPullRequestAlreadyMergedError, e o target não se move — alinhado ao GitHub, que recusa o merge repetido. - Gate pendente é AVISO, nunca recusa (decisão do dono, 30/09). A aba PRs
e o "Mergear" do chat dizem em TEXTO qual gate falta (
qa-verificadacomawaiting_qaou gate ainda não aberto,secops-seguracomawaiting_secops), e o botão e o card seguem ativos.awaiting_user, tarefadonee PR sem tarefa não avisam. O "gate bloqueado" (tarefablocked) continua como era. - Nenhum teto se move. O merge em branch protegida segue
require_approvalincondicional e humano (RN-418,decide.ts): esta regra só impede propor ou aprovar o que não há o que mergear. Declarado: duas propostas simultâneas da mesma PR não são serializadas (não há trava por PR), e é a regra 2 que segura a segunda.
-
Código:
src/domain/actions/merge-de-pr.ts:56(recusaDeMerge),:40(mergeouAPr);src/application/use-cases/actions/propose-action.use-case.ts:124(recusaDeMerge);src/application/use-cases/actions/approve-action.use-case.ts:327(recusaDeMerge);src/infrastructure/git/local-git-provider.ts:339(GitPullRequestAlreadyMergedError);apps/web/src/lib/gate-do-merge.ts:19(gatePendenteNoMerge);apps/web/src/routes/ProjectPrsTab.tsx:214(gatePendenteNoMerge);apps/web/src/routes/session-timeline-montagem.tsx:1288(gatePendenteNoMerge)apps/web/src/routes/ProjectPrsTab.tsx:214(gatePendenteNoMerge);apps/web/src/routes/session-timeline-montagem.tsx:1288(gatePendenteNoMerge) -
Teste:
test/application/use-cases/actions/propose-action.use-case.spec.ts:675(merge_ja_proposto),:698(pr_ja_mergeado— caso de falha),:696apps/web/src/routes/session-timeline-montagem.tsx:1288(gatePendenteNoMerge) -
Teste:
test/application/use-cases/actions/propose-action.use-case.spec.ts:675(merge_ja_proposto),:698(pr_ja_mergeado— caso de falha),:696(negada não bloqueia, outra PR não colide — caminho feliz);test/application/use-cases/actions/approve-deny-action.use-case.spec.ts:473;test/infrastructure/git/local-git-provider.contract.spec.ts:150;apps/web/src/routes/ProjectPrsTab.test.tsx:266,:300,:315;apps/web/src/routes/MergearNoChat.test.tsx:135,:148;apps/web/src/lib/gate-do-merge.test.ts:5,:18 -
Origem: AT-249 (item A3/extra E3 da análise do uso real de 29/09)
RN-665 — O custo real que o provider devolve é o número do metering; o catálogo fica onde ele não vem
Decisão do dono (30/09): o custo real vira o número. Quando a resposta de
uma chamada de LLM diz quanto o provider cobrou — no OpenRouter, usage.cost
no frame final do stream —, esse valor é o token_usage.cost_micros da linha,
o que o turno devolve ao engine (e o laço soma ao orçamento local), o que
incrementa os budgets de projeto, sessão e área, e o que os relatórios de
gasto somam. Sem ele, o preço congelado do catálogo produz o número, como no
ADR 0042.
- Uma regra, três caminhos.
RunLlmTurnUseCase,StreamLlmTurnUseCaseeSendChatMessageUseCasepassam pela mesmacustoDaChamada; não há segunda régua. - Preço implícito, marcado. Com custo real, as duas colunas de preço
gravam
custo ÷ tokens(o mesmo valor: o provider devolve UM custo) eprice_implicit = true— o que mantémtokens × preço = custo(RN-044) sem fingir um preço de tabela. Sem custo real,price_implicit = falsee as colunas são as do catálogo. - O catálogo ao lado.
catalog_cost_microsgrava o que o preço de catálogo teria cobrado, só nas linhas cujo número é o real (nullnas outras) — é a distância entre estimativa e fatura, por linha. estimatedsegue falando dos TOKENS (RN-041). A linha com custo real temestimated = falseporque o custo só chega com ousagedo provider; quem marca o CUSTO como real éprice_implicit.- O que a resposta disse sobre si é gravado.
resolved_model_name(omodeldo frame — o alias resolvido) egeneration_id(oid,gen-…no OpenRouter), por todo provider do dialeto.model_namecontinua sendo o do catálogo, a dimensão dos relatórios. - Só quem provou lê o custo. O hook
extrairCustoRealexiste só no OpenRouter; provider sem ele ignora umusage.costpresente. Custo que não é número finito e não negativo é "não disse" (zero de verdade é custo real), e comis_byok: trueo custo NÃO é lido: é a taxa do hub, e a inferência é cobrada por fora — a linha cai no catálogo.
- Onde:
src/domain/llm/custo-da-chamada.ts:43(custoDaChamada),src/application/use-cases/llm/run-llm-turn.use-case.ts:288(custoDaChamada),src/application/use-cases/llm/stream-llm-turn.use-case.ts:327(custoDaChamada),src/application/use-cases/llm/send-chat-message.use-case.ts:286(custoDaChamada),src/infrastructure/llm/openai-compatible-provider.ts:410(extrairCustoReal),src/infrastructure/llm/openrouter-provider.ts:272(extrairCustoRealOpenRouter),src/db/schema/llm.ts:375(priceImplicit) - Teste:
test/domain/llm/custo-da-chamada.spec.ts,test/infrastructure/llm/openrouter-provider.contract.spec.ts(resposta gravada: custo, modelo resolvido, id; BYOK; semcost; provider sem o hook),test/application/use-cases/llm/run-llm-turn.use-case.spec.ts,test/application/use-cases/llm/stream-llm-turn.use-case.spec.ts,test/application/use-cases/llm/send-chat-message.use-case.spec.ts; com credencial, o smoke manualopenrouter-provider.smoke.spec.ts - Origem: ADR 0188 (AT-270)
RN-666 — O metering grava quanto da entrada veio de cache e quanto da saída foi raciocínio
Cada linha de token_usage grava duas PARTES que o provider informa no
usage do dialeto OpenAI: cached_input_tokens
(prompt_tokens_details.cached_tokens — quantos dos input_tokens foram
servidos de cache, cobrados a uma fração do preço) e reasoning_tokens
(completion_tokens_details.reasoning_tokens — quantos dos output_tokens
foram raciocínio).
- Parte, nunca soma.
input_tokens/output_tokenscontinuam sendo os totais que o provider disse, e já incluem as partes; nada é somado a eles e nenhum custo é recalculado por elas — o número do custo é o da RN-665. - "Não disse" não é zero.
null= o provider não informou; 0 = informou que não houve. Valor que não é inteiro não negativo é "não disse". - A medição diz quanto foi cache.
medir-execucao.tspassa a mostrar, por agente, o cache lido e o raciocínio com a porcentagem do total, SÓ sobre as chamadas que informaram (e em quantas, quando não foram todas), e "não medido" quando nenhuma informou — além de em quantas chamadas o custo é o real. - Lido para todo provider do dialeto, porque os dois campos são da
própria OpenAI; quem não os manda fica com
null. O Anthropic informa cache no protocolo dele (cache_read_input_tokens) e isso NÃO é lido aqui — ficanull, declarado; o Ollama não informa.
- Onde:
src/infrastructure/llm/openai-compatible-provider.ts:414(cachedInputTokens),src/infrastructure/llm/openai-compatible-provider.ts:817(contagem),src/application/use-cases/llm/run-llm-turn.use-case.ts:230(cachedInputTokens),src/application/use-cases/llm/record-llm-usage.use-case.ts:121(cachedInputTokens),src/db/schema/llm.ts:395(cachedInputTokens),scripts/medir-execucao.ts:89(formatarParteMedida) - Teste:
test/infrastructure/llm/openrouter-provider.contract.spec.ts(resposta gravada com as duas partes; sem os detalhes; valor inválido e zero),test/application/use-cases/llm/run-llm-turn.use-case.spec.ts,test/scripts/medir-execucao.spec.ts(formatarParteMedida) - Origem: ADR 0188 (AT-272)
RN-669 — O escopo de caminho compara com a pasta REAL de execução: /work e o /tmp do container quando ele está de pé
O teto de escopo do ADR 0055 comparava todo comando com a pasta do projeto no
HOST, inclusive quando o comando roda DENTRO do container (RN-492/RN-502). No
uso real de 29/09, os 37 pedidos "escopo" eram /work/... e /tmp
comparados com /home/<usuario>/projetos-brabo/<projeto> (AT-258).
Com um container running REGISTRADO num projeto container ou mounted, a
raiz do escopo é /work — o ponto de montagem, onde moram a pasta e os
.worktrees do projeto — mais o /tmp do PRÓPRIO container. O cwd chega
como caminho de host e é traduzido para /work pela mesma regra do engine
(cwd_para_container/2): a raiz vira /work, o que está sob ela vira
/work/..., o que está fora segue como veio e fica fora. Em mounted a
tradução é bijetiva. Sem container, a raiz continua a pasta do projeto no host,
e o /tmp do host fica FORA.
Vale para quem não está no piloto (a regra específica terminal): o piloto não
passa pelo escopo (RN-603, mantida
pela RN-670). projectScopeRoot não muda nem se une a
permissionsFilePath (RN-478); o PISO de auto-aprovação continua só
do modo container (RN-493). runner fica fora, de propósito: a api
não sabe se o runner vai rotear para o container ou para o host (RN-558).
Argumento de comando com caminho de HOST passa a contar como fora quando há
container — o engine traduz só o cwd, e dentro do container esse caminho não
existe.
- Código:
src/domain/actions/path-scope.ts:153(cwdNoContainer),:174(comandoNoEscopoDoContainer),:112(comandoNoEscopo, agora com várias raízes),:137(PONTO_DE_MONTAGEM_DO_CONTAINER);src/domain/actions/decide.ts:291(execucaoNoContainer),:660(execucaoNoContainer, emterminalNoEscopo);src/application/use-cases/actions/propose-action.use-case.ts:203(containerExecutionActive, separado deexecucaoNoContainer) - Teste:
test/domain/actions/decide.spec.ts:1274(com container/work,.worktrees,/tmpe..dentro — caminho feliz;/etc, outro projeto,/workspace,/tmp/../etcfora; sem container/tmpe/workfora — casos de falha);test/domain/actions/path-scope.spec.ts:216(e a cópia de/worktravada contrapackages/docker-port);test/application/use-cases/actions/propose-action.use-case.spec.ts:1169(o modomountedcom e sem container, e sem o piso) - Origem: AT-258 (item A12 da análise do uso real de 29/09), decisão do dono de 01/10, ADR 0189
RN-670 — O modo automático é o PILOTO AUTOMÁTICO, e "Sempre permitir" não o desliga
Decisão do dono de 01/10 (ADR 0189): a
RN-603 NÃO é revogada. A curinga
*: auto_approve aprova TUDO, inclusive o composto sintetizado do
permissions.json, caminho fora da pasta, git commit e criação de branch
LOCAL. Seguem fora, nunca auto-aprováveis: push/PR/merge/deploy pelo terminal
e sudo/doas (RN-418), merge em branch protegida,
container_remove, instruction_patch e paralelismo. deny explícito vence, e
ask escrito no arquivo pede. A garantia de que isso não alcança o código do
Brabo é a contenção do container — uma pasta só, em /work, estritamente
abaixo da raiz do broker, sem socket do Docker nem privilégio —, provada em
apps/broker/src/contencao-do-brabo.spec.ts; o que a prova NÃO cobre está
declarado no ADR 0189.
"Sempre permitir" não desliga o piloto (AT-255). O clique de dev agent
grava terminal: auto_approve (RN-509), e o repositório devolvia
essa específica ANTES da curinga, com origem especifica — decide() deixava
de reconhecer o modo automático (97 + 37 pedidos no uso real de 29/09, com o
modo aceso na tela). Agora a específica auto_approve sob a curinga
auto_approve resolve como a CURINGA. Específica que diz outra coisa
(require_approval, deny) continua vencendo, e com o toggle de volta em
manual a específica gravada pelo clique volta a valer sozinha, com o escopo. O
clique continua gravando a específica; quem decide se ela já existe é o campo
especifica da resolução, nunca o modo resolvido.
A tela diz o que o piloto libera e o que não libera, com UMA lista nos três
lugares onde o modo é ligado: o cartão de lote dos Executores
(RN-661, aberta), o ApprovalCard e o toggle do card do agente
(recolhida). O "não libera" é o texto que a RN-661 já tinha, reusado.
- Código:
src/infrastructure/persistence/drizzle/agent-autonomy.repository.ts:25(resolve),:67(modoEspecifico, a exceção do piloto);src/application/ports/agent-autonomy-repository.port.ts:5(AutonomiaResolvida, comespecifica);src/application/use-cases/actions/approve-always-action.use-case.ts:263(especifica);src/domain/actions/decide.ts:311(modoAutomaticoDoAgente);apps/web/src/components/OQueOPilotoLibera.tsx:16(OQueOPilotoLibera);apps/web/src/components/ApprovalCard.tsx:434,apps/web/src/components/AgentCard.tsx:164,apps/web/src/components/ModoAutomaticoDoTime.tsx:134 - Teste:
test/application/use-cases/actions/approve-always-action.use-case.spec.ts:850(o cenário medido: curinga + "Sempre permitir" segue sem pedido — caminho feliz; toggle em manual volta ao escopo — caso de falha);test/infrastructure/persistence/drizzle/agent-autonomy.repository.spec.ts:115;test/domain/actions/decide.spec.ts:1210(commit e branch local livres; push, merge, PR, deploy edoasseguem pedindo);apps/broker/src/contencao-do-brabo.spec.ts:105(a contenção);apps/web/src/components/ModoAutomaticoDoTime.test.tsx:86,apps/web/src/components/ApprovalCard.test.tsx:145,apps/web/src/components/AgentCard.test.tsx:81 - Origem: AT-259 e AT-255 (itens A9 e A13 da análise do uso real de 29/09), decisão do dono de 01/10
RN-671 — A Infra sobe o container no aceite do handoff: o servidor propõe, e a autonomia semeada aprova
Decisão do dono em 01/10 (ADR 0190),
revisando o "nunca semeado" da RN-491. No uso real de 29/09 a Infra
anunciou a subida "em paralelo" e não a propôs; o container só subiu pela
/containers (AT-260).
- A semente. Aceitar o handoff endereçado a
infragrava, além deopen_infra_pr: auto_approveeterminal: deny, a autonomiacontainer_start: auto_approve. Só ela entre as de container:container_start_via_runnerecontainer_stopseguem nunca semeados, econtainer_removesegue no teto absoluto (RN-495). - O disparo é do SERVIDOR. No kickoff do Infra Lead, ANTES da primeira ida
ao modelo, havendo roteamento vigente (
artifact.module_routingroteado, com candidata) e projetocontainer/mounted, o servidor propõecontainer_startpelo MESMO caminho da toolpropose_container_start: as recusas locais por modo e estado (RN-566, RN-610), depoispropose_action, onde a api recusa sem broker (RN-591), resolve a autonomia e, auto-aprovada, executa a subida, elegendo a imagem porDecidirImagemDoProjetoUseCasecomdecidedBy: 'infra-lead'(RN-491).runnernão tem passo do servidor: segue pelocontainer_start_via_runner, proposto pelo modelo e decidido por humano. - A eleição é determinística. A candidata do maior número de módulos; no
empate, a primeira do roteamento. Rede
none, recursos padrão, e umrationaleque diz que foi o servidor. (Revisado pela RN-683: os recursos vão omitidos e a api sobe com o mínimo derivado domodule_map, não com o padrão.) - O rastro diz quem chamou.
tool.call(comorigem: "servidor") etool.resultduráveis, e nenhuma mensagem de ferramenta no histórico do modelo; o kickoff diz em texto o que JÁ aconteceu (status devolvido, ou o motivo da recusa). - O fecho da RN-668 segue verdadeiro. A subida do servidor marca
o turno como a da tool: proposta, o fecho não fala; recusada, ele diz que foi
recusada. Proposta NEGADA pela política (status
denied, como uma sessão aberta por quem não émaintainer) conta como recusa, nas duas origens.
Nenhum teto se move: o papel mínimo segue maintainer (o efetivo de quem abriu
a sessão), deny de permissions.json segue vencendo, e a /containers não
muda (ela propõe como humano, e a autonomia de agente não vale para ela).
Declarado: o toggle manual/auto do card escreve a curinga, e a regra específica
vence — pôr a Infra em "manual" não desliga esta semente.
-
Código:
src/application/use-cases/agents/accept-handoff.use-case.ts:38(INFRA_AUTONOMY_SEEDS);apps/engine/lib/engine/infra/infra_lead_server.ex:259(handle_cast),:710(propor_container_start),:791(subir_no_aceite),:794(eleger_candidata),:1435(passo_da_subida):626(propor_container_start),:707(subir_no_aceite),:759(eleger_candidata),:1319(passo_da_subida) -
Teste:
test/application/use-cases/agents/accept-handoff.use-case.spec.ts:250(a semente, e só ela — caminho feliz),:267(outro agente não semeia — caso de falha);apps/engine/test/engine/infra/infra_lead_server_test.exs:1263(o servidor elege e propõe antes do modelo — caminho feliz),:1458(mounted),:1468(runnernão sobe pelo servidor),:1377(sem roteamento),:1387(a api recusa sem broker — caso de falha),:1427(container járunning),:1439(negada pela política),:1580(eleger_candidata/1) -
Origem: AT-260 (item A14 da análise do uso real de 29/09)
RN-672 — O handoff ao Dev Lead sai da Infra, e só com o container do projeto running
Decisão do dono em 01/10 (ADR 0190,
AT-262). Até aqui "Confirmar arquitetura pronta" oferecia DOIS handoffs — à
Infra e ao Dev Lead (FASE 14d, handoff-duplo em packages/contracts/fluxo.yml) —, e o Dev
Lead podia ser aceito antes de existir o container em que os dev agents
trabalham (RN-502). No uso real de 29/09 os dois nasceram da mesma
confirmação (sessão be70, seq 252/254).
- A confirmação de arquitetura oferece só à Infra.
OfferInfraHandoffUseCasetem um destino (infra); oofferDevHandoffda porta do engine, a rotaPOST /internal/sessions/:id/agent/offer-dev-handoffe o:offer_dev_handoff/handoff_dev_pendentedo Arquiteto deixaram de existir.ja_oferecidopassa a significar "a Infra já tinha oferta ou estava ativa". - Quem oferece o Dev Lead é o servidor do Infra Lead, por
create_handoff_if_absent/5(infra → dev-lead, ADR 0182): oferta pendente em qualquer sessão do projeto volta como está, e Dev Lead já ativo (409agente_ja_ativo) não é falha. Não é ferramenta: o modelo não decide quando o Dev Lead entra. - Só com o container REGISTRADO
running(a leitura local deProjectContainerLifecycle). A pergunta é feita em dois momentos: no fim de todo turno da Infra (concluir) — o caso comum, porque a subida do servidor no aceite (RN-671) executa dentro do kickoff — e quando ocontainer.runningdo outbox chega fora de um turno (aprovação posterior,/containers,container_start_via_runner): oEngine.Outbox.Drainpassa a enfileirar também oEngine.Workers.InfraOfereceDevLeadWorker, que avisa por PubSub o Infra Lead do projeto. - Falha que não é "já atendido" é
agent.errordurável com origem (RN-116), sem derrubar o turno; o fim do próximo turno da Infra tenta de novo.
Fica como estava: a segunda porta do repositório no aceite ao Dev Lead
(RN-582) — o aceite é o mesmo, só mudou quem oferece — e a
ativação de execução encadeada no aceite (RN-161). Declarado: o aviso do
outbox é at-most-once, como o Engine.Dev.Wake; sem Infra Lead de pé naquele
instante, a oferta espera o fim do próximo turno dele.
-
Código:
src/application/use-cases/agents/offer-infra-handoff.use-case.ts:15(ALVOS_DA_CONFIRMACAO);apps/engine/lib/engine/infra/infra_lead_server.ex:1169(oferecer_ao_dev_lead),:1164(concluir),:368(handle_info),:1075:1057(concluir),:345(handle_info),:1074(topico_do_container);apps/engine/lib/engine/workers/infra_oferece_dev_lead_worker.ex:29(perform);apps/engine/lib/engine/outbox/drain.ex:107(handlers_for) -
Teste:
apps/engine/test/engine/infra/infra_lead_server_test.exs:1490(containerrunningno fim do turno: oferece — caminho feliz),:1504(semrunning: não oferece — caso de falha),:1515(o aviso fora de turno),:1532(o aviso com turno em curso),:1544(409 não é falha; 500 viraagent.error);apps/engine/test/engine/agents/arquiteto_server_test.exs:256(o Arquiteto não oferece mais ao Dev Lead);apps/engine/test/engine/workers/infra_oferece_dev_lead_worker_test.exs:9;apps/engine/test/engine/outbox/drain_test.exs:266;test/application/use-cases/agents/offer-infra-handoff.use-case.spec.ts:104,:147 -
Origem: AT-262 (item A16 da análise do uso real de 29/09)
RN-675 — "Sempre permitir" grava VERBO + SUBCOMANDO, um padrão por segmento do comando
Decisão do dono de 01/10 (AT-257, que fecha a AT-170 — o ponto 6 do ADR 0055,
a unidade da generalização; ADR 0189). Até
aqui o clique gravava o comando INTEIRO, byte a byte (Terminal(<comando>)):
o próximo comando quase nunca era igual — 173 cliques no uso real de 29/09 —, e
o composto virava UM padrão cujo conteúdo só casava o PRIMEIRO segmento, então
cd src && npm test liberava só cd src.
Agora cada SEGMENTO do comando (o mesmo corte de decide(): &&, ||, ;,
|, &) vira um padrão, e a unidade é:
- o verbo sozinho (
ls) → o verbo; - verbo + palavra (
npm test,git status,mix test,npx vitest) → os dois. "Palavra" é letra no começo, sem/,.,=, glob nem flag; um argumento que parece palavra (cd src) fica como subcomando — o lado ESTREITO da dúvida, porque distinguir os dois exigiria uma lista de verbos, que é o espaço que os achados Z/AD dizem não convergir; - verbo + argumento que não é palavra (
cat src/x.ts,cd ../lib) → o verbo; - verbo + FLAG (
ls -la src,git -C /x status) → o segmento EXATO: a forma com flag é onde verbo e invocação divergem, e generalizar ali seria escolher uma forma de dentro do código; - unidade que é PREFIXO de um teto da RN-418 (
git remote,gh pr) → o segmento EXATO; e se até o exato é prefixo (gitsozinho), nenhum padrão para aquele segmento.
O casamento continua o de sempre (prefixo de tokens), o clique sobre comando
com efeito externo ou privilegiado continua recusado inteiro antes
(motivoDeRecusaDoSempreAprovar, 400 teto_do_sempre_permitir), o teto de
decide() roda depois do arquivo, e o escopo de caminho segue limitando ONDE
fora do piloto (RN-669). Quem pode gravar é quem já podia (o papel
do endpoint, inalterado); a precedência com o modo automático é a da
RN-670. O "Sempre permitir" de dev agent de módulo continua indo
para agent_autonomy (RN-509), sem padrão. Só os padrões que
FALTAM são gravados e narrados; o permission.granted leva patterns (a lista
gravada) e pattern (a mesma lista unida por vírgula, para a timeline).
- Código:
src/domain/actions/pattern-for-action.ts:22(patternsForAction),:62(unidadeDoSegmento),:43(SUBCOMANDO);src/domain/actions/external-effect.ts:297(padraoAlcancaTeto);src/application/use-cases/actions/approve-always-action.use-case.ts:277(faltam),:315(patternsForAction) - Teste:
test/domain/actions/pattern-for-action.spec.ts:18(a unidade, caminho feliz),:103(segmento que é prefixo de teto não grava nada — caso de falha);test/domain/actions/external-effect.spec.ts:11(padraoAlcancaTeto);test/application/use-cases/actions/approve-always-action.use-case.spec.ts:936(o clique num composto libera o próximo com os mesmos verbos; outro subcomando continua pedindo; composto que empurra segue recusado);apps/web/src/components/ApprovalCard.test.tsx:253(a nota diz a unidade) - Origem: AT-257 (item A11 da análise do uso real de 29/09) e AT-170 (EP-025), decisão do dono de 01/10