LEIA no início de toda sessão. ALIMENTE ao fim de cada task: progresso, testes rodados, bugs achados/corrigidos, o que ficou pra trás, estado atual.
Spec: docs/superpowers/specs/2026-07-23-harness-evolution-design.md Plano da fase atual: docs/superpowers/plans/2026-07-23-harness-fase0-convergencia.md
- ÉPICO COMPLETO. A spec tem cinco fases, numeradas 0 a 4, e todas fecharam — a última (Painel de Evolução) em 2026-07-27, na branch
feat/operacao-visivel. (Correção: uma versão anterior desta linha dizia "falta a Fase 5". Não existe Fase 5 — a contagem "5 fases" virou um número de fase que a spec nunca teve. Confira emdocs/superpowers/specs/2026-07-23-harness-evolution-design.md: os cabeçalhos vão de "Fase 0" a "Fase 4".) - Continuação do épico: ENTREGUE em 2026-07-27 (
docs/superpowers/plans/2026-07-27-mapeamento-funil-agente.md) — a tela onde o tenant diz em qual etapa do funil o agente coloca o cliente. Era a única lacuna que o Painel de Evolução relatava sem oferecer conserto;crm_stages.agent_stage_hintdeixou de ser coluna que só sonda de teste escreve. Detalhe no fim deste arquivo. - Continuação da continuação: ENTREGUE em 2026-07-28 (
docs/superpowers/plans/2026-07-27-gerenciar-etapas-do-funil.md) — a seção onde o tenant renomeia, cria, reordena, marca ganho/perda e arquiva as ETAPAS do próprio funil. Fecha o achado B da feature anterior (is_won/is_lostsem caminho de produto) e provou, numa organização recém-criada, o funil de e-commerce de fábrica virando o de uma clínica e o agente movendo o card para a etapa que o dono nomeou. Detalhe no fim deste arquivo — inclusive dois achados de produto que não são dela (transbordo a 390px em qualquer tela; não existe criação de funil). - Uma prova continua em aberto, e é do Rafael: ninguém mandou uma mensagem real de WhatsApp fechando o ciclo completo. A receita de 1 minuto está no fim deste arquivo.
- 2026-07-23 — Task 1 concluída: PublishedAgentConfig expõe KB ativa e knobs de RAG (activeKbVersionId, ragTopK, ragSimilarityThreshold). Testes: 3/3 verde (campos RAG, defaults, null). Typecheck zerado. Pronta pra Task 2 (retrieval setup).
- 2026-07-23 — Task 2 concluída: lib/agent-engine/agent/search-knowledge.ts (searchKnowledge + citationsFromHits), 3/3 testes verdes; commit extra necessário no vitest.setup (loader de .env p/ import-time env validation).
- 2026-07-23 — Task 3 concluída: tool
search_knowledgecabeada no turno (inbound-turn.ts) — def estática em AGENT_TOOL_DEFS, execute com searchKnowledge/citationsFromHits, gate pós-montagem (só entra com activeKbVersionId), citações anexadas em messages.metadata na outbound 'sent'. Achado: READ_ONLY_TOOLS morava em inbound-turn.ts, não em tool-breaker.ts como o brief assumia — movido pra tool-breaker.ts (config do breaker, mais coeso) por decisão do coordenador. Testes: 11/11 verdes (lib/agent-engine), typecheck e lint zerados. - 2026-07-23 — Task 4 concluída: drain pula orgs em ai_dispatch_mode=external (spec 14). Implementação: query settings->>'ai_dispatch_mode' em processEvent após parse, early return se 'external', evento marcado done sem enfileirar job. Testes: 1/1 verde (mock WAHA pool, verifica guard consulta modo e evento vira done sem job). Typecheck zerado. Commit: 028ac19.
- 2026-07-23 — Task 5 concluída: dispatch nativo aposentado — cron
/api/v1/cron/agent-dispatchervirou no-op permanente em QUALQUER valor de AGENT_DISPATCH_CONSUMER (auth preservada), worker liga o drain incondicionalmente (warn senative), headers@deprecatedem lib/ai/dispatcher/index.ts e lib/ai/runtime/agent.ts (módulos mantidos, fora do caminho quente). TDD: teste do caso 'native' ajustado primeiro (FAIL confirmado com a rota ainda despachando), depois PASS após a mudança. Testes: 3/3 verdes na rota; typecheck e lint zerados (0 erros). Nenhum outro teste tocava o branch do worker (sem *.test.ts em workers/). - 2026-07-23 — Task 6 em curso: suite completa verde (343/343, typecheck/lint 0), worker NOVO no ar (healthz ok, 8787; worker antigo do worktree multimodal foi parado p/ evitar disputa de fila). BLOQUEIO: sem OPENAI_API_KEY/AI_GATEWAY_API_KEY no .env.local — embeddings indisponíveis, KB não pode ser indexada (ai_chunks=0). Aguardando chave + mensagem real de WhatsApp do Rafael.
- 2026-07-23 — ACHADO de calibração: threshold default 0.72 filtra hits legítimos do text-embedding-3-small (pergunta de frete deu 0.703). Ajustado knob do agente de prova p/ 0.5; recalibrar default em fase futura.
- 2026-07-23 ~13:50 — PROVA PAUSADA a pedido do Rafael (teste dele p/ vídeo usa o mesmo agente/sessão). Restaurei a v24 AgendaPlus dele (published_version_id=c54dc195, active_kb_version_id=null). Para RETOMAR a prova quando ele liberar: (1) ele fecha o worker dele; (2) publicar v25-like: published_version_id=nova cópia da v23/3003ce45 + active_kb_version_id=df9810c7; (3) subir
npm run workerda branch; (4) Rafael manda pergunta do frete; (5) validar sent+ack+citations e screenshot do inbox; (6) restaurar v24 dele de novo. Já provado nesta sessão: search_knowledge devolveu o fato da KB no turno 12:44 (resposta com R$ 199), envio realsent+external_id funcionou às 13:39 pós-fix da WAHA key. Dev server da branch na 3000 FICA no ar (o teste dele depende dele — envios passam por lá). - 2026-07-23 ~14:2x — TASK 6 CONCLUÍDA (prova real ponta-a-ponta): pergunta real de WhatsApp → turno v27 → search_knowledge → resposta com o fato da KB (R$ 199) ENVIADA de verdade (sent + external_id 3EB0640A213518922775DA) → citations em messages.metadata → painel "Citações da resposta IA" renderizado no inbox (screenshot em .superpowers/sdd/evidence/prova-fase0-citacoes.png). Contra-prova do guard external ao vivo: evento done, 0 jobs, log "org em modo external (spec 14) — evento pulado". Achados de UX p/ fase futura: painel de citações mostra "65%" sem rótulo e "message_id" cru — jargão p/ atendente leigo (UI herdada do EPIC-13, fora do escopo da Fase 0). Ambiente: v24 AgendaPlus do Rafael RESTAURADA como publicada (vídeo dele); KB df9810c7 existe e pode ser reativada a qualquer momento; OPENAI_API_KEY agora no .env.local; WAHA_API_KEY alinhada ao container.
- 2026-07-23 — FASE 0 FECHADA: review final da branch READY TO MERGE (1 fix aplicado: ee1873d, try/catch da RPC no searchKnowledge). Suite completa + typecheck verdes pós-fix. Minors deferidos documentados no ledger .superpowers/sdd/progress.md. Próxima fase do épico: Fase 1 — Memória Geral da Org (spec seção "Fase 1").
- 2026-07-23 — FASE 1 Task 1 concluída: migration 0067 (
org_memory_versions/org_memory_pointers/org_memory_entries+ novo valororg_memory_entryno check deflywheel_distiller_proposals.type+ RLStenant_isolation_*_all). NNNN renumerado de 0054→0067 no meio da task: o pre-commit hook bloqueou por colisão com0054_followup_flowsemfeat/followup-flows(outra branch local); 0066 era o maior NNNN entre TODAS as branches locais. Aplicada no dev viasupabase db query --linked(role postgres —SUPABASE_DB_URLdo.env.localé o roleagent_worker, sem CREATE/owner; achado registrado). Idempotência provada (2ª aplicação sem erro).database.types.tsregenerado viasupabase gen types typescript --linked(contém as 3 tabelas). RLS provada manualmente contra dados reais do dev (docker indisponível p/test:db): 0 rows cross-tenant, ≥1 na própria org. Seed dorls-isolation.test.tsestendido (org_memory_versions/entries) para quandotest:dbpuder rodar. - 2026-07-23 — FASE 1 Task 2 concluída: lib/agent-engine/agent/org-memory.ts (loader trio versão/ponteiro + render + compose). TDD: 5/5 testes verdes (loadOrgMemory—doc pelo ponteiro + entries em ordem estável, render—bloco determinístico, composeSystemPrompt—ordem playbook→memória→skills, zero separadores órfãos). Typecheck zerado. Padrão mirrored do playbook.ts (sem cache intencional, resolução em início de cada run, determinístico byte-a-byte para mesma versão+entries). Commit: bfc45d5.
- 2026-07-23 — FASE 1 Task 3 concluída: memória da org cabeada no turno (inbound-turn.ts) —
loadOrgMemorychamado logo apósskillIndex,systemremontado viacomposeSystemPrompt(playbook, orgMemoryBlock, skillIndex). Suite lib/agent-engine: 18/18 verde (composeSystemPrompt da Task 2 cobre a byte-identidade playbook-only/playbook+skills — não existe llm-cache.test.ts separado no repo). Typecheck zerado. Prova funcional no banco dev: inserida versão+ponteiro de teste p/ org 6e567068-fd1c-4f94-ae1f-40e0334be190, script tsx descartável confirmou o bloco "TESTE F1: prova de composição — v2." renderizado (junto com entry pré-existente da org); linhas de teste deletadas ao final (contagem 0/0 confirmada). - 2026-07-23 — FASE 1 Task 4 concluída: API
/api/v1/ai/memory(GET estado completo + POST publica versão),/entries(POST cria manual),/entries/[id](PATCH archive/active),/versions/[id](GET conteúdo p/ Dialog de histórico). 3 novas ações de audit (ai.org_memory_published,ai.org_memory_entry_created,ai.org_memory_entry_updated) adicionadas alib/audit/actions.ts(não estavam na union). TDD: 4/4 testes verdes em route.test.ts (401 sem auth, GET vazio document null, POST publica+upsert pointer+audit, 422 content vazio). Typecheck e lint zerados. Entries POST/PATCH e versions/[id] GET seguem o mesmo padrão mas sem teste dedicado (brief só pediu route.test.ts). - 2026-07-23 — FASE 1 Task 6 concluída: tela
app/app/ai/memory(hooks/ai/useOrgMemory.ts + page.tsx + _client.tsx) — doc-mãe versionado (textarea, contagem de caracteres, "Publicar versão" só habilita com mudança real, badge da versão ativa, histórico clicável abrindo Dialog somente-leitura com "Restaurar como nova versão"), timeline de aprendizados (badge manual/flywheel, arquivar/reativar, form colapsável). Item "Memória da IA" na Sidebar + permissõesai.memory.view(manager)/ai.memory.publish(admin) em AuthProvider. Typecheck e eslint zerados. Prova real clicando: manager (e2e-manager) criou aprendizado manual → apareceu na timeline → arquivou → reapareceu em "ver arquivados" → reativou; botão Publicar corretamente desabilitado p/ manager com nota explicativa. Admin (e2e-admin) publicou v1 e v2, testou histórico → Dialog com conteúdo read-only da v1 → "Restaurar" recarregou o textarea → publicou de novo. Achado de ambiente (não é bug do Task 6):.e2e-creds.jsondo repo estava com org_id/user_id STALE de outro seed run — org real usada pelos e2e users é6e567068-fd1c-4f94-ae1f-40e0334be190(confirmado viauser_organizations), não a do arquivo; e2e-admin não tinha MFA enrolado (o factor foi criado do zero durante o teste, secret do.e2e-creds.jsonficou desatualizado — specs que usamloginAdminTotp()/admin_totp.secretpodem falhar até re-seed). Cleanup:org_memory_pointers/versions/entriesdeletados para a org real, contagem 0/0/0 confirmada. - 2026-07-23 — FASE 1 Task 5 concluída: distiller decide escopo (
scope: "agent" | "org"no JSON) e gravatype='org_memory_entry'/target='org'quando é aprendizado da org inteira (live.ts).applyProposalganhou o ramoorg_memory_entry— insere emorg_memory_entries(source=flywheel, status=active, proposal_id) e marcaapplied_at/applied_byna proposta, SEM criar versão de agent.ApplyProposalResultvirou união de 2 sucessos ({versionId,versionNumber}vs{entryId}, distinguidos por presença de campo — shape do caminho playbook_bullet intocado); a rotaapplydistingue via"entryId" in resulte respondeok({entry_id}). UI:TYPE_LABEL/botão/toast cobrem "Memória da organização" sem alegar versão nova. Gate humano preservado: distiller só grava proposta, só o clique de apply grava em org_memory_entries. TDD: teste novo do ramo org_memory_entry FAIL→PASS; suite lib/ai 19/19 verde; typecheck e lint zerados. - 2026-07-23 — FOLLOW-UP registrado (review T5, minor 3): o distillerPrompt ainda é redigido em torno de "bullet de playbook" e higiene de memória do LEAD, mas agora pede escopo org — o caminho org tende a nascer pouco usado. Calibrar o prompt do distiller (ou criar um destilador próprio de memória da org) numa fase futura.
- 2026-07-23 (noite) — AMBIENTE (achados do controller antes da prova da F1): (1) published_version_id do agente 69d04579 estava apontando p/ uma v3 de 20/jul ("You are a helpful assistant") — RESTAURADO p/ v24 AgendaPlus (c54dc195), a persona do Rafael; (2) worker na 8787 (PID 38398, start 19:32) roda do WORKTREE feat-inbox-multimodal, que NÃO tem o código da F1 — a prova exige worker do repo principal (a fila é compartilhada: dois workers = corrida); (3) .e2e-creds.json com org/user obsoletos e admin_totp.secret dessincronizado após enrollment manual na Task 6 — specs que usam loginAdminTotp podem falhar até re-seed.
- 2026-07-24 — FASE 1 código 100% provado; prova de WhatsApp BLOQUEADA por billing externo. (a) Publiquei a regra de memória PELA TELA como admin (login+MFA reais; screenshot f1-memoria-v1-publicada.png: "v1 ativa"). (b) Prova de integração com dados reais do banco (script montando o system do inbound-turn:549-567): persona do agente NÃO contém a assinatura (false); bloco de memória da org contém (true); SYSTEM final que iria ao modelo contém (true) → a memória publicada pela tela entra no prompt real do turno ao lado da persona, sem tocar no prompt do agente. (c) A resposta no WhatsApp falhou SÓ na chamada ao modelo: "credit balance too low" — a conta Anthropic (chave env == BYOK ygAA, mesma chave) está sem saldo; google/openai BYOK existem mas o engine só registra anthropic. RETOMAR a prova de WhatsApp quando houver crédito: 1min — republicar a regra pela tela /app/ai/memory + subir worker do repo principal + mandar mensagem. Ambiente restaurado: memória de teste REMOVIDA da org (não deixar regra de teste no agente real do Rafael); persona v24 AgendaPlus intacta; MFA do e2e-admin re-enrollado no CLOUD (secret no scratchpad admin-mfa-secret.txt; .e2e-creds.json continua apontando p/ ambiente LOCAL desligado — não mexi).
- 2026-07-24 — FASE 1 review final: 2 fixes. (1) removidos
insertOrgMemoryVersion/setOrgMemoryPointerdeorg-memory.ts(órfãos — grep confirmou zero chamadores; a API escreve via Supabase admin client, não pg.Pool). (2)org-memoryentrou no mapa vivo (docs/architecture/agent-turn.workflow.json): nósorg_memory+memoryscreen(tela/APIapp/app/ai/memory) numa lane novaconfig, 2 arestas reais (memoryscreen→orgmemoryescreve,orgmemory→turninjeta no prefixo estável) —archify validate0 erros, HTML re-renderizado. Suite/typecheck/lint verdes. - 2026-07-24 13:03 — FASE 1 PROVA PONTA-A-PONTA COMPLETA (crédito Anthropic reposto pelo Rafael): mensagem real "oi, vocês atendem sábado?" → agente respondeu no WhatsApp real (external_id 3EB03DD39F6BE7F525CA1E, status sent) terminando com a linha exata "— Equipe AgendaPlus 🌱" — a assinatura que existe SÓ na memória da org (publicada pela tela), CONFIRMADO ausente do system_prompt do agente. Prova o contrato da Fase 1: memória da org muda o comportamento de conversa real sem tocar no prompt do agente. Ambiente limpo depois (regra de teste REMOVIDA da org — não deixar assinatura de teste no agente de produção). OBSERVAÇÃO (flake, não-bloqueante): 1ª tentativa do turno falhou com "JSON de checkpoint inválido no fechamento do turno" e a fila re-tentou com sucesso — vale investigar a robustez do parse de checkpoint numa fase futura. FASE 1 COMPLETA E PROVADA.
- 2026-07-24 — FASE 2 Task 1 concluída: migration 0068 (
skill_versionsganhamanifestjsonb +forked_from_version_id; novaskill_activationstelemetria hard/probe; bucketskill-assetsprivado 5MB; policy SELECT de catálogoorganization_id is nullem skill_versions/pointers). Apêndice espelhado no baseline.sql + bloco storage + MANIFEST. Aplicado no banco dev viasupabase db query --linked(roleagent_workersem DDL); re-aplicação sem erro (idempotente); bucket + colunas + policies confirmados por query direta. Types regenerados e reduzidos ao diff de 0068 (banco dev compartilhado trouxe tabelas de outras branches).npm run test:db: 34/34 arquivos, 198/199 testes verdes (1 skip pré-existente) — RLS deskill_activationsincluída no invariante de isolamento. Typecheck zerado. - 2026-07-24 — FASE 2 Task 2 concluída: LoadedSkill expõe versionId (pré-requisito da telemetria). TDD: 1/1 teste verde (mock db, skills[0].versionId='ver-1'). Implementação: interface +versionId:string, SELECT +v.id, Map +versionId:r.id. Typecheck zerado. Nenhum outro consumidor de LoadedSkill necessitou ajuste em mocks. Commit: eb51e2d.
- 2026-07-24 — FASE 2 Task 3 concluída:
lib/ai/skills/package.ts(parser puro.zip→ParsedSkillPackage) viafflate(unzipSync); reusaskillMatcherSchema/validateSkillBodyde skills.ts (já exportados, sem mudança lá). Frontmatter lido à mão (só name/description/matcher.any_keywords/probe_keywords, sem dep de YAML). Guards: ≤64 arquivos, ≤5MB total, ≤1MB por arquivo, path traversal (../absoluto/backslash), whitelist de extensão de asset, paths fora de SKILL.md/references//assets/ recusados — todos checados ANTES de materializar conteúdo além do necessário. TDD: 5/5 testes verdes (brief). Typecheck e eslint zerados. package.json/pnpm-lock.yaml só ganharam a linha dofflate(isolei do diff pré-existente não relacionado no package.json via blob staged manualmente). - 2026-07-24 — FOLLOW-UP (review T3): parser de skill usa fflate unzipSync (síncrono) — o filter/originalSize rejeita bombs honestamente-declarados ANTES de inflar, mas um header que MENTE o originalSize ainda infla o real. Defesa completa = streaming Unzip com corte por bytes acumulados. Revisitar antes de expor o import a upload não-autenticado (hoje é atrás de requireRole manager).
- 2026-07-24 — FASE 2 Task 4 concluída:
lib/ai/skills/install.ts(importSkillPackageinsere a skill_version PRIMEIRO, sobe cada arquivo do pkg em{org}/{skill}/{versionId}/{path}no bucketskill-assets, move o ponteiro só no fim; upload falho remove os já subidos e lança, versão órfã fica no banco inofensiva) +installPlatformSkill(fork-on-install: lê a versão de plataforma pelonameviaskill_pointers→skill_versions, copia body/matcher/manifest pra uma versão da org comforked_from_version_id, move o ponteiro da org).insertSkillVersion(skills.ts) estendido commanifest?/forkedFromVersionId?opcionais (default[]/null) — sem chamador existente quebrado (grep confirmou zero chamadores externos). TDD: 5/5 testes verdes (insert-then-upload-then-pointer, org_id sempre deinput.organizationIdnunca do pkg, cleanup de upload falho, fork com forked_from_version_id, plataforma inexistente). Typecheck e lint zerados; suite lib/ai+lib/agent-engine 52/52 verde. - 2026-07-24 — FASE 2 Task 5 concluída: API de skills (
GET /api/v1/ai/skills,POST .../importmultipart,POST .../[name]/install,DELETE .../[name]). 3 ações de audit novas (ai.skill_imported,ai.skill_installed,ai.skill_uninstalled). Achado do "Before You Begin": NENHUMA rota do app usavapg.Poolantes desta — só o worker/scripts chamavamcreatePooldo agent-engine. Resolvido reusando o MESMOcreatePool(não duplica setup de conexão) atrás de um singleton lazy novo (lib/ai/skills/db.ts::getSkillsPool()), comSUPABASE_DB_URLpromovido de env exclusivo do engine pralib/env.ts(schemarequired(), tolerante em dev — já existia no.env.example). GET resolveinstalledvia os pointers da PRÓPRIA org (join em skill_versions;sourcederivado deforked_from_version_id) ecatalogvia pointers de plataforma cujo name não está em installed — 3 queries simples, sem embed do PostgREST (padrão do repo). DELETE só removeskill_pointers, nuncaskill_versions(histórico imutável). Achado de teste: multipart real (File/FormData) corrompe em ambiente jsdom (default do projeto) ao passar pelo parser undici doNextRequest—import/route.test.tsroda em// @vitest-environment node(isolado, só este arquivo). TDD: 13/13 testes verdes (4 arquivos: GET, import, install, delete). Typecheck e lint zerados (0 erros, 0 warnings). - 2026-07-24 — FASE 2 Task 6 concluída: tool
read_skill_referenceno turno + telemetriaskill_activations.LoadedSkill.manifestnão existia (só Task 2 tinha trazidoversionId) — trazido praloadSkills(SELECTv.manifest+ campo na interface, 2 linhas, sem quebrarskills.test.tsque mocka rows sem o campo). Achado de ordenação:matchSkillsrodava DEPOIS dewrapToolsWithBreakerno arquivo original — movido pra ANTES da montagem derawTools(junto da telemetria fire-and-forget do brief) porque o gate "só entra quando há skill casada com references" precisa do resultado do match no MESMO ponto ondesearch_knowledge/request_human_handoffjá são ligados/desligados, antes do breaker envolver as tools. Storage:deps.crmCfg.supabasejá era o admin client (service role) usado pelas outras bordas do CRM — nenhum seam novo. Guarda anti-vazamento entre skills:readSkillReferencesó aceitaskill_namepresente emskillMatch.matched(as skills CASADAS deste turno, nunca todas as skills do tenant) — pedir uma skill instalada mas não casada voltaskill_not_activeantes de tocar manifest/storage. TDD: 8/8 testes novos verdes (skill-references.test.ts+ 1 emtool-breaker.test.ts); suite completalib/agent-engine27/27 verde; typecheck e lint zerados. Não existellm-cache.test.ts/teste de byte-identidade dedicado no repo (confirmado por busca — mesmo achado da Task 3 da Fase 1). - 2026-07-24 — FASE 2 Task 8 concluída: migration 0069 — seed de 2 skills de plataforma (
organization_id null) no catálogo do marketplace:objecao-preco(diagnostica o motivo real por trás do "caro" — orçamento/valor não visto/concorrente/tática/timing — antes de reagir, nunca cede desconto não documentado) eagendamento(nunca inventa disponibilidade sem checar agenda real, oferece horários fechados em vez de pergunta aberta, exige confirmação explícita por escrito). Bodies markdown if-then (~80/76 linhas, teto 200) +matcher{any_keywords, probe_keywords}validado porskillMatcherSchema— não há CHECK no banco pro shape (confirmado lendo migration 0050: "shape validado no código"). Idempotente via guardwhere not exists (select 1 from skill_pointers where organization_id is null and name=...)emdo $seed$blocks — aplicado 2x no banco dev: 2 pointers/1 versão cada nas duas rodadas (sem duplicação). Espelhado no apêndice do baseline.sql + linha no MANIFEST. Sem mudança de contrato —database.types.tsnão regenerado (seed de dados). Typecheck zerado. - 2026-07-24 — Fase 2 CONCLUÍDA — prova real ponta-a-ponta (skill .zip muda resposta real no WhatsApp on-keyword, código só na skill não no prompt, skill_activations hard; contra-prova sem keyword = skill dormente); skills no mapa vivo.
- 2026-07-24 — FASE 2 Task 7 concluída: tela
app/app/ai/skills(hooks/ai/useSkills.ts + page.tsx + _client.tsx) — "Skills instaladas" (cards com badge manual/do catálogo, Desinstalar, nota de que reenviar um .zip com o mesmo nome personaliza) e "Catálogo" (skills de plataforma não instaladas, Instalar) + botão "Enviar skill (.zip)" (upload multipart viafetchdireto —apiClientsempre serializa JSON, não dá pra FormData). Item "Skills da IA" na Sidebar + permissõesai.skills.view/ai.skills.manage(ambas manager) em AuthProvider. ÍconePuzzlePiece(nãoPuzzle— nome real no phosphor 2.1.10) +UploadSimple/DownloadSimpleadicionados ao mapa canônicolib/ui/icons.ts. Typecheck e eslint zerados. Prova real clicando (manager e2e-manager, sem MFA): catálogo mostrou os 2 seeds da Task 8 → Instalarobjecao-precomoveu pra Instaladas com badge "do catálogo" → Desinstalar removeu; upload de um .zip mínimo (SKILL.md+references/, montado com fflate) → apareceu em Instaladas com badge "manual". Screenshot em.superpowers/sdd/evidence/f2-skills-tela.png. Cleanup:skill_pointers(0)/skill_versions(0) da org de teste deletados via psql + objeto de storage emskill-assets/{org}/teste-f2-upload/...removido via admin client — confirmado 0/0 e catálogo de plataforma intacto (os 2 seeds voltaram a aparecer). - 2026-07-24 (F2 T7) — RESSALVA de ambiente compartilhado: o commit 225ba2d (Skills UI) varreu a linha de nav "Radar" no Sidebar.tsx de uma sessão PARALELA (feature Radar de Risco, commit 786d2f6 em outra branch). A pasta app/app/radar/ está UNTRACKED (??) no working tree desta branch. NÃO removi (desfazer apaga trabalho paralelo do Rafael). Se a Fase 2 for mergeada antes da sessão do Radar commitar a rota, o item vira link morto — coordenar ordem de merge. A feature de Skills em si está limpa e aprovada.
- 2026-07-24 (F2 T9) — PROVA REAL COMPLETA: skill instalável muda o comportamento do agente em WhatsApp real, sob demanda (keyword), sem tocar no prompt do agente; telemetria skill_activations hard registrada; disclosure provado por contra-prova (msg sem keyword → skill dormente). Skill de teste limpa do banco+storage.
- 2026-07-26 — FASE 3 Task 1 concluída: migration 0085 —
ai_routers(1 ativo por channel_session via índice parcial, config classifier_model/sticky/min_confidence, fallback_agent_id),ai_router_members(agente + intenção declarada + exemplos, unique router_id+intent_name),ai_router_decisions(telemetria append-only sem PII);conversationsganhaactive_ai_agent_id/active_intent/active_agent_set_at(stickiness). Triggers de audit+updated_at nas editáveis (padrão ai_agents). NNNN confirmado viagit log --all --name-only(branch atual 0067-0069,main0070-0084 — 0085 é o primeiro livre nas duas linhas). Aplicada viasupabase db query --linked(roleagent_workerdo.env.localsem CREATE); reaplicação sem erro (idempotente). Types regenerados viasupabase gen types typescript --linkede reduzidos ao diff de 0085 (banco dev compartilhado trouxe tabelas de outras branches — agent_cases/followup_*/crm_lead_risk_states etc. ficaram de fora). Docker indisponível paratest:db— RLS provada manualmente contra o dev real dentro de uma transação com ROLLBACK (org A/B + channel_sessions + ai_routers + ai_router_decisions sintéticos, JWT claims simulados viaset_config('request.jwt.claims', ...), cross-tenant=0/own-org=1 nas duas tabelas, 0 linhas remanescentes após o rollback). Seed dorls-isolation.test.tsestendido (ai_routers usa ochannel_sessionsque o seed já cria por org; ai_router_decisions semeada em paralelo) para quandotest:dbpuder rodar. Typecheck zerado. - 2026-07-26 — FASE 3 Task 2 concluída:
lib/agent-engine/agent/router-config.ts(loadActiveRouter— 2 queries: router ativo por org+channel_session, membros por router_id+org ordenados position/intent_name; leitura defensiva deconfigjsonb com defaults haiku/sticky/0.6) +loadPublishedAgentConfigByIdemagent-config.ts(mesmo SELECT deloadPublishedAgentConfig, filtroa.id=$2em vez dev.channel_session_id=$2, sem order/limit — membros de router não têm vínculo com a sessão, quem tem é o router). Mapeamento Row→PublishedAgentConfig extraído pramapAgentConfigRow()compartilhada pelas duas variantes (refactor sob teste, comportamento idêntico). TDD: 3/3 testes novos verdes (null sem router, membros ordenados, config malformada→defaults); suitelib/agent-engine/agent7 arquivos/25 testes verde (agent-config.test.ts pré-existente intacto). Typecheck zerado. - 2026-07-26 — FASE 3 Task 3 concluída:
lib/agent-engine/agent/intent-classifier.ts(buildClassifierPrompt/parseIntentVerdict/classifyIntent) pelo MESMO seamrunModelCall(purposeintent_router, modelo derouter.classifierModel, nunca hardcoded).parseIntentVerdictnunca lança: JSON malformado, campo faltando,intentfora da lista demembersdo router (defesa contra alucinação) econfidencenão-numérico/fora de [0,1] tudo vira{intentName:null,confidence:0}(clamp em vez de rejeitar quando o resto do shape é válido).classifyIntentembrulha tudo em try/catch — qualquer erro (falha do modelo,LlmModelNotEnabledError, timeout) viralog.warn+null, chamador cai nofallbackAgentId.deps.runModelCallinjetável (default =runModelCallreal) — mesmo padrão dedeps.embeddosearchKnowledge(Fase 0). TDD: 9/9 testes verdes (brief, transcritos). Typecheck e eslint zerados. - 2026-07-26 — FASE 3 Task 4 concluída:
lib/agent-engine/agent/resolve-turn-agent.ts(resolveTurnAgent) — decisão sticky→classificação→fallback→genérico, as 4 dependências (loadActiveRouter/loadPublishedAgentConfigById/loadPublishedAgentConfig/classifyIntent) injetáveis viadepscom default real. 2 decisões do Rafael aplicadas: sem match e sem fallback responde com o agente GENÉRICO (config:null, não silêncio); stickiness só troca com confiança >= minConfidence na intenção DIFERENTE (senão mantém sticky). Todo o branch do router embrulhado em try/catch — erro inesperado cai noloadPublishedAgentConfigde hoje com outcomeclassifier_failed, nunca derruba o turno. Ambiguidade resolvida: membro sticky removido do router ⇒ tratado como sem sticky (classifica normal), decisão segura documentada no report. TDD: 9/9 testes verdes (8 do brief + 1 extra de robustez a erro de DB). Typecheck e eslint zerados. - 2026-07-26 — FASE 3 Task 5 concluída:
resolveTurnAgentcosturado emrunAgentTurn(inbound-turn.ts:538) exatamente ondeloadPublishedAgentConfigera chamado — DEPOIS do guardisLeadInHandoff(L530), confirmado por leitura direta antes de editar. Sticky (conversations.active_ai_agent_id/active_intent) e sinal de roteamento (últimamessagesinbound da conversa, direto —getLeadContextsó roda mais abaixo) lidos em 2 queries baratas antes da chamada.agentConfigcontinua a mesma variável (~15 call sites intocados) — só a fonte virourouted.config. Persistência do sticky + insert emai_router_decisionsfire-and-forget (try/catch,runLog.warn, nunca lança) só quandorouted.routerId !== null(canal sem router = outcomeno_router, zero side effect, comportamento idêntico a hoje).performHumanHandoff(human-handoff.ts) ganhou o passo (b2): zeraactive_ai_agent_id/active_intent/active_agent_set_atda conversa — quem vai pro humano perde a aderência ao router. Suitelib/agent-engine: 56/56 verde (nenhum teste dedicado a inbound-turn/human-handoff existe no repo — regressão coberta pelos 47 testes dos módulos consumidos, todos intactos). Typecheck zerado. - 2026-07-26 — FASE 3 Task 6 concluída: API
/api/v1/ai/routers(GET lista+member_count, POST cria),/[id](GET detalhe+membros, PATCH, DELETE hard-delete com cascade),/[id]/members(PUT substitui a lista inteira, position=índice),/[id]/test(POST classifica mensagem de teste reusandoloadActiveRouter/classifyIntent, NUNCA grava emai_router_decisions). 4 ações de audit novas (ai.router_created/updated/deleted/members_updated). Unique parcial index (channel_session ativo) tratado:23505→409router_already_existsno POST/PATCH; duplicidade deintent_nameno PUT de members idem→409duplicate_intent_name. Achado real (não cosmético):classifyIntent(Task 3) tipavaleadId/jobIdcomostringobrigatório — mapeiam prallm_calls.contact_id/job_id, FKs reais; um uuid inventado pro clique de "testar" teria estourado a FK, otry/catchdo classificador teria engolido o erro e o endpoint SEMPRE devolveria "sem match" silenciosamente enquanto ainda queimava a chamada real ao modelo (achado antes de escrever o teste, não depois). Fix cirúrgico:leadId/jobIdviraramstring | nullemintent-classifier.ts(retrocompatível — callers existentes passam string;resolve-turn-agent.ts/resolve-turn-agent.test.tsintactos, 26/26 verde nos 3 arquivos afetados),/testpassanullpros dois.getSkillsPool()(Fase 2) reusado só ondepg.Poolé exigido pela assinatura deloadActiveRouter/classifyIntent; as 3 rotas CRUD usam o admin client Supabase como os moldes. TDD: 11/11 testes verdes (5 do brief + 6 extras de cobertura de erro/borda: 500 de listagem, 404/409 de/test, etc.). Typecheck e eslint zerados (0 erros). - 2026-07-26 — FASE 3 Task 7 concluída: tela
app/app/ai/routers(lista com cards nome/canal/N intenções/badge ativo + dialog "Novo roteador" nome+canal) e[id](editor: nome, canal read-only pós-criação — PATCH não aceita trocar,fallback_agent_idcom "Nenhum — atendimento padrão", lista de intenções agente+nome+descrição+exemplos com add/remove, painel "Testar classificação").hooks/ai/useRouters.tsno molde deuseSkills.ts(7 hooks: useRouters/useRouter/useCreateRouter/useUpdateRouter/useDeleteRouter/useSaveMembers/useTestRouter) —useRoutercolide de nome comnext/navigation, resolvido com aliasuseRouter as useRouterData/useNextRouternos 2 client components. Permissõesai.routers.view(manager)/ai.routers.manage(admin) no AuthProvider + item "Roteadores" (íconeSignpost, novo no mapalib/ui/icons.ts) na Sidebar. Aviso na tela do agente (AgentForm.tsx, thread viaAgentTabs→page.tsx com embedai_router_members→ai_routers(name)) quando o agente é membro de um router: "o campo de número abaixo não se aplica" + link pro router. Prova real clicando (admin e2e-admin, MFA TOTP): criar roteador no canal E2E Wave12 → 2 intenções (quer comprar→QA W11 TC-05 retest, quer suporte→Lia—AgendaPlus) → salvar → testar classificação com frase de cada intenção → confiança 95% nos dois, agente certo nos dois → toggle inativo (painel de teste se autodesabilita com aviso "Ative o roteador") → excluir com confirm dialog. Notice do agente confirmado à parte (router temp via psql). Typecheck e eslint zerados. Cleanup: 0 routers/0 members na org e2e ao final. - 2026-07-27 — MERGE DA MAIN (693 commits) na feat/operacao-visivel — commit
43b8dad, pedido do Rafael antes da Fase 4 ("deixar tudo igual à main, vai te trazer muita coisa que você não tinha"). Estado: 0 atrás / 28 à frente. Backup completo antes de começar emscratchpad/pre-merge-backup/(patch dos rastreados + tgz dos não-rastreados + SHA de origem7fbee4c). 5 conflitos, todos resolvidos mantendo OS DOIS LADOS:lib/audit/actions.ts(união de 35 literais de ação);components/shell/Sidebar.tsx(união dos ícones; nav mantém Roteadores da F3 + Follow-ups + Radar — o item Radar deixou de ser link morto porque a main trouxe a rota, fechando a ressalva de F2 T7);lib/agent-engine/agent/inbound-turn.ts(as 5 tools coexistem —send_message,search_knowledgeda F0,read_skill_referenceda F2,open_human_caseeprovide_case_updateda main; imports mesclados SEMloadPublishedAgentConfig, confirmado zero uso no corpo após a F3);supabase/baseline.sql(apêndices em ordem numérica 0067→0068→0069→0070-0084 da main→0085);MANIFEST.md(18 linhas por timestamp). 3 defeitos que só o merge revelou: (1)lib/ui/icons.tscomDownloadSimpleduplicado — main e F2 adicionaram o mesmo ícone em blocos diferentes do mesmo import, o git aceitou os dois; (2) fixture deresolve-turn-agent.test.tsincompleto — a main deu 4 campos novos aoPublishedAgentConfig(splitMessages,splitMaxChars,multimodalInput,casesEnabled), ou seja o roteador da F3 agora despacha agentes que sabem quebrar mensagem, ler mídia e abrir caso humano, de graça; (3) o guardatests/unit/evidencia-citada.test.tsda main pegou o plano da F3 citando uma screenshot de roteadores que nunca foi entregue como arquivo (a prova da F3 foi inline + ledger) — texto corrigido para não apontar para o vazio. E o conserto reincidiu no defeito: a primeira redação deste parágrafo escreveu o nome do arquivo entre crases ao descrevê-lo, recriando a citação morta que ele narra. O cabeçalho do próprio guarda avisa disso ("documentar o falso positivo CRIOU falso positivo"); a lição é que o nome do arquivo não pode aparecer nem na explicação. Também não re-rodei a suíte depois de acrescentar este parágrafo — a medição de "1114 testes verdes" abaixo é anterior a ele, e foi ele que a invalidou. Quem escreve prosa neste repo re-roda o guarda. Trabalho de sessões paralelas do Rafael restaurado por cima (16 arquivos, stash preservado): 4 conflitos a mais no pop, com um caso que exigiu COMBINAR em vez de escolher —markConversationmudou de assinatura na main (ganhouorganizationId) enquanto a sessão paralela melhorava o preview do inbox com a transcrição do áudio; ficar com um lado perdia a transcrição em silêncio, ficar com o outro chamava assinatura inexistente. Resolvido com assinatura nova + preview transcrito. Convergência independente: main e sessão paralela escreveram os MESMOS helpersmediaUrlOf/mediaMimeOfem lugares diferentes doingest.ts(git aplicou ambos sem conflito textual →TS2393 Duplicate function implementation); ficou a da main, que é exportada e tem teste dedicado fixando a precedência (waha-ingest-media.test.ts). Verificação: typecheck 0,vitest149 arquivos / 1114 testes verdes, lint 0 erros (150 warnings pré-existentes). Migrations sem número duplicado,0085é a última, apêndice do baseline termina nela; 0074 e 0076 não têm apêndice de propósito — foram substituídas por 0075 e 0077, e o baseline carrega só o estado final (correto para instalação nova). - 2026-07-27 — FASE 4 CONCLUÍDA (Painel de Evolução, 7 tasks). O que entrou:
knowledge_searches(migration 0086 + apêndice no baseline + MANIFEST + invariante de RLS) e a telemetria escrita pelosearchKnowledge(hits, top_score, limiar do CHAMADOR — gravar o piso-1do RPC faria toda busca parecer acima do corte e mataria o sinal de "quase acertou"); a ponte do funil deixou de ser stub (mirrorLeadStageToCrmdelega parasincronizaEstagioDoAgente— o agente move o cartão do tenant pela primeira vez); o agregador purolib/ai/evolution/aggregate.ts; a rotaGET /api/v1/ai/evolution; e a telaapp/app/ai/evolution(3 blocos + "o que está travando", com CTA por lacuna). Commits:bb53b78+4410985(T1),54e7b52→386a7a1→c320a50(T2),f518624→3b5eecf→78b9b55(T3),8dbdc17→ca73db4(T4),717930e→9f77e9e(T5),84114aa→b065da2→e5b639c→f8eb785(T6). - 2026-07-27 — Bugs achados e corrigidos NA RAIZ durante a Fase 4 (nenhum era da fase; a fase os acordou): (1)
lib/leads/agent-stage-sync.tsdescartava oerrorde dois selects — esupabase-jsNÃO lança em falha de rede, então banco fora do ar viravasem_negocio→not_configured→warn silencioso, indistinguível de configuração ausente; (2) o mesmo arquivo mapeava erro de UPDATE paraja_esta_lae o UPDATE não tinha.select(), então quando a trava otimista atuava (humano arrastou o cartão) o motor devolvia "movido" e emitia atividadestage_changedde um movimento que não aconteceu — história fabricada no CRM; (3) a taxa de handoff do painel liaevent_log, escrito só pelo runtime nativo ANTIGO, enquanto o agent-engine (canônico) gravaagent_inbox_items— medido:event_logzerado nas duas orgs, a taxa saía 0,0% e "0% de handoff" lê-se como elogio; corrigido somando as duas fontes (0,0%→5,9% e 0,0%→2,9%); (4) três links da tela apontavam para/app/ai/knowledge, rota que não existe (só/app/ai/knowledge/sources) — é o que um tenant recém-instalado veria; (5) "Negócios ganhos" vinha delead_state_transitions, que só a máquina do agente escreve: dono que fechou 12 na mão leria "0" num bloco chamado "o que mudou no resultado" — rótulos reescritos para dizer pelo agente; (6)contaPorcom objeto literal perdia a contagem de uma skill chamada__proto__(nome de skill vem de.zipenviado pelo usuário) —Object.create(null). - 2026-07-27 — Verificação final da Fase 4 (exit code capturado direto, sem
| tail):npm run typecheckEXIT=0;npm run lintEXIT=0 (0 erros, 151 warnings pré-existentes);npx vitest run153 arquivos / 1171 testes, EXIT=0;npm run test:db56 arquivos / 372 testes + 1 skip, EXIT=0;npm run buildEXIT=0. Mapa vivo: o painel entrou emdocs/architecture/agent-turn.workflow.json(lane "Evolução da IA (Fase 4)") com 3 arestas reais —Turno IA → Buscas na base(toda busca grava hits/top_score/limiar),Buscas na base → Evolução da IA(alimenta o painel) e a de SAÍDAEvolução da IA → Routers(a lacuna vira ação);archify validate --quality standard0 erros, HTML re-renderizado a partir do JSON. - 2026-07-27 — O que a prova de fechamento cobriu, e por qual caminho. (a) Emissor real + pool real:
searchKnowledgechamado contra o banco de dev (org6e567068, KBdf9810c7,topK/limiar lidos da config publicada do agente — limiar 0,5) gravou 5 linhas verdadeiras emknowledge_searches, que estava zerada:hits=1/top=0.717,hits=1/top=0.556, e trêshits=0(top=0.174,0.389,0.301). (b) Tela, logado comoe2e-managerem produção (next build+next start -p 3025, HEADf8eb785): o painel mostrou "5 no período" em "Consultas aos seus materiais" e a lacuna "3 perguntas de clientes não encontraram resposta nos seus materiais" com o botão "Abrir a base de conhecimento" —f4-painel-com-dado.png. (c) Contraste controlado: apagadas as 5 linhas (contagem de volta a 0) e reaberta a MESMA tela, no MESMO período, o cartão virou o vazio "O agente não consultou seus materiais" e a lacuna sumiu, com todo o resto igual (as 3 decisões de roteamento continuaram lá) —f4-painel-sem-dado.png. É durante × depois, uma variável só. O que isso NÃO prova: ninguém mandou mensagem no WhatsApp. O eloWhatsApp → turno do agente → busca → painelestá provado dosearchKnowledgepara a frente; o pedaçomensagem real → turnocontinua sem prova desta fase. - 2026-07-27 — Ficou para trás (dívida enumerada, não esquecida): o painel não mostra vereditos do juiz, comparativo antes/depois automático nem follow-ups executados — as três exclusões estão justificadas na spec. "Quase acertou" (
knowledge_near_misses) tem código e teste mas nunca foi exercitado na tela: nenhuma pergunta real de teste caiu na faixa de 0,1 abaixo do limiar. Menores adiados para a revisão final da branch: semcheck (hits >= 0)no banco (o guarda vive no emissor); telemetria de busca só sai peloinbound-turn(se follow-up/case-reply ganharem RAG, ojobIddefaultnullnão vai gritar); truncamento de 50k linhas só chega ao log, o payload não carrega bandeira, então a tela não tem como dizer "período truncado"; a taxa de handoff soma unidades diferentes (episódio do engine + evento do runtime antigo — sem efeito vivo porque só um runtime está ativo por org);resolveRangeduplicado com/ai/usage, agora com 2º consumidor que justifica extrair; a sondasonda-agente-move-card.tsescreveagent_stage_hintnos pipelines do banco de dev e não desfaz (o dev não representa um tenant virgem). Ambiente, não código: o Upstash de.env.local(lucky-polliwog-108881.upstash.io) dá NXDOMAIN — rate limit está fora em todo o app; e o AI Gateway está no teto do free tier (GatewayRateLimitErrorem duas chamadas de embedding seguidas, hoje), o que derruba a busca de conhecimento inteira comknowledge_unavailable.
O painel já foi provado da telemetria para a frente. O que falta é o ciclo completo com uma mensagem de verdade — e todos os bloqueios foram removidos em 2026-07-27. Restou só o que exige um humano: mandar a mensagem.
Ambiente já preparado (não precisa configurar nada):
- Base de conhecimento ligada na
Lia — AgendaPlus(KBdf9810c7, 2 verbetes: garantia de 12 meses e frete grátis acima de R$ 199), limiar de semelhança em 0,5 — o padrão 0,72 cortaria acertos legítimos, já medido em sessão anterior. - Worker rodando o código atual (
npm run worker, healthz na 8787). Isto importa: um worker antigo processa o turno e não grava a telemetria, e a prova falharia em silêncio. - App em produção na porta 3000, health
healthynos três (Supabase, Redis, WAHA). - Redis local no ar (
docker start deskcomm-redis-local deskcomm-srh-localse tiver reiniciado a máquina). - Embedding indo direto na OpenAI, sem passar pelo gateway (commit
e5d702d) — era o teto do plano anônimo que derrubava a busca inteira.
Os dois passos que são seus:
- Mande duas mensagens do seu WhatsApp para o número conectado: uma que a base responde — "qual o prazo de garantia dos produtos?" — e uma que ela não responde — "vocês patrocinam torneios de xadrez na Islândia?".
- Olhe a tela:
/app/ai/evolution, período incluindo hoje. "Consultas aos seus materiais" sobe 2, e em "O que está travando" aparece "1 pergunta de cliente não encontrou resposta nos seus materiais", com o botão para a base.
Como conferir por baixo, se quiser:
select hits, top_score, threshold, job_id, created_at
from knowledge_searches order by created_at desc limit 2;A primeira com hits >= 1, a segunda com hits = 0 e top_score preenchido (baixo). O job_id é o que distingue a prova real: turno de verdade preenche; a prova de bancada desta fase gravou nulo de propósito.
Se não gravar nada, a ordem de suspeita mudou: worker velho (confira que o processo é posterior ao último commit) → agente errado respondendo (a KB está na Lia, não em outro) → só então o código.
- AI Gateway no teto: RESOLVIDO na causa raiz, sem mexer em cobrança.
lib/ai/embed.tsprometia no cabeçalho usar o provider OpenAI direto quando não háAI_GATEWAY_API_KEY, e esse caminho não existia no código — passava a stringopenai/text-embedding-3-smallparaembed(), e no AI SDK id com barra é resolvido pelo gateway da Vercel mesmo sem chave, caindo no plano anônimo. Commite5d702d+ teste que assere o TIPO do que chega emembed({model})(string = gateway; objeto = provider explícito), sabotado e vermelho. Prova real: 1536 dimensões / 8 tokens direto na OpenAI. O caminho de CONVERSA já estava certo (lib/agent-engine/edge/llm/providers.tsusa providers explícitos com endpoint contido) — o defeito era exclusivo do embedding. - Upstash morto: substituído por Redis local. A instância da nuvem dá NXDOMAIN (foi removida). Subi o mesmo par do
docker-compose.prod.yml—deskcomm-redis-local(redis:7-alpine) +deskcomm-srh-local(serverless-redis-http na porta 8079, que fala o protocolo REST do Upstash sobre o redis normal)..env.localaponta para lá, com os valores da nuvem comentados logo acima para reverter. Backup em.env.local.bak-*. Health voltou ahealthy. - Servidor da porta 3000: o processo antigo (quebrado porque o
pnpm installdo merge trocou onode_modulespor baixo dele) morreu sozinho; subi umnext startnovo do build atual. - Worker reiniciado para pegar o código da fase — o anterior era das 07:54 e não tinha a telemetria nem a ponte do funil.
O elo que faltava está provado. Rafael mandou duas mensagens reais no WhatsApp e o ciclo inteiro andou:
- "qual o prazo de garantia dos produtos?" → o agente respondeu com o fato da base ("12 meses contra defeitos de fabricação, contando da data de entrega") e gravou
hits=1, top_score=0.676531, threshold=0.5. - "vocês patrocinam torneios de xadrez na Islândia?" → o agente recusou honestamente ("Não temos essa informação aqui") e gravou
hits=0, top_score=0.165184.
As duas linhas têm job_id preenchido — que era o discriminador declarado entre prova de bancada e prova real. A bancada gravou nulo de propósito; turno de verdade preenche.
O top_score de 0,165 é o ponto do épico: ele diz "a base não tem esse assunto", e não "o corte estava apertado demais". Antes desta fase o sistema não sabia distinguir os dois — e são problemas com consertos opostos.
Na tela (/app/ai/evolution, logado como e2e-manager): "Consultas aos seus materiais — 2 no período" com o pico em 27/07, e em "O que está travando": "1 pergunta de cliente não encontrou resposta nos seus materiais. São os assuntos que ainda faltam escrever — cada um deles é uma conversa em que o agente teve que improvisar ou passar adiante", com o botão para a base. Prova visual em f4-prova-real-whatsapp.png.
ACHADO DE AMBIENTE (não é bug, mas custa tempo de quem for repetir): a conta de login do Rafael (rafael@maudibrasil.com.br) é admin da org Deskcomm Admin, enquanto o número de WhatsApp, a agente Lia e toda a telemetria vivem na org E2E Test Org. Abrir o painel com a conta dele mostra zeros — corretamente, porque o isolamento entre empresas está funcionando. Quem for repetir a prova precisa entrar com uma conta da org que tem o número, ou o painel vai parecer quebrado quando está certo.
A lacuna de funil apareceu com dados reais, confirmando o plano docs/superpowers/plans/2026-07-27-mapeamento-funil-agente.md: os funis "Pedidos" (4 passos sem etapa) e "CRM Vivo — Clínica" (2 passos) estão listados na tela, e hoje não existe interface para consertá-los.
Plano: docs/superpowers/plans/2026-07-27-mapeamento-funil-agente.md · Ledger das 4 tasks: .superpowers/sdd/2026-07-27-mapeamento-funil-agente/progress.md
Uma seção dentro da tela de Funis (/app/settings/tenant/pipelines) com uma linha por passo do atendimento, na ordem em que o cliente avança, cada uma escolhendo a etapa do funil do tenant. A tela inverte o banco de propósito: o banco guarda etapa → passo (crm_stages.agent_stage_hint), a tela mostra passo → etapa — o modelo mental de quem configura, e a forma que torna impossível pedir dois destinos para o mesmo passo. «Não mover o card» é escolha legítima, nunca pendência.
lib/leads/agent-mapping.ts— regras puras (coerência com ganho/perda antes do banco,diffParaUpdatescom os UNSETs antes dos SETs).app/api/v1/pipelines/[id]/agent-mapping/route.ts—GETmapa + etapas;PUTtotal (os 7 passos sempre,nullexplícito), papelmanager.app/app/settings/tenant/pipelines/_mapping.tsx+hooks/pipelines/useAgentMapping.ts— a seção e sua leitura/gravação.components/ai/EvolutionGaps.tsx— o CTA da lacuna passou a levar a essa tela (antes apontava para o quadro com o verbo "Ver").docs/architecture/agent-turn.workflow.json(+agent-turn.htmlregerado) — faixa Mapeamento do funil, com as duas arestas que fecham o ciclo:Evolução da IA → Mapa do funil("lacuna vira ação") ecrm_stages → Turno IA("o card anda para a etapa escolhida").
typecheck 0 · lint 0 (159 warnings pré-existentes, 0 erros) · vitest 1232/1232 em 157 arquivos · test:db 372 passaram + 1 skip em 56 arquivos (inclui baseline install fresh e update re-aplicado) · build 0.
Da feature em si, medido arquivo a arquivo agora (não somado dos relatórios): agent-mapping.test.ts 19 · agent-mapping/route.test.ts 16 · _mapping.test.tsx 18 = 53 nos três arquivos que a feature criou; mais tests/unit/evolution-gaps-copy.test.ts 17, arquivo da Fase 4 que esta feature ampliou com as guardas do CTA (href, verbo e nomes dos passos).
Sabotagens: 49 (17 + 16 + 16) — número relatado pelas tasks, não recontado por mim; cada uma exigindo a marca no disco por grep -cF antes de rodar.
tests/prova-ciclo-funil.ts — um processo só, com controle negativo e a mesma chamada dos dois lados:
mirrorLeadStageToCrm(a função que o turno chama) com o passo «Qualificado» →not_configured, card parado em «Primeiro contato»;- navegador de verdade, login como
e2e-manager, tela de Funis, escolhe «Negociação» na linha «Qualificado» pelo NOME visível, salva; - a MESMA chamada, o MESMO card →
{"ok":true}, card em «Negociação», e timelinestage_changed · Movido de Primeiro contato para Negociação · ator=system.
Uma variável muda entre as duas medições: o clique. Estado restaurado pela própria tela ao fim (mapa idêntico ao inicial; contacts/crm_leads/crm_lead_activities 55/53/337 antes e depois). Evidência: .superpowers/evidence/ciclo-funil-tela.png.
- Permuta de passos colidia com o índice único (
16310b7): trocar dois passos de etapa gerava 2 SETs e nenhum UNSET, e o primeiroUPDATEbatia emuniq_crm_stages_pipeline_hint(23505 — índice é imediato, a transação não salva). Os UNSETs passaram a sair primeiro; o repro{qualifying:'e2', qualified:'e1'}foi re-executado contra o HEAD. - Mapa parcial apagava mapeamento em silêncio quando a etapa alvo já carregava outro passo — passou a recusar citando o conflito. Perda silenciosa era o pior desfecho possível.
23514/23505escapavam como 500 com texto cru do Postgres em inglês (830530a) — viraram 409state_conflictcom frase em português citando o nome da etapa.- O CTA mandaria um manager para 403: a página de Funis era admin-only e o painel é manager+. O ciclo que a feature existe para fechar estava quebrado por permissão. Página passou a manager+, com o editor de vocabulário/campos ainda admin.
- O painel tinha uma SEGUNDA tradução dos 7 passos — quem clicasse no CTA procuraria linhas com nomes que não existem no destino. Apagada;
ROTULO_DO_PASSOvirou fonte única dos dois lados. - A tela imprimia qualquer mensagem do servidor como copy — incluindo «Auth required.», a palavra "role" e texto cru do Postgres. Passou a filtrar por status (409/422 vão inteiras; 401/403/resto viram frase de tela).
- Texto prometia um caminho que não existe: a lista vazia mandava "crie um funil no quadro" e o quadro manda "ir para configurações" — pingue-pongue fechado (ver achado A abaixo).
A. O produto não cria funil nem etapa — mas o BANCO semeia (a premissa do achado original estava errada)
O que é verdade: nenhuma rota, server action ou tela insere em crm_pipelines ou crm_stages. Grep no HEAD: os únicos insert nessas tabelas estão em tests/invariants/* e no supabase/baseline.sql. O tenant não consegue criar um segundo funil, acrescentar/renomear etapa, nem reordenar — só editar vocabulário, campos e motivos de perda (updatePipelineConfig, admin).
O que NÃO é verdade, e o ledger da Task 3 afirmou: que "como o instalador não provisiona nenhum, toda instalação nova nasce sem funil — P0". Provei o contrário executando: inseri uma organização nova no banco real e li o que apareceu. O trigger trg_seed_default_pipeline_for_org (supabase/baseline.sql:2766, função em :682) cria, a cada INSERT em organizations, o funil "Pedidos" com 8 etapas — Pago com is_won, Cancelado com is_lost. A org de teste foi apagada em seguida (0 funis restantes). É o mesmo funil "Pedidos" que a E2E Test Org tem, com os mesmos slugs na mesma ordem.
Então o P0 é outro, e menor: instalação nova nasce funcional; o teto aparece quando o tenant quer um funil que não seja "Pedidos" (clínica, imobiliária, serviços) — aí ele depende de SQL/script. Quem for atacar isto: o caminho que revelou foi a mensagem de lista vazia da tela de mapeamento, que precisou ser reescrita para não prometer um botão inexistente.
Só pelo trigger de seed. Grep no HEAD: todo o resto é leitura (.eq("is_won", true) na rota de fechar negócio, filtros da tela de mapeamento). Consequência prática: o funil semeado já vem com ganho e perda marcados, mas qualquer funil criado por fora fica sem caminho de produto para marcar a etapa de fechamento — e é exatamente o que a tela de mapeamento diz ao usuário quando «Ganho»/«Perdido» não têm candidata ("quem montou o funil precisa marcar a etapa de ganho"). A frase é honesta; o caminho que ela pressupõe não existe. Revelado pela avaliação de experiência da Task 3 (E2), ao conferir se o CTA cumpre a promessa "você mesmo escolhe".
- O CTA leva à página, não ao funil específico —
unmapped_agent_stepsnão carregapipeline_id. Com dois funis com lacuna, o usuário procura qual é. - Audit não sai quando a mutação falha no meio do laço de
UPDATEs (M3) e não há concorrência otimista (M4): depois de qualquer erro a tela relê o servidor e substitui o rascunho, que é o antídoto escolhido — reenviar sobre um funil que mudou gravaria por cima da decisão de outra pessoa. - Sugerir mapeamento por semelhança de nome ("Proposta" →
negotiating) ficou FORA de propósito: é a adivinhação que o cabeçalho delib/leads/agent-stage-sync.tsproíbe. Se um dia entrar, tem que ser sugestão que o usuário confirma. - A branch segue atrás da
main(o merge foi deixado para depois da feature, por doutrina de higiene de branches).
Plano: docs/superpowers/plans/2026-07-27-gerenciar-etapas-do-funil.md · Ledger das 4 tasks: .superpowers/sdd/2026-07-27-gerenciar-etapas-do-funil/progress.md
Fecha o achado B da feature anterior (is_won/is_lost sem caminho de produto) e metade do achado A (o tenant já cria, renomeia, reordena e arquiva ETAPAS; criar um segundo FUNIL continua sem caminho — ver achado 2 abaixo).
Uma seção «Etapas deste funil» dentro da tela de Funis (/app/settings/tenant/pipelines), acima do mapeamento, onde o dono da operação renomeia, cria, reordena, marca ganho/perda e arquiva as colunas do próprio quadro. As duas seções se completam e se referenciam nas duas direções: quem arquiva uma etapa vinculada ao assistente é avisado e levado ao mapeamento; quem não tem etapa de fechamento para o passo «Ganho» é levado às etapas.
lib/leads/stage-editing.ts— regras puras (nome→slug,posicaoEntre, guarda de desfecho, validação de arquivamento e do destino dos negócios).app/api/v1/pipelines/[id]/stages/route.ts(POST) e.../stages/[stageId]/route.ts(PATCH/DELETE), papelmanager, com_funil.tsde leitura compartilhada.app/app/settings/tenant/pipelines/_stages.tsx+hooks/pipelines/useStages.ts.docs/architecture/agent-turn.workflow.json(+agent-turn.htmlregerado) — a faixa virou «Funil: etapas + mapeamento» com a peça nova e 3 arestas reais:Etapas → crm_stages(cria/renomeia/reordena/ganho-perda/arquiva),Etapas → Mapa do funil(arquivar mata o vínculo com o assistente) e a de voltaMapa do funil → Etapas(sem etapa de fechamento, o passo não tem destino).archify validate --quality standard0 erros, 1 warning de cruzamento próprio; HTML regerado a partir do JSON.
typecheck EXIT=0 · lint EXIT=0 (0 erros, 159 warnings — as mesmas de antes da feature) · vitest 1355 passaram / 1356 em 163 arquivos, EXIT=1 · test:db 372 passaram + 1 skip em 56 arquivos, EXIT=0 · build EXIT=0.
A única vermelha é HERDADA da main e não é desta feature: tests/unit/evidencia-citada.test.ts reprova porque docs/growth/lp-prompts-imagens.md cita 11 imagens 3D que nunca foram versionadas. O arquivo veio da main (60216e7) e esta branch não tocou em docs/growth/ nem no teste — conferido por git diff origin/main...HEAD.
Testes da feature, medidos arquivo a arquivo agora (não somados dos relatórios): lib/leads/stage-editing.test.ts 31 · stages/route.test.ts 10 · stages/[stageId]/route.test.ts 34 · _stages.test.tsx 34 = 109 nos quatro arquivos que a feature criou; mais _mapping.test.tsx, que saiu de 18 para 19 ao ganhar a guarda do link de volta.
Sabotagens: 97 (24 + 38 + 34 + 1 desta task) — os três primeiros números são relatados pelas tasks, com script versionado ao lado de cada relatório; o último é meu, descrito abaixo.
tests/prova-org-fresca-clinica.ts — 19/19, numa organização criada do zero e apagada no fim.
INSERTemorganizations→ o gatilhotrg_seed_default_pipeline_for_orgsemeia "Pedidos" com vocabulário de e-commerce ("Carrinho abandonado" … "Pos-venda"),Pagocomis_won, nenhuma etapa comagent_stage_hint;- no navegador, logado por senha como manager, as 8 etapas são renomeadas para o vocabulário de uma clínica ("Primeiro contato", "Avaliação", … "Tratamento concluído", "Desistiu") e a marcação de fechamento é movida de «Pago» para «Tratamento concluído» — slugs intactos;
- CONTROLE: a ponte do agente recusa os dois passos por
not_configured, e o card não sai do lugar; - ainda no navegador, «Qualificado» → «Avaliação» e «Ganho» → «Tratamento concluído», salvo;
- as MESMAS chamadas movem o card para «Avaliação» e depois «Tratamento concluído» — nomes que não existem no funil de fábrica;
- a org é apagada;
organizations/crm_pipelines/crm_stages/crm_leads/contactsvoltam a 5/6/46/71/108, idênticos.
O caminho de cada afirmação, declarado (não são a mesma coisa, e confundir os dois é como uma dívida foi dada como quitada nesta sessão):
- passos 2 e 4 passaram por HTTP, no navegador, com sessão de usuário real — é o clique, não a rota chamada por fora;
- passos 3 e 5 chamaram
mirrorLeadStageToCrm, a mesma função que o turno chama (inbound-turn.ts:1200), no processo do script, com o client Supabase real. Não é um turno de IA completo — nenhum modelo foi chamado. ChamarsincronizaEstagioDoAgentedireto passaria com a ponte desligada, por isso a entrada é a ponte; - o servidor sob prova foi um
next startrecém-buildado da árvore desta sessão (PROVA_APP=http://localhost:3055), não o:3000— que roda um build anterior a esta sessão.
A prova se prova (sabotagem 97): trocar e.agent_stage_hint === passo por nada em resolveDestinoDoAgente deixa 5 asserções vermelhas (14/19), inclusive "o card vai para a etapa marcada na tela". Mutação confirmada no disco por grep antes de rodar, arquivo restaurado e re-medido em 19/19.
E o instrumento errou primeiro, como nas três rodadas anteriores: a 1ª versão esperava o value do próprio campo virar o nome novo — o que o fill() já satisfaz antes de qualquer ida ao servidor. Sete renomeações passaram por sorte (a iteração seguinte dava tempo do PATCH voltar) e a oitava, sem ninguém depois dela, reprovou. O detector passou a ser o banco.
Evidência visual: .superpowers/evidence/org-fresca-{1-ecommerce,2-clinica,3-mapeado,4-final}.png.
- 🔴 A guarda de desfecho protegia a coisa errada, e o vazio era exatamente a instalação fresca (
b06545f): ela se apoiava emagent_stage_hint === passo, mas o gatilho de seed insere as etapas sem hint — «Pago» nasceis_won=true, hint=null, e o backfill da 0084 só pegou linhas pré-existentes. Toda org criada depois podia desmarcar a única etapa de ganho, e/leads/[id]/win:52passava a responder 422. A correção encolheu o código:etapa[campo]não é uma guarda diferente da do hint, é o superconjunto dela (o CHECK garantehint='won' ⇒ is_won). - 🔴 Arquivar a etapa de ganho passava, e a consequência é pior que a prevista (
b06545f):/leads/[id]/winconsultais_won=truesem filtrar arquivada — o negócio fechado ERA movido, para uma coluna fora do quadro, em silêncio. - 🔴 O
PATCHaceitava etapa ARQUIVADA como alvo da marcação (462aee1): liberava a etapa de ganho real e ocupava a arquivada; 200, e a tela não via. O índice parcial não barra (ignora arquivadas de propósito). Alcançável por aba desatualizada. Passou a recusar 409 guardando a operação inteira. - 🟠 Destino do arquivamento sem checagem de ganho/perda (
462aee1): destino = etapa de ganho fazia N negócios viraremstatus='won'comclosed_at=now()em silêncio — receita mexida por ação de configuração; destino = perda estouraria22023 lost_reason_required. - 🟠 Segunda irreversibilidade não avisada (
af743d4):validarArquivamentonão olhavaagent_stage_hinte oDELETEnão o limpava. A tela dizia "O assistente usa esta etapa para «X»" e mantinha o Arquivar habilitado a 300px dali; arquivar matava o vínculo em silêncio. O painel passou a avisar, com link para o mapeamento. - Rota sem filtro de
pipeline_idna leitura (achado por sabotagem sobrevivente): etapa de outro funil da MESMA org passava pelo 404 e oUPDATEcasava zero linhas — "200 alegre, nada gravado". - Texto que mentia: "Cada funil tem uma de cada" (o guarda só impede remover A ÚLTIMA, nunca garante que exista) → "Cada funil precisa de uma de cada"; "1 negócios estão nesta etapa"; e o rótulo «Ordem» desalinhado no celular (
y=4893vsy=4883), corrigido e re-medido.
E o achado sobre o instrumento, 3ª vez nesta sessão: ao ligar a emissão de atividade do arquivamento em massa, a suíte ficou VERDE — as atividades não saíam. O dublê de banco devolvia data: null para select(cols, { count }), o que só vale com head: true. Um dublê que mentia em silêncio tinha acabado de esconder uma feature inteira.
app/app/_components/AppShell.tsx:16 aplica ml-60 (240px) sem breakpoint, desde a078f83 (2026-04-28). Medido por ferramenta num next start do build desta sessão, logado como manager, viewport 390×844:
| rota | scrollWidth |
innerWidth |
marginLeft do shell |
|---|---|---|---|
/app |
486 | 390 | 240px |
/app/kanban |
532 | 390 | 240px |
/app/settings/tenant/pipelines |
610 | 390 | 240px |
/app/contacts |
984 | 390 | 240px |
Não é da feature: é qualquer tela. O caminho que revelou foi a correção I3 da Task 3 (rótulos aplicados num viewport só), quando o implementador foi medir no celular. O raciocínio dele para medir a SEÇÃO e não a página é preciso e vale guardar: "medir a página reprovaria esta seção por defeito do shell, e passaria a esconder um transbordo meu quando alguém consertasse o shell" — a seção cabe (scrollWidth = clientWidth = 224).
Nenhuma rota, server action ou tela insere em crm_pipelines — só o gatilho de seed. Depois desta feature, is_won/is_lost passaram a ter caminho de produto (o seletor «O que acontece nesta coluna»), então o achado B da feature anterior está fechado; o que sobra é só o funil. Como o gatilho garante um funil por organização, o teto real é estreito e específico: o tenant não consegue um SEGUNDO funil (clínica que quer separar "Consultas" de "Cirurgias", agência com dois produtos). Revelado ao escrever o brief de escopo desta feature, e reconfirmado pela prova da org fresca (1 funil, sempre "Pedidos").
Terceiro achado, menor, de higiene: razaoDaMudancaPeloAgente (lib/leads/agent-stage-sync.ts:69) é órfã — grep no HEAD, zero chamadores de produção, só o próprio teste unitário. Pior: o docstring dela argumenta que a timeline precisa nomear os dois lados ("Movido para Avaliação" esconde que foi o agente), e o código que de fato roda, 40 linhas abaixo, decidiu o contrário de propósito (mesma gramática do arrasto humano; quem moveu está no ATOR). Duas doutrinas opostas no mesmo arquivo, uma delas morta. É da feature de mapeamento, não desta.
lerFunile a tradução de23505/23514estão duplicadas entreapp/api/v1/pipelines/[id]/stages/_funil.tse.../agent-mapping/route.ts. O_funil.tsé o superconjunto; a irmã deveria importar dele. Duas fontes que começam iguais e divergem no primeiro ajuste — a doença que a própria feature de mapeamento nomeou.- A mensagem gentil do 500 virou genérica na tela de etapas (defensável: status desconhecido não deve virar copy), mas é uma regressão de tom em relação ao que a tela de mapeamento faz.
_stages.tsximportamensagemDeErrode_mapping.tsx— acoplamento unidirecional e declarado; a decisão registrada é extrair para módulo próprio no terceiro consumidor.- Sem transação nos dois caminhos de escrita dupla (a ordem escolhida garante que falha nunca deixa negócio apontando para coluna arquivada);
position_in_stagedos negócios movidos em massa não é recalculado. posicaoEntrepode devolverNaN— o modo de falha foi MEDIDO e é alto e claro (JSON.stringifyviranull, coluna éNOT NULL→23502), não corrupção silenciosa.
- Criação e arquivamento de FUNIS — o gatilho garante um por organização, e depois desta feature um segundo funil é quase de graça (a metade difícil, as etapas, já existe). É a continuação natural.
- Vocabulário do funil (
crm_pipelines.vocabulary, que renomeia lead/negócio/ganho/perdido): já tem editor, hoje admin-only. Mexer nele junto misturaria duas conversas na mesma tela. - Sugerir nomes de etapa por nicho — é a mesma adivinhação que o cabeçalho de
lib/leads/agent-stage-sync.tsproíbe. Se entrar, tem que ser sugestão que o usuário confirma, e é feature de onboarding, não de configuração. - A branch segue atrás da
main(merge deixado para depois da feature, por doutrina de higiene de branches).
O servidor da :3000 está servindo 500 nos chunks estáticos (/_next/static/chunks/*) e a tela de login cai para submit nativo (vira GET /login?email=…). Causa: o npm run build da verificação final reescreveu .next sob um next start que já rodava desde 08:47 — e que, além disso, é Next 16.2.11 enquanto node_modules está em 16.2.12, ou seja, já era um servidor de build antigo. Não matei o processo (PID 79644) por ser de outra sessão. Quem retomar precisa reiniciá-lo. A prova desta task não dependeu dele: rodou contra um next start próprio na :3055, encerrado ao fim.