diff --git a/.agents/rules/projects/emendas-mandato.mdc b/.agents/rules/projects/emendas-mandato.mdc
index 932a00b9..15e399a8 100644
--- a/.agents/rules/projects/emendas-mandato.mdc
+++ b/.agents/rules/projects/emendas-mandato.mdc
@@ -38,4 +38,4 @@ Defeso desde 04/07 (transferência voluntária vedada; empenhar pode); inaugura
- Roteiro da sessão real: `docs/research/roteiro-entrevista-emendas-responsavel-real.md`
- Fichas: `persona-emendas-parlamentar.md` · `persona-emendas-assessor-orcamento.md` · `persona-emendas-coordenador-comunicacao.md` · `persona-emendas-coordenador-campanha.md` (anexo [R] = extrato da sessão real 2026-07-23)
- Evidência real de campo: `docs/CUSTOMER.md` (Interview Snapshot 2026-07-23 — O6 dossiê pré-agenda com "emendas aportadas", caso Nazaré das Farinhas)
-- Precedente metodológico: ciclos 1–2 em `docs/research/README.md`; notebook irmão `.cursor/rules/projects/financas-campanha.mdc`
+- Precedente metodológico: ciclos 1–2 em `docs/research/README.md`; notebook irmão `.agents/rules/projects/financas-campanha.mdc`
diff --git a/.agents/rules/projects/nucleos-eleitorais.mdc b/.agents/rules/projects/nucleos-eleitorais.mdc
index c0d337ea..5e801f32 100644
--- a/.agents/rules/projects/nucleos-eleitorais.mdc
+++ b/.agents/rules/projects/nucleos-eleitorais.mdc
@@ -5,7 +5,7 @@ alwaysApply: false
# Núcleos Eleitorais → Municípios
-**Como ler este notebook:** as entradas "**entregue em código**", decisões de sessão e micro-escolhas de implementação abaixo são **changelog** (o que aconteceu e onde está o plano). Não são locks. Não impeçam editar o código que uma entrega anterior criou; não obriguem a espelhar um twin em vez de mudar o dono. Constraints reais de produto/segurança vivem em `AGENTS.md`, `docs/ARCHITECTURE.md` e `.cursor/rules/engineering-standards.mdc`. Para o estado vivo da fila, use Issues + `project-status` — não trate este arquivo como fonte canônica de "o que ainda pode mudar".
+**Como ler este notebook:** as entradas "**entregue em código**", decisões de sessão e micro-escolhas de implementação abaixo são **changelog** (o que aconteceu e onde está o plano). Não são locks. Não impeçam editar o código que uma entrega anterior criou; não obriguem a espelhar um twin em vez de mudar o dono. Constraints reais de produto/segurança vivem em `AGENTS.md`, `docs/ARCHITECTURE.md` e `.agents/rules/engineering-standards.mdc`. Para o estado vivo da fila, use Issues + `project-status` — não trate este arquivo como fonte canônica de "o que ainda pode mudar".
## Status
@@ -110,7 +110,7 @@ alwaysApply: false
- **B28 entregue em código (2026-07-26):** e-mail + celular na lista `/campanha/liderancas` — colunas **E-mail** e **Celular** (copy-on-click, toast, "—" vazio) mais uma coluna de **Ações** com botão WhatsApp (`wa.me`, desabilitado com `aria-label` honesto sem celular válido), ordem final Nome → E-mail → Celular → Status → Municípios → Organizações → Acesso ao app → Ações, no mesmo padrão visual/interativo de `/campanha/assessores` (**B19 ✓**). `whatsAppHrefForPhone` promovido de `AdvisorsTable.tsx` para `src/lib/phone.ts` (pura, client-safe); novo `shared/CampaignCopyableCell.tsx` (+ `campaignReadCellClassName` exportado) nasce **já no 2º call site** — `AdvisorsTable.tsx` migrado no mesmo PR para consumi-lo, eliminando o `copyText`/`READ_CELL_CLASS` duplicados. `leadershipData.ts`: `contactNameAndPhone` virou `contactSummary` (+ `email`), `LeadershipRowViewModel.email` populado sem query nova (`depth: 1` já trazia o `Contact` inteiro), e `loadLeadershipDetail` parou de reparsear `doc.contact` — reusa `row.email` de `toLeadershipRows`. Sem migration/collection/Consent/URL novo; access inalterado. Gate verde: tsc/lint/format/check:cycles limpos, 393 unit+int, build produção ok; `pnpm exec knip` mantém o erro pré-existente ao carregar `payload.config.ts` (P3, não relacionado); `pnpm test:e2e` reproduziu só o flake pré-existente já ledgerado em `campaignMunicipalities.e2e.spec.ts` (checkbox de consentimento do `LeaderContactsPanel` antes da hidratação) — nenhuma rota tocada por este item falhou. Aikido 0 achados nos 5 arquivos novos/editados. Plano: [`email-celular-lista-liderancas.md`](../../../docs/plans/email-celular-lista-liderancas.md).
- **Fill-in "Cobertura da meta" → "Cobertura" entregue em código (2026-07-28):** coluna `goalCoverage` e sort `deficit` em `/campanha/municipios` passam a mostrar **"Cobertura"** via SSoT pós-B17 (`municipalityListSortLabels.deficit` → `municipalityColumnLabels.goalCoverage`); `
` do card mobile cita o mesmo registro (sem literal). B22 ✓ mantém a definição longa no hover; título E18, strips, Conta da cadeira e coluna de territórios seguem "Cobertura da meta". Pins em `campaignComponents.unit.spec.ts` e `municipalityList.unit.spec.ts`. Sem migration. Plano as-built: [`renomear-coluna-cobertura-meta.md`](../../../docs/plans/renomear-coluna-cobertura-meta.md).
- **Fill-in registrado (2026-07-25):** remover coluna "Tipo" de `/campanha/municipios` — município e ZE tratados iguais na coordenação; filtrar ZE = filtrar Salvador (19 slugs). Remove coluna + filtro/sort UI + `?kind=` do contrato URL; schema/catálogo `kind` intacto (mapa, B8, dossiê). Appetite ~0,25–0,5d; Janela 1; cortável; Impeccable B. Plano: [`remover-coluna-tipo-municipios.md`](../../../docs/plans/remover-coluna-tipo-municipios.md).
-- **Pass 2 — Consolidação de engenharia entregue (2026-07-25, W0–W5 + D3):** tudo que foi one-shotted desde o Pass 1 virou infraestrutura compartilhada e arquitetura documentada. **W1 sistema de listas:** colunas como dado em `shared/CampaignTable.tsx` (seams `mandatory`/`defaultVisible` deixam **B17 trivial**; serialização canônica congelada deixa **B18 trivial**), o god module `municipalityUi.ts` (765 linhas/59 exports) dividido em `municipalityListUrl`/`municipalityListFilters`/`municipalityLabels`/`municipalitySignal`, 8 superfícies migradas com URLs byte-idênticas (pins do W0 verdes) e **B16+ absorvido** (`useOptimistic` no popover, facet por slugs, hrefs via serializador canônico com pin de byte-igualdade, facets no mesmo `Promise.all`); fora por decisão D5: planos (cards), `TerritoryOverviewTable`, comparação, LeaderContacts, preview de import. **W2 fronteiras:** `slug`/`phone`/`wordStartFilter`/`voteEstimate` → `lib/`, chaves de consent em `lib/campaignConsentKeys.ts`, 21 loaders `server-only`, `lib/campaignRoles.ts` client-safe (sidebar sem o barrel), `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 (`utilities/` adiado com gatilho: 3º módulo novo de um domínio num mês). **W4a:** `electionInsights.ts` (941 linhas, 2 exports vivos) **deletado** — sobreviventes em `electionFormat.ts`/`voteTrend.ts`; **E10 nasce em módulo novo**. **W4b:** knip `exports`/`types`/`enumMembers` em **ERROR** no CI, 0 findings (kit ui sem peças mortas; `duplicates` off — únicos hits eram os 6 aliases deliberados `canUpdateX = canReadX`). **W4c:** tabs de detalhe extraídas + factory `detailTabUi`. **W4d:** `runCampaignFormAction` fecha o C8 F4 (13 ações em 7 arquivos; exceções comentadas). **W4e:** tipos de fonte única (cenários, trend, strategy VM nomeado). **W4f:** copy Praça→Município varrida (~70 strings; apoiadores desambiguado: cidade do contato = "Cidade"). **D3:** `submitWhatsapp` resolve `Consent.key='whatsapp-inscricao'` fail-closed; migration `20260725_170000` tagueia o doc vivo — **revisar o SQL antes de push/deploy**. Artefatos novos: `docs/ARCHITECTURE.md` + `.cursor/rules/codebase-map.mdc` (atualizados no mesmo PR que mover coisas). Suites: 46 unit / 48 int / 7 e2e; gates verdes por entrega. Tracker: [`IMPROVE-CODE-QUALITY-PLAN.md`](../../../docs/IMPROVE-CODE-QUALITY-PLAN.md) § Pass 2.
+- **Pass 2 — Consolidação de engenharia entregue (2026-07-25, W0–W5 + D3):** tudo que foi one-shotted desde o Pass 1 virou infraestrutura compartilhada e arquitetura documentada. **W1 sistema de listas:** colunas como dado em `shared/CampaignTable.tsx` (seams `mandatory`/`defaultVisible` deixam **B17 trivial**; serialização canônica congelada deixa **B18 trivial**), o god module `municipalityUi.ts` (765 linhas/59 exports) dividido em `municipalityListUrl`/`municipalityListFilters`/`municipalityLabels`/`municipalitySignal`, 8 superfícies migradas com URLs byte-idênticas (pins do W0 verdes) e **B16+ absorvido** (`useOptimistic` no popover, facet por slugs, hrefs via serializador canônico com pin de byte-igualdade, facets no mesmo `Promise.all`); fora por decisão D5: planos (cards), `TerritoryOverviewTable`, comparação, LeaderContacts, preview de import. **W2 fronteiras:** `slug`/`phone`/`wordStartFilter`/`voteEstimate` → `lib/`, chaves de consent em `lib/campaignConsentKeys.ts`, 21 loaders `server-only`, `lib/campaignRoles.ts` client-safe (sidebar sem o barrel), `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 (`utilities/` adiado com gatilho: 3º módulo novo de um domínio num mês). **W4a:** `electionInsights.ts` (941 linhas, 2 exports vivos) **deletado** — sobreviventes em `electionFormat.ts`/`voteTrend.ts`; **E10 nasce em módulo novo**. **W4b:** knip `exports`/`types`/`enumMembers` em **ERROR** no CI, 0 findings (kit ui sem peças mortas; `duplicates` off — únicos hits eram os 6 aliases deliberados `canUpdateX = canReadX`). **W4c:** tabs de detalhe extraídas + factory `detailTabUi`. **W4d:** `runCampaignFormAction` fecha o C8 F4 (13 ações em 7 arquivos; exceções comentadas). **W4e:** tipos de fonte única (cenários, trend, strategy VM nomeado). **W4f:** copy Praça→Município varrida (~70 strings; apoiadores desambiguado: cidade do contato = "Cidade"). **D3:** `submitWhatsapp` resolve `Consent.key='whatsapp-inscricao'` fail-closed; migration `20260725_170000` tagueia o doc vivo — **revisar o SQL antes de push/deploy**. Artefatos novos: `docs/ARCHITECTURE.md` + `.agents/rules/codebase-map.mdc` (atualizados no mesmo PR que mover coisas). Suites: 46 unit / 48 int / 7 e2e; gates verdes por entrega. Tracker: [`IMPROVE-CODE-QUALITY-PLAN.md`](../../../docs/IMPROVE-CODE-QUALITY-PLAN.md) § Pass 2.
- **Correções e débitos pós-B16 (2026-07-25):** a revisão paralela achou três defeitos funcionais, **corrigidos na própria entrega**: as facetas omitiam só o param multi, então "Prioritária" escondia os não-prioritários do popover de Município e "Sem assessor" esvaziava o de Assessores (cada faceta passou a omitir o grupo do seu popover — `slugs`+`priority`, `advisors`+`coverage`, com int test); "todas as tendências" tinha duas codificações (as três marcadas deixavam `trend=` inútil na URL com o funil inativo — agora `parseMunicipalityListParams` canonicaliza o conjunto completo para ausente e `narrowingMunicipalityTrends` foi deletado); e o Território havia sumido do mobile ao virar multi (voltou como `MobileMultiFilterField` alimentado pela faceta, via novo prop `regionFilterOptions`). O `/simplify` da entrega (quality / performance / reuse em paralelo) aplicou o cleanup barato no próprio branch — setter único sobre params crus em `municipalityUi.ts` (fim de três casts e da escada de `if`), resumo de filtros de três funções para uma, remoção do array `municipalitySlugFilterOptions` (435 objetos que iam no bundle sem leitor), linha de opção única no Popover, facetas reusando o escopo já carregado e `getEligibleAdvisorOptions` de volta a coordenador-only. O que sobrou é maior que cleanup e virou o fill-in **B16+**: `useOptimistic` no lugar do par `{baseKey,next}` manual, payload dos rótulos de município (~20 KB por navegação) e hrefs do Popover a partir de uma base `URLSearchParams` em vez de duas serializações por opção — [`escala-dry-pos-b16.md`](../../../docs/plans/escala-dry-pos-b16.md). Adiados com gatilho no plano: gêmeo `MunicipalityFilterHead` (3º header com filtro), `ui/Command` no Popover (2º popover de busca), predicado de busca unificado, multi-seleção mobile honesta. Fora do `/campanha`: o `next build` inteiro morre quando o global `metadata` vem vazio (`/abaixo-assinado/[id]` chama `stripTrailingSlash` no `undefined`) — registrado em "Site público" no roadmap.
- **B16 entregue em código (2026-07-25):** filtros no header das colunas em `/campanha/municipios` — funil + Popover (`MunicipalityHeaderFilter`) ao lado do sort do B15, com multi-seleção OR em Território (`region`), Município (`slug`, com busca acento-insensível e o check "Prioritária" no mesmo popover), Assessores (`advisor`, que também hospeda o toggle com/sem assessor) e Tendência (`trend`); "Limpar" nos multi grandes; mobile mantém os selects empilhados. Header sticky resolvendo contra o scroller do `` (sem scroller interno na tabela) e empty state que troca só as linhas de conteúdo — visão geral e linha de filtros continuam montadas, senão o usuário fica preso em zero resultados. Facetas cross-filtradas (as opções de um filtro respeitam os demais, mantendo o que já está selecionado visível) reaproveitando o escopo já carregado em `loadMunicipalityListFilterFacets`. A pedido do usuário, a coluna "Assessoria" saiu: a ordenação `coverage` passou para "Assessores", o filtro com/sem assessor virou opção do popover, e o alerta "sem responsável" do E9 (prioridade alta sem ninguém) migrou para a célula de assessores, onde antes só haveria um "Sem assessor" apagado. Plano: [`filtros-no-header-lista-municipios.md`](../../../docs/plans/filtros-no-header-lista-municipios.md).
- **E16 entregue em código (2026-07-25):** Dossiê do município (pré-agenda) — aba `?tab=dossie` no detalhe (`MunicipalityDossier.tsx`, server component em Suspense) compondo os loaders existentes via `municipalityDossierData.ts` (baseline TSE + rank A11, conta da cadeira E8, pledges, rede de lideranças com frescor `updatedAt`, agenda `actionPlan` `overrideAccess: false`, sinais C12, perfil IBGE A8 com caveat para zonas — reentrada do artefato órfão); caps 8 lideranças / 5 sinais / 3+2 planos com "ver tudo" por seção; novo campo staff-only `municipality.budgetNotes` ("Emendas aportadas", G11 manual-first) via migration `20260725_022155_add_municipality_budget_notes` (o `.json` da migration também sara a cadeia de snapshots pós-`campaign_goals`); form de estratégia + zod + view model estendidos; **print CSS** (chrome `print:hidden`, shells `print:h-auto print:overflow-visible`, tipografia A4 em `styles.css`) — dossiê imprime 1–2 páginas com capa autocontida ("Gerado em"). Int test `municipalityDossierData.int.spec.ts` (mock do `unstable_cache` do baseline). Gate verde (tsc/lint/knip/363 tests/build); Aikido bloqueado por sessão expirada (re-login navegador pendente). Plano: [`dossie-municipio.md`](../../../docs/plans/dossie-municipio.md).
@@ -130,7 +130,7 @@ alwaysApply: false
- **E4R entregue em código + seed em produção (2026-07-24):** `pnpm db:seed:projecao` (`scripts/import-projecao.mjs` + `src/lib/projectionSheetParse.ts`) importa MAPA GERAL canônico → `expectedVotes` (Bom→otimista, Regular→média, Mínimo→pessimista) + `priority` com **overwrite-always**; Salvador pulado; zero PII. No mesmo dia o grupo duplicado `voteGoals` foi removido do app (ver Decisões). Seed local e **produção Neon** (2026-07-24): arquivo canônico `docs/sheets/Mapa projeção de votos Solla 2026.xlsx` (+ referência `Mapa_projecao_votos_Solla_2026.xlsx` só A×B), **189** municípios atualizados, **50** `priority=alta`, Salvador pulado, re-dry-run pós-apply **delta=0**. Plano: [`import-planilha-projecao.md`](../../../docs/plans/import-planilha-projecao.md).
- **Sync roadmap × sessão de campo (2026-07-24):** os achados da sessão real do CG (2026-07-23, `docs/CUSTOMER.md`) foram incorporados ao roadmap e aos planos de inteligência — âncora da mesa = **% da própria votação** (A11 entrega a lente; E9/E10/B13 expõem ao lado de LQ/captura); piso 150 mil resolve a meta inicial de E8; O6 vira **E16 dossiê do município** + gap **G11 emendas por município**; O7 confirma C12 não cortável; E13 rebaixado na fila de corte ("perna" é a restrição dominante); gatilho de D3 reformulado (deltas do ZAP não absorvidos pelo fluxo sede-digita/C12); FD2 absorvido por R6; O8 "ilhas isoladas" em A validar. **Decisão de produto: E4 reaberto como E4R** — seed único via script das planilhas de `docs/sheets/` (arquivo com espaços = canônico de estratégia; underscore = referência Salvador; v1 só `expectedVotes`/`priority`, sem PII; Salvador pulado com relatório). Novos itens: **E4R** (janela 1, não cortável), **A11**, **E17 tabela TI no Início** (pedido do candidato; primeira fatia de E12), **B14 município mais próximo** (geolocalização → atalho no Início staff; prompt 1×/sessão; client-only), **B15 ordenar lista por coluna**. Planos novos: `import-planilha-projecao.md`, `ranking-votos-municipio.md`, `tabela-ti-inicio.md`, `dossie-municipio.md`, `municipio-mais-proximo.md`, `ordenacao-colunas-lista-municipios.md`, `cenario-junto-filtros-municipios.md`.
- **DEPLOY EM PRODUÇÃO REALIZADO (2026-07-23).** A cadeia destrutiva da remodelagem (`20260721_020109_remodel_municipalities` → `20260723_200000_remodel_municipalities` + `20260723_202000_reconcile_municipality_remodel`) foi aplicada em produção pelos builds Vercel de 2026-07-23 (deploys production READY dos commits `a7b8f1b`/`c7a1b74`/`59868a9`). **Este item supersede toda menção "deploy pendente" nas entradas abaixo.** Pendências operacionais: smoke pós-deploy + onboarding do time (Onda 0 §2/§4 em [`docs/roadmap.md`](../../../docs/roadmap.md)); hold de PII real segue no lote jurídico.
-- **Hardening de engenharia entregue e em produção (2026-07-23, Fases 0–6):** (0) access lockdown — `users`/`signature`/`subscription`/`consent` admin-only, escrita de CMS (`post`/`tag`/`media`/`petition`) admin-only, gates de rota do leader em `demandas*`/`planos*`, `createLeaderSupporter` com escopo correto, `candidate` tratado como staff irrestrito em todas as superfícies; (1) tooling gate — knip no CI (`.github/workflows/ci.yml`, mirror GitHub), `pnpm lint --max-warnings=0`, ban `as never` (helpers `hookFilledCreateData`/`stub`), tsconfig consolidado + `noUnusedLocals`, ~12 módulos mortos deletados; (2) caching — artefato TSE commitado (`pnpm build:election-aggregates` → `src/lib/electionAggregates/`, loader `bahiaElectionAggregates.ts`), `unstable_cache` com tag `election-tse` nos loaders de eleições, `loadMunicipalityScope` (React `cache()`) dedupe dashboard+mapa+lista; (3) pending honesto — bug de revalidação do toggle de tarefa, `CampaignListPendingBoundary` esmaecendo resultados, transição no compare do mapa, mapa do dashboard + abas do detalhe em Suspense; (4) state scoping — hover do mapa isolado em `MunicipalityMapSelection`, provider de cenário estreitado, `LeaderContactForm` extraído como ilha cliente; (5) estrutura — `campaignAccess` dividido em `src/utilities/access/*` (re-export preservado), `supporterImport.ts` separado, `runStaffEntityMutation` + `runCampaignRedirectFormAction` dedupando twins e 4 ladders de formActions, rename `PLAZA_*`→`MUNICIPALITY_*` concluído; (6) docs + regra `.cursor/rules/engineering-standards.mdc`. Tracker: [`docs/IMPROVE-CODE-QUALITY-PLAN.md`](../../../docs/IMPROVE-CODE-QUALITY-PLAN.md) · ledger: [`docs/TECH-DEBT.md`](../../../docs/TECH-DEBT.md) · mapa de testes: [`docs/TESTING.md`](../../../docs/TESTING.md).
+- **Hardening de engenharia entregue e em produção (2026-07-23, Fases 0–6):** (0) access lockdown — `users`/`signature`/`subscription`/`consent` admin-only, escrita de CMS (`post`/`tag`/`media`/`petition`) admin-only, gates de rota do leader em `demandas*`/`planos*`, `createLeaderSupporter` com escopo correto, `candidate` tratado como staff irrestrito em todas as superfícies; (1) tooling gate — knip no CI (`.github/workflows/ci.yml`, mirror GitHub), `pnpm lint --max-warnings=0`, ban `as never` (helpers `hookFilledCreateData`/`stub`), tsconfig consolidado + `noUnusedLocals`, ~12 módulos mortos deletados; (2) caching — artefato TSE commitado (`pnpm build:election-aggregates` → `src/lib/electionAggregates/`, loader `bahiaElectionAggregates.ts`), `unstable_cache` com tag `election-tse` nos loaders de eleições, `loadMunicipalityScope` (React `cache()`) dedupe dashboard+mapa+lista; (3) pending honesto — bug de revalidação do toggle de tarefa, `CampaignListPendingBoundary` esmaecendo resultados, transição no compare do mapa, mapa do dashboard + abas do detalhe em Suspense; (4) state scoping — hover do mapa isolado em `MunicipalityMapSelection`, provider de cenário estreitado, `LeaderContactForm` extraído como ilha cliente; (5) estrutura — `campaignAccess` dividido em `src/utilities/access/*` (re-export preservado), `supporterImport.ts` separado, `runStaffEntityMutation` + `runCampaignRedirectFormAction` dedupando twins e 4 ladders de formActions, rename `PLAZA_*`→`MUNICIPALITY_*` concluído; (6) docs + regra `.agents/rules/engineering-standards.mdc`. Tracker: [`docs/IMPROVE-CODE-QUALITY-PLAN.md`](../../../docs/IMPROVE-CODE-QUALITY-PLAN.md) · ledger: [`docs/TECH-DEBT.md`](../../../docs/TECH-DEBT.md) · mapa de testes: [`docs/TESTING.md`](../../../docs/TESTING.md).
- **Remodelagem Municípios entregue em código (2026-07-23; deploy pendente — migrações `20260723_200000_remodel_municipalities` + `20260723_202000_reconcile_municipality_remodel` são destrutivas para a vertical campanha).** Decisões pós-reunião 2026-07-23: rename Praça→Município (`municipality`, `/campanha/municipios`); **435** entradas (Camaçari única; Salvador 19 zonas); role `candidate` (visão irrestrita); demandas staff-only; leader lockdown (ferramenta de contatos de apoiadores); dobradinhas (`stateDeputy`); mapa no Início (dashboard). Plano-mestre: [`remodelagem-municipios.md`](../../../docs/plans/remodelagem-municipios.md) (substitui `remodelagem-pracas.md`).
- **A10 entregue em código (2026-07-23; deploy pendente com remodelagem):** cenários pessimista/média/otimista em `votePledge.estimatedVotes` e `municipality.expectedVotes` (groups + migration `20260723_124200_add_vote_estimate_scenarios`, backfill scalar→`central`); agregação por cenário em `votePledgeData`/`municipalityMapData`/overview/dashboard; seletor de cenário no mapa 2026; lista/dashboard com média + faixa; liderança intacta (`declaredVotes` único). Plano: [`cenarios-estimativa-votos.md`](../../../docs/plans/cenarios-estimativa-votos.md). Desbloqueia **E8**.
- **Polish overview “Votos estimados” (2026-07-23):** `MunicipalityEstimateScenarioProvider` sincroniza Cenário do mapa com overview + células da lista; seletor Cenário visível em qualquer ano (hint quando ≠2026); labels `Média nas Praças filtradas` + `Pessimista · Otimista`; `sem estimativa` sob o hero. Critique: `.impeccable/critique/2026-07-23T21-29-20Z__src-components-campaign-municipalitylistoverview-tsx.md`.
@@ -208,13 +208,13 @@ alwaysApply: false
- Em 2026-07-24, **uma única série por cenário — "Metas" eliminadas:** o grupo `municipality.voteGoals` ("Meta Bom/Regular/Mínimo") duplicava o conceito de votos estimados e foi removido do app (collection, zod, view models, form/card, `utilities/voteGoals.ts`); a série do município é `expectedVotes` (pessimista/média/otimista). Planilha EXPECTATIVA mapeia **Bom→otimista, Regular→média, Mínimo→pessimista**. Migration `20260724_133600_drop_municipality_vote_goals` com backfill metas→estimativas (estimativa existente vence). **Supersede** o "não fundir com `voteGoals`" da decisão A10 abaixo. **Rejeitado:** manter os dois grupos com semânticas distintas (meta de planejamento × estimativa) — a mesa trabalha com um número por cenário.
- Em 2026-07-24, **E4R overwrite-always:** `pnpm db:seed:projecao` sempre sobrescreve `expectedVotes` + `priority` nas linhas casadas (planilha nova sobre DB já populado). **Rejeitado:** empty-only / `--overwrite` opt-in (quebraria o re-seed). Dry-run continua obrigatório antes de produção.
- Em 2026-07-23, **access explícito em toda collection (hardening Fase 0):** o default do Payload ("qualquer autenticado pode CRUD") é tratado como bug; toda collection declara `access` explícito — `users`/`signature`/`subscription`/`consent` só Payload admin (`payloadAdminOnly`), escrita de CMS admin-only com leitura pública preservada. JWTs de `campaignUser` não operam mais essas collections via `/api`. **Rejeitado:** confiar no default do Payload; RBAC só na camada de UI.
-- Em 2026-07-23, **padrões de engenharia vigentes (hardening Fases 1–6):** zero lint warnings (`--max-warnings=0`); `as never` banido (helpers tipados `hookFilledCreateData` / `stub` nos testes); knip no CI com files/dependencies em nível de erro; dead code se apaga imediatamente após qualquer mudança; convenção `lib/` (dado estático + lógica pura) vs `utilities/` (runtime Payload/Next); caching ladder (React `cache()` → `unstable_cache`+tag → artefato commitado). Regra sempre aplicada: [`.cursor/rules/engineering-standards.mdc`](../engineering-standards.mdc); débitos: [`docs/TECH-DEBT.md`](../../../docs/TECH-DEBT.md).
-- Em 2026-07-23, **auto-save sem botão Salvar:** campos de edição devem persistir automaticamente sempre que for seguro (change de select, blur ou debounce curto); sem botão “Salvar” em edição in-context / campo único. Submit explícito só para formulários multi-campo atômicos, nota/confirmação ou consentimento. Princípio 4 de [`PRODUCT.md`](../../../PRODUCT.md); regra Cursor [`.cursor/rules/campanha-edit-where-you-see.mdc`](../campanha-edit-where-you-see.mdc). **Rejeitado / supersede:** preferência B9 por Enter + botão Salvar em quick-edit; Save “por precaução” em Popover.
+- Em 2026-07-23, **padrões de engenharia vigentes (hardening Fases 1–6):** zero lint warnings (`--max-warnings=0`); `as never` banido (helpers tipados `hookFilledCreateData` / `stub` nos testes); knip no CI com files/dependencies em nível de erro; dead code se apaga imediatamente após qualquer mudança; convenção `lib/` (dado estático + lógica pura) vs `utilities/` (runtime Payload/Next); caching ladder (React `cache()` → `unstable_cache`+tag → artefato commitado). Regra sempre aplicada: [`.agents/rules/engineering-standards.mdc`](../engineering-standards.mdc); débitos: [`docs/TECH-DEBT.md`](../../../docs/TECH-DEBT.md).
+- Em 2026-07-23, **auto-save sem botão Salvar:** campos de edição devem persistir automaticamente sempre que for seguro (change de select, blur ou debounce curto); sem botão “Salvar” em edição in-context / campo único. Submit explícito só para formulários multi-campo atômicos, nota/confirmação ou consentimento. Princípio 4 de [`PRODUCT.md`](../../../PRODUCT.md); regra Cursor [`.agents/rules/campanha-edit-where-you-see.mdc`](../campanha-edit-where-you-see.mdc). **Rejeitado / supersede:** preferência B9 por Enter + botão Salvar em quick-edit; Save “por precaução” em Popover.
- Em 2026-08-02, **WhatsApp no produto = só `wa.me` (supersede D3–D5 de 2026-07-23):** bridge não oficial / QR / sidecar / número pessoal pareado descartados — risco de ban do chip do assessor/CG. Deep link `wa.me` (humano envia no app dele) permanece para convites, share e kit. **Rejeitado também:** WABA; disparo em massa (TSE art. 33); chatbot eleitoral. Issues #30–#32 `wontfix`.
- Em 2026-07-23, **estimativas staff em três cenários (A10):** pessimista / média (`central`) / otimista no pledge e no total da Praça; liderança declara um valor; default de leitura = média; não fundir com `voteGoals`. **Rejeitado:** um único estimado; expor faixa à liderança; três choropleths. Desbloqueia E8.
- Em 2026-07-21, **embasamento de produto/domínio via `docs/research/`:** decisões de análise territorial e mapa citam o relatório de discovery (aprovado 2026-07-21) antes de inventar métrica nova. Regras derivadas: leitura relativa (quantis/LQ/rank/captura) sobre % absoluto; limiares defesa/ataque 35/20/10 degeneram para DF e serão recalibrados com âncoras relativas (E10); roll-off (brancos+nulos DF − majoritária) é o headroom de orientação; abstenção estrutural corrige denominador (válidos projetados) e não é mercado; agregação regional default = Território de Identidade com salvaguardas MAUP (praça manda no voto, TI na logística). **Rejeitado:** cartogramas; pesquisa estadual de intenção para DF; persuasão de território hostil como funcionalidade.
- Em 2026-07-21, **toolchain TypeScript dual:** `typescript` → `@typescript/typescript6@6.0.2` (API/editor/ESLint + `tsc6`); `typescript-7` → `typescript@7.0.2` (Go `tsc` para `pnpm typecheck`). Motivo: typescript-eslint peer `<6.1.0` e API estável só em TS ~7.1. Não usar `@typescript/native-preview`/`tsgo` (canal nightly). `typecheck:tsc6` existe para paridade. Ambient CSS/SCSS em `src/css-modules.d.ts` (TS6 `noUncheckedSideEffectImports`).
-- Em 2026-07-21, **decisão de produto (Field Desk):** em `/campanha`, superfícies staff de visualização que mostram dados mutáveis devem oferecer edição no contexto quando o access permitir (“edit where you see”) — princípio 3 de [`PRODUCT.md`](../../../PRODUCT.md); regra Cursor [`.cursor/rules/campanha-edit-where-you-see.mdc`](../campanha-edit-where-you-see.mdc). Precedente: **B9** ([`edicao-rapida-lista-pracas.md`](../../../docs/plans/edicao-rapida-lista-pracas.md) — `MunicipalityList*Control` + `municipalityStaffFormActions`). Complementado em 2026-07-23 pelo princípio **Auto-save, no Save button**. **Rejeitado:** lista/detalhe só-leitura com toda mutação em `/editar`; data-grid / planilha full-row; service genérico de “quick edit”. `/editar` e Sheets longos ficam para formulários multi-campo, campos raros e fluxos com nota/confirmação. Liderança continua no escopo já definido (sem campos staff-only).
+- Em 2026-07-21, **decisão de produto (Field Desk):** em `/campanha`, superfícies staff de visualização que mostram dados mutáveis devem oferecer edição no contexto quando o access permitir (“edit where you see”) — princípio 3 de [`PRODUCT.md`](../../../PRODUCT.md); regra Cursor [`.agents/rules/campanha-edit-where-you-see.mdc`](../campanha-edit-where-you-see.mdc). Precedente: **B9** ([`edicao-rapida-lista-pracas.md`](../../../docs/plans/edicao-rapida-lista-pracas.md) — `MunicipalityList*Control` + `municipalityStaffFormActions`). Complementado em 2026-07-23 pelo princípio **Auto-save, no Save button**. **Rejeitado:** lista/detalhe só-leitura com toda mutação em `/editar`; data-grid / planilha full-row; service genérico de “quick edit”. `/editar` e Sheets longos ficam para formulários multi-campo, campos raros e fluxos com nota/confirmação. Liderança continua no escopo já definido (sem campos staff-only).
- Núcleo Eleitoral é a unidade operacional; Zona TSE é uma referência oficial adicional.
- Todo núcleo exige geografia; natureza organizativa e setor são dimensões complementares.
- Exclusão no app é substituída por arquivamento.
diff --git a/.agents/skills/agent-work-issue/SKILL.md b/.agents/skills/agent-work-issue/SKILL.md
index 7684d0a9..52da689a 100644
--- a/.agents/skills/agent-work-issue/SKILL.md
+++ b/.agents/skills/agent-work-issue/SKILL.md
@@ -46,7 +46,7 @@ pnpm i # se node_modules ausente/stale
### Cloud (sem Docker)
-Não use `pnpm db:start`. Postgres via `.cursor/environment.json`. `gate:fast` / `gate:push` não precisam de DB. **Proibido** `git push --no-verify` no fechamento.
+Não use `pnpm db:start`. Postgres via `.agents/environment.json`. `gate:fast` / `gate:push` não precisam de DB. **Proibido** `git push --no-verify` no fechamento.
## Passo 1 — Sessão (Issue já claimada)
diff --git a/.agents/skills/context-engineering/SKILL.md b/.agents/skills/context-engineering/SKILL.md
index be991103..3429079f 100644
--- a/.agents/skills/context-engineering/SKILL.md
+++ b/.agents/skills/context-engineering/SKILL.md
@@ -72,7 +72,7 @@ Create a rules file that persists across sessions. This is the highest-leverage
```
**Equivalent files for other tools:**
-- `.cursorrules` or `.cursor/rules/*.md` (Cursor)
+- `.cursorrules` or `.agents/rules/*.md` (Cursor)
- `.windsurfrules` (Windsurf)
- `.github/copilot-instructions.md` (GitHub Copilot)
- `AGENTS.md` (OpenAI Codex)
diff --git a/.agents/skills/engineering-audit/SKILL.md b/.agents/skills/engineering-audit/SKILL.md
index a2ab9ef0..6f73d3df 100644
--- a/.agents/skills/engineering-audit/SKILL.md
+++ b/.agents/skills/engineering-audit/SKILL.md
@@ -46,8 +46,8 @@ Method: the engineering skills this repo follows (improve-code-quality, clean-co
Read, in this order:
-1. `.cursor/rules/engineering-standards.mdc` — gates, type honesty, client boundary, caching ladder, access control.
-2. `.cursor/rules/codebase-map.mdc` — dependency direction, where things live, the list system, invariants.
+1. `.agents/rules/engineering-standards.mdc` — gates, type honesty, client boundary, caching ladder, access control.
+2. `.agents/rules/codebase-map.mdc` — dependency direction, where things live, the list system, invariants.
3. `docs/ARCHITECTURE.md` — layers, bounded contexts, decision log.
4. `AGENTS.md` — operational rules + "Recently resolved" history (Pass 1, Pass 2, every post-Pass-2 delivery's /simplify findings).
5. `docs/AGENT-OPS.md` — paradigma de agentes paralelos vigente (claim→PR→main, skills plan-issue / work-issue / agent-work-issue / project-status). O audit avalia o repo **contra** esse fluxo, não contra o fluxo legacy de roadmap.md.
diff --git a/.agents/skills/local-database/SKILL.md b/.agents/skills/local-database/SKILL.md
index fd58b5aa..ed1e0965 100644
--- a/.agents/skills/local-database/SKILL.md
+++ b/.agents/skills/local-database/SKILL.md
@@ -15,7 +15,7 @@ Production is a live **Neon Postgres** with real citizens' PII. Local developmen
## Cursor Cloud (sem Docker)
-VMs Cloud **não rodam** `docker compose` / `pnpm db:start`. Postgres nativo: `.cursor/environment.json` (`cloud-setup.sh` + `ensure-postgres.sh`). Mesmos URLs `localhost:5432/teqo` e `teqo_test`. Agentes em `work-issue` / `agent-work-issue`: não tente Docker; `gate:fast`/`gate:push` não precisam de banco.
+VMs Cloud **não rodam** `docker compose` / `pnpm db:start`. Postgres nativo: `.agents/environment.json` (`cloud-setup.sh` + `ensure-postgres.sh`). Mesmos URLs `localhost:5432/teqo` e `teqo_test`. Agentes em `work-issue` / `agent-work-issue`: não tente Docker; `gate:fast`/`gate:push` não precisam de banco.
## Local connection values
diff --git a/.agents/skills/rebase-on-main/SKILL.md b/.agents/skills/rebase-on-main/SKILL.md
index 81a431ec..96154b1f 100644
--- a/.agents/skills/rebase-on-main/SKILL.md
+++ b/.agents/skills/rebase-on-main/SKILL.md
@@ -75,7 +75,7 @@ Objetivo: working tree limpa **antes** do rebase, sem pedir autorização. Escol
| # | Condição | Caminho |
| --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- |
| 1 | Há secretos / credenciais / env local no dirty set (`.env`, `.env.*`, `credentials.json`, `*.pem`, tokens óbvios no diff) | **stash** |
-| 2 | Dirty é só ruído local / efêmero: untracked scratch, IDE/OS (`.DS_Store`, `.idea/`, `*.swp`), cache, arquivos fora de `src/` `tests/` `docs/` `scripts/` `.cursor/skills/` `.github/` e sem relação com o tip da branch | **stash** |
+| 2 | Dirty é só ruído local / efêmero: untracked scratch, IDE/OS (`.DS_Store`, `.idea/`, `*.swp`), cache, arquivos fora de `src/` `tests/` `docs/` `scripts/` `.agents/skills/` `.github/` e sem relação com o tip da branch | **stash** |
| 3 | Dirty é mistura: parte parece WIP da feature **e** parte é ruído/local/secrets → **não** faça commit parcial no escuro | **stash** |
| 4 | Diff é WIP intencional da feature: toca os mesmos paths (ou domínio) de `origin/main...HEAD`, ou continua o intent dos commits recentes da branch; dá para redigir **uma** mensagem de commit coerente com o estilo do repo | **commit** |
| 5 | Branch ainda sem commits próprios (`origin/main..HEAD` vazio) mas o dirty é claramente o trabalho da feature (plano/roadmap item, arquivos de entrega) e a mensagem fecha em uma frase | **commit** |
diff --git a/.agents/skills/work-issue/SKILL.md b/.agents/skills/work-issue/SKILL.md
index e6792ef1..65768bff 100644
--- a/.agents/skills/work-issue/SKILL.md
+++ b/.agents/skills/work-issue/SKILL.md
@@ -73,7 +73,7 @@ Idênticos a `agent-work-issue`:
- `capture-review-debts` **com** gate humano (Passo 5 daquela skill) — não use o modo autônomo do pool
- `pnpm push` → PR Ready `--base main` + `Closes #N` → auto-merge → `gh pr checks --watch --required`
-Detalhes, Prep Cloud, invariantes e templates: leia [`agent-work-issue/SKILL.md`](../agent-work-issue/SKILL.md) e os arquivos em `.cursor/skills/work-issue/` (`engineering-brief.md`, `implementation-template.md`, `decision-quality.md`).
+Detalhes, Prep Cloud, invariantes e templates: leia [`agent-work-issue/SKILL.md`](../agent-work-issue/SKILL.md) e os arquivos em `.agents/skills/work-issue/` (`engineering-brief.md`, `implementation-template.md`, `decision-quality.md`).
## Resumo final
diff --git a/.agents/skills/work-issue/engineering-brief.md b/.agents/skills/work-issue/engineering-brief.md
index eccb746a..3e3d822d 100644
--- a/.agents/skills/work-issue/engineering-brief.md
+++ b/.agents/skills/work-issue/engineering-brief.md
@@ -4,9 +4,9 @@ Leia o que for relevante **antes** de travar a abordagem no `*-impl.md`. Não in
## Pinos do repo (sempre)
-- `.cursor/rules/codebase-map.mdc` — direção `lib/` → `utilities/` → `components/` → `app/`; dono da feature
-- `.cursor/rules/engineering-standards.mdc` — gates, type honesty, client boundary, caching ladder, access
-- `.cursor/rules/security-critical.mdc` — quando tocar access/Local API/overrideAccess
+- `.agents/rules/codebase-map.mdc` — direção `lib/` → `utilities/` → `components/` → `app/`; dono da feature
+- `.agents/rules/engineering-standards.mdc` — gates, type honesty, client boundary, caching ladder, access
+- `.agents/rules/security-critical.mdc` — quando tocar access/Local API/overrideAccess
- `AGENTS.md` — auth campanha, Consent fail-closed, transactions, naming, DB local
- `docs/ARCHITECTURE.md` — se a mudança cruza verticais ou modelo de domínio
diff --git a/.prettierignore b/.prettierignore
index 26a028ae..68703336 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -33,5 +33,6 @@ output/
# Static assets and vendored agent material
public/
.agents/
+.agent/
.cursor/
docs/sheets/
diff --git a/README.md b/README.md
index 125f9616..894a667f 100644
--- a/README.md
+++ b/README.md
@@ -111,7 +111,7 @@ Then run:
- `pnpm test:e2e` — Playwright (requires the test DB schema and a free port)
- `pnpm test:all` — unit + integration + E2E
-Quality gates (also enforced by CI on the GitHub mirror): `pnpm lint` (zero warnings — `--max-warnings=0`), `pnpm typecheck`, and `pnpm exec knip` (dead files/dependencies fail; delete what your change orphaned). Standards: `.cursor/rules/engineering-standards.mdc`.
+Quality gates (also enforced by CI on the GitHub mirror): `pnpm lint` (zero warnings — `--max-warnings=0`), `pnpm typecheck`, and `pnpm exec knip` (dead files/dependencies fail; delete what your change orphaned). Standards: `.agents/rules/engineering-standards.mdc`.
## Tech Stack
@@ -122,7 +122,7 @@ Quality gates (also enforced by CI on the GitHub mirror): `pnpm lint` (zero warn
## Campaign (`/campanha`)
-Internal campaign tool for electoral nuclei, local leaderships, vote estimates, field updates, and WhatsApp invites. Authenticated separately from `/admin` via the `campaignUser` collection (`geral` / `coordenador` / `lideranca`). Operational status and decisions: [`.cursor/rules/projects/nucleos-eleitorais.mdc`](.cursor/rules/projects/nucleos-eleitorais.mdc). Conventions and deploy checklist: `AGENTS.md` (“Campaign auth” / “Campaign nuclei MVP”).
+Internal campaign tool for electoral nuclei, local leaderships, vote estimates, field updates, and WhatsApp invites. Authenticated separately from `/admin` via the `campaignUser` collection (`geral` / `coordenador` / `lideranca`). Operational status and decisions: [`.agents/rules/projects/nucleos-eleitorais.mdc`](.agents/rules/projects/nucleos-eleitorais.mdc). Conventions and deploy checklist: `AGENTS.md` (“Campaign auth” / “Campaign nuclei MVP”).
**Production blocker:** do not load real leadership data or enable invites until counsel-approved `Consent.key = 'lideranca-autopreenchimento'` exists. Absolute invite URLs require `NEXT_PUBLIC_SITE_URL` as an exact HTTPS DNS origin in production.
diff --git a/docs/AGENT-OPS.md b/docs/AGENT-OPS.md
index e03de271..3ac72228 100644
--- a/docs/AGENT-OPS.md
+++ b/docs/AGENT-OPS.md
@@ -20,7 +20,7 @@ main → ci.yml full suite → vercel deploy --prod (se verde) → requeue se HE
**Skills:** `plan-issue` (intenção + Issues) → `work-issue` (humano: claim → impl plan → confirmação → execução) ou `agent-work-issue` (pool: já claimada → impl plan → execução sem pausa) → `/simplify` → `capture-review-debts` → PR `--base main` → `project-status`. `docs/roadmap.md` = legado congelado; fonte canônica = GitHub Issues.
-- **Agente faz sozinho:** `pnpm agent:claim` → implementa → **`pnpm push`** → PR **Ready** (nunca draft) com `Closes #N` → `gh pr merge --auto --merge` → `gh pr checks --watch --required`. Regra always-on: `.cursor/rules/agent-pr-workflow.mdc`. Em Cursor Cloud: `ManagePullRequest` com `draft: false`, depois armar auto-merge via `gh pr merge --auto --merge` (o default draft da tool **não** vale neste repo).
+- **Agente faz sozinho:** `pnpm agent:claim` → implementa → **`pnpm push`** → PR **Ready** (nunca draft) com `Closes #N` → `gh pr merge --auto --merge` → `gh pr checks --watch --required`. Regra always-on: `.agents/rules/agent-pr-workflow.mdc`. Em Cursor Cloud: `ManagePullRequest` com `draft: false`, depois armar auto-merge via `gh pr merge --auto --merge` (o default draft da tool **não** vale neste repo).
- **Só humano:** secrets Vercel (`VERCEL_TOKEN`, `VERCEL_ORG_ID`, `VERCEL_PROJECT_ID`), `POOL_GITHUB_TOKEN`, `pnpm configure:branch-protection`, editar envs Neon/Vercel.
### Dono do PR, dono do CI
@@ -91,7 +91,7 @@ Vercel Git builds: `scripts/vercel-ignore-build.sh` skipeia **todas** as branche
## Cursor Cloud
-`.cursor/environment.json` instala deps + Postgres nativo + `db:seed:minimal`. Sem secrets de prod. Preferir `pnpm push`; escape `git push --no-verify` só no cutover documentado acima.
+`.agents/environment.json` instala deps + Postgres nativo + `db:seed:minimal`. Sem secrets de prod. Preferir `pnpm push`; escape `git push --no-verify` só no cutover documentado acima.
## Agent pool
diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md
index dac065af..75724d7b 100644
--- a/docs/ARCHITECTURE.md
+++ b/docs/ARCHITECTURE.md
@@ -29,7 +29,7 @@ flowchart LR
- **`src/lib/`** — lógica pura e dados estáticos, importável de client e server: catálogos (`municipalityCatalog`, `bahiaTerritories`, geometrias TopoJSON), schemas zod (`lib/schemas/*`), helpers puros (`phone`, `slug`, `wordStartFilter`, `voteEstimate`, `electionFormat`, `voteTrend`), constantes client-safe (`campaignConsentKeys`, `campaignRoles`), artefato TSE (`electionAggregates/` + `bahiaElectionAggregates.ts`). `lib/` **nunca importa de `utilities/`** (as 9 inversões históricas foram zeradas no Pass 2 W2).
- **`src/utilities/`** — código acoplado a Payload/Next. Loaders que tocam banco ou `unstable_cache` são marcados **`import 'server-only'`** na primeira linha (21 marcados no Pass 2). Módulos de UI-helper client-safe (ex.: `municipalityListUrl`, `supporterUi`) ficam aqui sem a marca por importarem apenas TIPOS do Payload. **Subpastas de domínio (P3-J):** `access/` (RBAC), `municipality/`, `supporter/`, `territory/`, `leadership/`, `visit/`, `webauthn/` — imports diretos sem barrels. O top-level é pinado por convenção programática: um módulo novo top-level quebra o build até ser registrado (o gatilho D1 do Pass 2 codificado — 3 módulos novos de um domínio no mês ⇒ subpasta).
-- **`src/collections/` + `src/globals/`** — o modelo de domínio É o Payload. Toda collection declara `access` explicitamente; hooks passam `req` (transação) — ver `AGENTS.md` e `.cursor/rules/security-critical.mdc`.
+- **`src/collections/` + `src/globals/`** — o modelo de domínio É o Payload. Toda collection declara `access` explicitamente; hooks passam `req` (transação) — ver `AGENTS.md` e `.agents/rules/security-critical.mdc`.
- **`src/components/campaign/`** — subpastas por domínio (Pass 2 W2): `municipality/ activity/ supporter/ leadership/ advisor/ demand/ organization/ stateDeputy/ votePledge/ invite/ map/ dashboard/ auth/ shell/ shared/`. `shared/` é o sistema transversal (listas, tabela, comboboxes); `shell/` é o chrome do app (sidebar, nav, PWA).
- **Atravessando a fronteira client:** um client component nunca importa um módulo de dados de servidor por VALOR. Tipos/constantes compartilhados vivem num módulo de contrato client-safe — precedentes: `municipalityMapContract.ts` (mapa), `votePledgeViews.ts` (matemática de pledges; loaders em `votePledgeData.ts` são `server-only`), `lib/campaignRoles.ts` (predicados de role para a sidebar — a fonte da qual `utilities/access/shared.ts` deriva). `import type` é sempre seguro.
@@ -105,4 +105,4 @@ Dois contexts principais + um de suporte, espelhados nos prefixos de módulo e n
## Manutenção deste documento
-Atualize na mesma entrega que mover camadas, criar um context novo ou tomar uma decisão estrutural — junto com o mapa compacto do agente em `.cursor/rules/codebase-map.mdc` (convenção: os dois andam no MESMO PR).
+Atualize na mesma entrega que mover camadas, criar um context novo ou tomar uma decisão estrutural — junto com o mapa compacto do agente em `.agents/rules/codebase-map.mdc` (convenção: os dois andam no MESMO PR).
diff --git a/docs/CHANGELOG-AGENTS.md b/docs/CHANGELOG-AGENTS.md
index 2697d587..79775ea1 100644
--- a/docs/CHANGELOG-AGENTS.md
+++ b/docs/CHANGELOG-AGENTS.md
@@ -77,7 +77,7 @@ Backlog consolidado (bloqueadores, site, campanha, white-label): [`docs/roadmap.
**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-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: `.agents/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).
@@ -85,7 +85,7 @@ Backlog consolidado (bloqueadores, site, campanha, white-label): [`docs/roadmap.
**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-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: `.agents/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`.
@@ -108,7 +108,7 @@ Backlog consolidado (bloqueadores, site, campanha, white-label): [`docs/roadmap.
**Recently resolved (2026-07-27):** **E13** Planejador de presença/giros — visit eligibility as **five conditions ✓/—** (volume, headroom, rede, janela, encaixe) plus three advisory contraindications, each carrying a counter-offer and **never** blocking; the calendar phase (`construcao|consolidacao|ativacao`) changes the suggested "produto da visita". Pure math in `src/utilities/visitEligibility.ts`, cut points in `src/lib/visitPlannerAnchors.ts` (illustrative, on the `territorialClassAnchors.ts` precedent; the phase turns at midnight in Bahia via the new `formatBahiaCivilDate`), candidates grouped by identity territory in `src/utilities/visitPlannerData.ts` (`server-only`). Surfaces: an "Elegibilidade para visita" card on the municipality `OverviewTab`, and a composer at **`/campanha/atividades/giros`** (staff-only, reached from a "Planejar giro" button — no new sidebar entry) that turns TI → âncora/satélites/semente into draft `activity` rows in one transaction. **Three plan premises died in the pre-implementation audit and should not come back:** `kind: 'visita'` never shipped with C12 and is **not** wanted — `activity.deputyPresent` already IS the candidate-presence marker, so a giro is derivable from `deputyPresent` + território + date window and each stop keeps its real kind; `allocationDecision` still has no application write path, so the "não vá" is advisory text and recording an override is **E11**'s first job; and "satélites contíguos" was never possible — `bahiaTerritories.ts` carries membership only (polygon adjacency exists solely inside the build scripts), so **encaixe = same TI**, said in those words in the UI. Two implementation facts worth keeping: the uncaptured field ceiling and the catalog medians moved out of E10's classifier into `municipalityPotential.ts` (memoized), so the card and the "Classe" column cannot compute the same number twice; and `createTourDraftActivitiesRecord` re-reads every stop's município id with `overrideAccess: false` **inside** the transaction and aborts if one is missing, because `activity` create access is a plain staff boolean and Payload cannot express a per-município constraint on create — without it an advisor could seed drafts statewide by posting ids the composer never offered. Caps: 3 satellites, 8 stops. No migration, no collection, no `Consent`, no persisted `tour` entity. Plano: [`docs/plans/planejador-de-giros.md`](docs/plans/planejador-de-giros.md).
-**Recently resolved (2026-07-27):** **B34** Chips editáveis de municípios na lista de lideranças — the "Municípios" column of `/campanha/liderancas` was `municipalityNames.join(', ')`, dead text with no link; it is now chips that navigate to `/campanha/municipios/[slug]`, with a territory chip (`MapPinIcon` + count) when the leadership covers a whole TI, and it is **editable in place**. **The decision that generalizes beyond this item: what picks inline vs. drawer is the `pointer:` media query, not viewport width.** On `pointer-fine` the editor is always mounted — the "×" sits at the chip's corner revealed on hover/focus (absolutely positioned, so revealing it moves nothing) and a search input trails the chip row; on `pointer-coarse` a tap anywhere in the cell opens a **Drawer** with 44 px targets, because a thumb scrolling a table must not be asked to distinguish "open the município" from "delete the link". A tablet with a mouse gets the inline editor; a touch laptop gets the Drawer. This **amends** the anti-goal "always-mounted inputs on every list row" (`PRODUCT.md` principle 3 + `.cursor/rules/campanha-edit-where-you-see.mdc`) **for chip-based relations on fine pointers** — the input IS the readout there, and a per-row Popover would add a click to a target already on screen. It does **not** reopen B28/B31/B32: nothing here is a table mode, and `AdvisorsTable`'s toggle was renamed **"Editar nome e contato"**, which is all it still unlocks. Engineering, all divergences from the plan measured: `AdvisorMunicipalityCell` **was** promoted to `src/components/campaign/shared/MunicipalityPortfolioCell.tsx` (the plan rejected it — but the `ResizeObserver` + "Ver mais…" machinery it called dead weight became the requirement the moment the cell was always visible), and `/campanha/assessores` migrated onto the shared piece in the same PR, so the 2nd call site was born with the 1st; **no context provider** (the catalog index and `addableIds` travel as props of the column factory the RSC builds once per table); **one batch action only** — `setLeadershipMunicipalitiesMembership` (repeated `municipalityIds` + `assigned`), the single-id twin never existed, and the advisor twin (`setAdvisorMunicipalityMembershipRecord` + its schema) was **deleted** with its int tests migrated to the batch path; `LeadershipRowViewModel.municipalityNames` → `municipalityIDs: number[]` (names/slugs resolve client-side against the catalog the cell already holds); the floor of 1 uses `aria-disabled` + the reason in a toast rather than `disabled`, because `disabled` drops the button from the tab order and the reason lived only in its `aria-label`; the cap of 30 is refused client-side **before** the optimistic apply, so a TI batch does not bounce back whole with a message that never says which limit blew. **`/impeccable` closed two real P0/P1s:** suggestions were mouse-only (now a full ARIA 1.2 combobox — arrows/Home/End/Enter, `aria-activedescendant`, portaled listbox with its own name, "Nenhum resultado." kept **outside** the listbox) and auto-save was mute (`aria-busy` + dimmed row + `sr-only` live region, rollback scoped to the failed delta so a neighbouring saved toggle is not reverted, plus **undo** on batch removal); target size of the "×" went 16 → **24 px** via `before:-inset-1` (SC 2.5.8 has no mouse exemption, and the 44 px Drawer equivalent is `pointer-fine:hidden`), the "×" border got real contrast (the token's is 1.17:1 on a `secondary` chip), and the chips are links **inside the Drawer** again. **`/simplify` caught two real bugs the gate could not see**, both in the same shape — state captured by the render that scheduled the work. Every optimistic delta is now applied **functionally**, because the success path computed from the `assignedIds` of its own render: the toast's "Desfazer", pressed after a later add, dropped that add from the UI while the server kept it, and the reconcile effect only clears the optimistic value when both id lists match exactly, so the divergence outlived every later revalidation. **That fix was half a fix, and the second half is the thing to remember: making the updater functional is not enough if its fallback is not.** `withDelta(current ?? municipalityIds, …)` still read the render that created the closure whenever `current` was null — which is the normal state by the time an undo is pressed, since the server reconcile clears it — so the identical bug survived under a shape that reads as correct. The baseline is now a ref (`latestIds`) tracking `effectiveIds`, advanced **synchronously** in the `draft` branch so two toggles batched into one tick cannot drop each other, and the whole sequence is pinned in `municipalityPortfolioCell.unit.spec.ts` (verified red against the old code). Worth knowing that the two reviewers **disagreed** here — the performance pass read the functional `withDelta` and cleared it; only tracing the fallback branch finds it. And the `saved`/`errorMessage` pair was **sticky**, so from the first save onward the live region said "Municípios salvos." forever and never announced the result count again — the one thing an ARIA combobox needs; they collapsed into one `feedback`, cleared on typing, with an explicit priority (pending > error > searching > saved). Also: the pointer policy had been encoded **twice** (a `matchMedia('(pointer: fine)')` in the root `onClick` next to the `pointer-*` variants — the coarse overlay now stops propagation and the policy lives only in CSS); `MunicipalityPortfolioIndexEntry.city`/`zoneNumber` were read by **nothing** (TI/ZE resolution runs against the static catalog by slug) and were shipping in 435 entries of two routes' RSC payload; the advisor route's local `parseMunicipalityIds` (which swallowed invalid ids silently) gave way to the shared `repeatedRelationshipFormValues`; the advisor-scope refusal became `LEADERSHIP_MUNICIPALITY_SCOPE_MESSAGE` (a bare literal in 4 files, matched by `safeMessages` on exact string); the membership helper returns `{ next, added }` instead of letting the caller re-derive `added` for the scope check; and `onPersisted` died because `setAdvisorMunicipalitiesBatch` already revalidates. **One fix was created by the simplification itself:** unifying the suggestion gate would have opened the inline list — which is **portaled**, so `pointer-coarse:hidden` cannot reach it — on top of the Drawer, hence separate `inlineSuggesting` / `drawerSuggesting`, which also closes the pre-existing case of opening the Drawer with the inline popover already up. **Post-rebase `/simplify`, where a premise of this delivery died:** the rebase brought B42's `CampaignCellEditOverlay`, judged inapplicable because "it switches on viewport and B34 switches on pointer" — **false in the code**. The container switches on nothing: `variant` is a prop, there is no `matchMedia` in it, and `LeadershipListSupportStatusControl` in this same table already hardcodes `"popover"` because a caller that knows its own policy just states it. B34's Drawer half now passes `variant="sheet"` unconditionally with `pointer-fine:hidden` on `triggerClassName`, so the pointer policy stays in CSS — and the point is not the ~25 lines (14 of them verbatim `DrawerHeader`/`DrawerFooter`) but `initialFocus={titleRef}`: the hand-rolled Drawer handed focus to the **first chip**, a link that navigates away from the row being edited, or to the search field on a draft row, which is the virtual-keyboard-over-the-sheet failure B42 had already fixed once. The trigger also gained the `aria-haspopup="dialog"`/`aria-expanded`/`aria-busy` every other cell trigger in both lists has, and the cell became the **5th case** of the shared table-driven spec — the Drawer half had no coverage at all, since e2e runs on a fine pointer. **Adopting cost one real bug, worth knowing before adopting the shell elsewhere:** an always-mounted `Drawer` reports `onOpenChange(false)` when a dismissal anywhere on the page sweeps the layer stack, not only when it closes — so the new "clear the search when the sheet closes" handler was erasing what the **fine** pointer had just typed inline (caught by e2e, which passes only in file order; guard is `if (!next && drawerOpen)`). Fixed in the same pass, and it is behaviour rather than style: `setAdvisorMunicipalitiesBatch` revalidated only the **last** município of a batch (`lastSlug`) — pre-existing from B19, but B34 made the batch the _only_ advisor write path and put a território chip within thumb reach, so dropping a 30-município TI left 29 detail pages stale; it now returns `slugs: string[]`. **Rejected with reason:** the container's **Popover** branch must not be adopted — it yields `role="dialog"` behind a `PopoverTrigger`, and a dialog cannot be the popup of an ARIA 1.2 combobox.
+**Recently resolved (2026-07-27):** **B34** Chips editáveis de municípios na lista de lideranças — the "Municípios" column of `/campanha/liderancas` was `municipalityNames.join(', ')`, dead text with no link; it is now chips that navigate to `/campanha/municipios/[slug]`, with a territory chip (`MapPinIcon` + count) when the leadership covers a whole TI, and it is **editable in place**. **The decision that generalizes beyond this item: what picks inline vs. drawer is the `pointer:` media query, not viewport width.** On `pointer-fine` the editor is always mounted — the "×" sits at the chip's corner revealed on hover/focus (absolutely positioned, so revealing it moves nothing) and a search input trails the chip row; on `pointer-coarse` a tap anywhere in the cell opens a **Drawer** with 44 px targets, because a thumb scrolling a table must not be asked to distinguish "open the município" from "delete the link". A tablet with a mouse gets the inline editor; a touch laptop gets the Drawer. This **amends** the anti-goal "always-mounted inputs on every list row" (`PRODUCT.md` principle 3 + `.agents/rules/campanha-edit-where-you-see.mdc`) **for chip-based relations on fine pointers** — the input IS the readout there, and a per-row Popover would add a click to a target already on screen. It does **not** reopen B28/B31/B32: nothing here is a table mode, and `AdvisorsTable`'s toggle was renamed **"Editar nome e contato"**, which is all it still unlocks. Engineering, all divergences from the plan measured: `AdvisorMunicipalityCell` **was** promoted to `src/components/campaign/shared/MunicipalityPortfolioCell.tsx` (the plan rejected it — but the `ResizeObserver` + "Ver mais…" machinery it called dead weight became the requirement the moment the cell was always visible), and `/campanha/assessores` migrated onto the shared piece in the same PR, so the 2nd call site was born with the 1st; **no context provider** (the catalog index and `addableIds` travel as props of the column factory the RSC builds once per table); **one batch action only** — `setLeadershipMunicipalitiesMembership` (repeated `municipalityIds` + `assigned`), the single-id twin never existed, and the advisor twin (`setAdvisorMunicipalityMembershipRecord` + its schema) was **deleted** with its int tests migrated to the batch path; `LeadershipRowViewModel.municipalityNames` → `municipalityIDs: number[]` (names/slugs resolve client-side against the catalog the cell already holds); the floor of 1 uses `aria-disabled` + the reason in a toast rather than `disabled`, because `disabled` drops the button from the tab order and the reason lived only in its `aria-label`; the cap of 30 is refused client-side **before** the optimistic apply, so a TI batch does not bounce back whole with a message that never says which limit blew. **`/impeccable` closed two real P0/P1s:** suggestions were mouse-only (now a full ARIA 1.2 combobox — arrows/Home/End/Enter, `aria-activedescendant`, portaled listbox with its own name, "Nenhum resultado." kept **outside** the listbox) and auto-save was mute (`aria-busy` + dimmed row + `sr-only` live region, rollback scoped to the failed delta so a neighbouring saved toggle is not reverted, plus **undo** on batch removal); target size of the "×" went 16 → **24 px** via `before:-inset-1` (SC 2.5.8 has no mouse exemption, and the 44 px Drawer equivalent is `pointer-fine:hidden`), the "×" border got real contrast (the token's is 1.17:1 on a `secondary` chip), and the chips are links **inside the Drawer** again. **`/simplify` caught two real bugs the gate could not see**, both in the same shape — state captured by the render that scheduled the work. Every optimistic delta is now applied **functionally**, because the success path computed from the `assignedIds` of its own render: the toast's "Desfazer", pressed after a later add, dropped that add from the UI while the server kept it, and the reconcile effect only clears the optimistic value when both id lists match exactly, so the divergence outlived every later revalidation. **That fix was half a fix, and the second half is the thing to remember: making the updater functional is not enough if its fallback is not.** `withDelta(current ?? municipalityIds, …)` still read the render that created the closure whenever `current` was null — which is the normal state by the time an undo is pressed, since the server reconcile clears it — so the identical bug survived under a shape that reads as correct. The baseline is now a ref (`latestIds`) tracking `effectiveIds`, advanced **synchronously** in the `draft` branch so two toggles batched into one tick cannot drop each other, and the whole sequence is pinned in `municipalityPortfolioCell.unit.spec.ts` (verified red against the old code). Worth knowing that the two reviewers **disagreed** here — the performance pass read the functional `withDelta` and cleared it; only tracing the fallback branch finds it. And the `saved`/`errorMessage` pair was **sticky**, so from the first save onward the live region said "Municípios salvos." forever and never announced the result count again — the one thing an ARIA combobox needs; they collapsed into one `feedback`, cleared on typing, with an explicit priority (pending > error > searching > saved). Also: the pointer policy had been encoded **twice** (a `matchMedia('(pointer: fine)')` in the root `onClick` next to the `pointer-*` variants — the coarse overlay now stops propagation and the policy lives only in CSS); `MunicipalityPortfolioIndexEntry.city`/`zoneNumber` were read by **nothing** (TI/ZE resolution runs against the static catalog by slug) and were shipping in 435 entries of two routes' RSC payload; the advisor route's local `parseMunicipalityIds` (which swallowed invalid ids silently) gave way to the shared `repeatedRelationshipFormValues`; the advisor-scope refusal became `LEADERSHIP_MUNICIPALITY_SCOPE_MESSAGE` (a bare literal in 4 files, matched by `safeMessages` on exact string); the membership helper returns `{ next, added }` instead of letting the caller re-derive `added` for the scope check; and `onPersisted` died because `setAdvisorMunicipalitiesBatch` already revalidates. **One fix was created by the simplification itself:** unifying the suggestion gate would have opened the inline list — which is **portaled**, so `pointer-coarse:hidden` cannot reach it — on top of the Drawer, hence separate `inlineSuggesting` / `drawerSuggesting`, which also closes the pre-existing case of opening the Drawer with the inline popover already up. **Post-rebase `/simplify`, where a premise of this delivery died:** the rebase brought B42's `CampaignCellEditOverlay`, judged inapplicable because "it switches on viewport and B34 switches on pointer" — **false in the code**. The container switches on nothing: `variant` is a prop, there is no `matchMedia` in it, and `LeadershipListSupportStatusControl` in this same table already hardcodes `"popover"` because a caller that knows its own policy just states it. B34's Drawer half now passes `variant="sheet"` unconditionally with `pointer-fine:hidden` on `triggerClassName`, so the pointer policy stays in CSS — and the point is not the ~25 lines (14 of them verbatim `DrawerHeader`/`DrawerFooter`) but `initialFocus={titleRef}`: the hand-rolled Drawer handed focus to the **first chip**, a link that navigates away from the row being edited, or to the search field on a draft row, which is the virtual-keyboard-over-the-sheet failure B42 had already fixed once. The trigger also gained the `aria-haspopup="dialog"`/`aria-expanded`/`aria-busy` every other cell trigger in both lists has, and the cell became the **5th case** of the shared table-driven spec — the Drawer half had no coverage at all, since e2e runs on a fine pointer. **Adopting cost one real bug, worth knowing before adopting the shell elsewhere:** an always-mounted `Drawer` reports `onOpenChange(false)` when a dismissal anywhere on the page sweeps the layer stack, not only when it closes — so the new "clear the search when the sheet closes" handler was erasing what the **fine** pointer had just typed inline (caught by e2e, which passes only in file order; guard is `if (!next && drawerOpen)`). Fixed in the same pass, and it is behaviour rather than style: `setAdvisorMunicipalitiesBatch` revalidated only the **last** município of a batch (`lastSlug`) — pre-existing from B19, but B34 made the batch the _only_ advisor write path and put a território chip within thumb reach, so dropping a 30-município TI left 29 detail pages stale; it now returns `slugs: string[]`. **Rejected with reason:** the container's **Popover** branch must not be adopted — it yields `role="dialog"` behind a `PopoverTrigger`, and a dialog cannot be the popup of an ARIA 1.2 combobox.
**Latent bug fixed for free:** `CampaignTableHead`'s `description` seam (**B22**) broke **any** RSC caller — `campaignHoverTooltipAlign`/`campaignHoverExplanationClassName` lived in the `'use client'` module of `CampaignHoverTooltip`, so the server was calling a client reference; they moved to `src/lib/campaignHoverTooltip.ts`, and the leaderships list is the first RSC header with an explanation. **Post-merge `/simplify` follow-up (same day), all measured:** the perf pass cut real CPU — `buildMunicipalityPortfolioChips` 4.2 → **0.78 ms** cold per 25-row table and **0.043 ms** on every later render (a `WeakMap` keyed on the index array, which one RSC builds once and all rows share, so it survives re-renders where a per-row `useMemo` would not), and search 4.59 → **0.077 ms** per keystroke (normalize the query once instead of inside the matcher for each of ~660 candidates; `matchesNormalizedAtWordStart` is the new shared entry point and `matchesAtWordStart` delegates to it). Also: `sameIdSet` replaced a hand-rolled sorted-key comparison (3rd call site of a single-purpose helper), `scrollIntoView` is keyboard-only (the mouse is already where it points — it was a forced layout per option the pointer crossed), and `revalidatePath` now uses the real delta (`changed`), so removing a município that was never linked no longer busts its route. **One reviewer suggestion was rejected by measurement, and it looked obvious:** dropping `sort: 'name'` from the loader ("nothing depends on the DB ordering") **broke the suggestion list** — `searchMunicipalityPortfolio` walks the index in arrival order and stops at `limit`, so the alphabetical suggestions were alphabetical only because of that `ORDER BY`. The B34 e2e caught it; the sort is back with the reason written beside it. **The finding that did NOT get fixed, because both fixes oppose each other:** the delivery costs **+58 kB** First Load JS on `/campanha/liderancas` (246 → 304) and **+42 kB** on `/campanha/assessores` (253 → 295), measured with two full builds against a stashed clean `main`. Assessores already imported the static Bahia catalog, so its +42 kB is the **cell**, not the catalog — only ~16 kB of lideranças' delta is the catalog reaching a new route. Pulling the catalog out of the browser fattens the payload of every chip toggle; shrinking the payload to `{id,slug}` keeps the bundle. Registered as **B34+** with the recommendation (payload-minimal) and its one real caveat. No migration, collection, `Consent` or URL change. **Debt with a trigger:** `LeadershipStateDeputyRelationCell` (B31) keeps its own Popover and measuring machinery in the same table — **it**, not B37, is the extraction trigger for a relation-agnostic chip editor (B37 plugs the existing shared cell into a third relation without new machinery). **Measured, and now P1 in the ledger:** two e2e tests fail **deterministically at one worker on a clean `main` tree** (verified by stashing this whole delivery) — `campaignNearestMunicipality:156` and `campaignMunicipalities:394`. That is not the fichado 2-worker flake; do not dismiss a red `pnpm test:e2e` as flakiness without re-checking those two by name. Plano: [`docs/plans/chips-municipios-lista-liderancas.md`](docs/plans/chips-municipios-lista-liderancas.md).
**Recently resolved (2026-07-27):** **`users.roles`** — Payload admin RBAC. Field `roles` (`admin` | `editor`) on `users` (migration `20260727_032523_add_users_roles`, backfill `admin`); `isPayloadAdmin` requires the `admin` role; editors open `/admin` and write `post`/`tag`/`media`; PII/`petition`/campaign collections stay admin-only. Unifying `users` + `campaignUser` was rejected (25 FKs; structural isolation). Plano: [`docs/plans/roles-admin-payload.md`](docs/plans/roles-admin-payload.md).
diff --git a/docs/GUARDRAILS.md b/docs/GUARDRAILS.md
index e415c2e7..73e4b963 100644
--- a/docs/GUARDRAILS.md
+++ b/docs/GUARDRAILS.md
@@ -8,7 +8,7 @@ Ledger dos guardrails determinísticos colhidos de `kind:agent-miss` (fluxo `pnp
| Guarda | Origem | Classe | Mecanismo | Status |
| ---------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- |
-| `agent:ship` exige stamp de /simplify casado com HEAD; PR Ready + auto-merge + watch até MERGED | [#83](https://github.com/fsolla/teqo/issues/83) | 3 (script + hooks) | `scripts/agent-ship.mjs`, `.cursor/hooks/guard-pr-create.mjs` (bloqueia `gh pr create` manual), `.cursor/hooks/pr-merge-watchdog.mjs` (re-engata com PR aberto) | vivo desde 2026-07-31 (PR #89); miss fechada no Pass 4 |
+| `agent:ship` exige stamp de /simplify casado com HEAD; PR Ready + auto-merge + watch até MERGED | [#83](https://github.com/fsolla/teqo/issues/83) | 3 (script + hooks) | `scripts/agent-ship.mjs`, `.agents/hooks/guard-pr-create.mjs` (bloqueia `gh pr create` manual), `.agents/hooks/pr-merge-watchdog.mjs` (re-engata com PR aberto) | vivo desde 2026-07-31 (PR #89); miss fechada no Pass 4 |
| Alocador de municípios em specs — sem slug fixo + mutação; sem `municipio=` em e2e; regex de nome de catálogo ancorada | [#73](https://github.com/fsolla/teqo/issues/73) | 3 | `tests/unit/testMunicipalityAllocatorConventions.unit.spec.ts` (3 regras; mutações = assign/touch/create\*/`payload.update` municipality) | PR [#93](https://github.com/fsolla/teqo/pull/93) (Pass 4) |
| Sheet compartilhada por lista — chip cells exigem `CampaignListSheetProvider`; invariante dev no overlay | [#52](https://github.com/fsolla/teqo/issues/52) | 3 + 5 | `tests/unit/sharedSheetHostConventions.unit.spec.ts`; `console.error` development-only em `CampaignCellEditOverlay` | PR [#94](https://github.com/fsolla/teqo/pull/94) (Pass 4) |
| Navegação e2e assentada — goto→goto sem settle falha o build | [#54](https://github.com/fsolla/teqo/issues/54) | 3 | `tests/unit/e2eNavigationConventions.unit.spec.ts` (settle = await expect/waitForURL/click/reload/login/catch) | PR [#95](https://github.com/fsolla/teqo/pull/95) (Pass 4) |
diff --git a/docs/IMPROVE-CODE-QUALITY-PLAN.md b/docs/IMPROVE-CODE-QUALITY-PLAN.md
index 1e157d09..41bc1d07 100644
--- a/docs/IMPROVE-CODE-QUALITY-PLAN.md
+++ b/docs/IMPROVE-CODE-QUALITY-PLAN.md
@@ -71,7 +71,7 @@ Statuses: pending · in-progress · awaiting-evidence · done · deferred: \, params: [...] }` no body do `POST /v1/agents`. A API valida contra
`GET /v1/models` (ids + aliases + params, ex.: `composer-2.5` com param `fast`).
- **Namespace:** os slugs do repo (Task tool) e os da Cloud API são tabelas irmãs, não idênticas.
@@ -274,7 +274,7 @@ rotação documentada no runbook). `GITHUB_TOKEN` com `issues: write` (flip/come
| `tests/unit/agentPoolModels.unit.spec.ts` | novo — mapping/fallback/validação | 2 |
| `tests/unit/agentPoolPrompt.unit.spec.ts` | novo — contrato do prompt (issue N, sem claim, PR base stage) | 2 |
| `docs/plans/agent-pool-orchestrator.md` | este plano + resultados do spike | 0 |
-| `.cursor/skills/agent-pool/SKILL.md` | novo — runbook humano (start/stop/status/triage) | 4 |
+| `.agents/skills/agent-pool/SKILL.md` | novo — runbook humano (start/stop/status/triage) | 4 |
| `docs/AGENT-OPS.md` | linha na tabela de comandos + seção curta de secrets | 4 |
| `docs/CHANGELOG-AGENTS.md` | uma entrada curta | 4 |
@@ -347,7 +347,7 @@ ou deixar bloqueada.
- **Fase 0 — spike (30–45 min, antes de qualquer PR):** `GET /v1/me` + `GET /v1/models` (dump
commitado neste plano); spawn de 1 agente de teste no repo com `model.id` explícito e prompt
- trivial; confirmar: boot via `.cursor/environment.json`, `gh` autenticado no worker, tempo real
+ trivial; confirmar: boot via `.agents/environment.json`, `gh` autenticado no worker, tempo real
de boot, formato exato do `agentId` cliente (`bc-…`) e o `409` no re-POST. Resultado: seção de
evidências atualizada + tabela de mapeamento de modelos.
- **Fase 1 — fundação read-only (appetite ~2–3h de sessão):** PR A — eligibility, state,
@@ -367,7 +367,7 @@ de spec — ops infra). **Schedule só ativa após o promote humano a `main`.**
- `pnpm agent:promote` (promote `stage→main` é humano — o pool nunca chama).
- Qualquer `DATABASE_URL` de stage/prod no supervisor ou nos workers (Cloud usa
- `.cursor/cloud-setup.sh` + seed mínimo; o workflow não recebe secrets de banco).
+ `.agents/cloud-setup.sh` + seed mínimo; o workflow não recebe secrets de banco).
- Editar Issues `in-progress` de outros agentes/humanos (o pool só toca as que ele claimou).
- Substituir ou alterar o fluxo `plan-issue` → `work-issue` para Issues normais.
- Supervisor local (`/loop`, `while` em shell, daemon em VPS) como default — rejeitado.
@@ -488,7 +488,7 @@ EXPIRED`, com `git.branches[].prUrl`. SSE stream existe mas não é necessária
cron "pode atrasar, nunca adianta". Avaliado como supervisor e rejeitado para v1 (D1); fica
como plano B de superfície.
Fonte: .
-7. **Ambiente do worker:** repo-file managed (`.cursor/environment.json` presente → install
+7. **Ambiente do worker:** repo-file managed (`.agents/environment.json` presente → install
`pnpm install + cloud-setup.sh`: Postgres nativo + migrate + seed mínimo, sem secrets de
stage/prod). Confirmado pelo MCP `cursor-cloud-environment-info` deste próprio run.
8. **MCP `cursor-cloud`** (diagnóstico): `list-cloud-agents` com filtros de status/fonte,
@@ -500,7 +500,7 @@ EXPIRED`, com `git.branches[].prUrl`. SSE stream existe mas não é necessária
Verificado neste run (Cloud Agent `bc-9cd1a18c-…`, repo fsolla/teqo):
- **Environment repo-file confirmado em produção:** o `environment-info` deste run reporta
- `source: Repository` + `environmentJsonPath: .cursor/environment.json` — workers spawnados
+ `source: Repository` + `environmentJsonPath: .agents/environment.json` — workers spawnados
pela API neste repo bootam com `pnpm install` + `cloud-setup.sh` (Postgres nativo + migrate +
seed mínimo), sem secrets de stage/prod. Item 7 acima confirmado empiricamente.
- **Formato de id:** runs usam `bc-` (este run: `bc-9cd1a18c-3f2b-4661-b18b-401db63c1182`),
diff --git a/docs/plans/ajuste-votos-wizard.md b/docs/plans/ajuste-votos-wizard.md
index ed6b86e1..93506d6e 100644
--- a/docs/plans/ajuste-votos-wizard.md
+++ b/docs/plans/ajuste-votos-wizard.md
@@ -165,4 +165,4 @@ Componentes:
- [fluxos-acao-primeiro-inicio.md](fluxos-acao-primeiro-inicio.md) · [cenarios-estimativa-votos.md](cenarios-estimativa-votos.md) · `src/lib/voteEstimate.ts` · `MunicipalityListExpectedVotesControl.tsx` · `VoteEstimateScenarioInputs.tsx` · collection `Municipality` hook de ordem
- AGENTS.md — assimetria leader; `overrideAccess: false`
-- `PRODUCT.md` / `DESIGN.md` · `.cursor/rules/campanha-edit-where-you-see.mdc` (exceção submit explícito)
+- `PRODUCT.md` / `DESIGN.md` · `.agents/rules/campanha-edit-where-you-see.mdc` (exceção submit explícito)
diff --git a/docs/plans/autosave-status-lista-liderancas.md b/docs/plans/autosave-status-lista-liderancas.md
index 9d584901..3991d0e5 100644
--- a/docs/plans/autosave-status-lista-liderancas.md
+++ b/docs/plans/autosave-status-lista-liderancas.md
@@ -13,7 +13,7 @@ Implementado como planejado, com duas defasagens (o repo andou desde o rascunho
## Design (Impeccable)
-Âncoras: `PRODUCT.md` (princípio 3 **Edit where you see**, princípio 4 **Auto-save, no Save button**, princípio 8 **Feel the action**) / `DESIGN.md` (register `product` — Field Desk) · tema `data-theme='campaign'` · regras `.cursor/rules/campanha-edit-where-you-see.mdc` e `.cursor/rules/campanha-action-feedback.mdc` · precedente vivo: [`MunicipalityListExpectedVotesControl.tsx`](../../src/components/campaign/municipality/MunicipalityListExpectedVotesControl.tsx) (máquina de auto-save com rollback) e o [`MunicipalityListTrendControl.tsx`](../../src/components/campaign/municipality/MunicipalityListTrendControl.tsx) atual (Popover + select — o que este item **não** deve repetir é o botão "Salvar" dele, hoje ainda sem o passe do B24).
+Âncoras: `PRODUCT.md` (princípio 3 **Edit where you see**, princípio 4 **Auto-save, no Save button**, princípio 8 **Feel the action**) / `DESIGN.md` (register `product` — Field Desk) · tema `data-theme='campaign'` · regras `.agents/rules/campanha-edit-where-you-see.mdc` e `.agents/rules/campanha-action-feedback.mdc` · precedente vivo: [`MunicipalityListExpectedVotesControl.tsx`](../../src/components/campaign/municipality/MunicipalityListExpectedVotesControl.tsx) (máquina de auto-save com rollback) e o [`MunicipalityListTrendControl.tsx`](../../src/components/campaign/municipality/MunicipalityListTrendControl.tsx) atual (Popover + select — o que este item **não** deve repetir é o botão "Salvar" dele, hoje ainda sem o passe do B24).
Na implementação (`implement-roadmap-item`): craft compacto → critique → polish (sem shape — é um único select sobre um campo que já existe, na mesma família visual dos outros popovers de lista).
diff --git a/docs/plans/autosave-tendencia-lista-municipios.md b/docs/plans/autosave-tendencia-lista-municipios.md
index 7969af15..01e01a64 100644
--- a/docs/plans/autosave-tendencia-lista-municipios.md
+++ b/docs/plans/autosave-tendencia-lista-municipios.md
@@ -13,7 +13,7 @@ Responsável: —
## Design (Impeccable)
-Âncoras: `PRODUCT.md` (princípio 3 **Edit where you see**, princípio 4 **Auto-save, no Save button**, princípio 8 **Feel the action**) / `DESIGN.md` (register `product` — Field Desk) · tema `data-theme='campaign'` · regras `.cursor/rules/campanha-edit-where-you-see.mdc` e `.cursor/rules/campanha-action-feedback.mdc` · precedente vivo: [`MunicipalityListExpectedVotesControl.tsx`](../../src/components/campaign/municipality/MunicipalityListExpectedVotesControl.tsx).
+Âncoras: `PRODUCT.md` (princípio 3 **Edit where you see**, princípio 4 **Auto-save, no Save button**, princípio 8 **Feel the action**) / `DESIGN.md` (register `product` — Field Desk) · tema `data-theme='campaign'` · regras `.agents/rules/campanha-edit-where-you-see.mdc` e `.agents/rules/campanha-action-feedback.mdc` · precedente vivo: [`MunicipalityListExpectedVotesControl.tsx`](../../src/components/campaign/municipality/MunicipalityListExpectedVotesControl.tsx).
Na implementação (`implement-roadmap-item`): craft compacto → critique → polish (sem shape — o popover já existe e o padrão de auto-save já está no produto ao lado, na mesma linha da tabela).
diff --git a/docs/plans/chips-liderancas-lista-dobradinhas.md b/docs/plans/chips-liderancas-lista-dobradinhas.md
index 37449d3a..3da6a45d 100644
--- a/docs/plans/chips-liderancas-lista-dobradinhas.md
+++ b/docs/plans/chips-liderancas-lista-dobradinhas.md
@@ -20,7 +20,7 @@ As-built bate com as Decisões travadas de mutação/reuso; só o packaging de U
## Design (Impeccable)
-Âncoras: `PRODUCT.md` (princípios 3 **Edit where you see**, 4 **Auto-save, no Save button**, 8 Feel the action; anti-goals "spreadsheet mode" e "always-mounted inputs on every row") / `DESIGN.md` (register `product`; Field Desk) · tema `data-theme='campaign'` · precedentes de interação [`AdvisorMunicipalityCell.tsx`](../../src/components/campaign/advisor/AdvisorMunicipalityCell.tsx) (B19 ✓ — chips removíveis + typeahead + otimista + `ResizeObserver`/"Ver mais…") e o desenho ainda não implementado do **B31** ([`dobradinhas-lista-liderancas.md`](dobradinhas-lista-liderancas.md) — chips + Popover + `Command`, sem "Ver mais…", para a mesma relação `leadership.stateDeputies` na direção oposta) · shells `CampaignTable` / `CampaignPageShell` · regras `.cursor/rules/campanha-edit-where-you-see.mdc` e `campanha-action-feedback.mdc`.
+Âncoras: `PRODUCT.md` (princípios 3 **Edit where you see**, 4 **Auto-save, no Save button**, 8 Feel the action; anti-goals "spreadsheet mode" e "always-mounted inputs on every row") / `DESIGN.md` (register `product`; Field Desk) · tema `data-theme='campaign'` · precedentes de interação [`AdvisorMunicipalityCell.tsx`](../../src/components/campaign/advisor/AdvisorMunicipalityCell.tsx) (B19 ✓ — chips removíveis + typeahead + otimista + `ResizeObserver`/"Ver mais…") e o desenho ainda não implementado do **B31** ([`dobradinhas-lista-liderancas.md`](dobradinhas-lista-liderancas.md) — chips + Popover + `Command`, sem "Ver mais…", para a mesma relação `leadership.stateDeputies` na direção oposta) · shells `CampaignTable` / `CampaignPageShell` · regras `.agents/rules/campanha-edit-where-you-see.mdc` e `campanha-action-feedback.mdc`.
Na implementação (`implement-roadmap-item`): craft compacto (a maior parte da interação já foi decidida no B31; craft aqui é sobretudo a direção nova + o clamp de overflow) → critique → polish. `harden`/`optimize` só sob gatilho do Passo 8.
@@ -138,4 +138,4 @@ Depth check: reusa `CampaignTable`, `Popover`/`Command`/`Badge` do kit, `wordSta
- `src/utilities/access/leaderships.ts` (`canReadLeadership`, `canManageLeadership`) — access que a leitura/escrita reusa sem alterar
- `src/lib/wordStartFilter.ts` (`matchesAtWordStart`, `normalizeSearchPhrase`) — busca acento-insensível reusada
- AGENTS.md — Campaign Municípios model, Transaction Safety, naming em inglês/copy em pt-BR
-- `.cursor/rules/campanha-edit-where-you-see.mdc`, `.cursor/rules/campanha-action-feedback.mdc` — princípios de UX herdados de Assessores/B31
+- `.agents/rules/campanha-edit-where-you-see.mdc`, `.agents/rules/campanha-action-feedback.mdc` — princípios de UX herdados de Assessores/B31
diff --git a/docs/plans/chips-municipios-lista-dobradinhas.md b/docs/plans/chips-municipios-lista-dobradinhas.md
index fba79b75..ec64d0db 100644
--- a/docs/plans/chips-municipios-lista-dobradinhas.md
+++ b/docs/plans/chips-municipios-lista-dobradinhas.md
@@ -172,4 +172,4 @@ Depth check: reusa `Popover`/`Command`/`Badge` do kit, `CampaignTable`, a lib pu
- [`campaignRelationOptions.ts`](../../src/utilities/campaignRelationOptions.ts) (`loadMunicipalityOptions`) — escopo do assessor para sugestões
- [`combobox-assessores-lista-municipios.md`](combobox-assessores-lista-municipios.md) (**B27**), [`dobradinhas-lista-liderancas.md`](dobradinhas-lista-liderancas.md) (**B31**), [`gerenciar-assessores.md`](gerenciar-assessores.md) (**B19 ✓**), [`ordenacao-filtros-lista-dobradinhas.md`](ordenacao-filtros-lista-dobradinhas.md) (**B33**)
- AGENTS.md — Campaign Municípios model, Transaction Safety, naming inglês/copy pt-BR
-- `.cursor/rules/campanha-edit-where-you-see.mdc`, `.cursor/rules/campanha-action-feedback.mdc`
+- `.agents/rules/campanha-edit-where-you-see.mdc`, `.agents/rules/campanha-action-feedback.mdc`
diff --git a/docs/plans/chips-municipios-lista-liderancas.md b/docs/plans/chips-municipios-lista-liderancas.md
index 773b64ea..9f4094a1 100644
--- a/docs/plans/chips-municipios-lista-liderancas.md
+++ b/docs/plans/chips-municipios-lista-liderancas.md
@@ -9,7 +9,7 @@ Responsável: —
## Entregue (2026-07-27) — as-built
-**O conflito foi resolvido a favor do pedido literal, emendando o anti-goal em vez de reabrir B28/B31/B32.** A coluna é **sempre editável em ponteiro fino** (`pointer-fine`): "×" no canto do chip só no hover/foco (posicionado em `absolute`, para revelar não mexer no tamanho do chip nem na linha) e input de busca no fim da linha de chips. Em **ponteiro grosso** (`pointer-coarse`) o toque em qualquer ponto da célula abre um **Drawer** com "×" de 44 px — um polegar rolando a tabela não pode ser convidado a distinguir "abrir o município" de "apagar o vínculo". O que decide não é largura de viewport, é a mídia `pointer:` (um tablet com mouse recebe o editor inline; um laptop touch recebe o Drawer). `PRODUCT.md` (princípio 3) e `.cursor/rules/campanha-edit-where-you-see.mdc` passaram a admitir input sempre montado **para relações por chips em `pointer-fine`**: o input É o readout, e um Popover por linha seria um clique a mais para um alvo que já está na tela. Nada aqui virou "modo de tabela" — o toggle de `AdvisorsTable` foi renomeado para **"Editar nome e contato"**, que é tudo o que ele ainda destrava depois desta entrega.
+**O conflito foi resolvido a favor do pedido literal, emendando o anti-goal em vez de reabrir B28/B31/B32.** A coluna é **sempre editável em ponteiro fino** (`pointer-fine`): "×" no canto do chip só no hover/foco (posicionado em `absolute`, para revelar não mexer no tamanho do chip nem na linha) e input de busca no fim da linha de chips. Em **ponteiro grosso** (`pointer-coarse`) o toque em qualquer ponto da célula abre um **Drawer** com "×" de 44 px — um polegar rolando a tabela não pode ser convidado a distinguir "abrir o município" de "apagar o vínculo". O que decide não é largura de viewport, é a mídia `pointer:` (um tablet com mouse recebe o editor inline; um laptop touch recebe o Drawer). `PRODUCT.md` (princípio 3) e `.agents/rules/campanha-edit-where-you-see.mdc` passaram a admitir input sempre montado **para relações por chips em `pointer-fine`**: o input É o readout, e um Popover por linha seria um clique a mais para um alvo que já está na tela. Nada aqui virou "modo de tabela" — o toggle de `AdvisorsTable` foi renomeado para **"Editar nome e contato"**, que é tudo o que ele ainda destrava depois desta entrega.
**Divergências do plano abaixo (que fica como registro do rascunho):**
@@ -166,4 +166,4 @@ Depth check: reusa `Popover`/`Command`/`Badge` do kit, `CampaignTable`, `wordSta
- [`MunicipalityEstimateScenarioContext.tsx`](../../src/components/campaign/municipality/MunicipalityEstimateScenarioContext.tsx) — precedente de provider cliente escopado
- `tests/int/campaignLeadershipActions.int.spec.ts`, `tests/int/campaignAdvisorManagement.int.spec.ts`, `tests/unit/advisorMunicipalityPortfolio.unit.spec.ts`, `tests/helpers/campaignFixtures.ts`
- AGENTS.md — Campaign Municípios model, Transaction Safety, naming inglês/copy pt-BR
-- `.cursor/rules/campanha-edit-where-you-see.mdc`, `.cursor/rules/campanha-action-feedback.mdc`
+- `.agents/rules/campanha-edit-where-you-see.mdc`, `.agents/rules/campanha-action-feedback.mdc`
diff --git a/docs/plans/ci-paralela-pr.md b/docs/plans/ci-paralela-pr.md
index 73772d88..bb5c76fb 100644
--- a/docs/plans/ci-paralela-pr.md
+++ b/docs/plans/ci-paralela-pr.md
@@ -86,5 +86,5 @@ Componentes:
- `.github/workflows/ci-pr.yml` (job único atual), `ci.yml`, `ci-stage.yml` (comentário do subset)
- `docs/AGENT-OPS.md` — tabela "CI por alvo" e "Contrato de PR"
-- AGENTS.md / `.cursor/rules/engineering-standards.mdc` — gate em duas velocidades, comandos bare
+- AGENTS.md / `.agents/rules/engineering-standards.mdc` — gate em duas velocidades, comandos bare
- Run 30558184687 — baseline de wall-clock (9m37s serial até a falha)
diff --git a/docs/plans/ci-proibir-closes-plans-only.md b/docs/plans/ci-proibir-closes-plans-only.md
index 4c3d8a07..e3f3b4ab 100644
--- a/docs/plans/ci-proibir-closes-plans-only.md
+++ b/docs/plans/ci-proibir-closes-plans-only.md
@@ -97,8 +97,8 @@ Componentes:
- `.github/workflows/ci-pr.yml` (job `checks` / `migration-lock` como paralelo barato)
- `.github/workflows/issue-done-on-main-merge.yml` — regex `closes|fixes`
- `.github/pull_request_template.md` — `Closes #` default
-- `.cursor/skills/plan-issue/SKILL.md` — Passo 6
-- `.cursor/rules/agent-pr-workflow.mdc`
+- `.agents/skills/plan-issue/SKILL.md` — Passo 6
+- `.agents/rules/agent-pr-workflow.mdc`
- `docs/GUARDRAILS.md`
- `scripts/ci-scope.mjs` — precedente de classificar diff
diff --git a/docs/plans/combobox-assessores-lista-municipios.md b/docs/plans/combobox-assessores-lista-municipios.md
index 651bb1a3..52060bde 100644
--- a/docs/plans/combobox-assessores-lista-municipios.md
+++ b/docs/plans/combobox-assessores-lista-municipios.md
@@ -9,7 +9,7 @@ Responsável: —
## Design (Impeccable)
-Âncoras: `PRODUCT.md` (princípio 2 clareza sob pressão, 3 **Edit where you see**, 4 **Auto-save, no Save button**, 8 **Feel the action**) / `DESIGN.md` (register `product` — Field Desk) · tema `data-theme='campaign'` · regras `.cursor/rules/campanha-edit-where-you-see.mdc` e `.cursor/rules/campanha-action-feedback.mdc` · precedentes vivos: [`MunicipalityListExpectedVotesControl.tsx`](../../src/components/campaign/municipality/MunicipalityListExpectedVotesControl.tsx) (auto-save por endpoint JSON, mesma linha da tabela), [`AdvisorMunicipalityCell.tsx`](../../src/components/campaign/advisor/AdvisorMunicipalityCell.tsx) (chips removíveis + busca + gravação por delta), [`MunicipalityHeaderFilter.tsx`](../../src/components/campaign/municipality/MunicipalityHeaderFilter.tsx) (busca acento-insensível dentro de Popover, na **mesma coluna**).
+Âncoras: `PRODUCT.md` (princípio 2 clareza sob pressão, 3 **Edit where you see**, 4 **Auto-save, no Save button**, 8 **Feel the action**) / `DESIGN.md` (register `product` — Field Desk) · tema `data-theme='campaign'` · regras `.agents/rules/campanha-edit-where-you-see.mdc` e `.agents/rules/campanha-action-feedback.mdc` · precedentes vivos: [`MunicipalityListExpectedVotesControl.tsx`](../../src/components/campaign/municipality/MunicipalityListExpectedVotesControl.tsx) (auto-save por endpoint JSON, mesma linha da tabela), [`AdvisorMunicipalityCell.tsx`](../../src/components/campaign/advisor/AdvisorMunicipalityCell.tsx) (chips removíveis + busca + gravação por delta), [`MunicipalityHeaderFilter.tsx`](../../src/components/campaign/municipality/MunicipalityHeaderFilter.tsx) (busca acento-insensível dentro de Popover, na **mesma coluna**).
Na implementação (`implement-roadmap-item`): craft compacto → critique → polish (sem shape — o Popover, a célula e os dois padrões que este item combina já estão no produto).
diff --git a/docs/plans/convite-whatsapp-lista-liderancas.md b/docs/plans/convite-whatsapp-lista-liderancas.md
index 35e52ab0..62758929 100644
--- a/docs/plans/convite-whatsapp-lista-liderancas.md
+++ b/docs/plans/convite-whatsapp-lista-liderancas.md
@@ -112,4 +112,4 @@ Componentes:
- `src/components/campaign/advisor/AdvisorsTable.tsx` (linhas ~86–93, ~411–437) — precedente de ícone de linha com disable por telefone
- `tests/unit/campaignInviteInteractions.unit.spec.ts`, `tests/int/campaignInvite.int.spec.ts` — padrão de teste da action/UI de convite existente
- AGENTS.md — Local API `overrideAccess: false` (já respeitado pela action reusada), Campaign auth
-- `PRODUCT.md` / `.cursor/rules/campanha-edit-where-you-see.mdc`, `.cursor/rules/campanha-action-feedback.mdc` — princípios de UI herdados
+- `PRODUCT.md` / `.agents/rules/campanha-edit-where-you-see.mdc`, `.agents/rules/campanha-action-feedback.mdc` — princípios de UI herdados
diff --git a/docs/plans/decisao-arquitetura-dominios.md b/docs/plans/decisao-arquitetura-dominios.md
index a7a118ca..19ef3965 100644
--- a/docs/plans/decisao-arquitetura-dominios.md
+++ b/docs/plans/decisao-arquitetura-dominios.md
@@ -37,5 +37,5 @@ Nenhum item bloqueado (moves per-domain em branches curtas); navegação de agen
## Referências
-- `.cursor/rules/engineering-standards.mdc` (regra lib/utilities), AGENTS.md (estrutura), auditoria de fronteiras 2026-07-25
+- `.agents/rules/engineering-standards.mdc` (regra lib/utilities), AGENTS.md (estrutura), auditoria de fronteiras 2026-07-25
- `src/utilities/campaignAccess.ts` (precedente de split within-layer com re-export surface)
diff --git a/docs/plans/desbloquear-pr50-ci.md b/docs/plans/desbloquear-pr50-ci.md
index 50a043b9..ef17ef25 100644
--- a/docs/plans/desbloquear-pr50-ci.md
+++ b/docs/plans/desbloquear-pr50-ci.md
@@ -74,4 +74,4 @@ Nenhum neste item.
- `.github/workflows/ci-pr.yml` — job `migration-lock`
- `tests/int/homeSearchLeaderships.int.spec.ts` + `tests/int/homeSearchDemands.int.spec.ts` (padrão-alvo)
- `scripts/lib/seed-minimal-manifest.mjs` — `MINIMAL_LEADERSHIPS`
-- `.cursor/skills/work-issue/SKILL.md` — Passo 6 (acompanhar checks até o merge)
+- `.agents/skills/work-issue/SKILL.md` — Passo 6 (acompanhar checks até o merge)
diff --git a/docs/plans/dobradinhas-lista-liderancas.md b/docs/plans/dobradinhas-lista-liderancas.md
index 574ff5ac..78f377e7 100644
--- a/docs/plans/dobradinhas-lista-liderancas.md
+++ b/docs/plans/dobradinhas-lista-liderancas.md
@@ -9,7 +9,7 @@ Responsável: —
## Design (Impeccable)
-Âncoras: `PRODUCT.md` (princípio 3 **Edit where you see**, 4 **Auto-save, no Save button**, 8 Feel the action; anti-goals "spreadsheet mode" e "always-mounted inputs on every row") / `DESIGN.md` (register `product`; Field Desk) · tema `data-theme='campaign'` · precedentes de interação [`AdvisorMunicipalityCell.tsx`](../../src/components/campaign/advisor/AdvisorMunicipalityCell.tsx) (B19 ✓ — chips removíveis + typeahead + otimista) e o popover de edição rápida da lista de municípios (B9 ✓ / **B27**) · shells `CampaignTable` / `CampaignPageShell` · regras `.cursor/rules/campanha-edit-where-you-see.mdc` e `campanha-action-feedback.mdc`.
+Âncoras: `PRODUCT.md` (princípio 3 **Edit where you see**, 4 **Auto-save, no Save button**, 8 Feel the action; anti-goals "spreadsheet mode" e "always-mounted inputs on every row") / `DESIGN.md` (register `product`; Field Desk) · tema `data-theme='campaign'` · precedentes de interação [`AdvisorMunicipalityCell.tsx`](../../src/components/campaign/advisor/AdvisorMunicipalityCell.tsx) (B19 ✓ — chips removíveis + typeahead + otimista) e o popover de edição rápida da lista de municípios (B9 ✓ / **B27**) · shells `CampaignTable` / `CampaignPageShell` · regras `.agents/rules/campanha-edit-where-you-see.mdc` e `campanha-action-feedback.mdc`.
Na implementação (`implement-roadmap-item`): craft compacto → critique → polish. `harden`/`optimize` só sob gatilho do Passo 8.
@@ -135,7 +135,7 @@ Depth check: reusa `CampaignTable`, `Popover`/`Command`/`Badge` do kit, `wordSta
- [`campaignRelationOptions.ts`](../../src/utilities/campaignRelationOptions.ts) — `loadStateDeputyOptions`
- [`combobox-assessores-lista-municipios.md`](combobox-assessores-lista-municipios.md) (B27) · [`email-celular-lista-liderancas.md`](email-celular-lista-liderancas.md) (B28) · [`ordenacao-filtros-lista-liderancas.md`](ordenacao-filtros-lista-liderancas.md) (B29) · [`convite-whatsapp-lista-liderancas.md`](convite-whatsapp-lista-liderancas.md) (B30) · [`gerenciar-assessores.md`](gerenciar-assessores.md) (B19 ✓)
- AGENTS.md — Campaign auth, naming, `overrideAccess: false`, escrita transacional, `Contact` join
-- `PRODUCT.md` / `DESIGN.md` — princípios 3, 4 e 8; Field Desk · `.cursor/rules/campanha-edit-where-you-see.mdc`
+- `PRODUCT.md` / `DESIGN.md` — princípios 3, 4 e 8; Field Desk · `.agents/rules/campanha-edit-where-you-see.mdc`
## Revisão na entrega (2026-07-26)
diff --git a/docs/plans/entrega-engenharia-p3.md b/docs/plans/entrega-engenharia-p3.md
index bb32a739..3515f6ee 100644
--- a/docs/plans/entrega-engenharia-p3.md
+++ b/docs/plans/entrega-engenharia-p3.md
@@ -1,6 +1,6 @@
# Pass 3 — Auditoria de engenharia e consolidação
-**Data:** 2026-07-28 · **Método:** skill [`engineering-audit`](../../.cursor/skills/engineering-audit/SKILL.md) (Pass-style; cinco varreduras paralelas read-only) · **Tracker:** [IMPROVE-CODE-QUALITY-PLAN.md](../IMPROVE-CODE-QUALITY-PLAN.md) · **Ledger:** [TECH-DEBT.md](../TECH-DEBT.md)
+**Data:** 2026-07-28 · **Método:** skill [`engineering-audit`](../../.agents/skills/engineering-audit/SKILL.md) (Pass-style; cinco varreduras paralelas read-only) · **Tracker:** [IMPROVE-CODE-QUALITY-PLAN.md](../IMPROVE-CODE-QUALITY-PLAN.md) · **Ledger:** [TECH-DEBT.md](../TECH-DEBT.md)
## Contexto e manchetes
diff --git a/docs/plans/escala-dry-pos-b2.md b/docs/plans/escala-dry-pos-b2.md
index 2b8ec5bb..c5c9f4b9 100644
--- a/docs/plans/escala-dry-pos-b2.md
+++ b/docs/plans/escala-dry-pos-b2.md
@@ -35,7 +35,7 @@ Este plano é o registro canônico desses follow-ups. Sem ele, o B3 pode embarca
## Questões em aberto
- **API pública pós-lazy: manter `getMunicipalityFeature` sync após warm, ou só async loaders?** **Recomendação:** loaders async (`loadMunicipalityGeometryModule()` / `loadTerritoryGeometryModule()`) que resolvem o módulo já indexado; os getters sync atuais viram métodos do módulo carregado (ou ficam em submodule lazy). Evitar Promise em cada hover do mapa.
-- ~~**Onde vive o helper de cache CLI?**~~ **RESOLVIDO POR PRECEDENTE (`capture-review-debts` pós-B8+, 2026-07-27):** `scripts/lib/`. O B8+ precisou compartilhar a política de escala de chão entre os dois scripts de geometria e criou **`scripts/lib/topology.mjs`** — o primeiro módulo da pasta. A F2 **pousa nele** (`scripts/lib/cached-download.mjs`) em vez de abrir um segundo endereço: dois lares para código de CLI é exatamente a duplicação que esta fase existe para remover. A recomendação anterior (`src/lib/cliCachedDownload.ts`) fica **rejeitada** — `src/lib/` é client-safe por contrato (`.cursor/rules/codebase-map.mdc`) e um helper que faz `mkdir`/`fetch`/`writeFile` não pertence a uma pasta que o bundle do browser pode alcançar, mesmo que hoje ninguém o importe de lá.
+- ~~**Onde vive o helper de cache CLI?**~~ **RESOLVIDO POR PRECEDENTE (`capture-review-debts` pós-B8+, 2026-07-27):** `scripts/lib/`. O B8+ precisou compartilhar a política de escala de chão entre os dois scripts de geometria e criou **`scripts/lib/topology.mjs`** — o primeiro módulo da pasta. A F2 **pousa nele** (`scripts/lib/cached-download.mjs`) em vez de abrir um segundo endereço: dois lares para código de CLI é exatamente a duplicação que esta fase existe para remover. A recomendação anterior (`src/lib/cliCachedDownload.ts`) fica **rejeitada** — `src/lib/` é client-safe por contrato (`.agents/rules/codebase-map.mdc`) e um helper que faz `mkdir`/`fetch`/`writeFile` não pertence a uma pasta que o bundle do browser pode alcançar, mesmo que hoje ninguém o importe de lá.
- **Unificar env `TSE_CACHE_DIR` / `GEOMETRIES_CACHE_DIR`?** **Recomendação:** não — um parâmetro `cacheDir` por chamada; cada script mantém sua env e pasta sob `data/`.
## Abordagem proposta
diff --git a/docs/plans/escala-dry-pos-b8f2.md b/docs/plans/escala-dry-pos-b8f2.md
index 5aa89fe9..80ac0d50 100644
--- a/docs/plans/escala-dry-pos-b8f2.md
+++ b/docs/plans/escala-dry-pos-b8f2.md
@@ -160,4 +160,4 @@ Componentes:
- `src/components/campaign/map/BahiaMap.tsx` (`:73-90` duas malhas) + `MunicipalityMapPanel.tsx` (ressalva; lista `zoneBreakdown` → fill-in [remover-lista-zonas-mapa-inicio.md](remover-lista-zonas-mapa-inicio.md)) — F3
- `src/utilities/municipalityMapNavigation.ts` + `src/lib/municipalityProximity.ts` + `src/lib/bahiaGeometriesTypes.ts` + `tests/helpers/featureBounds.ts` — F4
- AGENTS.md — geometrias B2/B8 F2, gate por entrega, `overrideAccess: false` com `user`
-- `.cursor/rules/engineering-standards.mdc` — "dead code dies immediately" (F4), depth check (F1/F2)
+- `.agents/rules/engineering-standards.mdc` — "dead code dies immediately" (F4), depth check (F1/F2)
diff --git a/docs/plans/feedback-pendente-compositor-giro.md b/docs/plans/feedback-pendente-compositor-giro.md
index adca4d6e..3ca0044d 100644
--- a/docs/plans/feedback-pendente-compositor-giro.md
+++ b/docs/plans/feedback-pendente-compositor-giro.md
@@ -111,5 +111,5 @@ Depth check: nenhum módulo novo — três edições em dois arquivos que já do
- `src/app/(campaign)/campanha/(app)/atividades/giros/page.tsx`
- `src/app/(campaign)/campanha/(app)/municipios/page.tsx` — precedente de montagem boundary + results
- `tests/unit/campaignListFilterNavigation.unit.spec.ts` — molde do teste
-- `.cursor/rules/engineering-standards.mdc` — § Loading feedback ("a região de RESULTADOS dima")
+- `.agents/rules/engineering-standards.mdc` — § Loading feedback ("a região de RESULTADOS dima")
- `PRODUCT.md` / `DESIGN.md` — Field Desk
diff --git a/docs/plans/gate-push-local.md b/docs/plans/gate-push-local.md
index 0b358b63..321dcfb3 100644
--- a/docs/plans/gate-push-local.md
+++ b/docs/plans/gate-push-local.md
@@ -15,7 +15,7 @@ Dados: N/A — chore de DX.
## Contexto
-`.husky/pre-push` é uma linha: `pnpm lint && pnpm format:check && pnpm typecheck`. Mas o fast gate exigido por `.cursor/rules/engineering-standards.mdc` (desde 2026-07-30) e por `work-issue` Passo 4.3 é **lint + tsc + test:unit + format:check + check:cycles**. Resultado: agentes e humanos pusham verde no hook e quebram no CI por unit/cycles que nunca rodaram localmente. Não existe pre-commit hook.
+`.husky/pre-push` é uma linha: `pnpm lint && pnpm format:check && pnpm typecheck`. Mas o fast gate exigido por `.agents/rules/engineering-standards.mdc` (desde 2026-07-30) e por `work-issue` Passo 4.3 é **lint + tsc + test:unit + format:check + check:cycles**. Resultado: agentes e humanos pusham verde no hook e quebram no CI por unit/cycles que nunca rodaram localmente. Não existe pre-commit hook.
Decisão de produto (2026-07-30, brief do lote CI): scripts DRY (`gate:fast`, `gate:push`), divisão pre-commit vs pre-push explícita, escape hatch documentado.
@@ -43,9 +43,9 @@ Componentes:
- **`package.json`** — scripts `gate:fast`, `gate:ci` (`scripts/gate-ci.mjs`) e `gate:push` (= `gate:ci`).
- **`.husky/pre-push`** — `pnpm gate:push`.
-- **`.cursor/rules/engineering-standards.mdc`** — item 3 do "After every change" passa a citar `pnpm gate:fast` / `pnpm gate:push`.
+- **`.agents/rules/engineering-standards.mdc`** — item 3 do "After every change" passa a citar `pnpm gate:fast` / `pnpm gate:push`.
- **`docs/AGENT-OPS.md`** — linha do fast gate local atualizada para os scripts.
-- **`.cursor/skills/work-issue/SKILL.md`** — Passo 4.3/6.1 cita `pnpm gate:push`.
+- **`.agents/skills/work-issue/SKILL.md`** — Passo 4.3/6.1 cita `pnpm gate:push`.
Sem migration, sem código de app.
@@ -69,5 +69,5 @@ Nenhum neste item.
## Referências
- `.husky/pre-push` (linha atual), `package.json` (scripts de gate existentes)
-- `.cursor/rules/engineering-standards.mdc` — "Gate em duas velocidades"
-- `docs/AGENT-OPS.md` — fast gate local; `.cursor/skills/work-issue/SKILL.md` — Passo 4.3
+- `.agents/rules/engineering-standards.mdc` — "Gate em duas velocidades"
+- `docs/AGENT-OPS.md` — fast gate local; `.agents/skills/work-issue/SKILL.md` — Passo 4.3
diff --git a/docs/plans/indicacao-carregamento-navegacao-wizard.md b/docs/plans/indicacao-carregamento-navegacao-wizard.md
index bd5fca2a..bb622e5f 100644
--- a/docs/plans/indicacao-carregamento-navegacao-wizard.md
+++ b/docs/plans/indicacao-carregamento-navegacao-wizard.md
@@ -9,7 +9,7 @@ Responsável: —
## Design (Impeccable)
-Âncoras: `PRODUCT.md` § **Feel the action** · `.cursor/rules/campanha-action-feedback.mdc` · precedente `CampaignListPendingBoundary` / `CampaignListResults` / `useCampaignListTransition` (`src/components/campaign/shared/CampaignListPending.tsx`) e fill-in do compositor de giro ([feedback-pendente-compositor-giro.md](feedback-pendente-compositor-giro.md)) · tema `data-theme='campaign'`.
+Âncoras: `PRODUCT.md` § **Feel the action** · `.agents/rules/campanha-action-feedback.mdc` · precedente `CampaignListPendingBoundary` / `CampaignListResults` / `useCampaignListTransition` (`src/components/campaign/shared/CampaignListPending.tsx`) e fill-in do compositor de giro ([feedback-pendente-compositor-giro.md](feedback-pendente-compositor-giro.md)) · tema `data-theme='campaign'`.
Na implementação (`implement-roadmap-item`): craft compacto → critique → polish. Sem shape novo.
@@ -104,7 +104,7 @@ Componentes:
- `src/components/campaign/shared/CampaignListPending.tsx` — boundary, hook, results, `CampaignTransitionAnchor`
- `docs/plans/chassis-wizard-campanha.md` (B59 ✓) · `docs/plans/header-mobile-wizard-campanha.md` (B75) · `docs/plans/busca-municipio-wizard.md` (B60 ✓)
- `docs/plans/feedback-pendente-compositor-giro.md` — precedente não-lista
-- `.cursor/rules/campanha-action-feedback.mdc` · `PRODUCT.md` § Feel the action
+- `.agents/rules/campanha-action-feedback.mdc` · `PRODUCT.md` § Feel the action
- AGENTS.md — sem Consent; naming
Qualidade de decisão: 4/5
diff --git a/docs/plans/mapa-mental-arquitetura.md b/docs/plans/mapa-mental-arquitetura.md
index d1b1929c..56ff497b 100644
--- a/docs/plans/mapa-mental-arquitetura.md
+++ b/docs/plans/mapa-mental-arquitetura.md
@@ -20,7 +20,7 @@ Um humano ou agente novo se orienta no repo em minutos, sem arqueologia de git/A
- Decision log: todas as decisões do Pass 2 (D1–D6) + as decisões estruturais herdadas (transações multi-collection, consent fail-closed, migrations only).
- Cross-links para PRODUCT/DESIGN/CUSTOMER/research para o "porquê" — sem duplicar conteúdo.
-**2. `.cursor/rules/codebase-map.mdc`** — mapa navegacional compacto (~150 linhas), always-applied, criado com o skill `create-rule`: onde vive cada coisa, como achar o dono de uma feature, convenção de manutenção ("atualizado no mesmo PR que move coisas").
+**2. `.agents/rules/codebase-map.mdc`** — mapa navegacional compacto (~150 linhas), always-applied, criado com o skill `create-rule`: onde vive cada coisa, como achar o dono de uma feature, convenção de manutenção ("atualizado no mesmo PR que move coisas").
## Regras
diff --git a/docs/plans/notifications.md b/docs/plans/notifications.md
index 438951f9..8e8b82eb 100644
--- a/docs/plans/notifications.md
+++ b/docs/plans/notifications.md
@@ -86,4 +86,4 @@ Componentes:
- `docs/roadmap.md` (linhas 57, 60, 61)
- `AGENTS.md` — Campaign auth, Campaign nuclei MVP, transações, `Consent` por chave
-- `.cursor/rules/projects/nucleos-eleitorais.mdc`
+- `.agents/rules/projects/nucleos-eleitorais.mdc`
diff --git a/docs/plans/ordenacao-filtros-lista-liderancas.md b/docs/plans/ordenacao-filtros-lista-liderancas.md
index 8a57a6e0..972a620b 100644
--- a/docs/plans/ordenacao-filtros-lista-liderancas.md
+++ b/docs/plans/ordenacao-filtros-lista-liderancas.md
@@ -164,4 +164,4 @@ Componentes (molde B33 — não re-extrair chrome):
- `src/components/campaign/municipality/MunicipalitySortableHead.tsx`, `MunicipalityHeaderFilter.tsx`, `src/components/campaign/shared/CampaignTable.tsx` — origem da extração e destino das colunas
- `src/collections/Leadership.ts`, `src/utilities/access/` — campos indexados (`supportStatus`, `sector`, `municipalities`, `user`) e escopo do assessor
- AGENTS.md — Campaign auth e RBAC, `overrideAccess: false` com `user`, pessoa é sempre `Contact` + join, naming (identificadores em inglês, copy pt-BR), sem migration neste item
-- `PRODUCT.md` / `DESIGN.md` — Field Desk, anti-goals de dashboard, "Edit where you see" (não se aplica na v1 desta tela) · `.cursor/rules/campanha-action-feedback.mdc` (pending honesto no resultado)
+- `PRODUCT.md` / `DESIGN.md` — Field Desk, anti-goals de dashboard, "Edit where you see" (não se aplica na v1 desta tela) · `.agents/rules/campanha-action-feedback.mdc` (pending honesto no resultado)
diff --git a/docs/plans/paradigma-agentes-paralelos.md b/docs/plans/paradigma-agentes-paralelos.md
index ad79502e..cff85eb1 100644
--- a/docs/plans/paradigma-agentes-paralelos.md
+++ b/docs/plans/paradigma-agentes-paralelos.md
@@ -15,7 +15,7 @@ todos:
content: 'Neon prod snapshot → branch stage; runbook refresh (nova branch + swap secret) via workflow_dispatch humano'
status: pending
- id: onda0-cursor-context
- content: 'AGENT-OPS.md + fatiar AGENTS + kernels + .cursorignore + Cloud env + hooks portáteis + standards fast-gate + PR template + README cheatsheet'
+ content: 'AGENT-OPS.md + fatiar AGENTS + kernels + .agentignore + Cloud env + hooks portáteis + standards fast-gate + PR template + README cheatsheet'
status: pending
- id: onda0-remove-codeberg
content: 'Remover Codeberg de package.json/AGENTS/TECH-DEBT/ci.yml e qualquer referência; GitHub = única casa'
@@ -221,13 +221,13 @@ Cloud env: `gh` autenticado (Cursor GitHub integration); sem secrets de stage/pr
## Cursor Cloud readiness
-| Item | Ação |
-| -------- | --------------------------------------------------------------- |
-| Contexto | Fatiar AGENTS; kernels PRODUCT/DESIGN/CUSTOMER; `.cursorignore` |
-| Env | Compose Postgres + `migrate` + `db:seed:minimal` |
-| Hooks | Paths relativos |
-| Regra | PR base `stage`; nunca Neon; 1 Issue/run |
-| Brief | stdout do claim |
+| Item | Ação |
+| -------- | -------------------------------------------------------------- |
+| Contexto | Fatiar AGENTS; kernels PRODUCT/DESIGN/CUSTOMER; `.agentignore` |
+| Env | Compose Postgres + `migrate` + `db:seed:minimal` |
+| Hooks | Paths relativos |
+| Regra | PR base `stage`; nunca Neon; 1 Issue/run |
+| Brief | stdout do claim |
---
@@ -248,7 +248,7 @@ Igual: `file-miss` → harvest → PR com `codebaseConventions`/ESLint → ledge
4. **Stage DB** (Neon prod snapshot) + runbook refresh **semanal**.
5. **Vercel preview** para branch `stage` na Onda 0.
6. Labels/prio + scripts `claim|register|prioritize|file-miss|promote` + seed Issues (body = plan atual, `depends` do roadmap).
-7. Contexto Cursor: `AGENT-OPS.md`, fatiar AGENTS, kernels, `.cursorignore`, Cloud env (Compose+seed:minimal), hooks portáteis, standards fast-gate, PR template.
+7. Contexto Cursor: `AGENT-OPS.md`, fatiar AGENTS, kernels, `.agentignore`, Cloud env (Compose+seed:minimal), hooks portáteis, standards fast-gate, PR template.
8. **README cheatsheet** — seção curta no topo do [`README.md`](README.md) para humanos: fluxo em 5 linhas, comandos `pnpm agent:*`, labels, o que o agente faz sozinho vs o que exige humano, links para `AGENT-OPS.md`/CI/skills. Não duplicar o setup local — só a operação agentic.
Ordenar: 0 primeiro (limpa base), depois 1/2, depois 3/4/5, depois 6/7/8.
diff --git a/docs/plans/plan-issue-lifecycle-ready-impl.md b/docs/plans/plan-issue-lifecycle-ready-impl.md
index c89faa38..e3f11363 100644
--- a/docs/plans/plan-issue-lifecycle-ready-impl.md
+++ b/docs/plans/plan-issue-lifecycle-ready-impl.md
@@ -53,7 +53,7 @@ flowchart LR
- **`scripts/lib/agent-plan-lifecycle.mjs`:** puro — `resolveRegisterStateLabel({ hasPlan, explicitBlocked })`, `canPromotePlanIssue(issue)` (blocked + link `docs/plans/` + open + sem in-progress/done/in-prod).
- **`scripts/agent-register.mjs`:** usa o helper; com `--plan` nasce `blocked` (mesmo sem `--blocked`); sem `--plan` preserva `ready` / `--blocked`.
- **`scripts/agent-ready.mjs` + `pnpm agent:ready`:** flip `blocked`→`ready` idempotente; comenta motivo; recusa se não for “aguardando plano”.
-- **`.cursor/skills/plan-issue/SKILL.md`:** Passo 5 = gate duro (sem Issue/PR até OK explícito ao lote); Passo 6 = register → PR `Related #N` → merge → `agent:ready`.
+- **`.agents/skills/plan-issue/SKILL.md`:** Passo 5 = gate duro (sem Issue/PR até OK explícito ao lote); Passo 6 = register → PR `Related #N` → merge → `agent:ready`.
- **`docs/AGENT-OPS.md` + skill `agent-pool` (menção curta) + nota em `capture-review-debts`:** contrato de labels / promote após plano em `main`.
- **Tests:** `tests/unit/agentPlanLifecycle.unit.spec.ts` (register label + canPromote).
- **Migration / Access / Consent / UI:** N/A (Impeccable A).
diff --git a/docs/plans/plan-issue-lifecycle-ready.md b/docs/plans/plan-issue-lifecycle-ready.md
index 80749ea5..cf792d0c 100644
--- a/docs/plans/plan-issue-lifecycle-ready.md
+++ b/docs/plans/plan-issue-lifecycle-ready.md
@@ -46,7 +46,7 @@ Omitido — superfície A (processo).
## Direção no codebase (hipótese)
-- **Áreas prováveis:** `.cursor/skills/plan-issue/` (SKILL + checklist Passo 5/6), `scripts/agent-register.mjs`, elegibilidade do pool (`scripts/lib/agent-pool-eligibility.mjs` + pins em `tests/unit/agentPoolEligibility.unit.spec.ts`), menção curta em `docs/AGENT-OPS.md` / skill `agent-pool` se o contrato de labels mudar.
+- **Áreas prováveis:** `.agents/skills/plan-issue/` (SKILL + checklist Passo 5/6), `scripts/agent-register.mjs`, elegibilidade do pool (`scripts/lib/agent-pool-eligibility.mjs` + pins em `tests/unit/agentPoolEligibility.unit.spec.ts`), menção curta em `docs/AGENT-OPS.md` / skill `agent-pool` se o contrato de labels mudar.
- **Precedente a olhar:** gate Passo 5 já escrito na skill (hoje só soft); `needs:consent` / `requirements-changed` como “não claimar”; OPS12 `plans-only-closes` (`Related #N`).
- **Risco de acoplamento:** fila `ready` é o contrato compartilhado de `agent:claim` e do pool — mudar o momento do flip afeta os dois.
@@ -76,8 +76,8 @@ Omitido — superfície A (processo).
## Referências
- GitHub Issue #292
-- `.cursor/skills/plan-issue/SKILL.md` (Passo 5 gate / Passo 6 register)
+- `.agents/skills/plan-issue/SKILL.md` (Passo 5 gate / Passo 6 register)
- `scripts/agent-register.mjs` (default `ready`)
- `scripts/lib/agent-pool-eligibility.mjs` (`issueHasPlanLink` = warn)
- `docs/AGENT-OPS.md`
-- `.cursor/skills/agent-pool/SKILL.md`
+- `.agents/skills/agent-pool/SKILL.md`
diff --git a/docs/plans/plan-issue-promote-dual.md b/docs/plans/plan-issue-promote-dual.md
index 75b9a6f2..eaf254de 100644
--- a/docs/plans/plan-issue-promote-dual.md
+++ b/docs/plans/plan-issue-promote-dual.md
@@ -46,7 +46,7 @@ Omitido — superfície A.
## Direção no codebase (hipótese)
-- **Áreas prováveis:** novo workflow (ou extensão cuidadosa de `issue-done-on-main-merge.yml`), helper puro testável para parse de `Related #N` + regra de promote, `.cursor/skills/plan-issue/SKILL.md` (Passo 6), nota em `docs/AGENT-OPS.md`.
+- **Áreas prováveis:** novo workflow (ou extensão cuidadosa de `issue-done-on-main-merge.yml`), helper puro testável para parse de `Related #N` + regra de promote, `.agents/skills/plan-issue/SKILL.md` (Passo 6), nota em `docs/AGENT-OPS.md`.
- **Precedente a olhar:** `issue-done-on-main-merge.yml` (`Closes`→`done`); OPS12 `plans-only-closes`; OPS17 `#292` / `docs/plans/plan-issue-lifecycle-ready.md`.
- **Risco de acoplamento:** flip frouxo promove Issue `blocked` por dependência de produto; misturar com o job de `Closes` sem cuidado.
@@ -76,5 +76,5 @@ Omitido — superfície A.
- GitHub Issue #296
- GitHub Issue OPS17 #292
- `.github/workflows/issue-done-on-main-merge.yml`
-- `.cursor/skills/plan-issue/SKILL.md`
+- `.agents/skills/plan-issue/SKILL.md`
- `docs/plans/plan-issue-lifecycle-ready.md`
diff --git a/docs/plans/politica-ci-dono-do-pr.md b/docs/plans/politica-ci-dono-do-pr.md
index 300badcf..7d491424 100644
--- a/docs/plans/politica-ci-dono-do-pr.md
+++ b/docs/plans/politica-ci-dono-do-pr.md
@@ -40,8 +40,8 @@ Decisão de produto (2026-07-30, brief do lote CI): **done = PR mergeado em `sta
Componentes (texto, não código):
- **`docs/AGENT-OPS.md`** — novo bloco na seção "Fluxo": "Dono do PR, dono do CI" (definição de done, triagem das 3 classes de falha, limite de blast radius, flaky→quarentena+débito).
-- **`.cursor/skills/work-issue/SKILL.md`** — Passo 6.5 reforçado: falha no ci-pr → corrige na mesma branch **qualquer que seja a origem**, citando AGENT-OPS; "fora do escopo" não é critério de parada.
-- **`.cursor/rules/engineering-standards.mdc`** — linha no "After every change": CI vermelho no seu PR é seu, mesmo pré-existente.
+- **`.agents/skills/work-issue/SKILL.md`** — Passo 6.5 reforçado: falha no ci-pr → corrige na mesma branch **qualquer que seja a origem**, citando AGENT-OPS; "fora do escopo" não é critério de parada.
+- **`.agents/rules/engineering-standards.mdc`** — linha no "After every change": CI vermelho no seu PR é seu, mesmo pré-existente.
- **`docs/CHANGELOG-AGENTS.md`** — entrada do incidente PR #50 → esta política.
Texto-núcleo proposto (núcleo único citado nos três):
@@ -70,6 +70,6 @@ Nenhum neste item.
## Referências
- Incidente: PR #50, run 30558184687 (migration-lock sem checkout; int fora da feature)
-- `docs/AGENT-OPS.md` — seção "Fluxo"; `.cursor/skills/work-issue/SKILL.md` — Passo 6
-- `.cursor/rules/engineering-standards.mdc` — "After every change"
+- `docs/AGENT-OPS.md` — seção "Fluxo"; `.agents/skills/work-issue/SKILL.md` — Passo 6
+- `.agents/rules/engineering-standards.mdc` — "After every change"
- Issue #43 (OPS2) — harvest agent-miss → guardrails
diff --git a/docs/plans/pwa-campanha.md b/docs/plans/pwa-campanha.md
index b3b63246..8e82c49d 100644
--- a/docs/plans/pwa-campanha.md
+++ b/docs/plans/pwa-campanha.md
@@ -154,7 +154,7 @@ Recomendação de instalação do `/campanha` como PWA, exibida no início da se
- `docs/roadmap.md` (linhas 58, 62, 81)
- `docs/plans/notifications.md` — push depende deste plano
- `AGENTS.md` — Campaign auth, naming conventions, transações
-- `.cursor/rules/projects/nucleos-eleitorais.mdc`
+- `.agents/rules/projects/nucleos-eleitorais.mdc`
- `src/app/(campaign)/layout.tsx` — root layout da vertical (onde os metadados entram)
- `src/app/(campaign)/campanha/(app)/layout.tsx` — shell autenticado (monta o ``)
- `src/components/ui/Toaster.tsx` — sonner já montado; reusar, não duplicar
diff --git a/docs/plans/registrar-sinal-lista-municipios.md b/docs/plans/registrar-sinal-lista-municipios.md
index a9bb0aa4..55bf082b 100644
--- a/docs/plans/registrar-sinal-lista-municipios.md
+++ b/docs/plans/registrar-sinal-lista-municipios.md
@@ -13,7 +13,7 @@ Entregue: `MunicipalitySignalFields` + `MunicipalityListSignalControl` (Popover
## Design (Impeccable)
-Âncoras: `PRODUCT.md` (princípio 2 **Clarity under pressure**, princípio 3 **Edit where you see**, princípio 4 **Auto-save, no Save button** — e a **exceção** nomeada nele para escrita atômica multi-campo, princípio 8 **Feel the action**) / `DESIGN.md` (register `product` — Field Desk) · tema `data-theme='campaign'` · regras `.cursor/rules/campanha-edit-where-you-see.mdc` (item 4: submit explícito continua válido para fluxo multi-campo) e `.cursor/rules/campanha-action-feedback.mdc` · precedentes vivos: [`MunicipalityListTrendControl.tsx`](../../src/components/campaign/municipality/MunicipalityListTrendControl.tsx) (Popover na célula) e [`MunicipalityUpdateForm.tsx`](../../src/components/campaign/municipality/MunicipalityUpdateForm.tsx) (campos do sinal tipado do C12).
+Âncoras: `PRODUCT.md` (princípio 2 **Clarity under pressure**, princípio 3 **Edit where you see**, princípio 4 **Auto-save, no Save button** — e a **exceção** nomeada nele para escrita atômica multi-campo, princípio 8 **Feel the action**) / `DESIGN.md` (register `product` — Field Desk) · tema `data-theme='campaign'` · regras `.agents/rules/campanha-edit-where-you-see.mdc` (item 4: submit explícito continua válido para fluxo multi-campo) e `.agents/rules/campanha-action-feedback.mdc` · precedentes vivos: [`MunicipalityListTrendControl.tsx`](../../src/components/campaign/municipality/MunicipalityListTrendControl.tsx) (Popover na célula) e [`MunicipalityUpdateForm.tsx`](../../src/components/campaign/municipality/MunicipalityUpdateForm.tsx) (campos do sinal tipado do C12).
Na implementação (`implement-roadmap-item`): craft compacto → critique → polish. Sem shape completo: a célula, o Popover de lista e o formulário de sinal já existem no produto; o que é novo é a **combinação** e a apresentação mobile (primeira vez que um controle de célula abre em bottom sheet).
diff --git a/docs/plans/renomear-plano-acao-para-atividade.md b/docs/plans/renomear-plano-acao-para-atividade.md
index d57bf4c0..9edcaeb6 100644
--- a/docs/plans/renomear-plano-acao-para-atividade.md
+++ b/docs/plans/renomear-plano-acao-para-atividade.md
@@ -99,7 +99,7 @@ Componentes:
- **`tests/unit/codebaseConventions.unit.spec.ts`**: o describe "banned campaign terminology" vira table-driven —
`[{ id, pattern, remedy, allowlist }]` com a linha existente (Praça/Núcleo → Município, allowlist `src/lib/cities.ts`) e a nova (termo antigo da agenda → Atividade/`activity`, allowlist do próprio spec). O walker passa a percorrer `src/`, `tests/` e `scripts/`, pulando `src/migrations/`.
- **Testes renomeados**: `tests/int/campaignActionPlan.int.spec.ts` → `campaignActivity.int.spec.ts`, `tests/unit/actionPlanUi.unit.spec.ts` → `activityUi.unit.spec.ts`, `tests/e2e/campaignActionPlan.e2e.spec.ts` → `campaignActivity.e2e.spec.ts`; `tests/helpers/campaignFixtures.ts` e `tests/e2e/fixtures/campaignE2EFixtures.ts` acompanham (inclusive a chave `action_plan_id` do cleanup de locked documents).
-- **Docs vivos**: `AGENTS.md` (seções "Campaign action plans (C3)" e a lista de collections), `.cursor/rules/codebase-map.mdc` (lista de domínios de `components/campaign`), `.cursor/rules/projects/nucleos-eleitorais.mdc` e o `docs/roadmap.md`. Planos entregues e migrations ficam como estão.
+- **Docs vivos**: `AGENTS.md` (seções "Campaign action plans (C3)" e a lista de collections), `.agents/rules/codebase-map.mdc` (lista de domínios de `components/campaign`), `.agents/rules/projects/nucleos-eleitorais.mdc` e o `docs/roadmap.md`. Planos entregues e migrations ficam como estão.
## Dependências
diff --git a/docs/plans/selecionados-no-topo-filtros-header.md b/docs/plans/selecionados-no-topo-filtros-header.md
index 921ee109..93d66dee 100644
--- a/docs/plans/selecionados-no-topo-filtros-header.md
+++ b/docs/plans/selecionados-no-topo-filtros-header.md
@@ -122,5 +122,5 @@ Componentes:
- [sistema-listas-campanha.md](sistema-listas-campanha.md) (Pass 2 W1 — rabbit hole de generalizar o header) · [pagina-territorios-identidade.md](pagina-territorios-identidade.md) (B21 — gatilho do head genérico) · [explicacao-colunas-header-listas.md](explicacao-colunas-header-listas.md) (B22 — mesma estratégia de peça compartilhada; vizinho no mesmo `TableHead`) · [seletor-colunas-lista-municipios.md](seletor-colunas-lista-municipios.md) (B17)
- `src/components/campaign/municipality/MunicipalityHeaderFilter.tsx`, `MunicipalityFilters.tsx` — os dois controles a alterar
- `src/utilities/municipalityListFilters.ts`, `src/utilities/municipalityPageData.ts` (facetas) — origem e tipos das opções
-- AGENTS.md — naming (identificadores em inglês, copy pt-BR), fronteira `lib/` puro vs `utilities/` acoplado; `.cursor/rules/campanha-action-feedback.mdc` (Feel the action)
+- AGENTS.md — naming (identificadores em inglês, copy pt-BR), fronteira `lib/` puro vs `utilities/` acoplado; `.agents/rules/campanha-action-feedback.mdc` (Feel the action)
- `PRODUCT.md` / `DESIGN.md` — Field Desk, Restrained, anti-goal de spreadsheet/data-grid
diff --git a/docs/research/README.md b/docs/research/README.md
index d44b8a41..d038f4de 100644
--- a/docs/research/README.md
+++ b/docs/research/README.md
@@ -16,7 +16,7 @@ Artefatos do ciclo de discovery "literatura → persona → entrevista → docs
## Ciclo 2 — Discovery de finanças de campanha (2026-07-28)
-Mesmo método (literatura → personas → entrevistas sintéticas → relatório → validação real), aplicado à **gestão financeira** de uma campanha de DF na Bahia, sob três ângulos (financeiro-operacional, jurídico, político). Notebook do projeto: `.cursor/rules/projects/financas-campanha.mdc`.
+Mesmo método (literatura → personas → entrevistas sintéticas → relatório → validação real), aplicado à **gestão financeira** de uma campanha de DF na Bahia, sob três ângulos (financeiro-operacional, jurídico, político). Notebook do projeto: `.agents/rules/projects/financas-campanha.mdc`.
| Artefato | O quê | Status |
| ------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------- |
@@ -34,7 +34,7 @@ Mesmo método (literatura → personas → entrevistas sintéticas → relatóri
## Ciclo 3 — Discovery de emendas parlamentares (mandato/comunicação) (2026-07-28)
-Mesmo método (literatura → personas → entrevistas sintéticas em subagents isolados, 2 rodadas com acareação → relatório → roteiro para a sessão real), aplicado ao **registro e acompanhamento das emendas parlamentares do mandato** — a visão de produto por trás (contexto, NÃO decisão): um ledger de emendas numa vertical nova `/mandato`, consumido pela campanha (`/campanha`, ex.: dossiê de município — pedido real O6) e por uma vertical de comunicação (`/comunicacao`). Quatro ângulos: o deputado (dono político), a assessoria de orçamento (operadora da planilha), a comunicação do mandato (transforma execução em anúncio) e o coordenador de campanha (consumidor — persona ancorada na evidência real de 2026-07-23, campos [R]/[S]). Notebook do projeto: `.cursor/rules/projects/emendas-mandato.mdc`.
+Mesmo método (literatura → personas → entrevistas sintéticas em subagents isolados, 2 rodadas com acareação → relatório → roteiro para a sessão real), aplicado ao **registro e acompanhamento das emendas parlamentares do mandato** — a visão de produto por trás (contexto, NÃO decisão): um ledger de emendas numa vertical nova `/mandato`, consumido pela campanha (`/campanha`, ex.: dossiê de município — pedido real O6) e por uma vertical de comunicação (`/comunicacao`). Quatro ângulos: o deputado (dono político), a assessoria de orçamento (operadora da planilha), a comunicação do mandato (transforma execução em anúncio) e o coordenador de campanha (consumidor — persona ancorada na evidência real de 2026-07-23, campos [R]/[S]). Notebook do projeto: `.agents/rules/projects/emendas-mandato.mdc`.
| Artefato | O quê | Status |
| -------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------- |
diff --git a/eslint.config.mjs b/eslint.config.mjs
index 613da7af..6672de1a 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -225,7 +225,6 @@ const eslintConfig = [
'test-results/**',
// Vendored agent material (same carve-out as .prettierignore):
'.agents/**',
- '.cursor/**',
]),
]
diff --git a/scripts/check-test-locations.mjs b/scripts/check-test-locations.mjs
index a4023c60..18dac7cb 100644
--- a/scripts/check-test-locations.mjs
+++ b/scripts/check-test-locations.mjs
@@ -41,7 +41,7 @@ function walk(dir, out = []) {
for (const entry of entries) {
if (entry.name.startsWith('.') && entry.name !== '.env.test') {
if (entry.isDirectory() && IGNORE_DIRS.has(entry.name)) continue
- if (entry.isDirectory() && entry.name !== '.github' && entry.name !== '.cursor') continue
+ if (entry.isDirectory() && entry.name !== '.github' && entry.name !== '.agents') continue
}
if (IGNORE_DIRS.has(entry.name)) continue
const absolute = path.join(dir, entry.name)
diff --git a/scripts/lib/agent-pool-prompt.mjs b/scripts/lib/agent-pool-prompt.mjs
index 224f277b..80955fd4 100644
--- a/scripts/lib/agent-pool-prompt.mjs
+++ b/scripts/lib/agent-pool-prompt.mjs
@@ -30,7 +30,7 @@ export const buildPoolWorkerPrompt = ({ issueNumber, issueTitle, issueId, planPa
'',
'## Fluxo',
'',
- 'Siga a skill `.cursor/skills/agent-work-issue/SKILL.md` por completo (Issue já claimada):',
+ 'Siga a skill `.agents/skills/agent-work-issue/SKILL.md` por completo (Issue já claimada):',
'1. Rode `pnpm i` se `node_modules` não existir.',
planPath
? `2. Plano de intenção: \`${planPath}\` — abra-o; depois Plan mode e escreva \`docs/plans/-impl.md\` (engenharia deliberada; pode divergir da hipótese de direção se o aceite de produto se mantiver).`
@@ -42,7 +42,7 @@ export const buildPoolWorkerPrompt = ({ issueNumber, issueTitle, issueId, planPa
'',
'## Proibido',
'',
- '- `DATABASE_URL` de prod ou `ALLOW_REMOTE_DB` — o setup local (`.cursor/cloud-setup.sh` + seed mínimo) cobre tudo.',
+ '- `DATABASE_URL` de prod ou `ALLOW_REMOTE_DB` — o setup local (`.agents/cloud-setup.sh` + seed mínimo) cobre tudo.',
`- Editar outras Issues \`in-progress\` ou trabalhar fora da Issue #${issueNumber}.`,
'- Tratar o plano de intenção como contrato de engenharia — ele é intenção; o `*-impl.md` é a engenharia.',
'',
diff --git a/src/components/campaign/municipality/SaveMunicipalityFilterControl.tsx b/src/components/campaign/municipality/SaveMunicipalityFilterControl.tsx
index 4d75bb33..0ceb66f7 100644
--- a/src/components/campaign/municipality/SaveMunicipalityFilterControl.tsx
+++ b/src/components/campaign/municipality/SaveMunicipalityFilterControl.tsx
@@ -30,7 +30,7 @@ type SaveMunicipalityFilterControlProps = {
* Names the current recorte and keeps it in the sidebar (B18).
*
* Naming is confirmatory, so this is the explicit-submit exception
- * `.cursor/rules/campanha-edit-where-you-see.mdc` already carves out for flows
+ * `.agents/rules/campanha-edit-where-you-see.mdc` already carves out for flows
* that need a note — and it keeps this control out of the auto-save machine the
* quick-edit cells share.
*/
diff --git a/tests/helpers/campaignFixtures.ts b/tests/helpers/campaignFixtures.ts
index ddb04b80..42f27e5b 100644
--- a/tests/helpers/campaignFixtures.ts
+++ b/tests/helpers/campaignFixtures.ts
@@ -1098,6 +1098,7 @@ export class CampaignFixtures {
levelNote: null,
strengths: [],
risks: [],
+ stateDeputies: [],
dobradinhaNotes: null,
nextSteps: null,
lastUpdateAt: null,
diff --git a/tests/unit/plansOnlyClosesGuard.unit.spec.ts b/tests/unit/plansOnlyClosesGuard.unit.spec.ts
index 864e232b..64df7dd5 100644
--- a/tests/unit/plansOnlyClosesGuard.unit.spec.ts
+++ b/tests/unit/plansOnlyClosesGuard.unit.spec.ts
@@ -33,7 +33,7 @@ describe('plansOnlyClosesGuard', () => {
expect(isPlansOnlyDiff(['docs/plans/bottom-drawer-peek-acoes-busca.md'])).toBe(true)
expect(isPlansOnlyDiff([])).toBe(false)
expect(isPlansOnlyDiff(['docs/plans/a.md', 'docs/plans/sub/b.md'])).toBe(true)
- expect(isPlansOnlyDiff(['docs/plans/a.md', '.cursor/skills/plan-issue/SKILL.md'])).toBe(false)
+ expect(isPlansOnlyDiff(['docs/plans/a.md', '.agents/skills/plan-issue/SKILL.md'])).toBe(false)
})
it('finds closing keywords case-insensitively', () => {