diff --git a/.agents/skills/react-audit/SKILL.md b/.agents/skills/react-audit/SKILL.md new file mode 100644 index 00000000..e52d68b4 --- /dev/null +++ b/.agents/skills/react-audit/SKILL.md @@ -0,0 +1,173 @@ +--- +name: react-audit +description: 'Audit React/Next.js anti-patterns com ciclo fechado: sweep focado → plano per-fix → implementação com gate completo → um único PR ready SEM auto-merge cuja descrição é o relatório.' +--- + +# React Audit (ciclo fechado plan→implement) + +Skill para rodar **sozinha** (à noite, sem supervisão): varre o repo por anti-patterns +**React/Next.js** do catálogo fixo (`anti-patterns.md` desta pasta), planeja e implementa +cada fix com o gate completo, e entrega **um único PR** para `main` — ready, +**sem auto-merge**, CI green, mergeable, sem conflitos — com **um commit por fix** e a +**descrição do PR como relatório consolidado**. Aí PARA: o humano explora os commits +e mescla quando quiser. Feedback humano = comentário no PR → ajusta na MESMA branch. + +Complementar ao `engineering-audit` (qualidade geral); NÃO re-varre o que ele já cobre. +Context7 MCP é aceleração opcional — ver `context7-setup.md`; sem ele, use as fontes +oficiais embutidas no catálogo. Nunca bloqueie por falta do MCP. + +## Proibido + +- DB de prod / `ALLOW_REMOTE_DB` / repontar envs — local only (`teqo_test` nos testes). +- Mergear qualquer coisa ou armar auto-merge. O PR da run nasce e PERMANECE sem auto-merge. +- Abrir draft e flipar ready no fim (corrida de instant-merge com check já verde). +- Expandir o catálogo de famílias durante a run; fix fora das 9 famílias vira follow-up. +- Fix sem os TRÊS selos (file:line + família + fonte oficial) — "cartilha genérica" não entra. +- `Closes #N` de Issue alheia no body do PR da run (use `Refs #N`). +- Tocar Issues `in-progress` alheias ou claimar na sessão. +- Gate commands piped (`| tail` engole exit code); `knip --fix` cego. +- Migration/schema Payload em commit noturno — fix que exigir migration vira follow-up. + +## Checklist + +``` +- [ ] 0. Precheck fail-closed (read-only): worktree provisionado, branch ≠ main, GITHUB_TOKEN válido, baseline bare anotada +- [ ] 1. Sweep read-only por família do catálogo (canon primeiro; Tasks ad-hoc por área) +- [ ] 2. Triage per-fix: 3 selos, dedup TECH-DEBT/guardas vivas, dono travado → follow-up, cap ≤ 6 +- [ ] 3. Loop por fix: mini-plano → implementar (editar o dono) → gate completo BARE → 1 commit → push da branch (sem PR) +- [ ] 4. Relatório consolidado (body do PR) + follow-ups como Issues + changelog da run +- [ ] 5. Abrir o PR ÚNICO Ready via scripts/github-pr.mjs → DESARMAR auto-merge → verificar auto_merge null +- [ ] 6. Estabilizar até checks green ∧ mergeable ∧ auto_merge null → PARAR sem mergear +``` + +## Passo 0 — Precheck fail-closed (read-only) + +Tudo aqui é leitura. Falhou um item → pare com o remédio nomeado, não improvise às 3h. + +1. Estar DENTRO do worktree provisionado da run (`pnpm worktree next --issue ` já + claimou e criou envs/DBs/porta dedicadas). Nunca rodar no repo principal nem claimar na sessão. +2. `git status --porcelain` limpo (commit/stash pendências alheias antes? Não são suas — PARE e relate). +3. Branch corrente ≠ `main`. Se estiver em main/detached, crie `agent/-react-audit` + a partir de `origin/main` — nunca commitar fix em main. +4. `GITHUB_TOKEN` presente e validado com chamada barata ANTES do trabalho: + `node -e "import('./scripts/lib/github-api.mjs').then(m=>m.githubApi.getIssue(901)).then(i=>console.log(i?.state))"`. +5. Baseline bare dos gates estáticos — vermelhos pré-existentes são BASELINE (política + "dono do PR, dono do CI", `docs/AGENT-OPS.md`), anote-os: + `pnpm exec tsc --noEmit`, `pnpm lint`, `pnpm exec knip`, `pnpm check:cycles`. +6. Pool morto desde OPS65 — nada a pausar. + +## Passo 1 — Sweep read-only + +Zero writes em `src/`, `tests/`, `scripts/` neste passo. + +1. Canon primeiro: `.agents/rules/engineering-standards.mdc`, `.agents/rules/codebase-map.mdc`, + `AGENTS.md`, `docs/TECH-DEBT.md` (dedup). +2. Carregue `.agents/skills/react-audit/anti-patterns.md` — as 9 famílias e suas heurísticas. +3. Âncora de delta: data da última entrada react-audit em `docs/changelog/` (primeira run = + repo inteiro). Foque o esforço no churn desde a âncora: + `git log --since="<âncora>" --name-only --format= | sort -u`. +4. Tasks ad-hoc por família/área (padrão `work-issue`: input mínimo + instrução explícita): + prompt do subagente carrega a seção da família + a âncora de delta e exige output ≤25 linhas + no formato `file:line | família | sintoma observado` — "**Não escrever código nem planos**". +5. Com Context7 ativo, consulte a doc oficial vigente ANTES de classificar (evita fix contra + a versão atual); sem Context7, confie nas URLs do catálogo e confirme na citação. + +## Passo 2 — Triage per-fix + +Para cada candidato do sweep: + +1. Exija os TRÊS selos: `file:line` conferido por você (abra o arquivo), família do catálogo, + fonte oficial. Faltou um → linha "observado sem fix" no relatório; não invente justificativa. +2. Dedup: já está em `docs/TECH-DEBT.md` (classe B14/P4-F aberta)? Guarda viva cobre + (ESLint restrictions, `tests/unit/codebaseConventions.unit.spec.ts`)? Já registrado → pule. +3. Dono travado (URL pública congelada, Consent/LGPD, migration shipped, collection schema) + → follow-up no Passo 4, NUNCA fix noturno. +4. Priorize churn × severidade; corte em **≤ 6 fixes por run** (cap duro — PR maior derrota + a revisão matinal commit a commit). Resto vira follow-up ou "adiado" no relatório. + +## Passo 3 — Loop de implementação (por fix) + +Um fix = mini-plano + gate completo + UM commit + push da branch (sem PR ainda — push de +branch isolada não dispara workflow nenhum). + +1. Task escritor (input: achado com os 3 selos + seção da família + norma citada; "Não escrever + código"): devolve mini-plano — receita, blast radius, pins/testes a tocar, critério de feito. +2. Implemente você mesmo (agente principal), **editar o dono, não twinar**: contratos client-safe / + `import type` ao cruzar fronteira server→client; nada de módulo paralelo ao lado do dono. +3. Gate completo BARE por fix (na ordem, cada comando sozinho no shell): + `pnpm exec tsc --noEmit`, `pnpm lint`, `pnpm format:check`, `pnpm exec knip`, + `pnpm check:cycles`, `pnpm test`, `pnpm build`. + Export órfão previsto? Resolva NO MESMO fix (`git grep -w ` antes de remover; + nunca `knip --fix`). Vermelho pré-existente da baseline do Passo 0 não é seu — documente. +4. E2E afetado (discricionário, OPS72): `pnpm test:e2e:affected` quando o fix tocar superfície + com spec; `gate:push` NÃO roda e2e — este passo é quem cobre. +5. Commit único do fix: mensagem citando família + `file:line` + (ex.: `react-audit(família 3): derived state via effect em MunicipalityFilter.tsx:88`). + Push: `git push -u origin HEAD` (branch sem PR ⇒ nenhum safety net dispara). +6. Volte ao Passo 2 para o próximo elegível. + +## Passo 4 — Relatório + follow-ups + changelog + +1. Monte o relatório consolidado (será o BODY do PR) com, nesta ordem: + (i) escopo/data/números do sweep (arquivos varridos, candidatos, fixes); + (ii) tabela de achados: file:line | família | fonte oficial; + (iii) POR FIX: decisão (implementado/rejeitado/adiado/follow-up) e O PORQUÊ (decisão, + gate, custo, sobreposição com dono existente) + SHA do commit + resultado do gate; + (iv) estado final do PR; (v) follow-ups recomendados com destino. +2. Follow-ups que sobrevivem ao PR → Issues reais via caminho canônico + (`scripts/lib/github-api.mjs` `createIssue`), título prefixado `[react-audit]`, + body com file:line + fonte + link do PR. Sem Issue per-fix implementada (geminação). +3. Changelog da run: `docs/changelog/-react-audit-run.md` (uma entrada curta; + docs-guards são additions-only — jamais tocar HISTORY/agregado). + +## Passo 5 — PR único + desarme do auto-merge + +O PR nasce UMA vez, já estabilizado localmente, e fica SEM auto-merge. + +1. `GITHUB_TOKEN= node scripts/github-pr.mjs --head \ + --title "react-audit : fixes" --base main --body-file ` + — nunca draft; `Refs #N` apenas; jamais `Closes` de Issue alheia. +2. Imediatamente após abrir, DESARME o auto-merge (o safety net + `agent-pr-ready-automerge.yml` arma em TODO PR same-repo→main não-draft): + + ```js + // node --input-type=module + import { githubApi } from './scripts/lib/github-api.mjs' + const pr = await githubApi.getPullRequest() // head.sha confere a branch + await githubApi.disableAutoMerge() // DELETE .../auto-merge; 404 tolerado + const after = await githubApi.getPullRequest() + if (after.autoMerge !== null) throw new Error('auto-merge ainda armado') + ``` + +3. REGRA DURA: todo push pós-abertura (fix de CI vermelho, rebase) é seguido IMEDIATAMENTE + do desarme + verificação acima — cada `synchronize` RE-ARMA o auto-merge silenciosamente. + +## Passo 6 — Estabilizar até o fim (Done condition) + +Loop até a condição de Done — a entrega só termina estável: + +1. CI: polle o check-run literal `checks` do head SHA (API check-runs ou `gh pr checks`) + com backoff. Vermelho dentro do blast radius dos fixes → conserte na branch (novo commit + próprio, gate completo, push, REDESARME). Infra estrutural → aplique o CAP (abaixo), + registre honestamente no relatório e pare com estado declarado. +2. Conflitos: `getPullRequest().mergeable` — `null` = GitHub computando (backoff, re-poll); + `false` = rebase sobre `origin/main`, resolva, push, REDESARME. +3. Caps anti-loop infinito: máx. 3 ciclos de fix pós-abertura OU 90 min de estabilização — + estourou → relatório declara o estado real e a skill termina SEM Done falso. +4. **Done = checks green ∧ `mergeable === true` ∧ `auto_merge === null` ∧ PR open+ready.** + Então PARAR. Não mergear. Não armar nada. O humano revisa de manhã. +5. Feedback humano posterior: pedido de mudança no PR → commits novos NA MESMA branch, + relatório atualizado (PATCH do body), re-estabilize pelo mesmo loop. + +## Context7 MCP (aceleração opcional) + +Setup manual do dono da máquina, FORA do git: `.agents/skills/react-audit/context7-setup.md`. +A skill FUNCIONA sem — fallback são as fontes oficiais do catálogo. + +## Done when + +Precheck verde (ou parada fail-closed nomeada); sweep com achados mapeados file:line+família+fonte; +cada fix com mini-plano, gate completo bare e commit próprio; cap ≤6 respeitado; relatório +consolidado É a descrição do PR; follow-ups com destino (Issue/relatório); changelog da run +escrito; PR único open+ready com checks green, mergeable true e `auto_merge` null — e a skill +PAROU, sem mergear nada. diff --git a/.agents/skills/react-audit/anti-patterns.md b/.agents/skills/react-audit/anti-patterns.md new file mode 100644 index 00000000..b9047910 --- /dev/null +++ b/.agents/skills/react-audit/anti-patterns.md @@ -0,0 +1,82 @@ +# Catálogo fixo de anti-patterns React/Next.js (react-audit) + +Enumeração FECHADA de famílias — expandir é PR próprio e deliberado da skill, nunca edição in-run. +Versões de referência (`package.json`): `next` 15.4.11 · `react`/`react-dom` ^19.2.4 · `eslint-config-next` 15.4.11. +Fontes oficiais conferidas vivas em 2026-08-25; re-verifique a URL antes de citá-la num relatório (Context7 ou fetch). + +Cada achado só vira fix com os TRÊS selos: `file:line` real + família desta lista + fonte oficial abaixo. +Normas canônicas moram nos `.mdc` — este catálogo NÃO as duplica: adiciona heurística de busca, fonte oficial e receita. + +--- + +## 1. Client boundary violada / server-first esquecido + +- **Sintoma/heurística:** `'use client'` em arquivo que não usa hooks/eventos (só renderiza props); import por VALOR de módulo server-only (`utilities/*Data.ts`, loaders) dentro de client component; estado elevado além dos consumidores; view model inteiro passado onde uma seleção basta. Buscar: `grep -rn "'use client'" src | `, imports de `@/utilities` em arquivos client. +- **Norma no repo:** `.agents/rules/engineering-standards.mdc:22-28` (server-first, contrato client-safe vs `server-only`, state no leaf); `.agents/rules/codebase-map.mdc:12` (client importa server só `import type`; valores compartilhados em módulos de contrato). +- **Fonte oficial:** https://react.dev/reference/rsc/use-client · https://nextjs.org/docs/app/api-reference/directives/use-client · https://react.dev/reference/rsc/server-components +- **Receita:** remover `'use client'` se nenhum hook/handler existir; trocar import de valor por `import type` ou extrair o valor para contrato client-safe (`lib/`); descer o state ao menor consumidor. +- **Precedente:** lição B14 (~21 kB de serializador de URL na sidebar); contratos `municipalityMapContract`, `votePledgeViews`, `lib/campaignRoles`. + +## 2. RSC payload bloat + +- **Sintoma/heurística:** loader passando doc Payload inteiro (ou array deles) para componente client como prop; campos richText/relacionamentos atravessando o fio sem seleção. Buscar: props tipadas como `Post`/`Municipality`/doc completo em componentes `'use client'`. +- **Norma no repo:** `.agents/rules/codebase-map.mdc` ("view models são por papel"); engineering-audit smell "RSC payload bloat". +- **Fonte oficial:** https://react.dev/reference/rsc/server-components +- **Receita:** selecionar view model mínimo no server (função `*Views` do domínio) e passar só o necessário; strings/formatos pré-computados no server. +- **Precedente:** padrão `*ViewModels`/`*Views` dos loaders `utilities/*Data.ts`. + +## 3. Effects fazendo derived-state / stale closures + +- **Sintoma/heurística:** `useEffect` escrevendo state derivável de props/state no render; closure sobre state do render que agendou o trabalho (autosave/optimistic); functional updater cujo FALLBACK lê state velho. Buscar: `useEffect(` seguido de `set` na mesma linha; updaters `(current) => current ?? outroState`. +- **Norma no repo:** engineering-audit smell list (:103-105). +- **Fonte oficial:** https://react.dev/learn/you-might-not-need-an-effect · https://react.dev/learn/synchronizing-with-effects +- **Receita:** derivar no render (const local ou `useMemo` se caro); updater funcional SEM fallback externo ou ref espelhando o valor vivo; evento no lugar de effect quando é resposta a ação. +- **Precedente:** classe B34 (closures/fallbacks obsoletos em autosave/optimistic). + +## 4. Forms & server actions (reset/transição/optimistic errados) + +- **Sintoma/heurística:** `
` onde o form não reseta após submit mas o código assumia reset (ou o contrário); botão sem pending (`useFormStatus`) duplicando submits; optimistic update sem invalidação (`revalidatePath`/`revalidateTag`); feedback de erro fora de `useActionState`. Buscar: `action={`, `useActionState`, `useOptimistic` sem `revalidate` no fluxo. +- **Norma no repo:** `.agents/rules/campanha-action-feedback.mdc` (feedback de action); shells de formAction existentes. +- **Fonte oficial:** https://react.dev/reference/react/useActionState · https://react.dev/reference/react-dom/hooks/useFormStatus · https://react.dev/reference/react/useOptimistic · https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations +- **Receita:** escolher DELIBERADAMENTE reset vs preservação (React 19 reseta uncontrolled forms com `action=`); pending no botão via `useFormStatus` dentro do form; optimistic sempre parado com revalidação explícita. +- **Precedente:** C139/C140 — ~21 forms da campanha com reset indesejado (`docs/plans/react-19-form-reset-campanha.md`). + +## 5. Caching ladder violada + +- **Sintoma/heurística:** dado vivo 2026 sob `unstable_cache`/`cache()` sem invalidação no write-path; auth/sessão dentro do core cacheado; fetch sem tag onde o write-path deveria revalidar. Buscar: `unstable_cache(`, `revalidateTag` ausente no domínio que escreve o dado cacheado. +- **Norma no repo:** `.agents/rules/engineering-standards.mdc:34-43` (ladder: `cache()` → `unstable_cache`+tag → artifact script → nunca live data sem invalidação). +- **Fonte oficial:** https://nextjs.org/docs/app/guides/caching · https://nextjs.org/docs/app/api-reference/functions/unstable_cache · https://react.dev/reference/react/cache · https://nextjs.org/docs/app/api-reference/functions/revalidatePath +- **Receita:** subir/descer na ladder conforme volatilidade; tag por domínio + `revalidateTag` no write-path; auth fora do core cacheado. +- **Precedente:** tags `posts`, `election-tse` com bust via `/api/revalidate`. + +## 6. URL serializer chegando ao browser + +- **Sintoma/heurística:** client component importando módulo de parse/canonicalização de URL de lista (`utilities/*ListUrl.ts`, parsers de `*Data.ts`) por valor; bundle cresce em rota que só renderiza link. Buscar: `import .*ListUrl` em `'use client'`. +- **Norma no repo:** `.agents/rules/codebase-map.mdc:33` (lição B14: casamento URL↔salvo tem de continuar puro e leve — sidebar custava ~21 kB). +- **Fonte oficial:** https://react.dev/reference/rsc/use-client +- **Receita:** mover regra pura para `lib/` (client-safe) e deixar serializador pesado server-only; `import type` quando só tipagem. +- **Precedente:** B14/P4-F (`docs/TECH-DEBT.md`). + +## 7. Live-region / a11y de componentes + +- **Sintoma/heurística:** `aria-live` montado DENTRO do que desmonta ao fechar (anúncio morre junto); região polite montada unconditionalmente em lista longa (N regiões); role/status faltando em feedback async. Buscar: `aria-live`, `role="status"` em `shared/` e células. +- **Norma no repo:** engineering-audit smell (:104); `.agents/rules/campanha-edit-where-you-see.mdc` quando tocar edição inline. +- **Fonte oficial:** https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Live_Regions (autoridade ARIA — react.dev/nextjs.org delegam a11y a ela) · https://react.dev/learn +- **Receita:** região persistente FORA do bloco desmontável; UMA região compartilhada por tela, texto injetado no anúncio. +- **Precedente:** B32+ (constantes de mensagem + live-region). + +## 8. Loading/streaming feedback errado + +- **Sintoma/heurística:** spinner desabilitando o CONTROLE em vez de dimar o resultado; falta ``/`loading.tsx` em rota com await visível; skeleton cobrindo página inteira onde uma ilha basta. +- **Norma no repo:** `.agents/rules/engineering-standards.mdc:29-32` (transitions + Suspense; "dima o resultado, não o controle"). +- **Fonte oficial:** https://react.dev/reference/react/Suspense · https://react.dev/reference/react/useTransition · https://nextjs.org/docs/app/guides/caching +- **Receita:** `useTransition`/`isPending` dimando resultado; `loading.tsx` por segmento; `CampaignListPending` como shell pendente de lista. +- **Precedente:** `shared/CampaignListPending.tsx`. + +## 9. next/image / next/fonts / assets mal usados + +- **Sintoma/heurística:** `` raw onde `next/image` serve (sem motivo documentado — ícone SVG inline é motivo); `width/height` ausentes (CLS); fontes via CSS `@import` em vez de `next/font`; `priority` faltando no hero acima da dobra. +- **Norma no repo:** convenção de Media/MediaProxy do site público (`AGENTS-public.md` quando tocar `/api/media/file`). +- **Fonte oficial:** https://nextjs.org/docs/app/api-reference/components/image · https://nextjs.org/docs/app/api-reference/components/font +- **Receita:** migrar para `next/image` com dims explícitas; `next/font/local|google` com subset; `priority` só no LCP real. +- **Precedente:** capas servindo 200 via proxy `/api/media/file/...` (OPS52). diff --git a/.agents/skills/react-audit/context7-setup.md b/.agents/skills/react-audit/context7-setup.md new file mode 100644 index 00000000..4b78da60 --- /dev/null +++ b/.agents/skills/react-audit/context7-setup.md @@ -0,0 +1,37 @@ +# Context7 MCP — setup manual do dono da máquina + +O Context7 dá acesso à documentação oficial vigente de React/Next.js dentro das sessões +do opencode (evita fix contra API desatualizada). É **config global da máquina, fora do +git** (precedente OPS89/OPS92) — o repo NÃO declara esse MCP. Sem ele, a skill +`react-audit` funciona igual usando as fontes oficiais embutidas em `anti-patterns.md`. + +## Declarar globalmente (uma vez, por máquina) + +Edite `~/.config/opencode/opencode.jsonc` e adicione ao bloco `"mcp"` existente +(mesmo shape do MCP `jina` que já vive lá): + +```jsonc +{ + "mcp": { + // ...mcp existentes... + "context7": { + "type": "remote", + "url": "https://mcp.context7.com/mcp", + "enabled": true + } + } +} +``` + +Valide antes de confiar: + +```bash +opencode debug config # o bloco context7 aparece resolvido, sem erro de schema +``` + +## Uso na skill + +- O executor consulta a doc oficial vigente ANTES de classificar/fixar (Passo 1 e Passo 3). +- Se o MCP não estiver disponível ou falhar: siga com as URLs de + `anti-patterns.md`, cite-as no relatório e marque o achado como "fonte conferida por URL". +- Nunca bloqueie uma run por falta do Context7 — é aceleração, não requisito. diff --git a/docs/plans/ops97-react-audit-skill-impl.md b/docs/plans/ops97-react-audit-skill-impl.md new file mode 100644 index 00000000..9b4ba0ac --- /dev/null +++ b/docs/plans/ops97-react-audit-skill-impl.md @@ -0,0 +1,140 @@ +# Impl: Skill /react-audit com ciclo fechado plan→implement de anti-patterns React/Next.js + +Status: em execução +Atualizado em: 2026-08-25 +Issue: #901 +Intenção: docs/plans/ops97-react-audit-skill.md +Appetite restante: herdado (~1,5–2 d eng) + +## Leitura da intenção + +- **Outcome:** skill funcional `.agents/skills/react-audit/SKILL.md` que roda sozinha (à noite, sem supervisão), varre o repo por anti-patterns **React/Next.js** (catálogo de famílias FIXO, cada achado com `file:line` + família + fonte oficial react.dev/nextjs.org), planeja e implementa cada fix com o gate completo do repo, e entrega **um único PR** para `main` — **ready, sem auto-merge, CI green, mergeable, sem conflitos** — com **um commit por fix** e **a descrição do PR como relatório consolidado**. A skill para aí: o humano explora e mescla de manhã. +- **O que NÃO negociar:** nada mergeia sem humano (nem armar auto-merge, nem flipar draft com checks verdes — ver decisão ii); catálogo fechado — achado sem `file:line` + família + fonte oficial **não vira fix**; fallback sem Context7 (MCP é aceleração, não requisito); precheck read-only antes de qualquer escrita; achado em dono travado (URLs públicas, Consent/LGPD, migrations shipped, collections) vira follow-up no relatório, não muda o PR; **sem migration e sem schema Payload** (tooling de desenvolvimento); fixes seguem "editar o dono, não twinar"; gate commands bare, nunca piped; `knip --fix` cego proibido; DB local only (`teqo_test`); nunca tocar Issues in-progress alheias nem claimar na sessão — a skill roda dentro do worktree já provisionado da run. +- **O que reavaliar:** a hipótese da intenção de que subagentes vivem em `.opencode/agent/*.md` — confirmado que lá só há agentes específicos (design-vision, penpot, postgres…); os subagentes da skill são **Tasks ad-hoc com prompt inline**, no padrão `work-issue` (input mínimo, "Não escrever código nem planos", output ≤25 linhas). A citação `codebase-map.mdc:26` na intenção aponta a lição client-boundary errada por um deslocamento — a lição B14 vive em `codebase-map.mdc:12` e `:33` (e `engineering-standards.mdc:22-28`). O shape exato do bloco `mcp remote` do Context7 é validado na execução com `opencode debug config` (OPS89/OPS92). + +## Abordagem recomendada + +Clonar o esqueleto operacional do `engineering-audit` (precheck fail-closed → canon → sweep por famílias fixas → triage com números) e trocar a cauda: onde o engineering-audit termina em ledger + PR Ready **com** auto-merge, o react-audit termina em **implementação + PR único ready SEM auto-merge**. O ciclo é: sweep read-only → triage per-fix (plano curto por fix) → loop de implementação com gate completo e um commit por fix → relatório consolidado → PR único com a descrição-relatório → desarme ativo do auto-merge → estabilização (CI green + mergeable + sem conflitos + `auto_merge = null`) → parar sem mergear. + +```mermaid +flowchart TD + A["Passo 0 · Precheck fail-closed
worktree provisionado · branch ≠ main
token válido · baseline bare"] --> B["Passo 1 · Sweep read-only
canon + catálogo FIXO de famílias
Tasks ad-hoc por área"] + B --> C["Passo 2 · Triage per-fix
file:line + família + fonte oficial
dedup TECH-DEBT · cap ≤ 6 fixes
dono travado → follow-up"] + C --> D["Passo 3 · Loop por fix
mini-plano → implementar (editar o dono)
gate completo bare + e2e afetado
1 commit · push da branch (sem PR)"] + D --> E{"elegíveis
restantes?"} + E -->|"sim"| C + E -->|"não"| F["Passo 4 · Relatório consolidado
descrição do PR + changelog da run
follow-ups → Issues"] + F --> G["Passo 5 · PR único Ready
github-pr.mjs --body-file relatório
nunca draft · nunca Closes alheio"] + G --> H["Desarmar auto-merge
DELETE /pulls/N/auto-merge
verificar auto_merge = null"] + H --> I["Passo 6 · Estabilizar
checks green + mergeable + sem conflitos"] + I --> J{"green ∧ mergeable
∧ auto_merge null?"} + J -->|"não"| K["conserta/rebase na branch
push → REDESARMAR auto-merge"] --> I + J -->|"sim"| L["Done · PARA sem mergear
humano revisa de manhã"] +``` + +### Decisões de engenharia + +**(i) Onde mora o catálogo de anti-patterns** + +```text +Opções: A) inline na SKILL.md | B) arquivo irmão na pasta da skill (.agents/skills/react-audit/anti-patterns.md) | C) só referência a engineering-standards.mdc / codebase-map.mdc +Recomendação: B — o catálogo é conhecimento volátil (amarra versões next 15.4.11 / react ^19.2.4 e URLs de docs oficiais) e grande (9 famílias × sintoma de busca + norma + fonte + receita + precedente); merece arquivo próprio carregado no Passo 1, mantendo a SKILL.md enxuta como máquina do ciclo. O catálogo CROSS-LINKA as normas canônicas (engineering-standards.mdc:22-28 client boundary, :29-32 loading feedback, :34-43 caching ladder; codebase-map.mdc:12 e :33 lição B14) em vez de as duplicar — a norma continua dona; o catálogo adiciona o que a norma não tem: heurística de busca, fonte oficial e receita de fix por família. Enumeração FECHADA: expandir o catálogo é PR próprio da skill, nunca edição in-run. +Alternativas rejeitadas: A porque infla o arquivo sempre-carregado da skill com conteúdo que só o Passo 1 usa e acopla fluxo estável a conteúdo que muda com versão; C porque os .mdc são alwaysApply (taxam TODA sessão) e não têm campos de auditoria (fonte oficial, sintoma, receita) — adicioná-los lá espalha conhecimento de audit num arquivo de convenções gerais. +``` + +**(ii) Mecanismo anti-auto-merge** + +Contexto: `.github/workflows/agent-pr-ready-automerge.yml` dispara em TODA PR same-repo → `main` nos eventos `opened, reopened, synchronize, ready_for_review, converted_to_draft` e arma o auto-merge nativo (rebase, via `AUTOMERGE_PAT`) em PR não-draft não-`cursor/*`; draft não-`cursor/*` = veto. A branch protection exige o check-run literal `checks` verde com `enforce_admins: true`. + +```text +Opções: A) abrir o PR Ready UMA vez, no fim, já estabilizado localmente, e desarmar o auto-merge após a abertura e após CADA push subsequente | B) manter o PR draft durante todo o trabalho (o safety net veta draft não-cursor), flipar para ready só no fim e desarmar em seguida +Recomendação: A — estruturalmente mais seguro. Os commits por fix são empurrados ANTES de qualquer PR existir (push de branch isolada não dispara workflow algum — resiliência sem risco); o PR nasce uma única vez, pronto, com CI frio ⇒ mesmo que o safety net arme em segundos, o merge espera o required check (janela de minutos, não de instantes) e o desarme idempotente (DELETE /repos/fsolla/teqo/pulls/N/auto-merge, 404 tolerado, verificar GET auto_merge = null) chega com folga. Regra dura da skill: TODO push pós-abertura (fix de CI vermelho, rebase) é imediatamente seguido do desarme + verificação — item de checklist obrigatório, nunca opcional; a condição final de Done inclui auto_merge = null. +Alternativas rejeitadas: B porque no momento do flip o required check JÁ está verde no SHA (estabilizamos no draft) ⇒ o evento ready_for_review arma o auto-merge e o servidor pode MERGEAR INSTANTANEAMENTE antes de qualquer DELETE — corrida de segundos com consequência inviolável (merge sem humano). Também rejeitada a variante ingênua de A (abrir a PR cedo e confiar num desarme único): cada synchronize re-arma silenciosamente, e defesa processualmente repetida numa madrugada sem supervisão é exatamente o tipo de guarda dodgeable que este repo aprendeu a rejeitar. +``` + +**(iii) Rastreamento per-fix ("plano impl + Issue trackeável")** + +```text +Opções: A) criar uma Issue GitHub por fix aceito | B) rastrear só no relatório (descrição do PR) | C) híbrido — ledger per-fix no relatório como registro canônico + Issues reais APENAS para follow-ups que sobrevivem ao PR (dono travado, fix adiado, família recorrente pedindo guarda) +Recomendação: C. Cada fix ganha no relatório um bloco rastreável: ID curto, família, file:line, fonte oficial, decisão (planejado/implementado/rejeitado/adiado + porquê), gate resultado e SHA do commit — isso SATISFAZ "Issue trackeável" no sentido da intenção (todo fix tem identidade e trilha auditável), sem poluir a fila. Follow-ups que precisam sobreviver ao PR viram Issues de verdade no Passo 4, pelo caminho canônico (pnpm agent:register ou a API em scripts/lib/github-api.mjs), título prefixado "[react-audit]", prio coerente, body com file:line + fonte + link do PR — nascendo DE PROPÓSITO na fila, não de acidente. +Alternativas rejeitadas: A porque Issue criada às 3h nasce claimable na fila compartilhada e convida um agente paralelo a implementar o mesmo fix FORA do PR — geminação garantida e violação do ciclo fechado (além da higiene "nunca tocar Issues in-progress alheias": as Issues da run seriam consumidas por outros); B porque follow-ups morreriam dentro do body de um PR que pode ser fechado sem merge. +``` + +**(iv) Branch dos fixes** + +```text +Opções: A) usar a branch do worktree provisionado da run | B) criar branch dedicada agent/-react-audit dentro do worktree +Recomendação: A com guarda — o ambiente provisionado (portas 3100+slot, DBs teqo_wt/teqo_wt_test, .env.local/.env.test.local) pertence ao WORKTREE, não a um nome de branch; a skill herda a branch corrente do provisionamento (pnpm worktree next) e só cria agent/-react-audit a partir de origin/main se estiver em main ou detached (guarda obrigatória do Passo 0: nunca commitar fix em main). +Alternativas rejeitadas: B porque cria segunda identidade para o mesmo trabalho (risco de divergência entre "a branch em que estou" e "a branch do PR") sem nenhum benefício — descoberta pelo humano é papel da notificação do PR e do título; rejeitado também rodar direto em main (coberto pela guarda). +``` + +### Componentes / mudanças + +- **`.agents/skills/react-audit/SKILL.md`** (novo): frontmatter `name: react-audit` + `description` (ciclo fechado sweep→plan→implement→PR único ready sem auto-merge; pt-BR no corpo, formato igual aos pares). Seções no padrão do repo: **"Proibido"** (DB de prod/`ALLOW_REMOTE_DB`; mergear ou armar auto-merge; `Closes #N` de Issue alheia no PR da run; `knip --fix` cego; gate piped; expandir catálogo in-run; tocar Issue in-progress alheia; claimar na sessão; draft→flip), **Checklist** em code block `- [ ] 0..6`, passos `## Passo N`, **"Done when"** + resumo final. Conteúdo por passo: + - **Passo 0 — Precheck fail-closed (read-only):** rodar dentro do worktree provisionado da run; `git status --porcelain` limpo; branch corrente ≠ `main` (senão criar `agent/-react-audit` de `origin/main`); `GITHUB_TOKEN` presente e validado com chamada barata ANTES do trabalho (não às 3h); baseline bare de `pnpm exec tsc --noEmit`, `pnpm lint`, `pnpm exec knip`, `pnpm check:cycles` com vermelhos pré-existentes anotados como baseline (política "dono do PR, dono do CI" — `docs/AGENT-OPS.md`); nota: pool morto desde OPS65, nada a pausar. + - **Passo 1 — Sweep read-only:** canon primeiro (`.agents/rules/engineering-standards.mdc`, `.agents/rules/codebase-map.mdc`, `AGENTS.md`, `docs/TECH-DEBT.md` para dedup); carregar `anti-patterns.md`; âncora de delta = data da última entrada react-audit em `docs/changelog/` (primeira run = repo inteiro); Tasks ad-hoc por família/área com prompt inline no padrão `work-issue` (input mínimo; "Não escrever código nem planos"; output ≤ 25 linhas: linhas `file:line + família + sintoma`); zero writes em `src/`, `tests/`, `scripts/`. + - **Passo 2 — Triage per-fix:** cada candidato precisa dos TRÊS selos — `file:line`, família do catálogo, fonte oficial (URL react.dev/nextjs.org); faltou um → linha "observado sem fix" no relatório; dedup contra `docs/TECH-DEBT.md` (classe B14/P4-F aberta) e guardas vivas (ESLint restrictions, `tests/unit/codebaseConventions.unit.spec.ts`); dono travado → follow-up; priorizar churn × severidade; **cap duro ≤ 6 fixes por run** (protege appetite e a revisão matinal; resto vira follow-up/adiado). + - **Passo 3 — Loop de implementação (por fix):** subagente escritor (Task) produz mini-plano per-fix (receita, blast radius, pins/testes a tocar, critério de feito) que vira seção do relatório; implementação com "editar o dono, não twinar" (contratos client-safe / `import type` ao cruzar fronteira); **gate completo BARE por fix**: `pnpm exec tsc --noEmit`, `pnpm lint`, `pnpm format:check`, `pnpm exec knip`, `pnpm check:cycles`, `pnpm test`, `pnpm build` + `pnpm test:e2e:affected` (discricionário, OPS72 — `gate:push` não roda e2e); exports órfãos resolvidos NO MESMO fix (`git grep -w ` antes de remover); **um commit por fix** (mensagem citando família + `file:line`); push da branch após cada commit (sem PR ainda ⇒ nenhum workflow dispara). + - **Passo 4 — Relatório + follow-ups + changelog:** montar o relatório consolidado (i escopo/data/números do sweep, ii tabela de achados file:line+família+fonte, iii estado e porquê por fix, iv estado final do PR, v follow-ups com destino); gravar como body-file; criar Issues de follow-up (decisão iii); escrever a entrada da run em `docs/changelog/-react-audit-run.md` (docs-guards: additions-only — nunca HISTORY nem agregado). + - **Passo 5 — PR único + desarme:** `GITHUB_TOKEN= node scripts/github-pr.mjs --head --title "react-audit : N fixes" --base main --body-file ` — nunca draft, **jamais `Closes/Fixes` de Issue alheia** (usar `Refs`); imediatamente desarmar: `disablePullRequestAutoMerge(n)` (novo helper) + verificar `GET auto_merge = null`. + - **Passo 6 — Estabilizar até o fim:** poll do check-run literal `checks` do head SHA (API de check-runs; `gh pr checks` onde houver gh) com backoff; `mergeable` via `getPullRequest()` (`scripts/lib/github-api.mjs:177` — `null` = computando, re-poll; `false` = rebase sobre `origin/main` → push → **redesarmar**); CI vermelho dentro do blast radius dos fixes → consertar na branch; infra morta → relatório honesto e parada com estado declarado; **caps de tentativa/tempo** (ex.: 3 ciclos de fix pós-abertura ou 90 min) para não rodar infinito; **Done = checks green ∧ `mergeable === true` ∧ `auto_merge = null` ∧ PR open+ready → PARAR sem mergear**. Feedback humano posterior: pedidos de mudança caem na MESMA branch (commits novos, relatório atualizado via PATCH do body, re-estabilização). +- **`.agents/skills/react-audit/anti-patterns.md`** (novo): catálogo FIXO, uma seção por família com `Sintoma/heurística de busca`, `Norma no repo` (âncora nos .mdc), `Fonte oficial` (URL react.dev/learn, react.dev/reference, nextjs.org/docs, nextjs.org/learn — conferidas contra next 15.4.11 / react ^19.2.4), `Receita de fix`, `Precedente` (B14 ~21 kB sidebar, B34 closures/fallbacks obsoletos, B32+ live-region, C139/C140 reset de form React 19 em `docs/plans/react-19-form-reset-campanha.md`, P4-F). Famílias fechadas (9): (1) client boundary/server-first (`'use client'` desnecessário, import de server module por VALOR, estado elevado além dos consumidores, view models não selecionados); (2) RSC payload bloat (doc Payload inteiro no fio); (3) effects fazendo derived-state / stale closures (B34); (4) forms & server actions (reset `action={submitAction}`, transição ausente, optimistic sem `revalidatePath`); (5) caching ladder (live data sem invalidação, auth dentro do core cacheado); (6) URL serializers chegando ao browser (B14); (7) live-region mistakes (B32+); (8) loading/streaming feedback (dim o resultado, não o controle; ``/`loading.tsx`); (9) next/image + fonts + a11y de componentes. +- **`.agents/skills/react-audit/context7-setup.md`** (novo): passo MANUAL do dono — declarar o MCP globalmente FORA do git em `~/.config/opencode/opencode.jsonc` (bloco `mcp` remote `https://mcp.context7.com/mcp`), validar com `opencode debug config`; deixado claro que a skill funciona sem (fallback = links oficiais embutidos no catálogo); o executor valida o shape vigente do bloco ao escrever o doc. +- **`scripts/lib/github-api.mjs`** (editar o dono — é o dono do acesso REST GitHub): adicionar `disablePullRequestAutoMerge(number)` (DELETE `/repos/{owner}/{repo}/pulls/{number}/auto-merge`, tolerando 404) e expor `auto_merge` no retorno de `getPullRequest`; reuso integral do que já existe (`createIssue`/`listIssues` para follow-ups, `mergeable` para estabilização). Único código novo da entrega; sem twin em curl/scripts avulsos. +- **Migration:** sem migration. **Access/Consent:** N/A (nenhuma collection/global tocada; fixes de produto continuam sujeitos às regras normais quando tocarem essas áreas — aí viram follow-up, não fix noturno). **UI:** Impeccable A — N/A sem UI. + +### Dados → forma (se aplicável) + +N/A — tooling de desenvolvimento, sem dado de negócio (herdado da intenção). + +## Fases verificáveis + +1. **Tracer — catálogo + sweep (≈0,5 d):** `anti-patterns.md` v1 (9 famílias com fontes conferidas nas versões reais do `package.json`) + `SKILL.md` Passos 0–2. **Verificação:** executar Passos 0–2 neste repo produz tabela de triage com ≥ 1 candidato real (`file:line` + família + fonte) e **zero writes** fora de `docs/`/`.agents/`. +2. **Ciclo fechado (≈0,75 d):** Passos 3–5 + helper `disablePullRequestAutoMerge` em `scripts/lib/github-api.mjs`. **Verificação:** dogfood com cap = 1 — um fix real pequeno elegível pelo próprio sweep, ciclado inteiro numa branch scratch (`agent/ops97-dogfood-react-audit`), resultando em PR Ready **sem auto-merge** (`auto_merge = null` verificado via GET) com o check `checks` disparado; o PR fica para o humano — a skill não mergeia nada. Se o sweep não achar nada elegível (legítimo), dogfood com um commit docs-only pelo mesmo caminho mecânico. +3. **Estabilização + endurecimento (≈0,25 d):** Passo 6 (loop CI/mergeable/redesarme + caps), `context7-setup.md`, changelog da entrega. +4. **Gates e PR da entrega OPS97:** `pnpm gate:fast`; push via `pnpm push`; PR da **entrega** segue o fluxo NORMAL do repo (Ready + auto-merge permitido — a restrição "sem auto-merge" vale para as RUNs da skill, não para o PR que a constrói); changelog `docs/changelog/2026-08-25-ops97.md`. + +## Rabbit holes / Não escopo (engenharia) + +- **Expandir o catálogo in-run** ("achei uma 10ª família à noite"): achado fora das 9 famílias vira linha de relatório/follow-up; expansão de catálogo é PR próprio e deliberado da skill. +- **Virar segundo engineering-audit:** smells gerais de Fowler, consolidation hunt e guardrails de miss permanecem lá; react-audit só as 9 famílias React/Next. +- **Guarda determinística por fix** (doutrina do Passo 4b do engineering-audit): fora do appetite desta entrega; família recorrente entre runs → follow-up de guarda via capture-review-debts. +- **Issues claimables por fix:** decisão (iii)-A rejeitada — polui a fila e convida geminação fora do PR. +- **Automatizar/depender do Context7:** config global é manual do dono (`context7-setup.md`); skill nunca bloqueia sem MCP. +- **Draft→flip no fim:** decisão (ii)-B rejeitada — corrida de instant-merge com checks já verdes. +- **`knip --fix` cego e gates piped:** herdados como proibições absolutas. +- **e2e full local / migrations / schema Payload:** `gate:push` não roda e2e (afetado apenas, discricionário); esta entrega não toca schema — fix de produto que exija migration vira follow-up planejado, nunca commit noturno. +- **Relatório como arquivo substituindo a descrição:** a descrição do PR É o relatório; arquivo em `docs/` é no máximo adicional, nunca substituto. +- **Rodar fora do worktree provisionado / claimar na sessão / tocar Issue in-progress alheia:** proibidos; a skill assume provisionamento prévio. +- **Cap de fixes estourar:** ≤ 6 por run, inegociável — PR maior derrota a revisão matinal commit a commit. + +## Riscos e mitigação + +- **Safety net re-armer o auto-merge a cada `synchronize`** (crítico — é o comportamento projetado do `agent-pr-ready-automerge.yml`): desarme idempotente é passo obrigatório PÓS-CADA-PUSH (não só pós-abertura); Done só com `auto_merge = null` verificado; PR aberto uma única vez, já estabilizado localmente, minimizando pushes pós-abertura. +- **Corrida de instant-merge:** estruturalmente evitada — nunca draft→flip (decisão ii); PR Ready nasce com CI frio (janela de minutos para o desarme, não segundos). +- **CI vermelho por causa alheia aos fixes:** política "dono do PR, dono do CI" com limite declarado — conserta se dentro do blast radius dos fixes; infra/estrutural → caps de tentativa/tempo, relatório honesto, parada com estado declarado (nunca fingir Done). +- **`mergeable = null` transitório** (GitHub computando): backoff e re-poll antes de decidir conflito. +- **Conflito com agente paralelo de madrugada:** risco baixo (pool morto desde OPS65); precheck lê Issues in-progress abertas e evita território ocupado; fixes concentram-se no delta desde a última run. +- **PAT ausente/sem escopo descoberto às 3h:** Passo 0 valida `GITHUB_TOKEN` com chamada barata ANTES de qualquer trabalho. +- **Fix órfã exports → knip ERROR:** resolução no mesmo fix com `git grep -w `; nunca `--fix` cego. +- **docs-guards (additions-only no pre-push E no CI):** cada run adiciona SEU arquivo de changelog; jamais tocar `CHANGELOG-AGENTS-HISTORY.md` ou o agregado. +- **Context7 indisponível:** fallback a links oficiais do catálogo; degradação silenciosa, nunca falha. +- **Sweep achar pouco/nada:** outcome legítimo — relatório registra; dogfood cai para o caminho docs-only; a skill não inventa achado para justificar o run. + +## Aceite de engenharia + +- [ ] Aceite de produto da intenção ainda coberto — mapa: sweep focado com file:line+família+fonte → Passos 1–2; fix com gate completo e commit próprio → Passo 3; PR único ready sem auto-merge/mergeable/sem conflitos → Passos 5–6; commits separados por fix → Passo 3; descrição do PR = relatório (i–v) → Passo 4; funcional sem Context7 + setup manual documentado → catálogo + `context7-setup.md`; só termina com PR estável → condição de Done do Passo 6. +- [ ] Invariantes AGENTS/engineering-standards — gates bare nunca piped; `knip` sem `--fix`; DB local only (`teqo_test`); "editar o dono, não twinar" (único código novo é o helper no dono REST); sem migration/schema; identificadores em inglês e copy pt-BR; changelog por entrega (additions-only); LGPD/Consent e URLs públicas intocados (achado nelas = follow-up). +- [ ] Testes de domínio previstos — N/A para access/write paths (nada muda); smoke do helper `disablePullRequestAutoMerge` exercitado no dogfood da Fase 2 contra PR real; validação fim-a-fim da skill é a própria execução dogfood (PR real, `auto_merge = null` verificado, checks disparados, nada mergeado). + +--- + +### Self-score decision-quality (gate ≥ 4) + +1. Decisões caras têm rejeitadas? — **5** (quatro decisões nomeadas com alternativas rejeitadas; o mecanismo anti-auto-merge foi escolhido por análise de corrida, não por gosto). +2. Abordagem cabe no appetite da intenção? — **4** (fases somam ≈1,5–2 d; cap duro ≤ 6 fixes/run e dogfood cap = 1 impedem o sweep de inflar o ciclo). +3. Rabbit holes nomeados? — **5** (11 itens, cada um com corte explícito). +4. Depth check: reusa shells/helpers existentes? — **5** (esqueleto do engineering-audit, `scripts/github-pr.mjs`, `scripts/lib/github-api.mjs`, `gate:fast`/`gate:push`, padrão de Tasks do work-issue, dedup em TECH-DEBT; código novo = 1 helper no dono). +5. Intenção (aceite de produto) permanece satisfeita? — **5** (nenhum aceite reescrito; todas as decisões são mecanismo — onde mora o catálogo, como impedir merge de bot, como rastrear fix, qual branch — nunca outcome). + +**Média 4,8 ≥ 4** — apto a marcar `aprovado` e executar.