Files
Rafael Melgaço f25cd3e2ee feat(G6-04): governance contract for external agents — spec 14 [gov-loop]
docs/specs/14 — the contract the Vendaval FG phase consumes. Edge-contract
style, header verified against gov/G6 @ ddcc511. Documents the 8 governance
tools with exact Zod I/O, assignee_kind/handoff/visibility_mode semantics,
ai_dispatch_mode (external → event stays pending), and prohibitions (never
cross-org, never bypass is_blocked/force_human/bot_silenced_until — 4 distinct
guards). Bidirectional 1:1 coverage (all 8 governance tools documented, none
omitted); ~20 file:line refs opened and verified. 'Consumer changes' section
feeds FG-01.
Verified-by: gov-verifier PASS 2026-07-18T21:53:23-0300
2026-07-18 21:53:24 -03:00

798 lines
43 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"_meta": {
"loop": "gov-loop",
"repo": "DeskcommCRM",
"active_phase": "G1",
"loop_doctrine": "Máquina portada do vendaval-loop. Doutrina de DOMÍNIO soberana: CLAUDE.md deste repo + docs/specs/ (multi-tenancy, migrations em tripla, idempotência, RBAC, LGPD, anti-patterns). Este arquivo só muda nos campos 'passes' e 'verification', via node loop/update-feature.ts — nunca editor. Feature nova/alterada = proposta em loop/inbox.items.md + aprovação humana + commit com DESKCOMM_GOV_PLAN_EDIT=1.",
"session_ritual": "Ler loop/LOOP.md e segui-lo à risca. Uma sessão = UMA feature. Maker≠checker: passes:true só com PASS do gov-verifier sob hash-check.",
"phase_gate": "G<n> elegível só se loop/checkpoints/G<n-1>.approved existe. Fase completa → relatório de checkpoint → PARAR (gate humano).",
"migration_doctrine": "Feature que toca schema segue a tripla do repo: supabase/migrations/<ts>_<NNNN>_<slug>.sql idempotente + apêndice em supabase/baseline.sql ('-- ---- ... (migration NNNN) ----') + linha em supabase/migrations/MANIFEST.md + lib/database.types.ts regenerado. Sequência NNNN validada contra TODAS as branches locais (a cadeia vendaval/F2-* pode ter migrations não mergeadas). Enforced por loop/hooks/check-migration-triple.sh.",
"provenance": "Backlog derivado de 7 eixos de feedbacks reais de usuários do sistema-modelo (TomikCRM), abstraídos por tema — ZERO PII neste repo. Spec-mãe: docs/specs/13-spec-governanca-atendimento.md (complementa 04 e 05, não duplica).",
"smoke": "pnpm typecheck && pnpm lint && pnpm test:unit"
},
"features": [
{
"id": "G1-01",
"phase": "G1",
"title": "Gate de CI consolidado (local + GitHub Actions)",
"description": "Hoje nenhum workflow roda typecheck/lint/test:unit (só perf.yml e publish-image.yml). Criar script local único e workflow de PR. É o 'verde' em que o verifier e o time passam a confiar.",
"acceptance": [
"Existe script `gov:verify` no package.json que roda `pnpm typecheck && pnpm lint && pnpm test:unit` e retorna exit!=0 se qualquer um falhar; executá-lo no repo atual termina verde.",
"Existe .github/workflows/ci.yml disparando em pull_request e push para main, com pnpm 9 + node 20 (mesmas versões de perf.yml), rodando os 3 comandos.",
"ci.yml passa em validação de sintaxe (actionlint se disponível, senão `node -e` com parser YAML) — sem depender de rodar no GitHub.",
"Nenhum teste existente foi alterado ou skipado."
],
"depends_on": [],
"priority": 10,
"lane": "core",
"kind": "build",
"passes": true,
"verification": {
"verdict": "PASS",
"by": "gov-verifier",
"at": "2026-07-16T19:01:24-0300",
"commit": "self"
}
},
{
"id": "G1-02",
"phase": "G1",
"title": "Postgres descartável: baseline install+update + isolamento 2-tenants",
"description": "Automatiza o gate manual da doutrina de migrations: aplicar supabase/baseline.sql num pgvector/pgvector:pg17 efêmero nos modos install (ON_ERROR_STOP=1) e update (re-apply idempotente), e provar isolamento RLS entre 2 orgs. Vira a fundação de tests/invariants.",
"acceptance": [
"Existe script `test:db` (package.json) que sobe container efêmero pgvector/pgvector:pg17 (docker), aplica baseline.sql com ON_ERROR_STOP=1 (modo install), re-aplica sem ON_ERROR_STOP (modo update) e falha se qualquer statement do modo install falhar.",
"Suíte vitest (ex.: tests/invariants/rls-isolation.test.ts) conecta no container, cria 2 orgs + 1 usuário em cada via SQL, e prova: usuário da org A não lê conversations/messages/contacts/crm_leads da org B (0 rows sob RLS com JWT/claims simulados via set_config, mesmo padrão fn_user_org_ids()).",
"`pnpm test:db` termina verde localmente e derruba o container ao final (trap), inclusive em falha.",
"README curto em tests/invariants/README.md explica como rodar e o contrato do harness."
],
"depends_on": [
"G1-01"
],
"priority": 20,
"lane": "core",
"kind": "build",
"passes": true,
"verification": {
"verdict": "PASS",
"by": "gov-verifier",
"at": "2026-07-16T19:26:00-0300",
"commit": "self"
}
},
{
"id": "G1-03",
"phase": "G1",
"title": "Suíte de invariantes de governança (o eval do épico — congelada)",
"description": "Codifica os 7 eixos de dor como testes executáveis contra o Postgres descartável e contra as rotas (unit). Gap conhecido = teste com test.fails explícito (catraca: quando a fase corrigir, o test.fails passa a falhar e obriga o flip para teste normal). Após merge, tests/invariants/** é congelado por hook.",
"acceptance": [
"Existe tests/invariants/ com ≥1 invariante por eixo (1-RBAC, 2-atribuição, 3-transferência, 4-roteamento/fila, 5-escopo de visualização, 6-handoff IA→humano, 7-tags), cada arquivo com comentário-cabeçalho citando o eixo e a spec 13.",
"Invariantes de estado atual que JÁ valem passam verdes (ex.: RLS org-flat isola entre orgs — G1-02; role helpers fn_role_at_least existem e ordenam viewer<agent<manager<admin).",
"Cada gap conhecido está como test.fails com comentário `GAP(G<fase que corrige>)` — ex.: 'agent vê conversa atribuída a outro agent' (GAP G4), 'transfer não gera evento de auditoria' (GAP G3), 'role de membro não é editável via API' (GAP G2).",
"`pnpm test:invariants` roda a suíte inteira verde (test.fails contam como esperados).",
"Tabela eixo→invariante→status(passa|GAP Gx) adicionada como apêndice A da docs/specs/13-spec-governanca-atendimento.md."
],
"depends_on": [
"G1-02"
],
"priority": 30,
"lane": "core",
"kind": "build",
"passes": true,
"verification": {
"verdict": "PASS",
"by": "gov-verifier",
"at": "2026-07-16T19:48:00-0300",
"commit": "self"
}
},
{
"id": "G1-04",
"phase": "G1",
"title": "Auditoria de gap: specs 04/05 + stories AT-* vs código real",
"description": "A spec 04 já define claim atômico (AT-02), fila round-robin em worker (AT-03), supervisor read-only (AT-04), heartbeat/status (AT-08), ReassignDialog e bulk-assign. Mapear mecanicamente o que existe implementado vs o que é só spec, para as fases G2-G5 não re-especificarem nem re-implementarem o que já há.",
"acceptance": [
"Apêndice B da spec 13 contém tabela: item da spec 04/05 (com §) → status {implementado|parcial|ausente} → evidência arquivo:linha do código real (app/, lib/, hooks/, supabase/baseline.sql).",
"Cobre no mínimo: claim atômico §9 (UPDATE condicional + 409), ReassignDialog, UnassignedQueueAlert §8.3, AttendantStatusToggle §8.1-8.2, supervisor read-only §10, bulk actions §6.5, handoff da spec 05, conversations.assigned_to_user_id/claimed/ai_handling no baseline.",
"Cada item 'parcial' ou 'ausente' aponta qual feature G* o cobre (ou registra proposta na inbox se nenhuma cobre).",
"Nenhuma afirmação sem evidência arquivo:linha — verificável por leitura."
],
"depends_on": [],
"priority": 40,
"lane": "core",
"kind": "build",
"passes": true,
"verification": {
"verdict": "PASS",
"by": "gov-verifier",
"at": "2026-07-16T20:12:00-0300",
"commit": "self"
}
},
{
"id": "G1-05",
"phase": "G1",
"title": "Modelo de dados alvo + matriz role×recurso (spec 13 §3-§4)",
"description": "Detalhar na spec 13 o DDL rascunho das estruturas novas (conversation_assignment_events, conversation tags, routing_rules/availability, assignee_kind) e a matriz completa role×recurso×ação que a G2 vai aplicar. Segue doutrina DIRC do repo antes de qualquer campo novo.",
"acceptance": [
"Spec 13 §3 tem DDL rascunho (CREATE TABLE comentado) para: eventos de atribuição (quem→quem, por quem, motivo, quando), tags de conversa, config de roteamento por org, disponibilidade/horário por atendente, e a decisão assignee_kind ('user'|'ai') unificando ai_handling com assignment.",
"Cada tabela nova justifica DIRC (Derive? Infer? Reuse? Create) contra o baseline atual (ex.: por que tags de conversa não reusa contacts.tags).",
"Spec 13 §4 tem matriz role×recurso: linhas = recursos (conversations, messages, contacts, crm_leads, pipelines, settings, api_tokens, billing, team, audit, métricas), colunas = viewer/agent/manager/admin, células = {none|own|org} × {read|write}.",
"Pontos que dependem de decisão do dono estão marcados 'PENDENTE G1-06' — nenhuma decisão de produto inventada."
],
"depends_on": [
"G1-04"
],
"priority": 50,
"lane": "core",
"kind": "build",
"passes": true,
"verification": {
"verdict": "PASS",
"by": "gov-verifier",
"at": "2026-07-16T20:24:00-0300",
"commit": "self"
}
},
{
"id": "G1-06",
"phase": "G1",
"title": "INPUT HUMANO: decisões de produto da governança",
"description": "Decisões que são do dono, não do loop: (a) default de visibilidade do role agent (vê só as suas? suas + fila não-atribuída? tudo?); (b) modos de roteamento desejados no MVP (manual, round-robin, por carga?); (c) 'agent' do RBAC = atendente do atendimento, ou nasce role novo?; (d) transferência exige aceite do destino ou é imediata?; (e) manager pode ver métricas individuais de todos?",
"acceptance": [
"Item registrado em loop/inbox.items.md com as 5 perguntas fechadas (formato: pergunta + opções + recomendação com 1 linha de porquê).",
"Respostas do dono registradas no mesmo item (status answered) e transcritas na spec 13 §4 (matriz sem PENDENTE) e §5 (decisões de roteamento).",
"Nenhuma feature de G2+ foi iniciada antes das respostas nos pontos que dependem delas."
],
"depends_on": [
"G1-05"
],
"priority": 60,
"lane": "core",
"kind": "human_input",
"human_input": true,
"passes": true,
"verification": {
"verdict": "PASS",
"by": "gov-verifier",
"at": "2026-07-16T20:36:00-0300",
"commit": "self"
}
},
{
"id": "G2-01",
"phase": "G2",
"title": "Matriz role×endpoint aplicada server-side em /api/v1",
"description": "Enforcement da matriz (spec 13 §4) nas rotas — não só RLS, não só UI. Helper único de autorização (reusar lib/auth/server.ts; padrão requireRole) aplicado a cada grupo de rotas; a lição do sistema-modelo é que matriz advisória no frontend = atendente com poder de owner.",
"acceptance": [
"Existe helper único (ex.: lib/auth/require-role.ts) que resolve o role efetivo via getUser() + fn_user_role_in_org e nega com fail() 403 padronizado (lib/api/wrappers.ts); nenhuma rota reimplementa a checagem na mão.",
"Rotas de settings/api-tokens/billing/team exigem ≥manager ou admin conforme matriz; rotas de leitura de inbox/leads permitem agent; testes unit por grupo de rota provam o 403 para role insuficiente e o 200 para role mínimo.",
"Invariantes de RBAC (eixo 1) que eram test.fails em G1-03 flipados para testes normais e verdes.",
"Audit: tentativa negada relevante gera entrada em api_audit_log com action='authz.denied' (append-only)."
],
"depends_on": [
"G1-03",
"G1-06"
],
"priority": 10,
"lane": "core",
"kind": "build",
"passes": true,
"verification": {
"verdict": "PASS",
"by": "gov-verifier",
"at": "2026-07-16T23:05:00-0300",
"commit": "self"
}
},
{
"id": "G2-02",
"phase": "G2",
"title": "Papel de membro editável pós-convite (API + UI + audit)",
"description": "No sistema-modelo, o nível de acesso não era editável após atribuição (bug recorrente). Aqui: PATCH de role em user_organizations com guarda admin, auditado, e UI na página de team.",
"acceptance": [
"PATCH /api/v1/team/[user_id] aceita {role} (viewer|agent|manager|admin), exige role admin do chamador, valida via Zod, retorna ok(); último admin da org não pode ser rebaixado (regra + teste).",
"Mudança gera api_audit_log com action='team.role_changed', actor e antes/depois.",
"UI app/app/team: seletor de papel por membro (admin only), com estado otimista e toast de erro; e2e ou teste de componente cobre o fluxo.",
"Migration NÃO é necessária (user_organizations.role já existe com CHECK) — se algo de schema for tocado, segue a tripla do repo."
],
"depends_on": [
"G2-01"
],
"priority": 20,
"lane": "core",
"kind": "build",
"passes": true,
"verification": {
"verdict": "PASS",
"by": "gov-verifier",
"at": "2026-07-16T22:52:28-0300",
"commit": "self"
}
},
{
"id": "G2-03",
"phase": "G2",
"title": "RLS por role nas tabelas de configuração + invariantes verdes",
"description": "Fechar no banco o que a G2-01 fechou na API (defesa em profundidade): policies por role nas tabelas de config que ainda estiverem org-flat, seguindo o padrão fn_role_at_least já usado em api_tokens/lgpd_requests/merge_queue.",
"acceptance": [
"Auditoria das policies atuais (baseline.sql) registrada: tabela → policy → role mínimo; tabelas de config org-flat identificadas.",
"Migration (tripla completa: migrations/ + baseline.sql + MANIFEST.md + database.types.ts se contrato mudou) aplicando fn_role_at_least nas write-policies das tabelas de config identificadas.",
"pnpm test:db verde (baseline install+update) e teste de isolamento por role no Postgres descartável: agent não escreve em tabela de config, manager sim (onde a matriz permitir).",
"Sequência NNNN validada contra todas as branches locais (hook check-migration-triple passa)."
],
"depends_on": [
"G2-01"
],
"priority": 30,
"lane": "core",
"kind": "build",
"passes": true,
"verification": {
"verdict": "PASS",
"by": "gov-verifier",
"at": "2026-07-16T23:07:32-0300",
"commit": "self"
}
},
{
"id": "G2-04",
"phase": "G2",
"title": "E2E de papéis (Playwright)",
"description": "Prova de ponta a ponta da matriz: cada papel navegando o app vê e consegue exatamente o que a matriz permite.",
"acceptance": [
"Spec Playwright (tests/e2e/rbac-roles.spec.ts) com 2 usuários seed (agent, admin) na mesma org: agent não acessa /app/settings/api-tokens nem /app/settings/billing (redirect ou 403 renderizado); admin acessa.",
"Agent vê inbox e kanban; viewer (se seedável) não consegue enviar mensagem (composer desabilitado ou 403).",
"Suíte roda com pnpm test:e2e local (webServer do playwright.config.ts) e passa.",
"Axe (já usado no repo) sem regressão nas telas tocadas."
],
"depends_on": [
"G2-02",
"G2-03"
],
"priority": 40,
"lane": "core",
"kind": "build",
"passes": true,
"verification": {
"verdict": "PASS",
"by": "gov-verifier",
"at": "2026-07-16T23:29:21-0300",
"commit": "self"
}
},
{
"id": "G3-01",
"phase": "G3",
"title": "Eventos de atribuição auditáveis (claim/transfer/release)",
"description": "Toda mudança de dono de conversa vira linha em conversation_assignment_events (from, to, by, reason, at) — spec 13 §3. Integra com o claim atômico da spec 04 §9 e habilita o ReassignDialog de verdade.",
"acceptance": [
"Migration (tripla completa) cria conversation_assignment_events (org_id, conversation_id, from_user_id null, to_user_id null, changed_by, reason enum claim|transfer|release|routing|handoff, created_at) com RLS org + índice por conversation.",
"APIs de claim (UPDATE condicional, 409 se já assumida — spec 04 §9.2-9.3) e transfer (reatribuir com motivo) gravam o evento na MESMA transação da mudança de assigned_to_user_id.",
"Transferência aplica a decisão do dono (G1-06d: aceite ou imediata); UI ReassignDialog funcional no inbox.",
"Invariante do eixo 3 (test.fails 'transfer não gera evento') flipado e verde; teste de idempotência: claim duplicado → 409 + zero evento duplicado.",
"unread_count_for_assignee re-zerado na transferência (coluna já existe no baseline)."
],
"depends_on": [
"G1-06"
],
"priority": 10,
"lane": "core",
"kind": "build",
"passes": true,
"verification": {
"verdict": "PASS",
"by": "gov-verifier",
"at": "2026-07-17T07:48:02-0300",
"commit": "self"
}
},
{
"id": "G3-02",
"phase": "G3",
"title": "IA como assignee de 1ª classe (assignee_kind)",
"description": "Unificar ai_handling e assignment: quem atende é 'user' ou 'ai' (spec 13 §3). Handoff IA→humano vira reassignment auditado (reason=handoff), e conversa atribuída a humano silencia o bot — a semântica que o Vendaval consome na fase FG.",
"acceptance": [
"Migration (tripla completa): conversations ganha assignee_kind ('user'|'ai') coerente com assigned_to_user_id (CHECK: kind='user' ⇒ user_id not null; kind='ai' ⇒ user_id null); backfill dos status existentes (ai_handling → kind='ai').",
"crm_request_human_handoff (lib/mcp/tools/handoff.ts) passa a gravar o assignment event (reason=handoff) e mover kind ai→user/fila conforme roteamento vigente.",
"Regra determinística testada: conversa kind='user' (humano atendendo) ⇒ pipeline de resposta do bot não dispara (mesma família de guard de force_human/bot_silenced_until — teste unit prova o veto).",
"Invariante do eixo 6 flipado e verde; database.types.ts regenerado."
],
"depends_on": [
"G3-01"
],
"priority": 20,
"lane": "core",
"kind": "build",
"passes": true,
"verification": {
"verdict": "PASS",
"by": "gov-verifier",
"at": "2026-07-17T08:38:44-0300",
"commit": "self",
"note": "repaired-1-round: anon leak on fn_member_role_in_org closed, re-verified fresh"
}
},
{
"id": "G3-03",
"phase": "G3",
"title": "Dono do lead visível e filtrável (kanban + lista)",
"description": "crm_leads.owner_user_id já existe; falta a superfície: card do kanban mostra o responsável (badge 'sem responsável' quando null), filtro por dono, coluna na lista. Feedback literal do sistema-modelo: 'não aparece quem está atendendo o lead no card'.",
"acceptance": [
"KanbanCard exibe avatar/inicial + nome do owner; badge visualmente distinta para owner null ('Sem responsável') — seguindo o design system do repo (app/design), sem cor hardcoded fora de tokens.",
"Filtro por owner no board (spec 04 §6.4) e na lista de leads; estado de filtro na URL (query param).",
"Atribuir/reatribuir owner pelo card ou painel do lead (mutação existente crm_update_lead/API v1), refletindo em tempo real (padrão de realtime já usado no board).",
"Teste de componente cobre owner presente/ausente; e2e smoke do filtro."
],
"depends_on": [
"G1-06"
],
"priority": 30,
"lane": "core",
"kind": "build",
"passes": true,
"verification": {
"verdict": "PASS",
"by": "gov-verifier",
"at": "2026-07-17T09:02:12-0300",
"commit": "self"
}
},
{
"id": "G3-04",
"phase": "G3",
"title": "Atribuição em massa de leads",
"description": "Bulk 'Atribuir' da spec 04 §6.5 de ponta a ponta — o feedback de operação 1:N do sistema-modelo (subida em lote precisava direcionar para atendente).",
"acceptance": [
"Seleção múltipla no kanban/lista com ação 'Atribuir a…' (assign_owner da spec 04 §6.5) chamando endpoint batch em /api/v1/leads (payload Zod: lead_ids[] + owner_user_id).",
"Endpoint exige ≥manager (matriz), aplica em transação única, audita 1 entrada agregada (action='leads.bulk_assigned', count) e retorna contagem afetada.",
"Owner inválido (fora da org) → fail() 422 com código estável; teste unit cobre sucesso, 422 e limite (ex.: máx 200 por chamada).",
"UI mostra progresso/resultado (toast com contagem)."
],
"depends_on": [
"G3-03"
],
"priority": 40,
"lane": "core",
"kind": "build",
"passes": true,
"verification": {
"verdict": "PASS",
"by": "gov-verifier",
"at": "2026-07-17T09:22:40-0300",
"commit": "self"
}
},
{
"id": "G3-05",
"phase": "G3",
"title": "Tags de conversa + filtros",
"description": "Eixo 7. contacts e crm_leads já têm tags text[]; conversas não. Adicionar tags de conversa (mesmo padrão text[] + canonical_tags da org — DIRC: Reuse do padrão) com UI de aplicar/remover e filtro no inbox.",
"acceptance": [
"Migration (tripla completa): conversations.tags text[] not null default '{}' + índice GIN; canonical_tags reusa o vocabulário em crm_pipelines.settings ou organizations.settings (decisão registrada na spec 13 com DIRC).",
"UI: aplicar/remover tag na conversa (ChatThread/side panel) com sugestão das canônicas; filtro por tag na ConversationList.",
"API v1 de conversas aceita tags no PATCH com Zod (max 20 tags, cada ≤40 chars, normalização lowercase/trim) e audita mudança.",
"Invariante do eixo 7 flipado e verde; teste de filtro (query por tag retorna só as marcadas, org-scoped)."
],
"depends_on": [
"G1-06"
],
"priority": 50,
"lane": "core",
"kind": "build",
"passes": true,
"verification": {
"verdict": "PASS",
"by": "gov-verifier",
"at": "2026-07-17T10:10:21-0300",
"commit": "self"
}
},
{
"id": "G4-00",
"phase": "G4",
"title": "Hardening: SECURITY DEFINER de escrita sem EXECUTE de anon (INB-07/INB-09)",
"description": "Aprovada pelo dono em 2026-07-17 (INB-07). Varredura da G3-02 achou 6 funções SECURITY DEFINER de ESCRITA executáveis por anon (herança de ALTER DEFAULT PRIVILEGES do baseline): fn_upsert_wa_contact, fn_upsert_wa_conversation, fn_mark_conversation_message, emit_event, fn_log_event, fn_audit_log_row. Defesa em profundidade: revogar anon. Inclui as 2 notas do INB-09 (bulk assign).",
"acceptance": [
"Auditoria prévia registrada (no PR/verification): nenhuma das 6 funções é invocada por fluxo anônimo legítimo (webhooks/cron usam service role) — evidência arquivo:linha dos call sites.",
"Migration em tripla completa (migrations/ + apêndice idempotente em baseline.sql + MANIFEST.md) com REVOKE EXECUTE FROM anon nas 6 funções; database.types.ts intocado (sem mudança de contrato) ou regenerado se mudar.",
"Invariante novo em tests/invariants/ provando SOB role anon real no container (set role anon) que cada uma das 6 → permission denied; caminho service_role continua funcional (probe positivo).",
"Notas do INB-09 avaliadas e fechadas: validação de owner do bulk assign não depende de service role presente (guard explícito ou evidência de que o caminho é impossível), e resolução de org do bulk usa a org do RECURSO consistentemente em cenário multi-org (teste ou evidência).",
"pnpm test:db verde nos modos install+update; pnpm test:invariants e test:unit verdes."
],
"depends_on": [],
"priority": 5,
"lane": "core",
"kind": "build",
"passes": true,
"verification": {
"verdict": "PASS",
"by": "gov-verifier",
"at": "2026-07-17T10:56:30-0300",
"commit": "self",
"note": "verifier re-spawned after API timeout; fresh verdict, hash unchanged"
}
},
{
"id": "G4-01",
"phase": "G4",
"title": "visibility_mode: RLS de conversas/mensagens por atendente",
"description": "O coração do eixo 5. organizations.settings.visibility_mode ('all'|'own_and_unassigned'|'own', default decidido em G1-06a) aplicado nas policies de conversations/messages para o role agent; manager+ segue org-wide. Hoje as policies são tenant_isolation_all flat.",
"acceptance": [
"Migration (tripla completa): policies de SELECT de conversations e messages passam a considerar role + visibility_mode + assigned_to_user_id (função SQL fn_can_view_conversation(org, conv) estável e testável); default do knob = decisão G1-06a.",
"Teste no Postgres descartável com 1 org, 2 agents e 1 manager: agent A não lê conversa atribuída ao agent B (modo own*), lê as próprias e (se own_and_unassigned) as sem dono; manager lê todas.",
"Invariante do eixo 5 flipado e verde; pnpm test:db verde nos modos install+update.",
"Realtime (postgres_changes) respeita as mesmas policies — teste ou evidência documentada de que a subscription do inbox não vaza conversa fora do escopo."
],
"depends_on": [
"G2-03",
"G3-01"
],
"priority": 10,
"lane": "core",
"kind": "build",
"passes": true,
"verification": {
"verdict": "PASS",
"by": "gov-verifier",
"at": "2026-07-17T11:31:28-0300",
"commit": "self"
}
},
{
"id": "G4-02",
"phase": "G4",
"title": "Inbox com escopo: minhas / fila / todas",
"description": "Superfície do escopo: filtros de visão no inbox coerentes com visibility_mode e papel — 'Minhas', 'Fila (sem dono)', 'Todas' (só manager+).",
"acceptance": [
"ConversationList ganha seletor de visão (Minhas | Fila | Todas), com 'Todas' oculto para agent quando visibility_mode ≠ all; contagens por visão corretas.",
"Deep-link ?filter= preservado (compatível com UnassignedQueueAlert da spec 04 §8.3).",
"Agent em modo own* não consegue abrir conversa fora do escopo nem por URL direta (a RLS G4-01 garante; a UI trata o vazio com estado claro, sem stack trace).",
"Teste de componente para as 3 visões + e2e smoke com 2 papéis."
],
"depends_on": [
"G4-01"
],
"priority": 20,
"lane": "core",
"kind": "build",
"passes": true,
"verification": {
"verdict": "PASS",
"by": "gov-verifier",
"at": "2026-07-17T13:09:02-0300",
"commit": "self"
}
},
{
"id": "G4-03",
"phase": "G4",
"title": "Escopo no kanban/leads para agent",
"description": "Mesmo princípio nos leads: agent vê os próprios (owner) conforme matriz decidida em G1-06; manager+ vê o board inteiro.",
"acceptance": [
"Policies/queries de crm_leads respeitam a matriz da spec 13 §4 para agent (own) — via RLS (migration tripla) ou filtro server-side documentado na spec (decisão justificada DIRC).",
"Board do agent renderiza só o escopo; manager vê tudo; teste 2-agents no Postgres descartável.",
"Filtros e contadores do board coerentes com o escopo (sem contar o que não vê).",
"Invariante correspondente do eixo 5 (leads) verde."
],
"depends_on": [
"G4-01"
],
"priority": 30,
"lane": "core",
"kind": "build",
"passes": true,
"verification": {
"verdict": "PASS",
"by": "gov-verifier",
"at": "2026-07-17T13:37:13-0300",
"commit": "self"
}
},
{
"id": "G4-04",
"phase": "G4",
"title": "Métricas por responsável (funil + performance individual)",
"description": "Feedback direto do sistema-modelo: 'não é possível filtrar por atendente nas métricas; impossibilita visualizar performance individual'. Filtro por responsável nas métricas de funil + visão de performance por atendente (manager+).",
"acceptance": [
"Métricas de funil/dashboard aceitam filtro por owner/assignee (query param + UI), org-scoped, respeitando matriz (agent só vê as próprias se G1-06e decidir assim).",
"Visão 'por atendente' (manager+): leads ganhos/perdidos, conversas atendidas, tempo até 1ª resposta — cada métrica com definição escrita na spec 13 §6 antes do código.",
"Queries agregadas com índice adequado (EXPLAIN sem seq scan em tabela grande documentado no PR/verification).",
"Teste unit das agregações com dataset seed conhecido (números exatos assertados)."
],
"depends_on": [
"G4-02",
"G4-03"
],
"priority": 40,
"lane": "core",
"kind": "build",
"passes": true,
"verification": {
"verdict": "PASS",
"by": "gov-verifier",
"at": "2026-07-17T15:00:26-0300",
"commit": "self"
}
},
{
"id": "G5-01",
"phase": "G5",
"title": "Config de roteamento + disponibilidade/horário por atendente",
"description": "Schema e API do roteador (spec 13 §5): modo por org (manual|round_robin|load), capacidade por atendente, disponibilidade (online/offline — persiste o AttendantStatusToggle da spec 04 §8) e janela de horário por atendente.",
"acceptance": [
"Migration (tripla completa): attendant_availability (org_id, user_id, is_available, capacity int, schedule jsonb tz-aware) + organizations.settings.routing (mode, knobs) — knobs nunca constantes hardcoded (doutrina do repo).",
"API v1: GET/PATCH de disponibilidade (o próprio atendente muda o seu; manager muda de todos) e PATCH de routing config (≥manager) — Zod + audit.",
"AttendantStatusToggle persiste no schema novo (heartbeat AT-08: auto-offline 15min via job/worker — comportamento testado com clock mockado).",
"Teste unit: horário fora da janela ⇒ atendente não elegível; capacidade cheia ⇒ não elegível."
],
"depends_on": [
"G3-01",
"G1-06"
],
"priority": 10,
"lane": "core",
"kind": "build",
"passes": true,
"verification": {
"verdict": "PASS",
"by": "gov-verifier",
"at": "2026-07-18T10:30:26-0300",
"commit": "self"
}
},
{
"id": "G5-02",
"phase": "G5",
"title": "Worker de roteamento via event_log",
"description": "AT-03 de verdade: distribuição automática de conversas sem dono segundo o modo configurado. Segue a doutrina do repo: trigger NUNCA faz HTTP — evento em event_log, worker consome com claim transacional e dedup.",
"acceptance": [
"Nova conversa (ou handoff para fila) emite evento (ex.: conversation.routing_requested) no event_log; worker consome com claim (consumed_by/attempts/next_attempt_at — padrão existente do repo) e atribui segundo o modo: round_robin (rodízio entre elegíveis) ou load (menor carga atual), respeitando disponibilidade+horário+capacidade da G5-01.",
"Atribuição do worker grava conversation_assignment_events (reason=routing) na mesma transação.",
"At-least-once seguro: reprocessar o mesmo evento não reatribui conversa que já ganhou dono (guard + teste de idempotência).",
"Sem elegível: conversa permanece na fila e evento re-agenda com backoff; teste cobre o caso.",
"Modo manual: worker não atribui nada (teste)."
],
"depends_on": [
"G5-01"
],
"priority": 20,
"lane": "core",
"kind": "build",
"passes": true,
"verification": {
"verdict": "PASS",
"by": "gov-verifier",
"at": "2026-07-18T10:58:19-0300",
"commit": "self"
}
},
{
"id": "G5-03",
"phase": "G5",
"title": "Fila visível com posição + notificação de atribuição",
"description": "O lead na fila e o atendente sabem onde estão: visão da fila (sem dono, ordenada por espera) com posição, e notificação in-app quando uma conversa é atribuída a você.",
"acceptance": [
"Visão de fila no inbox (extensão do filtro 'Fila' da G4-02): ordenada por tempo de espera, mostra posição e tempo aguardando; contagem bate com unassignedCount.",
"Atribuição (manual, transfer ou worker) dispara notificação in-app ao novo dono (padrão de notificações existente do repo; se não houver, badge + realtime no inbox conta como notificação — decisão registrada).",
"unread_count_for_assignee atualizado corretamente na atribuição via worker (teste).",
"e2e smoke: conversa entra → aparece na fila → worker/claim atribui → some da fila e aparece em 'Minhas' do dono."
],
"depends_on": [
"G5-02",
"G4-02"
],
"priority": 30,
"lane": "core",
"kind": "build",
"passes": true,
"verification": {
"verdict": "PASS",
"by": "gov-verifier",
"at": "2026-07-18T11:40:45-0300",
"commit": "self",
"note": "re-verified after hash false-positive (concurrent app/page.tsx edit by another terminal)"
}
},
{
"id": "G5-04",
"phase": "G5",
"title": "Painel admin de gestão de atendentes",
"description": "A tela que o sistema-modelo nunca teve: gestão central (manager+) de atendentes — status, carga atual, horário, capacidade — e do modo/cadência de roteamento da org.",
"acceptance": [
"Nova área em app/app/team (ou settings) 'Atendimento': lista de atendentes com status (online/offline), carga atual (conversas abertas atribuídas), capacidade, janela de horário; edição inline por manager+.",
"Config do modo de roteamento (manual|round_robin|load) e knobs editáveis na mesma tela, persistindo via API da G5-01.",
"Tudo dentro do design system (tokens; zero hardcode de cor/fonte); estados vazio/carregando/erro tratados.",
"e2e: manager altera modo e capacidade e as mudanças refletem no comportamento do worker (teste de integração ou unit no worker lendo config nova).",
"Screenshot da tela salvo em loop/checkpoints/evidence/G5/."
],
"depends_on": [
"G5-03"
],
"priority": 40,
"lane": "core",
"kind": "build",
"passes": true,
"verification": {
"verdict": "PASS",
"by": "gov-verifier",
"at": "2026-07-18T12:37:35-0300",
"commit": "self",
"note": "resumed after auth-error mid-screenshot; supabase local torn down + volumes pruned"
}
},
{
"id": "G6-00",
"phase": "G6",
"title": "Pré-G6: RLS por role em crm_lead_activities/crm_lead_links (INB-10)",
"description": "Aprovada pelo dono 2026-07-18 (INB-10). Timeline/vínculos de lead seguem org-flat: agent em escopo own não vê o lead mas lê activities/links por query direta. Pré-condição da exposição MCP (G6-03).",
"acceptance": [
"Migration em tripla completa: SELECT de crm_lead_activities e crm_lead_links passa a exigir visibilidade do lead pai (mesma fn_can_view_lead da G4-03); write segue a matriz; policies por-comando (sem FOR ALL).",
"Teste no Postgres descartável: agent em modo own não lê activities/links de lead de outro agent (0 rows); controle positivo do próprio; manager org-wide.",
"Invariante novo em tests/invariants/ cobrindo o fechamento; pnpm test:db install+update verdes.",
"Nenhuma regressão: timeline no painel do lead continua funcionando para quem vê o lead (teste ou evidência)."
],
"depends_on": [],
"priority": 2,
"lane": "core",
"kind": "build",
"passes": true,
"verification": {
"verdict": "PASS",
"by": "gov-verifier",
"at": "2026-07-18T19:07:30-0300",
"commit": "self"
}
},
{
"id": "G6-05",
"phase": "G6",
"title": "Forward-fix: agent-dispatcher grava status inválido no event_log (INB-13)",
"description": "Aprovada pelo dono 2026-07-18 (INB-13). Bug REAL de runtime: dispatcher usa status processed/failed que violam event_log_status_check — UPDATE falha em runtime.",
"acceptance": [
"Auditoria: reproduzir/provar o caminho do bug (teste que demonstra o UPDATE violando a constraint com os valores atuais).",
"Fix TS: dispatcher passa a usar somente os status válidos do event_log_status_check (padrão do routing-worker da G5-02); sem migration (a constraint está certa; o código estava errado).",
"Teste unit prova o ciclo completo pending→processing→done/dead sem violação; regressão do dispatcher verde."
],
"depends_on": [],
"priority": 4,
"lane": "core",
"kind": "build",
"passes": true,
"verification": {
"verdict": "PASS",
"by": "gov-verifier",
"at": "2026-07-18T19:30:19-0300",
"commit": "self"
}
},
{
"id": "G6-06",
"phase": "G6",
"title": "RLS de user_organizations: SELECT org-wide para manager+ (INB-14)",
"description": "Aprovada pelo dono 2026-07-18 (INB-14). RLS atual limita manager à própria linha — GET /team devolve 1 membro pro manager (aba Membros quebrada). Matriz §4: team=org:read a manager.",
"acceptance": [
"Migration em tripla completa: SELECT de user_organizations org-wide para manager+ via fn_role_at_least (padrão do repo), mantendo self-read para todos; write inalterado.",
"Teste no Postgres descartável: manager lê todas as linhas da org; agent segue lendo só a própria; cross-org 0 rows.",
"GET /api/v1/team volta a listar o time completo para manager (teste de rota); workaround do /attendants/availability da G5-04 permanece funcional.",
"Invariante coberto; pnpm test:db install+update verdes."
],
"depends_on": [],
"priority": 6,
"lane": "core",
"kind": "build",
"passes": true,
"verification": {
"verdict": "PASS",
"by": "gov-verifier",
"at": "2026-07-18T19:54:05-0300",
"commit": "self"
}
},
{
"id": "G6-01",
"phase": "G6",
"title": "MCP tools de governança (assign/tags/queue + handoff v2)",
"description": "A superfície para agentes externos (Vendaval): crm_assign_conversation, crm_manage_tags, crm_get_queue_status, e upgrade do crm_request_human_handoff para ser ciente de fila/horário (aceita atendente alvo opcional; sem elegível → fila com posição).",
"acceptance": [
"3 tools novas registradas em lib/mcp/tools/ + catalog.ts (sanity-check handler↔catalog passa), org-scoped, input Zod, audit em toda escrita.",
"crm_assign_conversation: atribui/transfere com reason (grava assignment event); recusa cross-org; idempotente por (conversation_id, to, reason) na janela curta.",
"crm_manage_tags: add/remove em conversation|contact|lead com as mesmas validações da G3-05.",
"crm_get_queue_status: fila da org (tamanho, tempo médio de espera, elegíveis online) — payload documentado.",
"crm_request_human_handoff v2: usa roteamento G5 (atendente alvo opcional; elegibilidade por disponibilidade/horário; fallback fila) e retorna o resultado estruturado (assigned_to|queued+position).",
"Testes unit por tool (sucesso, cross-org negado, input inválido)."
],
"depends_on": [
"G3-05",
"G5-02"
],
"priority": 10,
"lane": "core",
"kind": "build",
"passes": true,
"verification": {
"verdict": "PASS",
"by": "gov-verifier",
"at": "2026-07-18T20:33:57-0300",
"commit": "self",
"note": "re-verified after hash false-positive (external AGENTS/GEMINI regen); Maestro added .git/info/exclude"
}
},
{
"id": "G6-02",
"phase": "G6",
"title": "ai_dispatch_mode respeitado pelo dispatcher nativo",
"description": "A flag organizations.settings.ai_dispatch_mode ('native'|'external') do edge-contract do Vendaval: org 'external' é PULADA pelo dispatcher nativo sem consumir o evento (nem claim, nem no_match). Verificar se a cadeia vendaval/F2-* já implementou; implementar/completar se não.",
"acceptance": [
"Auditoria registrada: a flag existe? o dispatcher nativo (caminho do runtime de IA deste repo) a respeita? — evidência arquivo:linha, incluindo checagem da cadeia vendaval/F2-* local.",
"Se ausente/parcial: implementação com migration (tripla) se necessário + dispatcher pulando org external SEM tocar o evento (status permanece pending, consumed_by intacto) — teste unit prova.",
"Org 'native' (default) segue comportamento atual inalterado (teste de regressão).",
"Se já implementado pela cadeia vendaval: registrar evidência e marcar como no-op justificado na verification."
],
"depends_on": [],
"priority": 20,
"lane": "core",
"kind": "build",
"passes": true,
"verification": {
"verdict": "PASS",
"by": "gov-verifier",
"at": "2026-07-18T21:19:20-0300",
"commit": "self"
}
},
{
"id": "G6-03",
"phase": "G6",
"title": "Tools de leitura expõem governança",
"description": "crm_get_conversation/crm_list_conversations incluem assignee (kind+user), tags e estado de fila; crm_get_lead/crm_list_leads incluem owner, stage e tags — o payload que o get_lead_context do Vendaval compõe.",
"acceptance": [
"Payloads das 4 tools de leitura ganham os campos (assignee_kind, assigned_to_user_id+nome, tags, queue_position quando na fila; owner_user_id+nome, stage, tags) — sem quebrar consumidores atuais (campos aditivos; teste de shape).",
"Campos documentados no schema Zod/descrição de cada tool (catalog.ts).",
"Teste unit por tool com fixture cobrindo atribuída/na fila/IA atendendo.",
"Zero credencial/PII extra vazada (payload revisado contra doutrina LGPD do repo)."
],
"depends_on": [
"G6-01",
"G6-00"
],
"priority": 30,
"lane": "core",
"kind": "build",
"passes": true,
"verification": {
"verdict": "PASS",
"by": "gov-verifier",
"at": "2026-07-18T21:40:09-0300",
"commit": "self"
}
},
{
"id": "G6-04",
"phase": "G6",
"title": "Contrato de governança para agentes externos (spec 14)",
"description": "O artefato que a fase FG do Vendaval consome: docs/specs/14-contrato-governanca-agentes-externos.md no estilo do edge-contract (autocontido, refs arquivo:linha verificadas, proibições explícitas).",
"acceptance": [
"Spec 14 documenta: as tools de governança (input/output exatos), semântica de assignee_kind e handoff, visibility_mode e o que um agente externo PODE ler, ai_dispatch_mode, e as proibições (ex.: nunca atribuir cross-org, nunca furar force_human/is_blocked).",
"Toda referência de código com arquivo:linha verificada na data (cabeçalho com data de verificação, como o edge-contract do Vendaval).",
"Seção 'mudanças requeridas em consumidores' listando o que o Vendaval precisa fazer do lado dele (vira insumo do FG-01).",
"Revisão mecânica: cada tool citada existe em lib/mcp/tools/ e catalog.ts (verifier confere 1 a 1)."
],
"depends_on": [
"G6-01",
"G6-02",
"G6-03"
],
"priority": 40,
"lane": "core",
"kind": "build",
"passes": true,
"verification": {
"verdict": "PASS",
"by": "gov-verifier",
"at": "2026-07-18T21:53:23-0300",
"commit": "self"
}
}
]
}