# Changelog de contexto para agentes (fatias do AGENTS.md) Conteúdo movido do `AGENTS.md` em 2026-07-30 (fatiamento do paradigma de agentes paralelos): "Known Gaps" e todos os blocos "Recently resolved". Histórico — leia só quando precisar do contexto de uma entrega passada; o sempre-presente fica no `AGENTS.md`. Itens novos de "Recently resolved" passam a ser registrados aqui (uma entrada curta), não no AGENTS.md. --- **Recently resolved (2026-08-01):** **Cutover residue cleanup** — removed dead `pnpm db:refresh:stage` / `scripts/refresh-stage.mjs` and `pnpm agent:promote` / `scripts/agent-promote.mjs`; closed `docs/CUTOVER-MAIN-ONLY.md`; scrubbed PR template `STAGE_DATABASE_URL` checklist item. Branch + Environment `stage` already deleted. **Recently resolved (2026-08-01):** **PRs de agente = Ready + auto-merge (audit incluso)** — removida a exceção do `engineering-audit` (artefatos também armam `gh pr merge --auto --merge`). Safety net GHA `agent-pr-ready-automerge.yml`: PRs `cursor/*` → `main` Draft→Ready + auto-merge. Rule always-on `agent-pr-workflow.mdc`. **Recently resolved (2026-07-31):** **Cloud `ensure-postgres` falso negativo no start** — o script tratava “role/DBs `teqo` prontos” como “Postgres aceitando conexões”. Em snapshot warm o cluster já está `online` sem role/DBs; `start_postgres` falhava com `postgres is installed but not accepting connections` **antes** de `bootstrap_databases`, o `start` do `environment.json` saía ≠0 e a UI marcava `INSTALL_FAILED` mesmo com o agente utilizável. Fix: separar `server_ready` (`pg_isready` / peer) de `teqo_ready`, bootstrap após o server up, retry no wait; `cloud-setup.sh` pina `DATABASE_URL` e asserta `payload_migrations` pós-migrate (no-op silencioso visto no JIT). Ruído cosmético no mesmo log (dirs de recording, `.installed` de plugins ENOENT) é race do exec-daemon — não é do Teqo. **Recently resolved (2026-07-31):** **Pass 4 — auditoria de engenharia autônoma + guardrails das misses** — primeira execução em modo autônomo (Cursor Cloud) da skill engineering-audit, com precheck solitário (pool desligado) e primeiro harvest de `kind:agent-miss` (escopo OPS2: nasce `docs/GUARDRAILS.md`). **P0:** os 4 globals públicos (`site-settings`/`home`/`metadata`/`privacy-policy`) aceitavam qualquer autenticado no `update` — JWT de campanha (liderança inclusa) editava o site público via `/api/globals/*`; lockdown `payloadAdminOnly` + pins + guarda ([PR #91](https://github.com/fsolla/teqo/pull/91); editores perdem escrita nos globals de config deliberadamente). **P1:** `seed-posts.mjs` chamava `dieWithLabel` sem importar ([PR #92](https://github.com/fsolla/teqo/pull/92), com guarda de presença de import + `DAY_MS` single-source + ESLint lib→utilities sem type-import + guarda de `overrideAccess` omitido). **Misses:** #73 (deadlock `municipality_rels` — classe reproduzida: 7 violações int + 11 deep links e2e; migração para o alocador com teste de classe reescrito classe-agnóstico + guarda de 3 regras, [PR #93](https://github.com/fsolla/teqo/pull/93)); #52 (Drawer por célula em 3 listas de chips → `CampaignListSheetProvider` + invariante dev + guarda, [PR #94](https://github.com/fsolla/teqo/pull/94)); #53 (portão de readiness do autenticador virtual) + #54 (guarda de goto não assentado + convenção TESTING.md) em [PR #95](https://github.com/fsolla/teqo/pull/95); #83 fechada (guarda `agent:ship` já viva). **Registrado, não retry-fixed:** spec de chips de `campaignLeaderships` falha deterministicamente em dev na árvore limpa (nenhum POST dispara; prod 48/48 verde) — causa raiz via debug workflow (P4-L). Artefatos: seção Pass 4 no plano de qualidade, 15 linhas novas no ledger (2 stale fechadas: P3-A/P3-B), plano [`entrega-engenharia-p4.md`](docs/plans/entrega-engenharia-p4.md) com ondas P4-A→P4-L + mapa de guardas + defer+gatilho. **Recently resolved (2026-07-31):** **CI build skip para diffs fora da árvore buildável** — `scripts/build-affected.mjs` + `classifyBuildScope` (núcleo em `test-affected-core.mjs`) decidem `run|none`; job `build-scope` barato no `ci-pr.yml` e o job `build` (Postgres + `pnpm build`) só roda em `mode=run`. Docs/tests/tooling/workflows pulam o build; `src/`, `public/` e configs do Next (`next.config`/`tsconfig`/`tailwind`/`postcss`/`components.json`/lockfile) forçam. `gate:ci` espelha. Push em `main` (`ci.yml`) continua sempre full. **Recently resolved (2026-07-31):** **Consolidação de Issues (project-status)** — merge **B68+** (#39) → **O0+** (#35) (mesmo `model: kimi-k3-low`); **não** merge D3/D4/D5 (modelos mistos kimi vs composer); **não** merge B53–B55 (já `done`); fatia **B79** → chassis (#15) + **B91** busca no drawer (#76, `depends: [B79]`). Planos: `escala-dry-pos-onda0.md`, `busca-global-bottom-drawer.md`, `chassis-bottom-drawer-acoes-rapidas.md`. ## Known Gaps (as of 2026-07-24 — resolve before relying on this file for onboarding new devs) Backlog consolidado (bloqueadores, site, campanha, white-label): [`docs/roadmap.md`](docs/roadmap.md). 1. **RESOLVED 2026-07-27 (`users.roles`):** campo `roles` (`admin` | `editor`) em `users` — migration `20260727_032523_add_users_roles` com backfill `admin`. `isPayloadAdmin` exige papel `admin`; editor entra em `/admin` e escreve `post`/`tag`/`media`; PII/`petition`/campanha seguem admin-only. Unificação com `campaignUser` rejeitada (ver [roles-admin-payload.md](docs/plans/roles-admin-payload.md)). Matriz fina por collection e editor em `petition` ficam fora. 2. **RESOLVED 2026-07-25 (Pass 2 D3):** the last hardcoded consent document id is gone — `submitWhatsapp.ts` now resolves `Consent.key = 'whatsapp-inscricao'` fail-closed (migration `20260725_170000_whatsapp_subscription_consent_key` tags the live document; keys live in `src/lib/campaignConsentKeys.ts`). 3. **`Post`/`Tag` ship the news system, but there is still no `Pages` collection for institutional content.** Partly resolved: the `post`/`tag` collections back the news/publications system, and the home "Últimas notícias" list plus the `/[type]`, `/[type]/[category]`, and article routes are live (see "Posts & Tags" below). Still hardcoded/pending: the home hero heading + subtitle copy (`HomePage` global still exposes only a single `image` field — the text lives in `src/app/(frontend)/(home)/page.tsx`), and there is no `Pages` collection yet for institutional content such as the bio and propostas. 4. **Campaign LGPD consent texts are not yet configured in production.** Invites and self-service leadership flows fail closed until an admin creates `Consent.key = 'lideranca-autopreenchimento'` with counsel-approved copy. The C2 supporter registry (merged to `main`, production-blocked) additionally requires `Consent.key = 'apoiador-cadastro'` and `Consent.key = 'apoiador-intencao-voto'` (vote intention is sensitive data) — same fail-closed behavior. All four keys are bundled in the single legal batch of Onda 0 (see deploy checklist). **Recently resolved (2026-07-31):** **A6** Insight de dobradinhas 2026 (Issue #27) — card "Dobradinhas potenciais para 2026" na aba Eleições do detalhe do município: candidatos proporcionais com `runningAgain2026 = sim` ranqueados por score `0,6 alinhamento + 0,4 força local` (tiers compostos de `campoParties` 2026 + `electionPartySpectrum` — sem terceiro mapa; votos 2022 normalizados pelo máximo da geografia). Leitura derivada, sem migration/Consent; loader `municipalityTicketPartnerData` cacheado sob `election-tse` com a política dos irmãos; estado `pending2026` ("indisponível até as candidaturas de 2026") até a Fase 5 reconciliar pós-15/08 — o runbook da conciliação termina com `POST /api/revalidate?tag=election-tse`. Solla excluído (referência); link para `/campanha/dobradinhas` como caminho de registro (não cria registro paralelo). Int specs eleitorais agora se serializam pelo lease `election-collections` (`testDatabaseLease.ts`). Overview da lista ficou fora de escopo (strip de métricas + varredura estadual por request — follow-up possível pós-Fase 5). Plano: [`docs/plans/insight-dobradinha-2026.md`](docs/plans/insight-dobradinha-2026.md). **Recently resolved (2026-07-30):** **Agent pool orchestrator** — supervisor remoto stateless que mantém até 5 Cursor Cloud Agents rodando `work-issue` sobre Issues `ready` autônomas: workflow `.github/workflows/agent-pool.yml` (tick `*/10min` + merge em `stage` + `workflow_dispatch` como canal canônico) rodando `scripts/agent-pool.mjs` (zero deps npm); elegibilidade fail-closed `isAutonomousClaimable` (`blocked`/`requirements-changed`/`needs:consent` nunca claimadas; `needs:migration` cede ao `migration-lock`); claim coordenado pelo supervisor (alocador único + lock otimista + `agentId` idempotente `bc-` — zero duplicata por construção); workers spawnados via Cloud Agents API v1 com o `model:` da Issue (fallback `composer-2.5`); estado escalar em repo variables `POOL_*`, ativos derivados (marcadores `pool-worker` + status dos runs); falha terminal → Issue a `blocked` + archive; fila drenada → auto-stop. `buildClaimQueue` extraído de `agent-claim` (comportamento idêntico, pinado). Nunca chama `agent:promote`. Requer repo secret `CURSOR_API_KEY`; `schedule` ativa após promote a `main`. Ops: skill `agent-pool`; plano: [`docs/plans/agent-pool-orchestrator.md`](docs/plans/agent-pool-orchestrator.md). **Recently resolved (2026-07-30):** **CI/dev workflow + desbloqueio PR #50** — o PR #50 (B53) ficou BLOCKED por duas causas de infra empilhadas no job `migration-lock` (sem `actions/checkout` → `gh` sem `.git`; depois `GraphQL: Resource not accessible by integration` → `GITHUB_TOKEN` sem `pull-requests: read`) e por um spec int frágil ao `db:seed:minimal` (`homeSearchLeaderships` assertava conjunto global com nome fixo `'Lider'`, que casa word-start com `Seed Liderança Um` do seed). O incidente gerou: (a) política **"Dono do PR, dono do CI"** (`docs/AGENT-OPS.md`) — done = PR mergeado em stage com CI verde, falha fora da feature também é do dono; (b) `ci-pr.yml` reescrito em **jobs paralelos**; (c) **testes afetados pelo diff** — `scripts/test-affected.mjs` (`vitest --changed` vs base do PR) e `scripts/e2e-affected.mjs` + manifesto `scripts/lib/e2e-affected-manifest.mjs`, fallback full em paths de risco (núcleo puro em `scripts/lib/test-affected-core.mjs`, pins unit); e2e entra bloqueante no PR; (d) `ci.yml` (main) virou rede de segurança completa (seed:minimal + build + e2e full); stage segue smoke; (e) fast gate DRY: scripts `gate:fast`/`gate:push`, `.husky/pre-push` roda `gate:push` (knip fora, escape `--no-verify`). **Recently resolved (2026-07-18):** the `/campanha` nuclei MVP shipped (auth/RBAC, nuclei, leaderships, vote estimates, updates, WhatsApp invites, dashboard) with consolidated migration `20260718_010733_consolidate_campaign_schema`. Later the same day, cycle 2 was merged and deployed: multi-municipality/neighborhood territory arrays (migration `20260718_190559_territorio_multi_municipio_bairro`), TSE 2022 election baseline collections + `pnpm db:seed:tse` (migration `20260718_195854_add_election_results`), the filtered nuclei-list overview panel, the nucleus share dialog, and the installable `/campanha` PWA. Still on 2026-07-18, **A2** shipped: static Bahia municipality↔TSE zone map (`src/lib/bahiaTseZones.ts` from TSE 2024 `detalhe_votacao_munzona` BA) plus opt-in territory/zone suggestion chips in the nucleus form (`territorySuggestions`, `NucleusTerritoryAndZonesFields`) — no migration, no forced equality on save. Also the same day: **C2** Cadastro nominal de apoiadores (`supporter` join `Contact`↔campanha with optional `nucleus`, migration `20260718_222656_add_supporter` with `UNIQUE NULLS NOT DISTINCT (contact_id, nucleus_id)`, consent keys `apoiador-cadastro` / `apoiador-intencao-voto` resolved via a generalized `campaignConsent.ts` that fails closed, `/campanha/apoiadores` vertical with list+KPIs/detail/CSV-import wizard; phone required in v1, `lideranca` has no access to the area; production use with real data still waits on Consent keys + legal sign-off). Also the same day: C3 Planos de Ação (`actionPlan` + `/campanha/planos`, migration `20260718_222832_add_action_plan`, blocos "Próximos eventos") — **renamed to `activity` / `/campanha/atividades` by C13 on 2026-07-25**; the old names survive only in frozen migration history. Also the same day: **B2** map foundation — static TopoJSON for Bahia municipalities + identity territories, `bahiaMunicipalityCodes`, helpers in `bahiaGeometries`, re-runnable `pnpm build:geometries` (no migration, no UI; Leaflet is B3). Post-`/simplify` scale follow-ups for that delivery are registered as **B5** (`docs/plans/escala-dry-pos-b2.md`). Earlier (2026-07-15): local Postgres via `docker-compose.yml` (Postgres 17, `teqo_test`); Payload migrations baselined; dev/test database guards; `post`/`tag` news routes deployed (see "Posts & Tags" below). **Recently resolved (2026-07-18 evening / 2026-07-19):** **A4** Baseline no produto + Gap vs 2022 shipped and merged to `main` — `getNucleusElectoralBaseline` / `loadNucleusBaseline2022Overview` aggregate TSE 2022 by nucleus geography; `computeGapVs2022` + `NucleusInsights` on the overview tab; `NucleusElectoralBaseline` card (candidate / president / governor via `BASELINE_TICKET_2022`); "Baseline 2022" card on the filtered nuclei-list overview. No migration (reads existing A3 collections). A `/simplify` pass applied targeted cleanup (role-generic ticket config, per-city `zoneNumber in […]`, shared geography helpers). Scale/DRY debits larger than cleanup (aggregate federal ranking on detail instead of loading all nominal rows, filter by TSE `cityCode`, Alert `confirmed` + `Progress` reuse) are registered as **A7** (`docs/plans/escala-dry-pos-a4.md`). **Recently resolved (2026-07-19):** **C6** Escala e DRY pós-C2 shipped and merged to `main` — bulk supporter import via `payload.db.drizzle` inside the existing Payload transaction (`supporterImportBulk.ts`, `ON CONFLICT DO NOTHING` on `(contact_id, nucleus_id)`, 500-row chunks), `skipContactPhoneInvariant` opt-in on `Contact.enforceUniqueContactPhone` (fail-closed: only honored inside an active `req.transactionID`), single-SQL KPI aggregate (`supporterListOverviewAggregate.ts`, `COUNT(*) FILTER` mirroring `buildSupporterListWhere` + access constraint), HMAC-SHA256 single-use import token + ephemeral `supporterImportBatch` staging collection (migration `20260719_011015_add_supporter_import_batch`, TTL 10 min, actor-bound, consumed on confirm), and shared shells (`campaignListUrl.ts`, `CampaignListPagination`, `campaignFormFields.ts`, `mapCampaignFormActionError.ts`) replacing the three per-list pagination components. A `/simplify` pass applied targeted cleanup (identity maps, dead branches, `relationshipId`/`canManageCampaignUsers` reuse, native `base64url`, `rowCount ?? 0` bug fix). Scale/DRY debits the simplify reviewers flagged as larger than cleanup (locks in 1 round-trip, `.returning()`, `pg_trgm`, shared `drizzleBulk.ts`, column-name assertion, migrating the remaining `formActions`) are registered as **C8** (`docs/plans/escala-dry-pos-c6.md`). Production use with real supporter data still waits on the Onda 0 legal batch (Consent keys `apoiador-cadastro` / `apoiador-intencao-voto`). **Recently resolved (2026-07-21):** **B6** `BahiaMap` setStyle incremental — layer GeoJSON estável entre troca de ano/métrica/escala; `pathByKeyRef` + restyle O(2) no hover/select; `fitBounds` só em `mode`/`highlightKeys`; helpers em `src/lib/bahiaMapStyle.ts`. Fecha débitos de perf de B10/B11. Plano: `docs/plans/escala-dry-pos-b3.md`. **Recently resolved (2026-07-21):** **B11** Escala % dos válidos no Mapa das Praças — `validVotesByYear` in `MunicipalityMapBundle`, `scaleMode` selector (`absolute` | `percentValid`, default %), `computeValidVoteShares`, readout em % com votos absolutos secundários; `/simplify` fix `scaleMax` (legenda 0–100% alinhada ao fill). Plano: `docs/plans/escala-percentual-mapa-pracas.md`. **Recently resolved (2026-07-21):** **B12** Aproximar mapa ao footprint filtrado + correção hover — `fitToKeys` / `interactiveKeys` em `BahiaMap` (`municipalitiesByIbgeCode`); `canonicalMapKeysKey`; hover stroke-only; clear síncrono no mouseout; fit só quando footprint muda. Plano: `docs/plans/aproximar-mapa-pracas.md`. **Recently resolved (2026-07-21):** **Discovery "Inteligência de campanha"** — ciclo literatura→persona→entrevista sintética aprovado. Embasamento canônico de produto/domínio em **`docs/research/`** (compêndio de ~67 fontes sobre campanha de DF na Bahia; relatório com kernel, playbook de 25 padrões dado→decisão, anti-goals): agentes devem consultá-lo antes de propor métricas/análises territoriais novas. Programa registrado no roadmap: **E8–E16 + B13 + C12** (+ adjacentes A11/E17; sessão real com o CG em 2026-07-23 validou/calibrou o programa — `docs/CUSTOMER.md`; plano-mestre `docs/plans/inteligencia-campanha.md`) — conta da cadeira (metas derivadas sobre válidos projetados), fila de alocação, registro-fundação (versions em `votePledge`, sinais tipados, `allocationDecision`), classificação relativa (substitui limiares 35/20/10 p/ DF), escala relativa no mapa (quantis/LQ/rank + símbolo proporcional), níveis N0–N4 staff-only, motor de sugestões com humano no loop, camada TI (salvaguardas MAUP), planejador de giros, backtest pós-eleição. Anti-metas travadas: % estadual absoluto como KPI, contagem bruta de cadastros, cartograma, "caça ao alienado", sugestão automática sem decisão humana. **Recently resolved (2026-07-23):** **Export CSV de assinaturas e contatos** no admin Payload — `@payloadcms/plugin-import-export@3.82.0` habilitado em `signature` e `contact` (`import: false`, export CSV síncrono); flatten de `contact`/`petition` via `toCSV` em [`src/utilities/signatureExport.ts`](src/utilities/signatureExport.ts) (só assinaturas); `consent` excluído do export de assinaturas; collections `exports`/`imports` com access `isPayloadAdmin`; migration `20260723_025513_add_import_export_plugin`. Plano: [`docs/plans/exportar-csv-assinaturas.md`](docs/plans/exportar-csv-assinaturas.md). **Recently resolved (2026-07-23):** **Engineering-standards hardening** (tracker: [`docs/IMPROVE-CODE-QUALITY-PLAN.md`](docs/IMPROVE-CODE-QUALITY-PLAN.md); ledger: [`docs/TECH-DEBT.md`](docs/TECH-DEBT.md); safety net: [`docs/TESTING.md`](docs/TESTING.md); standards rule: `.cursor/rules/engineering-standards.mdc` — READ IT before coding). Phases: (0) explicit `access` on every collection — `users`/`signature`/`subscription`/`consent` admin-only, CMS writes admin-only, leader route gates on `demandas*`/`planos*`, `createLeaderSupporter` scope fixed, `candidate` treated as unrestricted staff consistently; (1) tooling gate — knip in CI (`.github/workflows/ci.yml` on the GitHub mirror), `pnpm lint --max-warnings=0`, `as never` banned (typed helpers `hookFilledCreateData` / tests' `stub`), tsconfig include consolidated + `noUnusedLocals`, ~12 dead modules deleted; (2) caching — committed TSE aggregate artifact (`pnpm build:election-aggregates` → `src/lib/electionAggregates/`, loader `bahiaElectionAggregates.ts`), `loadMunicipalityScope` React `cache()` dedup, `unstable_cache` + `election-tse` tag on elections-tab loaders; (3) "Feel the action" gaps closed — task-toggle revalidation bug, shared `CampaignListPendingBoundary` dimming results, map-compare transition, dashboard map + detail tabs stream behind Suspense; (4) state scoping — map hover isolated in `MunicipalityMapSelection`, scenario provider narrowed, `LeaderContactsPanel` server-rendered with a client form island; (5) structure — `campaignAccess` split into `src/utilities/access/*` (re-exported), supporter actions split (`supporterImport.ts`), `runStaffEntityMutation` + `runCampaignRedirectFormAction` dedupe the org/dobradinha twins and 4 formActions ladders, Plaza→Municipality identifier rename finished. **Recently resolved (2026-07-24):** **A11** Posição em votos do município — rank denso sobre as 435 unidades do catálogo + **% da própria votação** estadual (artefato `bahiaElectionAggregates`); helper `src/lib/municipalityVoteRank.ts`; UI no `MunicipalityBaselineCard` (por ano) e coluna/ordenação `?sort=votos` na lista (`sort`/`dir` mínimo `name`|`votos`; B15 amplia as demais keys). Plano: [`docs/plans/ranking-votos-municipio.md`](docs/plans/ranking-votos-municipio.md). **Recently resolved (2026-07-24):** **E4R** Import único da planilha de projeção — `pnpm db:seed:projecao` (`scripts/import-projecao.mjs`) lê o xlsx canônico em `docs/sheets/`, faz match via `canonicalizeMunicipalityName` + `municipalityCatalog`, e **sempre sobrescreve** `municipality.expectedVotes` + `priority` (re-seed quando a mesa manda planilha nova); Salvador pulado; `--dry-run` + guard `ALLOW_REMOTE_DB`; parser puro em `src/lib/projectionSheetParse.ts` (Bom→`optimistic`, Regular→`central`, Mínimo→`pessimistic`). **Fase 2 (mesmo dia, decisão do produto — não é mais zero PII):** o seed também importa as colunas de estratégia, com PRIORITÁRIAS sobrepondo MAPA GERAL quando não-vazia — SITUAÇÃO→`politicalTrend` (QUEDA/MANTÉM/AUMENTO→desfavorável/neutra/favorável, com nota de proveniência), DOBRADINHAS→`stateDeputy`+`stateDeputies` (união; célula crua em `dobradinhaNotes`), LIDERANÇAS→`contact` name-only **sem telefone** + `leadership` (dedup estadual por slug do nome; `Contact.phone` virou opcional via migration `20260724_175500_contact_phone_optional`, UI continua exigindo), ASSESSOR RESPONSÁVEL→`campaignUser`+`advisors` (união; Edizio=coordinator, Solla=candidate — `candidate` agora é elegível como assessor em `eligibleCampaignStaffWhere`; novos usuários com email `@planilha.invalid` + senha aleatória, sem login até um admin trocar), ENCAMINHAMENTOS→`nextSteps`, OBSERVAÇÃO→`strengths`/`risks` (classificação curada por slug). Relações são união (nunca removem vínculos da UI); re-run idempotente; nomes de atores políticos entraram sem trilha de consentimento — regularizar com a assessoria jurídica. No mesmo dia, o grupo duplicado `municipality.voteGoals` ("Meta Bom/Regular/Mínimo") foi **removido** — collection, zod, view models, form/card e utilitário `voteGoals.ts` — via migration `20260724_133600_drop_municipality_vote_goals` (backfill metas→estimativas onde a estimativa estava vazia): `expectedVotes` (pessimista/média/otimista) é a única série por cenário do município. Plano: [`docs/plans/import-planilha-projecao.md`](docs/plans/import-planilha-projecao.md). **Recently resolved (2026-07-25):** **B21 Página dos Territórios de Identidade** — nova rota staff-only `/campanha/territorios` com os 27 TIs no sistema de listas: sort/filtros/busca canônicos na URL, Metropolitano decomposto em sub-linhas, primeira coluna sticky no mobile e linha → `/campanha/municipios?region=`. O chrome rico agora é compartilhado por `CampaignSortableHead`/`CampaignHeaderFilterPopover`; wrappers de municípios mantêm a política de domínio. O painel de TI e `territorySlot` foram removidos do Início. Sem migration/collection/action. Plano: [`docs/plans/pagina-territorios-identidade.md`](docs/plans/pagina-territorios-identidade.md). **Recently resolved (2026-07-25): Engineering Consolidation Pass 2** (tracker: [`docs/IMPROVE-CODE-QUALITY-PLAN.md`](docs/IMPROVE-CODE-QUALITY-PLAN.md) § Pass 2; architecture: [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md); agent map: `.cursor/rules/codebase-map.mdc`). Highlights: **W1 sistema de listas** — colunas como dado em `CampaignTable` (seams B17/B18), URL state compartilhado (`campaignListUrl` + módulos por domínio; o god module `municipalityUi.ts` virou `municipalityListUrl`/`municipalityListFilters`/`municipalityLabels`/`municipalitySignal`), shells (`CampaignListFooter`, `CampaignListEmptyState`), 8 superfícies migradas (municípios, lideranças, organizações, dobradinhas, demandas, apoiadores, assessores, atividades-shells), B16+ absorvido (`useOptimistic`, hrefs por serializador canônico, facet por slugs, facets no mesmo `Promise.all`); **W2** — fronteiras: `slug`/`phone`/`wordStartFilter`/`voteEstimate` movidos para `lib/`, 21 loaders `server-only`, `campaignRoles.ts` client-safe (sidebar sem o barrel de access), `votePledgeViews` (contract) vs `votePledgeData` (loaders), ciclo supporter quebrado, `components/campaign` em 15 subpastas de domínio; decisão D1: sem `src/domains/`, sem ports-and-adapters; **W4a** — `electionInsights.ts` (941 linhas, 2 exports vivos) deletado; sobreviventes em `electionFormat.ts`/`voteTrend.ts`; E10 nasce limpo; **W4b** — knip `exports`/`types`/`enumMembers` em **ERROR** no CI (0 findings; kit ui sem peças mortas); **W4c** — tabs das páginas de detalhe extraídas (`MunicipalityDetailTabs`, `ActivityOverviewTab`) + factory `detailTabUi`; **W4d** — `runCampaignFormAction` fecha o C8 F4 (13 ações em 7 arquivos; exceções documentadas); **W4e** — tipos de fonte única; **W4f** — copy "Praça"→"Município" varrida (apoiadores desambiguado: cidade do contato = "Cidade"); **D3** — consent do WhatsApp por chave estável (`whatsapp-inscricao`, fail-closed). Suites: 46 unit / 48 int / 7 e2e. **Recently resolved (2026-07-30): Unificação das skills do fluxo (OPS1/OPS2)** — as 7 skills do ciclo roadmap foram fundidas em três: **`plan-issue`** (fundiu `roadmap-item`/`roadmap-items` + profundidade de `implement-roadmap-item`; GATE de confirmação com overview + wireframes ASCII antes de criar qualquer Issue; sugestão de modelo vira propriedade `model:` no frontmatter), **`work-issue`** (claim → verificação assimétrica de modelo → freshness audit → execução → /simplify → PR `--base stage` com CI acompanhado até o merge; promote continua humano) e **`project-status`** (`pnpm agent:status` — overview/fila com `model:`/mermaid/bloqueios/sugestões de consolidação; read-only, nunca toca `in-progress`). Suporte: `agent:register --model`; brief do `agent:claim` imprime `model:`; workflow `issue-done-on-stage-merge.yml` faz o flip `in-progress→done` deterministicamente no merge em stage. Conversão: as 38 Issues do seed ganharam `model:` (tabela ID→modelo aprovada pelo usuário); 6 itens abertos do roadmap legacy viraram Issues (C11 #44, PUB1–PUB4 #45–#48, ADM1 #49); guarda-chuvas ("UX-1", "Higiene PascalCase", smoke/onboarding, white-label, ledger TECH-DEBT) citados, não registrados; `docs/roadmap.md` virou **stub legado congelado**. Deletadas: `roadmap-item`, `roadmap-items`, `implement-roadmap-item`, `suggest-next-roadmap-items`, `ship-to-main`, `close-delivery`, `compile-roadmap`; referências vivas varridas (`model-selection`, `rebase-on-main`, notebooks de projeto, IMPROVE-CODE-QUALITY-PLAN). `capture-review-debts` registra via `agent:register`/`agent:file-miss` e **nunca** edita Issue `in-progress` (débito do mesmo pai = Issue nova com `depends: [pai]`); `engineering-audit` declarado **solitário** (pausa o paralelismo; executa remediações P0/P1 na sessão) com harvest `kind:agent-miss` → `docs/GUARDRAILS.md`. Docs: `docs/AGENT-OPS.md`, `README.md`. **Recently resolved (2026-07-30):** **B77** Simplificar wizard ajuste de votos — `WizardExpectedVotesStep` reescrito: três cenários numa tela (`VoteEstimateScenarioInputs` compact + strip + atalhos no foco); validação `pessimistic ≤ central ≤ optimistic` só no confirm com `Alert` + highlight do par conflitante (`getWizardVoteViolationHighlights`); POST batch inalterado (`MUNICIPALITY_EXPECTED_VOTES_ENDPOINT`); `?cenario=` legado redireciona para URL canônica sem param (`hasWizardScenarioParam` + `wizardActionHref` sem 3º arg); órfãos do ritual B61 removidos de `wizardVoteEstimate`/`campaignWizardCopy`. `VoteEstimateScenarioInputs` compact ganhou `activeScenario`/`onFocusScenario`/`errorScenarios`/`disabled` (lista inalterada). Sem migration. Plano: [`docs/plans/simplificar-ajuste-votos-wizard.md`](docs/plans/simplificar-ajuste-votos-wizard.md). **Recently resolved (2026-07-30):** **B70** Wizard atualizar liderança — `/campanha/acoes/atualizar-lideranca` substitui o stub B60 por `WizardLeadershipStep` (grid 2/3/4 + tile Adicionar + info Drawer + Tooltip de `notes` no hover fine) e `WizardLeadershipForm` (6 campos incl. `exclusive` B69); `loadWizardLeadershipTiles` no RSC; writes `updateLeadershipWizard`/`createLeadershipWizard` em transação Contact+Leadership (não `updateLeadershipInternal`); `WIZARD_ENTRY_ACTION_QUERY_KEY` + skip via `trailingAction` quando `entryAction !== 'update-leadership'`; Continuar após Salvar dirty → Início + toast; `truncateNameAtWordBoundary` em `lib/`. Sem migration. Plano: [`docs/plans/wizard-atualizar-lideranca.md`](docs/plans/wizard-atualizar-lideranca.md). Desbloqueia wiring A1 embutido. **Recently resolved (2026-07-30):** **B71** Polimento visual dos resultados de busca no Início — `src/lib/homeSearchUi.ts` centraliza bleed (`-mx-4 px-4 md:-mx-6 md:px-6`), heading discreto (`text-xs` sentence-case) e `ul list-none`; `HomeSearchHitRow` pinta hover/focus na faixa full-bleed; todos os `HomeSearch*Group` alinhados. Sem migration/action/contrato JSON. Plano: [`docs/plans/polimento-resultados-busca-inicio.md`](docs/plans/polimento-resultados-busca-inicio.md). **Recently resolved (2026-07-30):** **B69** Simplificar modelo de liderança — `leadership.exclusive` boolean NOT NULL DEFAULT true ("Apoio exclusivo") via migration `20260730_043306_simplify_leadership_fields` + DROP `sector`/`sectorNotes`/`consentNote` (collection, zod, forms, filtros/sort B29 da lista, invite autofill/redemption, dossiê; `?sector=` legado canonicaliza fora). **Decisão de produto 2026-07-30 reverteu o rascunho: `organization` FICA** — mapeia ramificações políticas (SindMed com várias lideranças); vertical `/campanha/organizacoes` e todos os vínculos intactos. Lista ganha coluna "Apoio exclusivo" (badge outline "Não exclusivo" só quando false); forms com checkbox default-on + hidden "false" na criação (último valor vence; ausência cai no default do zod). Nota operacional: o prompt interativo de rename do `migrate:create` trava sem TTY — gerar drops e adds em passadas separadas (`CI=1`) e fundir à mão foi o caminho; o `NOT NULL` foi emendado sobre o SQL gerado. Gate: tsc/lint/format/cycles/build; int 518/518; unit com as mesmas 16 falhas pré-existentes do clean tree (drift B66/B71 na base); knip idêntico à base (P3 + drift); Aikido 0 findings. Plano: [`docs/plans/simplificar-modelo-lideranca.md`](docs/plans/simplificar-modelo-lideranca.md). Desbloqueia **B70**. **Recently resolved (2026-07-29):** **B62** Simplificar modelo de sinal — `municipalityUpdate` kind `sinal` is now `signalType` + `body` only: migration `20260730_010601_simplify_municipality_signal_fields` drops `signal_source` and `triangulated` (the generator re-emitted `ADD VALUE 'adiada'` from snapshot drift — removed by hand; this migration's committed `.json` heals the chain), and the five signal-type labels/descriptions were rewritten for the field team (`Rede esfriou`, `Adversário apareceu`, `Alguém pediu algo` — no "broker" in the signal model; `activity.origin`'s `Pedido de broker` and suggestionCatalog copy were explicitly left out by session decision). `MunicipalitySignalFields` (B26 list control) and the inline block in `MunicipalityUpdateForm` lost Fonte/Triangulado; `municipalityUpdatePageData` + `MunicipalityUpdateFeed` (detail + dossier) render only the type badge. **E11 changed semantics: any adversary signal in the window reads as confirmed — P1 triage 1** (session decision, replacing the triangulated/single-source split), `MunicipalityTriggerInput.adversarySignal: { present: true } | null`, single factor copy, E18 glossary aligned. Bonus gate fixes: the `campaignMunicipalities` "advisor scopes…" e2e was stale since B43 (leader home → `/campanha/contatos`) and was updated, and the `municipalityElectoralBaseline` bypass pin dropped 9→8 (pre-existing main drift). Gate: 876 unit / 489 int / targeted e2e green / build; Aikido 0 findings. Plano: [`docs/plans/simplificar-modelo-sinal.md`](docs/plans/simplificar-modelo-sinal.md). **Recently resolved (2026-07-29):** **B59** Chassis do wizard de campanha — rotas staff em `/campanha/acoes/` (`campaignActionRoutes.ts`, `campaignActionEntryHref`), layout `acoes/layout.tsx` (gate staff, coluna `max-w-lg`), `CampaignWizardShell` (voltar mobile, caption de município sticky, `trailingAction`, foco no `h1` ao trocar passo), cinco páginas-ponte até B60 e strip do Início com `href` nos fluxos wizard. Sem migration. Plano: [`docs/plans/chassis-wizard-campanha.md`](docs/plans/chassis-wizard-campanha.md). **Recently resolved (2026-07-29):** **B49** Busca global — resultados Lideranças no Início staff: o mesmo `POST /campanha/home-search` agora inclui `searchHomeLeaderships` em paralelo com municípios e assessores; word-start no `contact.name` com pré-filtro `buildLeadershipListWhere` (`contains`) refinado em memória, cap 25, `municipalitiesSummary` via `truncatedNamesLabel`, desempate por `updatedAt`; assessor vê só a carteira (`overrideAccess: false`). Client: `HomeSearchLeadershipGroup` entre Municípios e Assessores, `HomeSearchHitRow` compartilhado, empty state via `homeSearchHasAnyHits`; linha → `/campanha/liderancas/[id]`. Sem migration. Plano: [`docs/plans/busca-global-resultados-liderancas.md`](docs/plans/busca-global-resultados-liderancas.md). **Recently resolved (2026-07-29):** **B48** Busca global — resultados Municípios (+ TIs) no Início staff: `POST /campanha/home-search` (`campaignJsonMutationRoute` + `homeSearchBodySchema`) chama `searchHomeMunicipalities` — municípios via `loadMunicipalityScope` + `matchesAtWordStart` + readout A11; territórios no mesmo grupo via `loadTerritoryOverview` (contexto regional E17, não carteira do assessor). Client: `HomeSearchResultsContext` (fetch único, extensível B49+), `HomeSearchMunicipalityGroup`, empty “Nenhum resultado.”; `MunicipalityVotePositionReadout` extraído de `MunicipalityList`. Sem migration. Plano: [`docs/plans/busca-global-resultados-municipios.md`](docs/plans/busca-global-resultados-municipios.md). **Recently resolved (2026-07-28):** **E11** Motor de sugestões v1 — the 8 highest-confidence data→decision patterns of the research report §6.1 (P1 reduto dormente/ameaçado, P2 ataque, P3 correligionário na frente, P5 expansão, P6 grande sem rede, P7 pledges estagnados, K-A não responde, K-B perdida vestida) now run as a human-in-the-loop decision queue: a "Sugestões" panel on the staff dashboard (top-5 by triage + "pauta do silêncio") and a card on the municipality Overview, with aceitar/adiar/descartar writing `allocationDecision`. Three layers: **`src/lib/suggestionCatalog.ts`** (client-safe — pure predicates over `MunicipalityTriggerInput`, a numbers-only input type, plus the exact card copy: probable reading, the "descartes" that become the dismiss form's quick picks, cheap-before-expensive menu, contraindication, statute; anchors are illustrative and private, calibration = E15); **`src/utilities/municipalityTriggers.ts`** (`server-only`, on the E13 planner's mold — own municipality read because the shared scope select lacks `engagementLevel`/`lastUpdateAt`, six reads in one `Promise.all`, and per-year LQ using E10's exact formula rather than a third LQ writing); **`resolveSuggestionRecord`** which RE-EVALUATES the pattern server-side before writing — the snapshot records decision-time factors, never render-time ones, and a pattern that left the queue refuses with the safe `SUGGESTION_STALE_MESSAGE`; advisory lock `municipality-suggestion:{id}:{patternId}` is taken before re-reading the latest decision. **The audit killed the plan's "sem migration":** the real outcome enum was `aceita|descarta|movimento` (the plan's `descartada|adiada` values never existed), so "Adiar" cost migration `20260728_041547_add_allocation_decision_adiada_outcome` (E14's `movimento` precedent), with `postponeDays` derived from the recomputed triage (7d; 14d on levels 4–5) and `suppressUntil` in the snapshot instead of a column. Suppression is pure and unit-pinned (`isSuggestionSuppressedByDecision`): aceita 14d, descarta 30d, adiada until snooze, malformed snapshot falls back to the shortest postpone — and **"nível 1 fura a fila"**: a trigger returning as confirmed risk pierces a suppression recorded at lower urgency. Structural differential is code (§6.3): P2 yields to P3/K-B, P7 yields to K-A when effort was recorded. P3 fires from the artifact alone (`runningAgain2026` is 100% `desconhecido` with no read path — "concorre de novo?" is the menu's first item, not a trigger). Silence review = alta/N2+ with no FIRED pattern (a suppressed one was answered, not silence) and no signal ≥30d; includes `priority: alta` because E14 shipped without backfill. UI: explicit submit on all three outcomes (the `campanha-edit-where-you-see` exception for note/confirmation flows), native radios (`accent-primary` — first radio precedent in the repo), managed focus across the trigger↔form swap, level 1 is the only red. **Two facts worth keeping:** the public-site base `ul { my-6 ml-6 list-disc }` leaks into campaign surfaces (the factors list needed B20's reset), and artifact-anchored patterns cannot be staged from DB rows — int tests that need an artifact property claim municípios through the fixture allocator until one qualifies (measured: 109/435 top-quartile, 205/435 quiet, and reduto+coreBlock is only 13/435, which moved P1's trigger coverage to the 45-case unit spec; explicit slugs would race the allocator's wrapping sequence). E18 concepts `triagem-de-sugestoes` + `pauta-do-silencio`; `/campanha` First Load JS 268 → 277 kB. Adjacent follow-ups live in the plan: wiring E13's "não vá" override to `resolveSuggestion`; catalog fase 2 (T/I/J, P4/P8–P12, K-C) after 2 weeks of real use + 20 decisions; a dedicated route if >15 recurring active suggestions. Plano: [`docs/plans/motor-de-sugestoes.md`](docs/plans/motor-de-sugestoes.md). **Recently resolved (2026-07-28):** **B32+** Escala e DRY pós-B32 — and one line of it is a convention every agent needs: **every JSON POST route under `/campanha` is now created by `campaignJsonMutationRoute(config, handler)`** (`src/utilities/campaignJsonMutationRoute.ts`), which exports the `POST` already same-origin-checked, body-parsed and error-mapped, and a convention guard in `tests/unit/codebaseConventions.unit.spec.ts` fails the build on a `route.ts` that exports `POST` without it — the guard sweeps **all of `src/app`**, not just `(campaign)`, because a cookie-authenticated endpoint written under `api/` would be exactly as exposed and exactly as invisible; the three exceptions (Payload's two re-exports, the secret-header `revalidate`) are declared with their reason. That wrapper replaced the helper the plan asked for, because the helper would still have been a line someone could forget: `isSameOriginRequest` was copied into five routes, so the CSRF policy it claims to own was **failing open by omission**. Two details not to undo: `bodySchema.parse` runs **inside the same `try`** as the handler (that is what keeps a malformed body answering with the route's existing `genericMessage` instead of a new one), and `engagement-level` keeps its 409 **inside** its handler so the wrapper never grows a "blocked" concept. `parseCampaignJsonRequestBody` and `campaignJsonMutationErrorResponse` are no longer exported. Client side: `postCampaignJson` (`src/lib/campaignJsonRequest.ts`) is the only place that spells `credentials: 'same-origin'` + JSON headers, and **`useCampaignCellAutosave`** (`src/components/campaign/shared/`) is the auto-save machine of the quick-edit cells (B24 tendência, B27 votos estimados, B32 status de apoio). **Three audit findings worth keeping:** `display` and `draft` never diverged — they were set together on every path, one value under two names, so the hook has one value and only the trend's raw `noteDraft` (raw text vs. normalized note) stays in the caller; `reportFailure` had **not** been born in all four controls as the plan claimed (`MunicipalityListAdvisorsControl` never imported `sonner`, so a failure after closing was mute); and the trend's `inFlightRef` was a workaround for having two code paths into one save, so it became an idempotent `flush()` that protects all three. The hook owns `open` because **closing is the commit**, and `valueRef` advances synchronously — the same "state captured by the render that scheduled the work" bug B33+ and B34 both paid for. Shell: `CampaignCellEditOverlay` took a `statusMessage` prop and renders the `aria-live` region **outside** the Popover/Drawer, because closing is what triggers the save and it was unmounting the node that would announce it; and `variant="sheet"` took a `footer` slot, which lifted "Registrar movimento" out of a scrollable body (E14) and let `MunicipalityListSignalControl` (B26) drop the Popover/Drawer it only kept for its footer — the planned `formId` prop turned out unnecessary, since `
` in the caller plus `