Destrava o #1747 (706 commits atras), a pedido do dono.
- A migration da onda 3 sai de 0433 (20260926210000) para 0500
(20260930170000): estava fora de ordem, com a main ate a 0499. Arquivo,
rotulos do baseline, linha do MANIFEST (depois da 0499), testes e
comentarios.
- A lista do CHECK de agent_inbox_items.kind na migration ganha os cinco
kinds da proposta (0464/0466/0475): rodando depois delas, a lista antiga os
apagaria. Igual a do baseline.
- Conflitos: baseline partiu da main com o delta do PR reaplicado (kinds na
lista unica; apendice antes da VARREDURA anon); InboxKind, rotulos e destino
da Central ficam com os kinds do Jev e os da proposta; mapa vivo com as
arestas da main (e121-e123) e as do Jev renumeradas e124-e129; selo das
traducoes do white-label regravado sobre o original mesclado; e2e.yml com os
dois comentarios.
O caminho da migration dentro de tests/invariants/jev-aviso-fecha-sozinho-e-e-unico.test.ts
muda no commit seguinte, separado: o guard freeze-invariants barra edicao de
invariante dentro da resolucao.
A VISION separa "nós não vendemos assinatura" de "quem instala pode
cobrar os próprios clientes" (ADR-0004). A spec 19 diz que o console de
agência segue sem faturamento do operador, e troca a afirmação "não
existe tabela de plano ou assinatura", que envelheceria com as tabelas
da cobrança, pela régua da doutrina. O plano da LP marca "sem cobrança
por usuário" como promessa do projeto.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Os quatro cabeçalhos que o #1830 acrescenta ao webhook de saída saem como
X-Webhook-Delivery, X-Webhook-Attempt, X-Webhook-Timestamp e
X-Webhook-Signature, e não com a marca do produto. Nome de protocolo vira
contrato no primeiro release e não se renomeia mais; com a marca, toda
instalação de marca própria mandaria a marca de origem ao sistema do cliente
em quatro nomes a mais, e a MARCA_CONGELADA cresceria quatro entradas
PROTOCOLO — a lista que só encolhe. Mesmo precedente de d89d2e55e.
X-Deskcomm-Event e o X-Deskcomm-Signature legado ficam: já são contrato
publicado. O vetor de referência não muda (nenhum nome de cabeçalho entra no
HMAC nem no uuid). Os três white-label*.md voltam ao que está na main — a
linha dos "dois nomes técnicos" volta a valer como está. A única mudança que
sobra no branding.test.ts é a contagem da guarda: o teste novo de ponta a
ponta confere o legado mais uma vez.
O fragmento ganha a linha de crédito do autor.
Sonda: git grep -inE 'x-deskcomm-(delivery|attempt|timestamp|signature-2)'
dá 0 nesta árvore e 91 no head do #1830 (6a746b335).
Refs: #1830#1529
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Quem recebe o webhook de saída (ação call_webhook) só conseguia
conferir que o corpo veio de quem tem o segredo. A assinatura era o
HMAC só do corpo, sem carimbo de tempo, então uma requisição capturada
continuava válida para sempre. Não havia id de entrega, e occurred_at
era a hora do envio, de modo que cada Reenviar montava outro corpo com
outra assinatura. O receptor não tinha como recusar uma repetição nem
como reconhecer uma retentativa como a mesma entrega.
Toda entrega passa a levar:
- X-Deskcomm-Delivery: uuid v5 de evento + regra + posição da ação +
lista de ações sem segredo; é o mesmo nas retentativas e no Reenviar;
- X-Deskcomm-Attempt: continua a contagem no Reenviar;
- X-Deskcomm-Timestamp;
- X-Deskcomm-Signature-2, só com segredo:
t=<ts>,v1=HMAC(segredo, "<ts>.<delivery>.<corpo>").
O X-Deskcomm-Signature legado continua byte a byte igual. occurred_at
passa a ser event.created_at, e o envelope ganha delivery_id.
A lista de ações entra no id porque a posição sozinha não identifica a
ação. Depois de remover ou reordenar ações, a que herdasse a posição de
outra sairia com o id de uma entrega que o receptor já processou, e ele
a descartaria em silêncio. Com a lista no id, editar a regra gera um id
novo: na dúvida o receptor recebe de novo, mas nunca deixa de receber.
O Reenviar tira a posição da ação antes de filtrar os webhooks e começa
o Attempt no número seguinte ao maior já registrado em todos os runs de
(org, regra, evento). O resultado grava detail.resent_from_run_id no
jsonb que já existe. Não há migration nem dependência nova: o uuid v5
usa node:crypto e é conferido contra o vetor do RFC 9562.
Entram também:
- o guia docs/integracao/webhooks-de-saida.md: janela de 300 s pelo t
assinado, comparação em tempo constante, deduplicação, e exemplos em
Node e Python executados por teste contra um vetor fixo;
- o apontamento do guia na tela da ação;
- os quatro cabeçalhos novos como PROTOCOLO na catraca de marca;
- notas de estado real na spec 07 e no guia de marca própria;
- o fragmento capacidade_nova, que avisa da mudança de significado do
occurred_at.
Refs #1529
Conflitos:
- lib/ai/inbox-destino.ts: os dois kinds do Jev (daqui) + o `other` com
`ai_provider_credential` (da main).
- supabase/baseline.sql: lado da main inteiro (0428, 0432) e o bloco dos avisos
do Jev reaplicado depois dele, antes do `notify pgrst` do fim da região.
A main ganhou a SUA 0426 (motivo de perda). A dos avisos do Jev vira 0433
(o próximo livre contra a main e os 12 PRs abertos; carimbo 20260926210000,
depois do maior da main, 0432 às 200200): arquivo, MANIFEST (linha movida para
depois da 0432), apêndice do baseline e a prosa que citava o número. Os
comentários de invariantes e o caminho que um deles lê vão num commit próprio.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
docs/specs/12-spec-ai-agents-ui.md ainda afirmava a premissa que o H7
tirou dos comentários: o badge cinza era "published_version_id == null —
pausado/nunca publicado", e "Despausar" era republicar a versão
(ai_agent.republished). Hoje pausar grava só paused_at, o ponteiro fica
(app/api/v1/ai/agents/[id]/pause/route.ts), despausar é
unpauseAgentAction limpando paused_at (audit ai_agent.updated com
metadata.unpaused), e ai_agent.republished não existe em lugar nenhum
do repositório (git grep vazio).
O badge passa a nomear paused_at nos três estados e aponta a régua viva,
estadoDoAgente em lib/ai/agents/no-ar.ts, em vez de repeti-la.
Sonda (git grep -nEi "pausad.{0,60}published_version_id|published_version_id.{0,60}pausad"
fora de docs/handoff/): antes 1 linha (esta), depois só a linha corrigida.
Sem teste novo: é prosa de spec, e um regex sobre ela mede a palavra, não
a afirmação. Suíte unitária: 1437 arquivos, 14739 passed, 0 failed.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
O #1592 pôs o DialButton no ConversationHeader. O DialButton chama
useVoiceCall, que lança fora do VoiceCallProvider; na aplicação o
provider envolve todo /app, mas tests/unit/inbox-header-nao-trava
renderiza o cabeçalho sem ele e quebrava 4 casos. Este teste só mede
a largura, então o discador sai da conta por vi.mock.
A spec 18 (§5.1 e checklist) dizia que o botão Ligar vivia só no
Customer 360; agora cita também o cabeçalho da conversa na Inbox.
Refs: #1592
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019pUk4A1WWHegBZNbHsqDnp
A ingestão do webhook WAHA engolia a falha do banco nos quatro pontos em
que a mensagem depende de uma escrita (contato, conversa, mensagem
recebida e mensagem enviada pelo celular): `console.error` + `return`, e
a rota devolvia 200. O WAHA riscava o evento achando que entregou, e a
mensagem do cliente sumia — medido em produção: três perdidas em 14/09
num dia de `statement timeout`, duas na troca de banco de 24/09.
- `lib/waha/falha-transitoria.ts`: a régua. Erro de dado, integridade,
schema, regra de negócio e pedido malformado são permanentes; o resto
(timeout, conexão, PostgREST sem banco, 5xx, sem código) é transitório.
A ingestão passa a lançar a classe certa nos quatro pontos.
- `lib/waha/desfecho-do-webhook.ts`: as duas rotas gravam o desfecho no
arquivo (`processed` / `error`), que até aqui nascia e morria
`received`. Falha transitória responde 503 + Retry-After: o WAHA
reentrega qualquer erro 15x com espera max(2s, Retry-After), e a
reentrega é segura porque o 23505 vira dedup.
- `app/api/v1/cron/webhook-replay`: relê a cada minuto o arquivo marcado
`transitoria:`, até 20 tentativas; depois `dead` e um aviso `event_dead`
na Central com título próprio (`MENSAGEM_QUE_NAO_ENTROU`), que o dreno
de handlers exclui do seu dedupe. Para na terceira falha seguida da
rodada para não martelar um banco que ainda está voltando.
É o `process-pending-webhooks` que o PRD 03 e a Spec 03 prometiam; os
dois passam a dizer o nome e a régua reais. Sem migration: `processed`
e `dead` já estavam no CHECK, `event_dead` já existia.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Spec 01 e a Spec 11 descreviam o plaintext do token de servidor como
`tok_<env>_<base62 32>` (7 linhas, 9 ocorrências) enquanto o código exige
`dsk_` (`lib/mcp/auth.ts:123`) e emite `dsk_<8 hex>_<base64url 32 bytes>`
(`app/api/v1/settings/api-tokens/route.ts:64`, `lib/tenants/api-key.ts:142`).
`tok_` nunca existiu no código — o CLAUDE.md já registra isso desde 17/09/2026
(PR #1128, que corrigiu CLAUDE.md, AGENTS.md e Spec 09 e deixou as Specs 01 e 11
de fora de propósito, como a issue aponta).
Condição de fechamento 1 da issue #1129: o esquema com AMBIENTE no prefixo é
abandonado, as 7 linhas passam a `dsk_` e a Spec 01 registra POR QUE o ambiente
saiu do prefixo — o produto não tem `live`/`test` por token (a separação é a
organização, e o estado da credencial vive em `revoked_at`/`expires_at`), e o
desenho implementado (EPIC-09, CLAUDE.md, código) nunca teve ambiente. O código
de emissão e validação não muda: trocar o prefixo invalidaria todo token já
emitido nas instalações e contradiziria o CLAUDE.md que o próprio #1128 corrigiu.
Entra junto o comentário de `lib/messaging/ritmo-do-envio-por-token.ts`, que
citava `Bearer tok_...` — com ele, `git grep tok_` zera em `lib/`, `app/` e nas
duas specs.
Gate: tests/unit/formato-do-token-de-servidor-bate-com-a-spec.test.ts — 4 casos
que prendem as três pontas: a validação recusa o prefixo antigo como
`malformed` e aceita `dsk_`, as DUAS portas de emissão montam o formato, e as
duas specs declaram `dsk_` e já não declaram `tok_`.
Três cantos da issue #290, nas duas rotas do webhook do canal por QR:
1. A rota por token recusava no estágio 1, ANTES de arquivar, a `session`
do corpo, que ali não resolve nada (quem resolve é o token do caminho).
Ganha um estágio 1 próprio (`lerRoteamentoWahaPorToken`: evento + id).
2. A recusa do estágio 2 gravava `status: "received"` no arquivo, igual a um
evento que deu certo. O contrato passa a ser conferido antes do INSERT e a
linha nasce `error`, com `contrato_violado: <campos>` (nomes, nunca valores).
5. A recusa do estágio 1 da rota por token (pública, antes do gate de
assinatura) sai como `warn`; a da rota global segue `error` (o Caddy do kit
barra quem não é o WAHA).
O comentário do 400 deixa de se apoiar numa premissa não medida sobre
retentativa do provider (item 3, lado WAHA). Primeiro teste de rota da rota
por token: tests/unit/recusa-do-webhook-waha-deixa-rastro.test.ts.
Porte do trabalho de 17/09 (branch fix/290-webhook-waha-recusa-deixa-rastro),
que estava com identidade de autor inventada.
Refs #290
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N9fyW7pXJCsyDPtu5PoQpN
Ajustes nossos sobre a fatia do @vgamkt (commit anterior), separados para o
diff dele ficar legível.
- O filtro que não casa nada devolve VAZIO. A versão do PR reexecutava a
consulta sem o filtro e entregava até 100 linhas ao modelo (C-013), pensando
num catálogo de produtos. A tool é genérica: numa tabela de clientes, o CPF
que não casa entregaria os registros de outras pessoas. A resposta agora
ensina o modelo a repetir com um trecho menor do termo. Provado por
sabotagem: com o fallback de volta, o caso novo recebe a linha
"de-outra-pessoa" e reprova.
- `motivoDoVazio` nas duas tools (#484): o erro devolvido como texto
(`acesso_negado`, `tabela_nao_encontrada`...) e a consulta sem linha passam
a ser `success: false` no audit, em vez de "nenhuma falha" no painel.
- `tests/unit/valor-de-filtro-nao-vai-ao-audit.test.ts`: roda a tool real
pelos dois ingressos e prova que o valor do filtro não chega ao audit.
Sabotado nos dois: auditar `args` crus no runtime ou no `/api/mcp` reprova
o caso respectivo.
- Espanhol dos rótulos novos no dicionário; spec 20 e mapa vivo passam a
descrever a camada do agente como entregue; fragmento em `.changes/`.
Refs: #1130
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019G7fFaatqXpzHA77xP9onS
Prospecção nativa, agente por conversa, canais sociais e painel de mensagens
rápidas (@saraivabr). 14.148 linhas em 161 arquivos, aberto em 16/set.
Os cinco obrigatórios verdes, zero vermelhos.
O QUE FICOU DE FORA, E NÃO É RECUSA: o atalho flutuante de mensagens
(`FloatingInbox`) foi RECORTADO e volta em PR próprio, com o crédito do autor.
A razão, medida: montar o dock fazia a PÁGINA INTEIRA aparecer duas vezes no
DOM, em seis casos de e2e, em telas sem relação entre si — agenda, construtor de
fluxos, tipos de evento, distribuição de atendimento. Provado por ablação (uma
tag comentada, num run só: seis violações viram zero).
E o mecanismo NÃO é do dock. O segundo nó é `<div hidden id="S:0">`, a caixa de
estadiamento do streaming SSR do React, filha direta do `<body>` e fora da
árvore da casca. Num stream correto todo `id="S:N"` tem um `$RC("B:N","S:N")`
que o consome; o documento fecha sem emitir o dele, e a caixa fica órfã com uma
cópia inteira da página. Medido em dois artifacts independentes
(`S:0=1 B:0=1 $RC=0`), e os `loading.tsx` que produzem esses boundaries já
estão na `main` — o dock não. Ele revela, não causa.
Está na issue #1374, com o comando que reproduz. Um PR de contribuidor não
carrega dívida da casa, e o dock não fica preso a ela: volta assim que o
mecanismo tiver desfecho.
DEFEITOS ACHADOS E CONSERTADOS NO CAMINHO, nenhum deles do autor:
- A cascata de anonimização de LGPD: o apêndice do baseline redefine a função
inteira, e `create or replace` faz valer só a última. Um merge da `main`
apagou em silêncio o passo `sales` que ela tinha ganhado — anonimizar
devolveria SUCESSO com o texto da comanda ainda legível. O invariante pegou.
- A reserva do rodapé: `AppShell` trocou `p-6` por `p-6 pb-20` enquanto o
`InboxLayout` desconta `2*var(--space-6)` do próprio calc. 24+80=104 gastos
contra 48 descontados: 56px de rolagem morta no Inbox, com o campo de envio
abaixo da dobra, em TODA instalação. Consertado pelo contrato do rodapé
(`lib/ui/rodape-ocupado.tsx`), não por esticar o calc.
- Migrations renumeradas 0359-0362 -> 0368-0371: o 0359 tinha sido tomado pela
cascata da comanda do #819. Troca por par completo (`carimbo_NNNN_slug`), não
por `sed` em "0359" — que teria reescrito as três referências da migration
alheia.
- O índice de identidade social nascia sem `where is_merged_into is null`,
reprovado pela cerca que entrou hoje no #1328 (@webtecnica).
- Três chaves duplicadas no dicionário, vindas da prévia do merge.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FHzAhMNW4yRWfgR5Ack89m
Trabalho de @vgamkt, recortado do PR #1130 (`feat/fluxos-de-atendimento`, 94
commits, 2130 atrás da main, 33 conflitos medidos). Esta fatia é a do BANCO
EXTERNO, e foi escolhida porque é 100% ADITIVA: `git ls-tree -r origin/main |
grep -c external-db` devolve 0, e nenhum dos 33 conflitos toca estes caminhos.
Recria, sobre a main de hoje, o estado final dos commits dele que constroem a
frente — 73a274eff, 2d4113022, f994fa15b, d750a2b80, 7e8e2bbf1, 8f145f189,
8ecfc4c30, 3cdf99e12, 7f8ca9309, fee7d4eac, 42e99715a — em vez de cherry-pick
um a um, que arrastaria os arquivos de costura de 94 commits.
─── O que entrou
- `lib/external-db/` — núcleo. Guarda de destino própria (o `pg` abre TCP cru e
não passa pelo allowlist HTTP): RFC1918 PERMITIDO de propósito (Postgres na
LAN é caso real, e quem cadastra é `admin`), link-local/metadata, loopback,
CGNAT, multicast e reservadas SEMPRE bloqueados, IPv6 normalizado antes de
classificar. Pool por conexão invalidado por `updated_at`, `BEGIN READ ONLY`
com `statement_timeout`/`lock_timeout`, introspecção ao vivo, e o SELECT
montado no servidor com identificadores validados contra o catálogo e valores
parametrizados. Testes co-localizados.
- `app/api/v1/external-db/` — CRUD, teste de conexão, catálogo e leitura. A org
vem de `requireRole` (cookie/JWT), nunca do corpo; o admin client filtra
`organization_id` à mão. Listar é `viewer`, configurar é `admin`.
- `app/app/integracao-dados/` — lista, cadastro/edição e explorador paginado.
- Migrations 0372 e 0373 + apêndice idempotente no `baseline.sql` + MANIFEST.
─── O que NÃO entrou, e por quê
As tools do agente (`crm_describe_external_data`, `crm_query_external_data`) e o
`redigirParaAuditoria` ficaram no #1130. Elas são CONSUMIDORAS deste núcleo e se
apoiam em `lib/ai/runtime/tools.ts`, `lib/mcp/server.ts`, `lib/mcp/types.ts` e
`lib/atendimento/fronteira-server.ts` — os quatro se moveram na main desde que o
PR foi escrito. A fatia que entrou se sustenta sozinha (cadastro → API → tela,
com log, porta na navegação e mapa vivo), então a camada do agente entra como um
segundo recorte sem retrabalho neste. A spec e o mapa dizem isso por escrito, e
`max_filters`/`max_response_bytes` ficam declarados como ainda sem consumidor.
─── O que eu mudei do trabalho dele, e por quê
- Migrations renumeradas de 0233/0234 para 0372/0373: os dois números já são de
outra coisa na main (0234 é chamada de voz). O teto não é a main: 0367 está
tomado por dois PRs em voo (#1359, #1363) e 0368-0371 por branches locais que
o pre-commit conhece e a varredura de PRs não via. Medido em TODAS as refs
(`git log --all --diff-filter=AMR -- supabase/migrations`), não só na main.
O conteúdo SQL não mudou.
- Spec renumerada de 18 para 20 (18 e 19 já existem na main).
- `placeholder="db.exemplo.com"` virou `meusistema.com`:
`tests/unit/branding.test.ts` tem a categoria AMOSTRA fechada, e esse domínio
já está declarado lá — medido, reprovava.
- Do dicionário de i18n entraram só as 56 chaves que ESTA fatia usa, nunca o
arquivo dele (a main tem 6.556 chaves e a branch dele 5.403 — substituir
apagaria trabalho).
- O mapa vivo perdeu a faixa do agente e ganhou a não-ligação declarada.
Refs: #1130
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FHzAhMNW4yRWfgR5Ack89m
- vercel.ts: a main APAGOU (o produto saiu da Vercel). Deleção aceita, não
ressuscitado.
- lib/ai/pontos/resolver.ts e heranca-de-provider-nos-pontos-auxiliares:
união — "prospecting_agent_setup_chat" (PR) e "case_chat" (main) são pontos
diferentes; perder um tiraria a herança de provider daquele ponto.
- lib/audit/actions.ts: o lado do PR já continha a linha da main, mais dois
verbos novos; ficou o do PR (a união seria idêntica).
- lgpd-pdf-controlador/meet/replies: fixtures — cada lado acrescentou campos à
MESMA estrutura. Ficam todos: fixture incompleta faz o teste passar sobre o
que não existe. No "meet", o início de appointment_notices aparecia nos dois
lados; entrou uma vez só.
- lib/lgpd/export-collector.ts (4 conflitos): união. O relatório do titular
leva prospecting_candidates (PR) E casos/eventos/demandas/chat/passagens/
avisos (main) — o que se apaga a pedido dele é o que se entrega a pedido dele.
- tests/invariants/rls-completude-varredura: união das tabelas declaradas.
Tabela que sai da lista é tabela sem prova de isolamento.
- docs/testing/user-journey-map.md e roteamento-por-canal.architecture.json:
união (JSON revalidado: 14 nós, 18 arestas).
- lib/i18n/dicionario.ts: união, e depois a varredura com a chave NORMALIZADA
(sem aspas) achou TRÊS duplicatas — "minutos", "Não foi possível concluir a
operação." e "Configuração salva." —, cada uma existindo nos dois lados, com
tradução idêntica. Saíram as cópias do PR (confirmadas pelo contexto das
linhas vizinhas), porque chave repetida é TS1117 e derruba os quatro
workflows de uma vez.
- supabase/baseline.sql: DERIVADO, não remendado. Parti do baseline da main e
inseri o apêndice do PR (355 linhas) ANTES do bloco da varredura de anon,
porque os blocos dele criam função e função depois da varredura nasce exposta
em quem ATUALIZA. Provado pelo portão do repo: varredura-anon-e-o-ultimo-bloco
5/5.
DESKCOMM_GOV_INVARIANTS_EDIT=1: o hook acusa tests/invariants/rls-completude-
varredura.test.ts, e a mudança é SÓ ACRÉSCIMO — as quatro tabelas que a main
declarou (config_aviso_de_caso, entregas_de_aviso_de_caso,
organization_extensions, extension_operations) mais calendar_locations,
chegando por este merge. Nenhuma linha removida, nenhum caso afrouxado: é a
união das duas listas, e tabela que saísse dela seria tabela sem prova de
isolamento.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ajuste da triagem sobre o trabalho de @webtecnica, sem reescrever nada dele:
- baseline.sql: a constraint idempotency_keys_recibo_ou_reserva sai do CORPO
(reprovava baseline-reaplicavel) e vai para o apêndice, SOMADA aos dois
`alter column ... drop not null`. Sem eles o update.sh de uma VPS já
instalada passava pelo `create table if not exists` sem efeito, as colunas
seguiam NOT NULL e toda reserva falhava com 23502 (medido em pg17 na triagem).
- migration renumerada 0314 -> 0321 (o 0314 é do #1123), mesmo timestamp, nos
três artefatos e nas citações do PR; MANIFEST volta a dizer o QUÊ/PORQUÊ.
- lib/api/idempotency.ts: efeito que lança vence a reserva na hora e propaga,
como a rota promete (route.ts: "propaga sem gravar recibo"); caso (12).
- route.test.ts: o dublê de idempotency_keys aprende `update`.
- tipos de idempotency_keys anuláveis; espanhol da mensagem nova; AGENTS.md e
spec 01 §7.3 deixam de dizer que a corrida está aberta; fragmento.
Refs: #1189 (trabalho de @webtecnica)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wvMod96K62qoXnmY5oYBF
O DoD manda procurar a afirmação de estado sobre o comportamento que o PR muda. Duas
estavam vencidas, e as duas foram apontadas por quem escreveu a skill — eu tinha o aviso
e ele ficou parado enquanto eu mexia em código.
DOUTRINA
- O não-negociável 2 dizia "hoje, `tasks.open`". Onde a afirmação podia virar comando, virou:
a prosa agora manda rodar o grep em `lib/extensions/capacidades.ts`, e escreve a RÉGUA de
admissão de uma porta nova em vez do inventário, que envelhece a cada ADR.
- Não-negociável 6-bis, novo: versão nova não troca o conjunto de portas. Enquanto havia uma
permissão só, a troca era impossível por construção; com a lista fechada ela passou a ser
possível, e por isso precisou virar recusa explícita.
SPEC
- Os tipos do bloco TypeScript, a frase sobre concessão e a linha do laço de retorno.
- O parágrafo que prometia `extension_permissions_changed` "quando o contrato admitir outra
permissão" agora diz que a recusa EXISTE — e guarda, entre parênteses, a premissa que
venceu. Prosa de estado tem prazo; apagar o rastro faria a próxima pessoa achar que nunca
houve promessa.
FRAGMENTO DE RELEASE
- Declara o efeito no operador, incluindo o que dói: pacote da versão anterior deixa de ser
compatível, porque foi o autor que escreveu até onde garantia. Diz também o que NÃO
acontece — nada é desinstalado sozinho e nenhuma configuração se perde.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
O contexto da passagem existia desde a onda 10, mas vivia num aviso da
Central — uma lista da organização inteira, que atualiza a cada 60s e leva
para a ficha do contato. Quem assume trabalha na CONVERSA, e é lá que o
cartão passa a aparecer: motivo em português, resumo, o que a IA tentou (lista
numerada), o que o cliente quer, e se o cliente foi avisado — com o porquê
quando não foi.
Sete estados, e a decisão de qual mostrar mora numa função PURA fora do
componente: o gate de vocabulário proíbe código de motivo dentro de
`components/`, e com razão — é o mesmo texto que a Central e o export usam.
A rota lê com o client de SESSÃO. É isso que faz a regra de visibilidade valer
para o TEXTO da passagem, que carrega o que o cliente disse.
Cobrador: passagem sem ninguém reconhecer há mais de 24h reabre o aviso, e
para na terceira cobrança — sem o contador, o vigia viraria spam diário sobre
a mesma linha. A auditoria só registra quando houve cobrança de verdade.
Laço de retorno (invariante 7): "o cliente repetiu tudo depois da passagem?"
vira número na tela de métricas, com a régua declarada (limiar de semelhança
0,7, janela de 24h, denominador = passagens em que o cliente voltou a falar).
Ausência de dado é `null`, nunca `0` — zero é uma afirmação.
Consertado de passagem: o merge anterior deixou marcadores de conflito
commitados no mapa vivo, e o JSON não parseava. Resolvido pela união dos dois
lados, com as arestas colidentes renumeradas.
Verificado: suíte completa 1.003 arquivos / 10.252 casos / ZERO falhas;
typecheck, lint, lint:channels, lint:role-rank, colisão de migration,
test:shell e release:conferir verdes; `test:db` do invariante novo com install
+ update + 7/7; 9 sabotagens batendo a previsão exata, restauração conferida
por md5.
PENDENTE: `test:db` completo e em pg17, `pnpm build`, e a jornada em tela —
as duas linhas J27.5/J27.6 seguem declaradas como NÃO COBERTAS, porque são da
onda 12.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PqKEskriDoLeY45kmFtCgE
Conflito único no dicionário: as ondas 8 e 10 acrescentaram chaves DIFERENTES
no fim do mesmo bloco. Ficaram as duas. typecheck zerado depois.
Overrides pelo mesmo motivo dos merges anteriores: o hook não distingue merge
de commit novo, e o que ele acusa (migration e invariantes) veio pronto dos
commits que este merge traz.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PqKEskriDoLeY45kmFtCgE
Antes: o resumo era do turno ANTERIOR, "o que a IA já tentou" não existia, o
motivo chegava como `Motivo: requested_human` em inglês de máquina, e a
segunda passagem do mesmo cliente era DESCARTADA em silêncio pelo dedup do
aviso.
Agora, os treze caminhos — pedido explícito, suspeita de descadastro, a
ferramenta do modelo, teto de gasto, o escalar de um caso, o orquestrador do
CRM, sentimento, os legados, o MCP e o runtime nativo — gravam a mesma
passagem, montada por uma função só.
A ferramenta que a IA usa ganhou três campos, com descrições que ENSINAM o
modelo: por que está passando, o que já tentou e o que o cliente quer. Onde
não há IA, o piso é determinístico: checkpoint + as mensagens pendentes do
turno + o motivo traduzido.
Três correções de verdade a quem assume:
- o aviso da Central virou CURTO e aponta para a CONVERSA, não para a ficha do
contato: aquele corpo é entregue a qualquer atendente da organização, e o
conteúdo da conversa não cabe ali;
- a segunda passagem acrescenta ADENDO em vez de sumir;
- "o cliente JÁ FOI avisado" passou a sair do status real da mensagem, e não
da ausência de exceção — antes o produto afirmava isso mesmo quando o envio
tinha falhado.
Divergência do plano, medida: esta onda TEM schema (migration 0293). A onda 9
não entregou as funções de reconhecimento, e sem escritor as colunas nasciam
mortas.
Verificado: typecheck, lint, lint:channels, lint:role-rank, colisão de
migration e release:conferir zerados; `test:db` com o baseline aplicando em
install E update e o invariante novo 10/10; suíte com 10.110 verdes (os 7
vermelhos são o Upstash local documentado no CLAUDE.md e estado transitório de
outra sessão — remedidos isolados, 55/55). 8 sabotagens, 7 batendo com a
previsão escrita antes; a que desviou está registrada com a previsão de pé.
Dois defeitos achados e consertados: um `.trim()` sobre linha de banco não
validada que derrubaria a rota de escalação, e uma semente de teste engolida
em silêncio por um unique parcial.
PENDENTE: pg17, build, test:shell, e o cartão na conversa (onda 11) — que é o
que fecha as duas linhas "não coberto pela tela" da jornada.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PqKEskriDoLeY45kmFtCgE
A tela vive em IA › Aviso no WhatsApp (admin), e o rótulo não é "Avisos" de
propósito: "Alertas" já é a Central, na mesma seção, e duas portas com nome
parecido na mesma lista é como se perde gente.
Ela faz três coisas: escolhe a conexão e o número, manda um aviso de teste DE
VERDADE, e mostra as 20 últimas entregas com destino mascarado, o motivo da
falha em português e o link para o caso. Sem a lista, o mecanismo seria
invisível — e mecanismo invisível é pior que ausente.
O seletor filtra por CAPACIDADE (aceita mensagem livre), nunca por nome de
provedor: é o que a doutrina de canais exige, e o gate confirma.
Onze estados de alerta, não sete: só canal oficial conectado, mesma conexão
que atende clientes, sem endereço público, casos desligados no agente, agente
assistido, atendimento externo, conexão arquivada, e o descarte de mensagens
do número interno ("é o esperado"). Cada um com texto de gente.
O aviso de teste paga no contador anti-banimento e NÃO escreve no registro de
entregas — a garantia é de tipo, provada por sabotagem contra o typecheck. E
a recusa volta 200 com o motivo traduzido, em vez de erro cru.
Segunda porta no fim do wizard de instalação: numa VPS nova a tela de Casos
está vazia, e ninguém descobriria o aviso por lá.
Duas correções ao plano, medidas: a frase do descarte não diz "nos últimos 7
dias" porque o contador é acumulado — esse número não existe no schema; e o
laço de retorno mede os dois grupos a partir da abertura do caso, porque medir
o grupo avisado a partir do envio enviesa a favor do aviso por construção.
Verificado: typecheck, lint (372 warnings — o mesmo número das ondas 1 a 7),
lint:channels e release:conferir zerados; suíte com 10.163 verdes; 10
sabotagens previstas antes e medidas, com as 2 previsões erradas declaradas.
Dois defeitos meus achados na releitura: um `||` que destravava o botão sem
endereço público, e a conexão arquivada que sumia da lista sem explicação.
PENDENTE, declarado: e2e e prova em tela (onda 12); o item na Central no
primeiro caso sem configuração precisa de um kind que ainda não existe.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PqKEskriDoLeY45kmFtCgE
A doutrina prometia `Authorization: Bearer tok_...` em CLAUDE.md, AGENTS.md e
na Spec 09. O código exige `dsk_` (lib/mcp/auth.ts:93), e `tok_` tem ZERO
ocorrências em lib/ e app/ — quem seguia o texto recebia 401 sem entender.
Também trocada a afirmação de alcance por direção mais comando. O texto
descrevia auth dual como se valesse em toda a /api/v1, quando ela é habilitada
rota por rota pelo helper lib/api/auth-dual.ts. Em vez de um número que
envelhece, o texto agora traz `git grep -ln "auth-dual" -- app/api/v1`.
Acrescentado o que a doutrina não dizia e custou 26 rotas duplicadas a um
contribuidor (PR #1008): chamar o helper na rota não basta, porque o proxy.ts
global roda antes do handler e só reconhece cookie — sem entrada em
lib/auth/public-paths.ts, todo bearer leva 401 antes de chegar lá.
Decisão do dono do produto em 17/09/2026 (documento de decisão 28, opção A):
converter as rotas que cada integração precisar, conforme aparecerem, em vez de
namespace paralelo por cliente.
AGENTS.md entra junto por decisão, não por inércia: ele já enumera as
superfícies não-cookie, e omitir as rotas de /api/v1 que aceitam bearer tornava
esse inventário falso para o agente que lê só ele.
Fora de escopo, e por isso não tocado: Spec 01 (§api-tokens) e Spec 11 definem
o FORMATO do token como `tok_<env>_<base62>`, com o prefixo exibido na UI. Ali
a divergência é de desenho contra implementação, não frase solta, e reescrever
desenho de spec em silêncio seria pior que a divergência. Vai para issue.
A regra IA-06 (o bot não reassume depois da passagem a humano até alguém
clicar "Devolver") continua sendo o padrão. O que entra é um prazo
OPCIONAL por organização — Configurações › Distribuição de atendimento,
`settings.routing.handoff_return_after_minutes`, 5 min a 24 h, null =
nunca — e o cron `handoff-devolucao` (a cada 5 min) que devolve ao agente
a conversa que está com humano de forma durável e ficou o prazo inteiro
sem NENHUM sinal humano: o maior entre a passagem, o assumir e a última
mensagem que saiu (inclusive pelo celular).
Motivo, medido numa instalação real: 12 de 31 conversas ativas do dia
estavam em handoff formal, nenhuma foi devolvida, e o cliente que
escreveu de novo ficou sem resposta — humano ocupado, IA proibida. O
botão existe; ninguém clica.
O cron não tem regra própria de devolução: cada conversa vencida passa
por `devolverAtendimentoAoAgente`, a MESMA função do botão e da tool do
agente, com uma origem `automatica` que o rastro registra — a
autorização do contato entra como `automacao:devolucao_apos_prazo` (não
"retomada_manual"), a atividade na linha do tempo diz "após N min" como
ato do sistema, e o audit leva `automatica: true`.
O que fica de fora, e por quê (lib/escalacao/devolucao-automatica.ts):
sessão sem agente publicado nem roteador ativo (devolver para ninguém
tira a conversa da fila humana e a deixa muda); conversa encerrada; a
pausa por resposta pelo celular (silêncio finito, vence sozinha); e
conversa sem nenhum carimbo de tempo.
A mescla do PATCH de /settings/routing saiu para
`mesclarSettingsDeAtendimento`, que a rota e o teste leem — o teste era
uma cópia da rota. Cliente antigo que omite a chave preserva o prazo em
vigor, como já valia para `visibility_mode`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Três tabelas nasceram graváveis por QUALQUER membro da organização, pelas
duas origens: o `ALTER DEFAULT PRIVILEGES … GRANT ALL ON TABLES TO
"authenticated"` do baseline, que vale para toda tabela criada depois dele,
e uma policy `for all`/`for insert` sem papel mínimo.
O que se pagava: um `viewer` escrevia, pelo PostgREST e com o JWT dele, o
título/resumo/bloqueio que a equipe lê para decidir um caso — e, com o aviso
de caso por WhatsApp (próximas ondas), esse texto sairia no celular da equipe
pelo número da empresa, com o link legítimo ao lado. Em
`conversation_assignment_events`, um INSERT forjado faz o histórico afirmar
que alguém assumiu um atendimento que ninguém assumiu.
A migration 0279 fecha as duas origens nas três tabelas e reserva
`ai.case_opened`/`ai.case_closed` no `emit_event` (a reserva sozinha não
fecha nada: ela só barra quem tem `auth.uid()`; quem fecha a forja da linha
é o revoke). SELECT continua aberto nas três — a tela, o MCP e o motor leem.
Censo no cabeçalho da migration: nenhum caminho legítimo escreve por
`authenticated`. O motor usa `pg.Pool`, o cron usa service role, e
claim/transfer/release passam por `fn_conversation_assign`, que é
`security definer` — o controle positivo do invariante novo prova isso.
O baseline deixou de CRIAR as três policies largas para derrubá-las no fim:
criar e derrubar no mesmo arquivo faz a regra antiga valer entre os dois
pontos de toda instalação.
`tests/invariants/gov-3-assignment-events.test.ts` ganhou uma ponte mínima:
ele traduzia "barrado" em zero linhas (negação por RLS), e agora o Postgres
barra antes, por privilégio. As asserções não mudaram.
PENDENTE, e declarado: `pnpm test:db` não foi exercitado — o Docker local
está devolvendo 500 em `docker exec` sob a carga desta máquina (208 arquivos
morreram no setup, 1 asserção). Verde hoje: typecheck, lint e os seis gates
estáticos do baseline (ordem do apêndice, derivação da cadeia, e o gate que
proíbe o baseline construir o que ele mesmo derruba).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PqKEskriDoLeY45kmFtCgE
Decisao do dono, hoje: "Vercel.ts sai". O arquivo era o inventario de crons
para quem hospedasse uma copia do CRM na Vercel, e
`tests/unit/cron-routes-scheduled.test.ts` obrigava toda rota de cron nova a ser
cadastrada nele — custo de manutencao pago a um destino que o produto nao usa
desde que o projeto foi desvinculado do GitHub.
O que sai: o `vercel.ts`, o caso do gate que o lia, o check do
`scripts/qa-wave-13-08.ts` que afirmava que o `maxDuration` morava nele (agora le
a propria rota, onde o valor esta), e a dependencia `@vercel/config`, cujo unico
importador era o arquivo apagado.
O que FICA vigiado, e e a cerca que importa: rota de cron sem linha no crontab do
`scheduler`, e linha de crontab apontando para rota que nao existe. A primeira
direcao foi provada por sabotagem (1 de 4 casos reprovou, o previsto). Fica
tambem a guarda da copia `CRON_SECRET` -> `INTERNAL_CRON_SECRET` do `lib/env.ts`,
que era a unica que existia — com a asercao consertada: ela casava `CRON_SECRET`
dentro da propria string `INTERNAL_CRON_SECRET` e passava mesmo sem a leitura da
variavel.
O runbook `vercel-hobby-relogio.md` existia em torno do arquivo apagado e do
plano gratuito daquela plataforma. Virou `relogio-http.md` e passou a tratar do
que realmente resolve: instalacao sem agendador de minuto, que bate o relogio
HTTP de fora. Todas as citacoes ao nome antigo foram atualizadas.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
O projeto do CRM na Vercel foi desvinculado do GitHub por decisao do dono: a
plataforma fica so com a landing page, em outro repositorio. Medido com a mesma
sonda nos dois lados — `gh api repos/.../commits/<sha>/status` devolve `Vercel`
no commit f65d04667 (antes) e vazio em a8c9e1ad3, 7168961ad e no head do #1081
(depois).
Com isso, dezenas de afirmacoes do repositorio publico viraram falsas. As que
falavam no presente sairam:
- mensagem automatica ao contribuinte, molde de PR, CONTRIBUTING e os espelhos
na skill de contribuir prometiam um check "Vercel" vermelho que nao aparece
mais. No lugar, a mensagem passa a dizer onde olhar o que trava o merge — os
checks marcados Required no proprio PR, com a ressalva de que `--required` so
lista os que ja reportaram;
- runbooks de operacao mandavam abrir o painel da plataforma e redeployar a
main; passam a descrever o `.env` da instalacao e a recriacao dos conteineres;
- `lib/env.ts` mandava, no boot, ajustar variavel "na Vercel";
- specs e PRDs descreviam topologia, crons e guarda de segredo na plataforma;
- comentarios de codigo ancoravam decisoes vivas em premissa morta.
Registros datados (pesquisa, pitch, epicos, handoffs) nao foram reescritos:
ganharam uma linha dizendo que sao registro da fase hospedada.
O `vercel.ts` fica: ele serve quem hospeda um fork, e o gate
`tests/unit/cron-routes-scheduled.test.ts` continua reprovando divergencia de
rotas entre ele e o crontab do scheduler.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
O lote 12 da triagem criou na main a J24 "O vocabulário de etiquetas da
organização", já citada em `evidence/triagem-16set-l12/` e em
`tests/e2e/qa-l12-tags-radar.spec.ts` (J24.1 a J24.6). O merge da main
juntou as duas J24, e `tests/unit/numero-de-jornada-e-unico.test.ts`
reprovou — o git une as duas seções sem reclamar.
A J24 da main fica como está. As das extensões passam a J25 (instalar e
usar) e J26 (atualizar, desfazer, remover e reinstalar), e as citações
acompanham só onde são das extensões: as duas seções do mapa, a doutrina,
a spec, o mapa de arquitetura, a spec de tela e a fixture do catálogo.
Nenhum PR aberto cria jornada nova (medido com controle positivo: dos
três que tocam o mapa, só o #1016 acrescenta seção).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A main andou 191 commits e trouxe a própria 0263
(`etapa_de_perda_grava_o_motivo`) e a 0264 (`vocabulario_de_tags`). A
migration das extensões colidia no NNNN e passa a ser
`20260917120000_0271_extensoes_declarativas.sql` — número E timestamp
trocados juntos. 0271 é o primeiro número livre medido na main (até
0264) e nos 69 arquivos de migration dos PRs abertos (reservados até
0270). A renumeração é escopada ao que é das extensões: nome do arquivo,
cabeçalho, os quatro rótulos do bloco no baseline, a linha do MANIFEST
(movida para depois da 0264), o teste que procura o arquivo e três
comentários. As citações da 0263 da main (lote de perda) não foram
tocadas.
Nenhuma das 13 funções das extensões é redefinida pela 0263/0264 da main
nem por migration posterior, então a nova posição na cadeia não muda
qual corpo vence. O bloco no baseline continua byte a byte igual à
migration (47.853 bytes) e já estava depois da 0264 e antes da varredura
de anon, que precisa ser o último bloco.
Conflitos, os dois de apêndice, resolvidos com os dois lados:
- `.github/workflows/e2e.yml`: as três specs de extensões ficam na parte
3, junto das duas de QA do lote 12. A main mediu a parte 3 em 22 min
contra o teto de 30; as três somam ~1,5 min na última rodada local.
- `lib/i18n/dicionario.ts`: 251 linhas das extensões e 80 da main, sem
chave repetida entre os blocos.
DESKCOMM_GOV_MIGRATION_EDIT=1 — o pre-commit acusou duas colisões que
não existem na árvore deste commit, medidas antes de usar a exceção:
(1) a 0263 da main contra o NOME ANTIGO da migration das extensões na
ponta da branch, que é exatamente o que este commit renomeia; (2) a 0264
da main contra a branch local `fix/887-prelude-acl-de-tabelas`, que tem
o MESMO arquivo (blob `10d0346f` nos dois lados). A autoridade é o gate
de CI `pnpm checar:colisao-de-migration`, rodado logo depois deste
commit.
DESKCOMM_GOV_INVARIANTS_EDIT=1: dos cinco invariantes que o merge toca,
quatro chegam da main idênticos a ela. O quinto,
`vocabulario-banco-x-typescript.test.ts`, difere da main só por 14
linhas ACRESCENTADAS por esta branch — as colunas `kind` e `status` do
recibo das extensões entrando no gate — e pelo número do comentário
(0263 → 0271). Nenhuma linha da main foi removida ou alterada.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>