diff --git a/.source/source.config.mjs b/.source/source.config.mjs
index 8405202c55..1b0644fe1e 100644
--- a/.source/source.config.mjs
+++ b/.source/source.config.mjs
@@ -4,7 +4,6 @@ var docs = defineDocs({
dir: "docs",
docs: {
files: [
- "./getting-started/**/*.md",
"./architecture/**/*.md",
"./guides/**/*.md",
"./reference/**/*.md",
diff --git a/PLANO-QUALITY-GATES-FASE6A.md b/PLANO-QUALITY-GATES-FASE6A.md
deleted file mode 100644
index 3025e6e95c..0000000000
--- a/PLANO-QUALITY-GATES-FASE6A.md
+++ /dev/null
@@ -1,209 +0,0 @@
-# Fase 6A — Auditoria Crítica das Fases 0–6: o que deixamos passar
-
-> **Para workers agênticos:** SUB-SKILL OBRIGATÓRIA: `superpowers:subagent-driven-development` (recomendado) ou `superpowers:executing-plans`, tarefa-a-tarefa. Tarefas P0/P1 maiores devem ser **expandidas em sub-plano bite-sized próprio** (`writing-plans`) no momento da execução. Hard Rule #18 (TDD/VPS) em tudo. Auditar subagentes (trust-but-verify) após cada task.
-
-> # ⏳ PORTÃO DE ATIVAÇÃO — NÃO INICIAR ANTES DE **2026-06-16**
-> Mesma janela da Fase 7 (decisão do owner 2026-06-09: 1 semana de uso em produção das Fases 0–6 antes de evoluir). **Ordem na ativação: Fase 6A ANTES da Fase 7** — primeiro consertamos/endurecemos o que já existe, depois adicionamos ferramentas novas.
-> **Exceção possível (decisão do owner):** as tasks **6A.1 e 6A.2 são bugs pré-existentes descobertos pela auditoria** (testes que nunca rodam + suíte vitest fora do CI), não "gates novos" — podem ser antecipados como fix avulso se o owner preferir não esperar a janela.
-
-**Goal:** Fechar os furos que a auditoria crítica pós-implementação (2026-06-09, análise inline dos 18 gates + motor + CI + baselines) encontrou nas Fases 0–6 — antes de adicionar qualquer ferramenta nova na Fase 7.
-
-**Architecture:** Zero ferramenta nova (tudo homegrown, padrão `check-*.mjs` + motor existente). Três frentes: (1) **bugs sistêmicos de runner** descobertos (testes órfãos, vitest fora do CI); (2) **endurecimento do padrão de catraca** (stale-allowlist enforcement + require-tighten, validados pela prática da Notion); (3) **expansão de escopo** dos gates existentes (diretórios/superfícies que ficaram de fora).
-
-**Tech Stack:** Node ≥20 ESM, ESLint 9 flat, c8, jscpd@4 (a pinar), GitHub Actions, Node native test runner, vitest. Nada novo em `dependency-allowlist.json` exceto a promoção do jscpd a devDependency (Task 6A.12).
-
----
-
-## Origem: o que a auditoria encontrou (resumo dos achados)
-
-Método: releitura inline de todos os `scripts/check/*.{mjs,ts}` criados nas Fases 0–6, `scripts/quality/*`, baselines, `ci.yml`, `package.json`, hooks Husky e docs — sem subagentes — mais validação por pesquisa (sistema de ratcheting da Notion; práticas de suppression-hygiene de linters).
-
-| # | Achado | Gravidade | Task |
-|---|--------|-----------|------|
-| A1 | **≈135 arquivos `*.test.ts` em subdiretórios de `tests/unit/` não são coletados por NENHUM runner** — `test:unit`, `test:coverage` e os shards do CI usam o glob não-recursivo `tests/unit/*.test.ts`; o vitest só inclui `autoCombo/**` (+ `.tsx`). Inclui `authz/routeGuard.test.ts` (Hard Rules #15/#17), 50 testes de `compression/`, 12 de `services/`, 10 de `gamification/`, 6 de `guardrails/`, 5 de `security/`. **Amostra rodada na auditoria: 2 asserts de `routeGuard.test.ts` FALHAM hoje** ("management policy allows /api/services/ (e /api/copilot/chat) from localhost with valid CLI token") — o arquivo apodreceu sem ninguém ver, provavelmente desde o redesign do peer-stamp (2026-05-31) | **P0 — falso verde sistêmico** | 6A.1 |
-| A2 | **Nenhum workflow roda `test:vitest`** (`grep -rln vitest .github/workflows/` = vazio). O CLAUDE.md afirma "Both test runners must pass… before merging", mas a suíte vitest (MCP server 43 tools, autoCombo, cache, componentes) está 100% fora da esteira | **P0** | 6A.2 |
-| B1 | **Nenhum gate falha quando uma entrada de allowlist deixa de ser necessária.** Os 18 gates congelam ~90 violações em `KNOWN_*`; quando alguém corrige a violação (ex.: criar `/api/gamification/level` da issue #3484, remover `krutrim` da #3483), a entrada vira um furo aberto — a regressão pode VOLTAR sem revisão. Só `check-error-helper` tem detecção parcial (WARN de arquivo inexistente, que ninguém lê). Prática validada: linters maduros "yell when an exclusion exists that doesn't break the rule" | **P0 — corrói a catraca com o tempo** | 6A.3 |
-| B2 | **Melhoria não-capturada vira folga permanente no motor**: sem `--update` manual, uma métrica que melhorou pode regredir de volta até o baseline antigo sem ninguém ver. A Notion auto-decrementa budgets no pre-commit; nosso motor não exige aperto | P1 | 6A.5 |
-| B3 | EPS único (0.01) para todas as métricas; o plano da Fase 4 pedia epsilon maior para `coverage.branches` (não-determinismo do v8) e não foi implementado | P1 | 6A.5 |
-| B4 | Métrica coletada sem entrada no baseline é ignorada em silêncio (coletor novo esquecido do baseline = falso conforto) | P2 | 6A.5 |
-| C1 | `check-fetch-targets` só varre `src/app/(dashboard)` — **20+ arquivos com `fetch("/api/…")` fora do escopo**: `src/shared/components/` (Sidebar, CommandPalette, modais…), `src/app/connect/`, `src/app/status/`, `src/lib/evals/` | P1 | 6A.7 |
-| C2 | `check-fetch-targets` ignora 100% dos template literals (`` fetch(`/api/x/${id}`) ``) — nem o prefixo estático é validado | P1 | 6A.7 |
-| C3 | `check-fetch-targets` não valida o método HTTP (fetch `POST` → rota só com `GET` = 405 em runtime, gate verde) | P2 | 6A.7 |
-| C4 | `check-deps` cobre só `package.json` raiz + `electron/` — **`@omniroute/opencode-plugin` (dep `zod` + 5 devDeps, pacote PUBLICADO no npm), `@omniroute/opencode-provider` e `open-sse/` ficam fora** | P1 | 6A.8 |
-| C5 | `check-public-creds` escaneia só 2 arquivos hardcoded — credencial literal em arquivo NOVO (executor, oauth provider) passa batida | P1 | 6A.8 |
-| C6 | `check-error-helper` cobre `open-sse/executors` + `handlers` — a Hard Rule #12 também fala de **MCP handlers** (`open-sse/mcp-server/`) e rotas HTTP (`src/app/api/`), fora do escopo | P1 | 6A.8 |
-| C7 | `check-file-size` e `check-complexity` varrem `src` + `open-sse` — `electron/` e `bin/` fora (god-file pode nascer lá) | P2 | 6A.11 |
-| C8 | `check-known-symbols` cobre executors/strategies/translators — **faltam as 3 superfícies de despacho por-string restantes: MCP tools (43), A2A skills (5, `A2A_SKILL_HANDLERS`), cloud agents (3, registry)** | P1 | 6A.9 |
-| C10 | `check-route-guard-membership` depende da lista manual `SPAWN_CAPABLE_ROUTE_ROOTS` (3 raízes) — rota nova que spawna processo FORA dessas raízes é invisível ao gate | P1 | 6A.8 |
-| C11 | `check-openapi-coverage` (THRESHOLD=36%) e `check-ui-keys-coverage` (65%) são pisos fixos manuais — não ratcheteiam; rotas/strings novas sem doc/i18n passam enquanto o % não cai do piso | P2 | 6A.11 |
-| C12 | `check-test-masking`: (a) `--diff-filter=M` não vê teste **DELETADO** (o masking mais brutal); (b) não vê `.skip`/`.todo`/`.only` adicionados (mantêm os asserts no texto, mas nunca rodam); (c) tautologia só cobre `assert.ok(true)` | P1 | 6A.10 |
-| D1 | **`ci.yml` roda gates apenas em `pull_request → main`** — todo o ciclo de PRs feature→`release/vX` passa SEM gate; as violações acumulam por semanas e estouram juntas no merge release→main (observado no gate da v3.8.18: 4 fixes de typecheck + ReDoS de última hora) | P1 — decisão do owner | 6A.6 |
-| D2 | `.husky/pre-push` está 100% comentado, mas o CLAUDE.md afirma "pre-push: npm run test:unit" (drift doc↔real) | P2 | 6A.12 |
-| D3 | **CLAUDE.md não menciona nenhum dos 18 gates** — um agente futuro não sabe que existem, qual a política de allowlist ("corrija, não congele"), nem como apertar baselines. Hard Rule #9 ainda diz "≥60%" (a catraca real é 80/80/82/73) | **P0 — anti-alucinação para os próprios agentes** | 6A.4 |
-| D4 | `check-duplication` roda `npx --yes jscpd@4` — pacote **não pinado por lockfile**, baixado do registry a cada run do CI (supply-chain + flakiness + latência); contraria o espírito do próprio `check-deps` | P2 | 6A.12 |
-| D5 | A skill `/quality-scan` roda ~20 comandos um a um — falta um runner agregador paralelo | P2 | 6A.12 |
-| D6 | Baselines de coverage com folga de ~2,5pt (80/80/82/73 vs real ~82,6/82,6/84,2/75,2) — a nota "_aperte após o 1º run verde_" existe no JSON mas não é tarefa de ninguém | P1 | 6A.5 |
-| E4 | Sem guarda contra artefato trackeado por engano — `node_modules` symlink já foi commitado 2× neste repo (`git add -A` em worktree) | P2 | 6A.12 |
-
-**Decisões conscientes de NÃO fazer (avaliadas e descartadas, com motivo):**
-- **Gate de idempotência de migrations via regex** — SQLite não tem `ADD COLUMN IF NOT EXISTS`; idempotência vive em try/catch do runner; regex seria frágil (FP/FN). O `check-migration-numbering` + revisão humana bastam.
-- **Endurecer `check-docs-counts-sync` para fail** — contagens em prosa são heurísticas; soft-fail é o design correto. A cobertura de MCP tools entra pela 6A.9 (símbolos, não prosa).
-- **Sentido inverso do provider-consistency (providers.ts → REGISTRY)** — muitos providers canônicos legitimamente não têm entrada no REGISTRY (web/OAuth-only); a allowlist nasceria com dezenas de entradas e baixa razão sinal/ruído. Reavaliar quando o refactor #3501 tocar o split de providers.
-- **Complexidade por-função/por-arquivo (formato any-budget)** — upgrade real, mas o count global + `max-lines-per-function` já bloqueiam o grosso; o formato per-file entra junto com `sonarjs/cognitive-complexity` na Fase 7 Task 5 para não pagar duas migrações de baseline.
-
----
-
-# Tasks
-
-## P0 — bugs sistêmicos + documentação
-
-### Task 6A.1 — `check-test-discovery` + religamento triado dos ~135 testes órfãos ⭐
-
-**O achado nº1 da auditoria.** Testes que não rodam são o falso verde definitivo — todo o investimento anti test-masking da Fase 4 protege asserts de testes que **nem executam**.
-
-**Files:**
-- Create: `scripts/check/check-test-discovery.mjs` + `tests/unit/check-test-discovery.test.ts`
-- Modify: `package.json` (globs de `test:unit`, `test:coverage`), `.github/workflows/ci.yml` (globs dos shards 8×/node24/node26), `vitest.mcp.config.ts` ou `vitest.config.ts` (se algum subdir for re-homed para vitest)
-- Create: `test-discovery-baseline.json` (órfãos ainda-não-religados, catraca `down` até zerar)
-
-**Approach (expandir em sub-plano na execução):**
-1. **Gate primeiro (TDD):** `check-test-discovery.mjs` enumera todo `**/*.{test,spec}.{ts,tsx}` do repo (fora de `node_modules`/`.next`) e verifica que cada arquivo é coletado por ≥1 runner: (a) globs do node test runner extraídos de `package.json`/`ci.yml`; (b) `include` dos dois `vitest.*config.ts`; (c) projetos Playwright. Órfão fora do baseline → exit 1. O baseline congela os órfãos atuais (catraca: não pode SUBIR; religamentos a fazem cair até `{}`).
-2. **Inventário verde/vermelho:** rodar cada subdir órfão isoladamente (`node --import tsx --test tests/unit/
/*.test.ts`), registrar passa/falha. *Não* ligar tudo de uma vez — a amostra já provou que há vermelhos (`authz/routeGuard.test.ts`: 2 asserts).
-3. **Religar os verdes:** trocar o glob principal para recursivo — `"tests/unit/**/*.test.ts"` ENTRE ASPAS (expandido pelo test runner do Node, não pelo shell; **Step 0: validar o suporte a glob do runner na menor versão de Node suportada pelo repo** — fallback: listar os subdirs explicitamente) — em `test:unit`, `test:coverage` e nos 3 lugares do `ci.yml` (shards 8×, node24, node26). Remover religados do baseline.
-4. **Triar os vermelhos (Hard Rule #18 em cada um):** teste desatualizado → atualizar para o comportamento real (e provar que o comportamento real é o desejado); bug real revelado → fix TDD; teste de feature morta → deletar com justificativa no commit. Os 2 asserts do `routeGuard.test.ts` ("allows … with valid CLI token") são o primeiro caso: provável drift do peer-stamp de 2026-05-31 — MAS, por ser superfície de segurança (#15/#17), confirmar com cuidado que é o teste que está errado, não o guard.
-5. **Recalibrar cobertura:** religar ≈135 arquivos muda o denominador/numerador da cobertura — re-medir e apertar `quality-baseline.json` via `--update` no mesmo PR (resolve também o D6).
-
-**Acceptance:** `check-test-discovery` no CI (job lint); zero órfãos fora do baseline; baseline decrescente documentado; suíte verde com os religados; cobertura recalibrada.
-
-### Task 6A.2 — vitest no CI
-
-**Files:** `.github/workflows/ci.yml` (job novo `test-vitest`, paralelo aos shards; NÃO tocar nos triggers — apenas adicionar job).
-
-**Approach:** job com `npm ci` + `npm run test:vitest` (+ `test:vitest:ui` se o tempo couber; senão segundo step). Rodar localmente primeiro para garantir verde (a suíte passa hoje fora da esteira — confirmar). Se houver vermelho pré-existente, triagem antes do wire (mesmo protocolo da 6A.1 passo 4).
-
-**Acceptance:** PR→main roda as DUAS suítes; o claim do CLAUDE.md ("both must be green") vira verdade mecânica.
-
-### Task 6A.3 — Stale-allowlist enforcement em todos os gates (suppression hygiene)
-
-**O endurecimento sistêmico nº1.** Padrão validado (ESLint `--report-unused-disable-directives`; Notion): exclusão que não exclui nada vivo é dívida fantasma e furo de regressão.
-
-**Files:** todos os gates com allowlist + seus testes:
-`check-fetch-targets` (KNOWN_MISSING, 7) · `check-provider-consistency` (KNOWN_REGISTRY_ONLY, 1) · `check-openapi-routes` (KNOWN_STALE_SPEC, 1) · `check-public-creds` (KNOWN_LITERAL_CREDS, 5) · `check-db-rules` (KNOWN_UNEXPORTED 25 + KNOWN_RAW_SQL 15) · `check-docs-symbols` (KNOWN_STALE_DOC_REFS, 30) · `check-migration-numbering` (KNOWN_GAPS/DUPLICATES) · `check-error-helper` (KNOWN_MISSING_ERROR_HELPER, 7 — promover o WARN existente a FAIL e cobrir também "arquivo existe mas não viola mais") · `check-deps` (dependency-allowlist: entrada sem dep correspondente em manifest algum = stale) · `check-file-size` (entrada `frozen` cujo arquivo foi deletado/renomeado) · `check-route-guard-membership` (KNOWN_UNCLASSIFIED — vazio hoje; implementar o check para quando deixar de ser).
-
-**Approach (mecânica única, TDD por gate):** após a detecção normal, re-avaliar cada entrada da allowlist: *"se esta entrada não existisse, o gate flagaria algo?"* — para allowlists de path/valor isso é `violationsDetectadas.has(entry)`; para arquivos, existência + violação presente. Entrada que não suprime nada → **exit 1** com mensagem `entrada obsoleta — a violação foi corrigida; REMOVA a entrada para travar a correção`. Extrair helper comum `reportStaleEntries(allowlist, liveViolations, gateName)` em `scripts/check/lib/allowlist.mjs` para não duplicar 11×.
-
-**Acceptance:** corrigir qualquer violação congelada (ex.: as issues #3483–#3501) passa a EXIGIR a remoção da entrada no mesmo PR; teste sintético prova fail-on-stale em cada gate.
-
-### Task 6A.4 — Documentar os gates no CLAUDE.md (+ corrigir drifts de doc)
-
-**Files:** `CLAUDE.md`, `AGENTS.md` (se houver seção espelho), `docs/architecture/` (página `QUALITY_GATES.md` referenciada pela tabela de docs).
-
-**Approach:** (1) seção nova "Quality Gates & Ratchets" no CLAUDE.md: tabela dos gates (nome → o que trava → allowlist/baseline), a política **"corrija a causa; allowlist só com justificativa + issue"**, como apertar (`npm run quality:ratchet -- --update`, `check: -- --update`), e o que fazer quando um gate falha num PR. (2) Corrigir: Hard Rule #9 (60% → "catraca de cobertura: nunca abaixo do baseline congelado em `quality-baseline.json`; piso absoluto 60"), claim do pre-push (refletir o real pós-6A.12), claim "both runners" (verdade pós-6A.2). (3) Página `docs/architecture/QUALITY_GATES.md` com o detalhe operacional (o CLAUDE.md fica curto, linka). Rodar `check:docs-all` após editar (o próprio docs-sync valida).
-
-**Acceptance:** agente novo lendo o CLAUDE.md descobre os gates e a política sem ler scripts; `check:docs-all` verde.
-
-## P1 — endurecimento do motor + escopos
-
-### Task 6A.5 — Motor v2: `--require-tighten`, eps por métrica, métricas órfãs
-
-**Files:** `scripts/quality/check-quality-ratchet.mjs`, `quality-baseline.json` (schema), `tests/unit/quality-ratchet.test.ts`, `.github/workflows/ci.yml` (flag no job quality-gate).
-
-**Approach (TDD):**
-1. Schema por métrica ganha campos opcionais: `eps` (default 0.01) e `tightenSlack` (default: igual a `eps`).
-2. Novo modo `--require-tighten` (ligado no CI): se `atual` melhor que `baseline` além de `tightenSlack`, **exit 1** com `melhorou de X para Y — rode 'npm run quality:ratchet -- --update' e commite o baseline apertado neste PR`. Métricas determinísticas (`eslintWarnings`) usam slack 0; cobertura usa slack 1.5 (flutuação v8). É o "auto-decrement" da Notion adaptado a CI sem bot de commit.
-3. Warning para métricas presentes em `quality-metrics.json` sem entrada no baseline (coletor órfão).
-4. Calibrar os 4 `coverage.*` para o real medido (fecha D6 — coordenar com a 6A.1 passo 5, que muda a base).
-
-**Acceptance:** melhoria sem aperto de baseline falha no CI; flutuação de coverage dentro do slack não falha; testes cobrem os 3 comportamentos novos.
-
-### Task 6A.6 — `quality.yml`: gates rápidos em PRs → `release/**` ⚠️ DECISÃO DO OWNER
-
-**Contexto sensível:** o owner já reverteu mudança de trigger no `ci.yml` ("não mexe na CI"). Esta task **não toca o `ci.yml`** — cria um workflow NOVO e enxuto. Ainda assim, **passo 0 = confirmação explícita do owner**.
-
-**Files:** Create: `.github/workflows/quality.yml`.
-
-**Approach:** `on: pull_request: branches: ["release/**"]`; um job único (~1–2 min) só com os gates determinísticos filesystem-only: provider-consistency, fetch-targets, openapi-routes, docs-symbols, deps, file-size, error-helper, migration-numbering, public-creds, db-rules, known-symbols, route-guard-membership, test-discovery (pós-6A.1) + `check:any-budget:t11`. SEM lint/test/build (continuam só no PR→main). Ganho: a violação aparece no PR que a introduz, não semanas depois no gate do release (padrão observado: 4 fixes de última hora no release da v3.8.18).
-
-**Acceptance:** PR de teste contra a release branch com uma rota inventada falha em <2 min; PRs limpos não ganham mais que ~2 min de CI.
-
-### Task 6A.7 — `check-fetch-targets` v2: escopo completo + prefixo de template + método HTTP
-
-**Files:** `scripts/check/check-fetch-targets.mjs`, `tests/unit/check-fetch-targets.test.ts`.
-
-**Approach (TDD):**
-1. **Escopo:** varrer todo `src/**/*.{ts,tsx}` client-side (excluindo `src/app/api/**`, testes, `src/lib/db`), não só `(dashboard)` — congela os misses pré-existentes que aparecerem em `KNOWN_MISSING` (com triagem/issue por cluster, igual Fase 2).
-2. **Template literals:** extrair o prefixo estático de `` fetch(`/api/x/y/${id}…`) `` e validar por **prefix-match** contra as rotas reais (existe alguma rota cujo path começa com `/api/x/y/`?). Pega diretório inteiro alucinado; não tenta resolver o sufixo dinâmico.
-3. **Método HTTP (heurístico, mesma chamada):** quando o 2º argumento literal contém `method: "POST"` (etc.), verificar que o `route.ts` resolvido exporta a função correspondente (`grep` por `export (async )?function POST` / `export const POST`). Sem method literal → assume GET-ok (rota existe basta). Casos dinâmicos → skip silencioso.
-
-**Acceptance:** fixture com fetch em `src/shared/components` + template com prefixo falso + `method: "DELETE"` para rota só-GET — 3 detecções; repo real verde com os novos congelados documentados.
-
-### Task 6A.8 — Escopo dos gates de segurança: error-helper, public-creds, route-guard, deps
-
-**Files:** `check-error-helper.mjs`, `check-public-creds.mjs`, `check-route-guard-membership.ts`, `check-deps.mjs` + testes.
-
-**Approach (TDD, um sub-commit por gate):**
-1. **error-helper** (+Rule #12 completa): incluir `open-sse/mcp-server/**` e `src/app/api/**/route.ts` no SCAN_DIRS; rodar; congelar os achados novos em KNOWN com comentário-justificativa cada (e issue por cluster).
-2. **public-creds**: além dos 2 arquivos âncora, varrer `open-sse/**` e `src/lib/oauth/**` com a mesma `CRED_KEY_RE` (linha a linha, barato); congelar achados. Limitação documentada: `const CLIENT_ID = "…"` (variável solta) continua fora — o gitleaks da Fase 7 cobre essa classe.
-3. **route-guard**: novo sub-check — todo `route.ts` (qualquer raiz) cujo fonte OU imports de 1º nível relativos contenham `child_process`/`spawn(`/`execFile(`/`worker_threads` deve ser classificado local-only por `isLocalOnlyPath()`. Mata a dependência da lista manual de 3 raízes.
-4. **deps**: `MANIFESTS` → descoberta automática de todo `package.json` do repo (fora `node_modules`/`.next`): hoje raiz, `electron/`, `open-sse/`, `@omniroute/opencode-plugin/` (dep `zod` entra na allowlist), `@omniroute/opencode-provider/`. Workspace novo amanhã entra sozinho.
-
-**Acceptance:** fixtures sintéticas por gate; repo real verde com achados congelados + issues; dep nova em QUALQUER manifest do repo dispara o gate.
-
-### Task 6A.9 — `check-known-symbols` v2: MCP tools, A2A skills, cloud agents
-
-**Files:** `scripts/check/check-known-symbols.ts`, `tests/unit/check-known-symbols.test.ts`.
-
-**Approach (TDD, mesmo padrão das 3 superfícies existentes — Step 0 de verificação dos exports reais antes de codar):**
-1. **MCP tools:** enumerar os tools registrados (via `createMcpServer()` ou parse determinístico do tool-set em `open-sse/mcp-server/tools/`) e congelar o snapshot de nomes (catraca: tool sumir = fail; tool novo = report). Cruzar com os scopes (~13) — tool sem scope atribuído = fail.
-2. **A2A skills:** chaves de `A2A_SKILL_HANDLERS` (`src/lib/a2a/taskExecution.ts`) ↔ skills expostas no Agent Card (`src/app/.well-known/agent.json/route.ts`) — divergência = fail.
-3. **Cloud agents:** entradas do `src/lib/cloudAgent/registry.ts` ↔ classes em `agents/` — incompleto/órfão = fail.
-
-**Acceptance:** remover um tool/skill/agent do registro quebra o gate; adicionar reporta (e `check-docs-counts-sync` continua cuidando da prosa).
-
-## P2 — refinamentos
-
-### Task 6A.10 — `check-test-masking` v2: deleções, skips, tautologias
-
-**Files:** `scripts/check/check-test-masking.mjs`, `tests/unit/check-test-masking.test.ts`.
-
-**Approach (TDD):** (a) `--diff-filter=M` → `MDR` (com `-M` para rename-detection): arquivo de teste **deletado** = flag automático ("N asserts removidos — arquivo deletado"); renamed = comparar contra o path antigo. (b) Contar `\.(skip|todo|only)\s*\(` + `\{\s*skip:\s*true` base vs HEAD — **aumento líquido de skips = flag** (skip novo esconde asserts sem removê-los); `.only` novo = flag sempre (filtra o resto da suíte). (c) Tautologias extras: `expect(true).toBe(true)`, `assert.equal(1, 1)`, `expect(x).toBeDefined()` como ÚNICO assert do teste.
-
-**Acceptance:** fixtures para os 3 bypasses (delete, skip, only) — todos flagados; suíte real verde.
-
-### Task 6A.11 — Pisos manuais → catraca do motor + escopo electron/bin
-
-**Files:** `scripts/quality/collect-metrics.mjs`, `quality-baseline.json`, `check-openapi-coverage.mjs`, `scripts/i18n/check-ui-keys-coverage.mjs` (só leitura do valor), `check-file-size.mjs`, `eslint.complexity.config.mjs` + baselines.
-
-**Approach:** (1) coletor emite `openapiCoverage.pct` e `i18nUiCoverage.pct` → baseline `{direction: up}` com o valor real atual (36→real, 65→real); os THRESHOLDs fixos viram redundância de segurança (mantidos). Rotas/strings novas sem doc/i18n agora REGRIDEM o % e falham. (2) `check-file-size` e `check-complexity`: adicionar `electron/` e `bin/` ao scan (congelar os >cap que existirem).
-
-**Acceptance:** rota nova não-documentada derruba `openapiCoverage.pct` → gate falha; god-file novo em `electron/` falha.
-
-### Task 6A.12 — Higiene operacional (4 itens pequenos)
-
-**Files:** `package.json`, `dependency-allowlist.json`, `.husky/pre-push`, Create: `scripts/check/check-tracked-artifacts.mjs`, `scripts/quality/run-all-gates.mjs`; Modify: `.agents/skills/quality-scan/SKILL.md`.
-
-**Approach:**
-1. **jscpd pinado:** `jscpd@^4` como devDependency (entra no lockfile + allowlist); `check-duplication` chama o binário local em vez de `npx --yes jscpd@4` (remove download de registry no CI + supply-chain risk + flakiness).
-2. **pre-push barato:** reativar com APENAS os gates determinísticos rápidos (<10s: fetch-targets, openapi-routes, db-rules, public-creds, migration-numbering, file-size, deps, error-helper) — NÃO `test:unit` (lento; CI cobre). Atualizar o claim do CLAUDE.md (coordenar com 6A.4).
-3. **check-tracked-artifacts:** falhar se `git ls-files` contém `node_modules/`, `.next/`, `coverage/`, `quality-metrics.json` ou symlink para fora do repo (o incidente do symlink trackeado já aconteceu 2×). Wire no lint job + pre-commit (é instantâneo).
-4. **Runner agregador:** `scripts/quality/run-all-gates.mjs` roda os gates em paralelo (pool ~4), agrega `{gate, exitCode, lastLine, durationMs}` e imprime a tabela consolidada; `npm run quality:scan`. A skill `/quality-scan` passa a chamá-lo (atualizar SKILL.md).
-
-**Acceptance:** `npm run quality:scan` < 3 min com tabela única; pre-push roda <10s; `git add node_modules && commit` falha no pre-commit.
-
----
-
-## Ordem de execução recomendada (na ativação, 2026-06-16+)
-
-1. **6A.1 + 6A.2** (bugs de runner — destravam números reais de cobertura para o resto)
-2. **6A.3 + 6A.4** (stale-enforcement + docs — endurecem o que já roda)
-3. **6A.5** (motor v2) → **6A.6** (quality.yml, após OK do owner)
-4. **6A.7 → 6A.9** (escopos)
-5. **6A.10 → 6A.12** (refinamentos)
-6. Só então **Fase 7** (ferramentas novas sobre uma fundação consertada)
-
-## Self-Review
-- **Cobertura dos achados:** todos os achados A*/B*/C*/D*/E* da tabela têm task (coluna Task); os descartados estão em "Decisões conscientes de NÃO fazer" com motivo. ✓
-- **Zero dependência nova** exceto a promoção do jscpd (que já roda hoje via npx, não-pinado — a task REDUZ risco). ✓
-- **Sem flag-day:** toda expansão de escopo congela os achados pré-existentes (allowlist + issue), igual Fases 0–6; o stale-enforcement só exige remoção quando a correção JÁ aconteceu. ✓
-- **Consistência com o motor:** novas métricas (`openapiCoverage.pct`, `i18nUiCoverage.pct`) usam o formato `{value, direction}`; `eps`/`tightenSlack` são opcionais e retrocompatíveis. ✓
-- **Não-duplicação com a Fase 7:** cognitive-complexity per-file, gitleaks (creds não-públicas), knip, osv — tudo continua na Fase 7; a 6A só conserta/endurece o existente. ✓
diff --git a/PLANO-QUALITY-GATES-FASE7.md b/PLANO-QUALITY-GATES-FASE7.md
deleted file mode 100644
index 2e62aae737..0000000000
--- a/PLANO-QUALITY-GATES-FASE7.md
+++ /dev/null
@@ -1,182 +0,0 @@
-# Fase 7 — Quality Gates: Segurança, Dead-Code, Mutação & Ferramental Community
-
-> **Para workers agênticos:** SUB-SKILL OBRIGATÓRIA: `superpowers:subagent-driven-development` (recomendado) ou `superpowers:executing-plans`, tarefa-a-tarefa. Cada tarefa aqui é um subsistema independente → **expandir em sub-plano bite-sized próprio** no momento da execução. Hard Rule #18 (TDD/VPS) em tudo.
-
-> # ⏳ PORTÃO DE ATIVAÇÃO — NÃO INICIAR ANTES DE **2026-06-16**
-> **Este plano está GUARDADO, não ativo.** Decisão do owner (2026-06-09): finalizar 100% as Fases 0–6 (PR #3471), **usar em produção por 1 semana** para validar na prática, e só então evoluir. **Data cravada de início da Fase 7: 2026-06-16.** Não ativar antes — o objetivo da semana é coletar sinal real (falsos-positivos dos gates, custo de CI, atrito) antes de adicionar mais.
-> **Pré-condições para ativar:** (1) PR #3471 (Fases 0–6) mergeada e rodada ≥1 semana; (2) re-home do PR para `release/v3.8.18` resolvido; (3) as issues #3483–#3501 com decisões aplicadas ou conscientemente adiadas; (4) **Fase 6A executada (ou conscientemente re-priorizada)** — a auditoria crítica de 2026-06-09 ([`PLANO-QUALITY-GATES-FASE6A.md`](./PLANO-QUALITY-GATES-FASE6A.md)) encontrou bugs sistêmicos de runner (≈135 testes órfãos, vitest fora do CI) e furos de escopo nos gates existentes que devem ser consertados ANTES de adicionar ferramentas novas por cima.
-
-**Goal:** Maximizar a cobertura de quality gates do OmniRoute adicionando catracas de **segurança** (Sonar/osv/CodeQL → zero na timeline), **dead-code**, **complexidade cognitiva**, **type-coverage**, **mutação**, **bundle-size**, **a11y** e completando o anti-slopsquatting — usando **somente ferramentas Community/OSS** (projeto é open-source, zero SaaS pago, dados na box).
-
-**Architecture:** Reusa o motor existente — toda métrica numérica entra como `{value, direction}` em `quality-baseline.json` (catraca só-regressão) ou vira um `scripts/check/check-*.mjs` dedicado (padrão `check-t11-any-budget.mjs`). Gates pesados vão no job paralelo `quality-gate`; gates rápidos no `lint`; mutação/visual em job nightly separado. Tudo só-regressão (sem flag-day).
-
-**Tech Stack (tudo OSS/Community):** SonarQube **Community Build** (self-hosted) · osv-scanner (Google) · CodeQL (GitHub, grátis p/ público) · knip · eslint-plugin-sonarjs · type-coverage · lockfile-lint · dpdm · Stryker (`@stryker-mutator/*`) · size-limit · `@axe-core/playwright` · semcheck · agent-lsp (MCP) · Qlty CLI (OSS, opcional) · **gitleaks** (secret scanning, MIT; avaliar o sucessor drop-in Betterleaks, 2026-03) · **actionlint + zizmor** (lint + auditoria de segurança dos workflows) · **license-compliance** (allowlist SPDX de licenças). ESLint 9 flat · c8 · Node native test runner · GitHub Actions.
-
----
-
-## Princípio (igual às Fases 0–6)
-
-Toda catraca é **só-regressão**: congela o baseline atual, bloqueia QUALQUER piora, decai a zero/melhor com o tempo via `--update`. Nenhum gate exige limpeza imediata (flag-day). Cada ferramenta nova que vira dependência **deve ser adicionada a `dependency-allowlist.json`** (o gate `check-deps` da Fase 2 vai exigir — é o ponto de revisão humana).
-
-## Mapa de arquivos (criar/modificar)
-
-| Arquivo | Responsabilidade |
-|---|---|
-| `quality-baseline.json` (modificar) | + `vulnCount`, `codeqlAlerts`, `sonarIssues`, `cognitiveComplexity`, `typeCoveragePct`, `deadExports` |
-| `scripts/quality/collect-metrics.mjs` (modificar) | + coletores: osv-scanner, CodeQL count, Sonar API, knip, type-coverage, sonarjs |
-| `scripts/check/check-vuln-ratchet.mjs` (criar) | osv-scanner → vulnCount (catraca) |
-| `scripts/check/check-dead-code.mjs` (criar) | knip → exports/files/deps mortos (catraca) |
-| `scripts/check/check-cognitive-complexity.mjs` + `eslint.sonarjs.config.mjs` (criar) | sonarjs/cognitive-complexity em config isolado (não polui o count principal) |
-| `scripts/check/check-type-coverage.mjs` (criar) | type-coverage % (catraca up) |
-| `scripts/check/check-lockfile.mjs` (criar) | lockfile-lint (host/https/integrity) |
-| `scripts/check/check-pr-evidence.mjs` (criar) | exige output de comando no corpo do PR (Rule #18 mecânica) |
-| `scripts/check/check-bundle-size.mjs` + `.size-limit.json` (criar) | size-limit → orçamento de bundle |
-| `tests/e2e/a11y.spec.ts` (criar) | `@axe-core/playwright` nas páginas-chave |
-| `stryker.conf.json` (criar) | mutação nos ~8 módulos críticos (nightly) |
-| `sonar-project.properties` (modificar) | remover `coverage`/`cpd` exclusions; ativar new-code gate |
-| `.github/workflows/ci.yml` (modificar) | wirar novos gates (lint / quality-gate / nightly) + `qualitygate.wait` no Sonar |
-| `semcheck.yaml` (criar) | semcheck: docs↔código (camada fuzzy LLM, opcional) |
-| `.mcp.json` / config de agentes (modificar) | registrar agent-lsp (LSP-in-the-loop) |
-| `.gitleaks.toml` + `scripts/check/check-secrets.mjs` (criar) | gitleaks → catraca de findings de secret (Task 18) |
-| `.github/workflows/quality.yml` ou job lint (modificar) | actionlint + zizmor sobre `.github/workflows/**` (Task 19) |
-| `scripts/check/check-licenses.mjs` + `.license-allowlist.json` (criar) | allowlist SPDX das licenças das deps (Task 20) |
-| `dependency-allowlist.json` (modificar) | + osv-scanner, knip, sonarjs, type-coverage, lockfile-lint, stryker, size-limit, axe-core, dpdm, license-compliance |
-
----
-
-## Tarefas (cada uma = 1 sub-plano bite-sized na execução)
-
-### Task 1 — Ativar SonarQube Community + "Clean as You Code" (gate de segurança nativo)
-- **Tool:** SonarQube Community Build (self-hosted, grátis).
-- **Files:** `sonar-project.properties`, `.github/workflows/ci.yml` (job `sonarqube`).
-- **Approach:** setar secrets `SONAR_TOKEN`/`SONAR_HOST_URL`; **remover** `sonar.coverage.exclusions=**/*` e `sonar.cpd.exclusions=**/*` (hoje neutralizam o Sonar); ativar o quality gate **new-code / "Clean as You Code"** (código novo não pode adicionar issue/bug/vuln/hotspot; legado grandfathered) + adicionar `-Dsonar.qualitygate.wait=true` para **bloquear** o PR (hoje o job é inerte: secrets-gated, sem wait).
-- **Acceptance:** PR que introduz um code-smell/bug/vuln em código novo falha o gate; legado não bloqueia. Documentar suppressions legítimas (já há h1–h6 no properties).
-
-### Task 2 — Catraca de vulnerabilidades (osv-scanner)
-- **Tool:** osv-scanner (Google/OSV, OSS) — `--format json`, on-box.
-- **Files:** `scripts/check/check-vuln-ratchet.mjs`, `quality-baseline.json` (+`vulnCount`), `collect-metrics.mjs`, `ci.yml` (job `quality-gate`), `dependency-allowlist.json`.
-- **Approach:** rodar `osv-scanner --format json` sobre os lockfiles → contar vulns → métrica `vulnCount {direction: down}`. Catraca: não pode subir, decai a zero. Mantém o `npm audit` escalonado da Fase 0 como bloqueio de crítico imediato; osv é o ratchet de timeline.
-- **Acceptance:** nova dep com vuln conhecida sobe o count → falha; remediar/remover baixa → `--update`.
-
-### Task 3 — Catraca de alertas CodeQL
-- **Tool:** GitHub CodeQL (já roda; grátis p/ repo público).
-- **Files:** `scripts/check/check-codeql-ratchet.mjs`, `quality-baseline.json` (+`codeqlAlerts`), `ci.yml`.
-- **Approach:** puxar a contagem de alertas abertos via `gh api /repos/{owner}/{repo}/code-scanning/alerts?state=open` → métrica `codeqlAlerts {down}`. (Respeitar Hard Rule #14 — dismiss só com justificativa; alertas dismissed não contam.)
-- **Acceptance:** novo alerta CodeQL sobe o count → sinaliza; resolver baixa.
-
-### Task 4 — Dead-code / unused-exports / unused-deps (knip)
-- **Tool:** knip (OSS, v6+) — `--reporter json`.
-- **Files:** `scripts/check/check-dead-code.mjs`, `quality-baseline.json` (+`deadExports`/`unusedDeps`), `knip.json` (config), `collect-metrics.mjs`, `ci.yml`, `dependency-allowlist.json`.
-- **Approach:** `knip --reporter json` sobre os workspaces `src/`+`open-sse/` → contar unused files/exports/deps → catraca `down`. Config knip ciente do monorepo + Next 16.
-- **Acceptance:** novo export/dep morto sobe → falha; remoção baixa.
-
-### Task 5 — Complexidade cognitiva (eslint-plugin-sonarjs, config isolado)
-- **Tool:** eslint-plugin-sonarjs (OSS) — `sonarjs/cognitive-complexity`.
-- **Files:** `eslint.sonarjs.config.mjs` (config standalone, NÃO o principal — não polui o `eslintWarnings=3482`), `scripts/check/check-cognitive-complexity.mjs`, `quality-baseline.json` (+`cognitiveComplexity`), `ci.yml` (job `quality-gate`), `dependency-allowlist.json`.
-- **Approach:** mesmo molde do `check-complexity` (Fase 6) mas com `sonarjs/cognitive-complexity` num config isolado; contar violações → catraca `down`. (Complementa a complexidade ciclomática core já existente.)
-- **Acceptance:** função acima do limite cognitivo sobe o count → falha.
-
-### Task 6 — Type-coverage ratchet
-- **Tool:** type-coverage (OSS).
-- **Files:** `scripts/check/check-type-coverage.mjs`, `quality-baseline.json` (+`typeCoveragePct {up}`), `dependency-allowlist.json`.
-- **Approach:** `type-coverage --detail --json` → % de símbolos tipados → catraca `up`. Complementa o `check:any-budget` (count de `any` por arquivo) com a visão %-global.
-- **Acceptance:** queda do % tipado → falha.
-
-### Task 7 — Lockfile policy (lockfile-lint)
-- **Tool:** lockfile-lint (OSS, v5).
-- **Files:** `scripts/check/check-lockfile.mjs`, `ci.yml` (lint), `dependency-allowlist.json`.
-- **Approach:** `lockfile-lint --path package-lock.json --type npm --validate-https --validate-integrity --allowed-hosts npm` → gate pass/fail (não é ratchet; é política anti-poisoning). Complementa o `check-deps` (Fase 2).
-- **Acceptance:** lockfile com host não-https/sem integrity → falha.
-
-### Task 8 — Completar anti-slopsquatting (registry-existence + age-cooldown)
-- **Tool:** npm registry API (`npm view time.created`).
-- **Files:** `scripts/check/check-deps.mjs` (estender), test.
-- **Approach:** além do allowlist-diff atual, para uma dep NOVA: verificar que existe no registry E que foi publicada há ≥72h (age-cooldown contra "registra o nome alucinado em horas"). Base: CSA 2026 (19,7% de nomes alucinados; 43% reaparecem).
-- **Acceptance:** dep nova inexistente no registry ou publicada há <72h → falha (a menos que allowlistada com justificativa).
-
-### Task 9 — Pisos de cobertura por módulo crítico (peça adiada da Fase 4)
-- **Files:** `scripts/quality/collect-metrics.mjs` (estender), `quality-baseline.json`.
-- **Approach:** emitir `coverage..lines` (lido do `coverage-summary.json` por-arquivo) para ~8 módulos de alto risco: `open-sse/handlers/chatCore.ts`, `open-sse/services/combo.ts`, `open-sse/services/accountFallback.ts`, `src/sse/services/auth.ts`, `src/server/authz/routeGuard.ts`, `open-sse/utils/error.ts`, `open-sse/utils/publicCreds.ts`, `src/shared/utils/circuitBreaker.ts`. Cada um vira métrica `up`. **Calibrar a partir do coverage mergeado real do 1º run verde na main.**
-- **Acceptance:** queda de cobertura num módulo crítico → falha, mesmo que o global não caia.
-
-### Task 10 — Evidence-in-PR-body (peça adiada da Fase 5)
-- **Files:** `scripts/check/check-pr-evidence.mjs`, `ci.yml` (job `pr-test-policy`).
-- **Approach:** se o corpo do PR afirma "tests pass"/"added endpoint X"/"fixed Y" sem um bloco de **output de comando** anexado (typecheck/test/grep), falha (torna a Rule #18 mecânica — "evidence before assertions"). Heurístico, no contexto de PR.
-- **Acceptance:** PR alegando sucesso sem output anexado → falha.
-
-### Task 11 — Mutation testing nos módulos críticos (Stryker, nightly)
-- **Tool:** Stryker (`@stryker-mutator/core` + runner; OSS).
-- **Files:** `stryker.conf.json`, `ci.yml` (job NIGHTLY separado — não no PR), `dependency-allowlist.json`.
-- **Approach:** escopar Stryker aos ~8 módulos críticos da Task 9 (não repo-wide — é caro + c8 já OOM-prone). Mutantes sobreviventes = **testes tautológicos** (passam sem provar nada) → complementa o `check-test-masking` da Fase 4. Rodar nightly/weekly, não por-PR.
-- **Acceptance:** mutation score por módulo crítico vira métrica (catraca `up`, nightly).
-
-### Task 12 — Bundle-size / perf budget (size-limit)
-- **Tool:** size-limit (OSS).
-- **Files:** `.size-limit.json`, `scripts/check/check-bundle-size.mjs`, `ci.yml`, `dependency-allowlist.json`.
-- **Approach:** definir orçamento por bundle Next 16; size-limit emite tamanhos → catraca `down` (bundle não pode inchar).
-- **Acceptance:** PR que estoura o orçamento de bundle → falha.
-
-### Task 13 — a11y gate (axe-core + Playwright)
-- **Tool:** `@axe-core/playwright` (OSS; Playwright já existe).
-- **Files:** `tests/e2e/a11y.spec.ts`, `ci.yml` (job `test-e2e`), `dependency-allowlist.json`.
-- **Approach:** rodar axe nas páginas-chave do dashboard; congelar violações atuais (catraca `down`). Atende o item a11y/visual do plano t15.
-- **Acceptance:** nova violação a11y → falha; correção baixa.
-
-### Task 14 — semcheck (camada fuzzy docs↔código, opcional/LLM)
-- **Tool:** semcheck (OSS, MIT) — `fail-on-issues`.
-- **Files:** `semcheck.yaml`, `ci.yml` (advisory).
-- **Approach:** regras ligando `docs/**` ao módulo de código que documentam; pega docs que descrevem o que o código NÃO faz (camada fuzzy sobre o determinístico `check-docs-symbols` da Fase 6). É LLM → rodar advisory/non-blocking ou em label, por custo.
-- **Acceptance:** doc que descreve comportamento inexistente → flag (advisory).
-
-### Task 15 — agent-lsp (LSP-in-the-loop para os agentes)
-- **Tool:** agent-lsp (MCP server, OSS).
-- **Files:** config MCP dos agentes (`.mcp.json`/equivalente).
-- **Approach:** expor `tsserver`/agent-lsp aos agentes para `blast_radius`/diagnostics/`preview_edit` ANTES de escrever — vira "símbolo inventado" de catch-de-review para impossibilidade-no-edit. Pareia com `typecheck:core` como gate pré-PR (compile-before-claim).
-- **Acceptance:** agentes resolvem símbolo/import via LSP; menos alucinação de símbolo na origem.
-
-### Task 16 — dpdm circular-deps JSON cross-check (opcional)
-- **Tool:** dpdm (OSS, v4) — `--circular --output`.
-- **Approach:** cross-check JSON de ciclos complementando o `check-cycles.mjs` existente (AST-TS mais preciso). Catraca de contagem de ciclos. Baixa prioridade (já temos check-cycles).
-
-### Task 17 — Avaliar Qlty CLI como consolidador (opcional, spike)
-- **Tool:** Qlty CLI (OSS, grátis).
-- **Approach:** spike: avaliar se Qlty (Baseline analysis + 70 analyzers) substitui N scripts caseiros sem perder o controle/determinismo. Decisão build-vs-buy. Não obrigatório.
-
-### Task 18 — Secret scanning local (gitleaks) ➕ *adicionada pela auditoria 6A (2026-06-09)*
-- **Tool:** gitleaks (OSS, MIT — binário Go, on-box). Nota 2026: o criador original (Zach Rice) lançou o **Betterleaks** (2026-03) como drop-in replacement (flags/config compatíveis) — avaliar os dois no Step 0 e escolher 1.
-- **Files:** `.gitleaks.toml`, `scripts/check/check-secrets.mjs`, `quality-baseline.json` (+`secretFindings {down}`), `ci.yml` (job quality-gate), pre-commit (modo `--staged`, é rápido).
-- **Approach:** complementa o `check-public-creds` da Fase 6 (que cobre apenas credenciais PÚBLICAS por chave de objeto em 2 arquivos): gitleaks pega a classe geral — `const API_KEY = "sk-…"`, tokens em config/teste/docs, secrets em histórico. Rodar `gitleaks dir --report-format json` → contar findings → catraca `down`. Findings legítimos (creds públicas já congeladas no check-public-creds, fixtures de teste) vão para `.gitleaks.toml` `[allowlist]` com comentário — sujeitos ao stale-enforcement da 6A.3 (conceitual: revisar allowlist a cada release).
-- **Acceptance:** secret real plantado em fixture é detectado; baseline congela os findings atuais; novo finding falha o gate.
-
-### Task 19 — Lint + auditoria de segurança dos workflows (actionlint + zizmor) ➕ *adicionada pela auditoria 6A*
-- **Tools:** actionlint (OSS — correção/sintaxe/shellcheck dos YAML) + zizmor (OSS, zizmorcore — 24+ audits de segurança: unpinned actions, script injection, `pull_request_target` perigoso, cache poisoning). Complementares por design; o repo tem 10 workflows sem NENHUMA validação hoje.
-- **Motivação 2026:** o incidente trivy-action/LiteLLM (2026-03) explorou exatamente uma misconfiguração de `pull_request_target` que o zizmor detecta estaticamente. Os release-workflows do OmniRoute (npm/Docker/Electron publish) são alvo de alto valor.
-- **Files:** `ci.yml` ou `quality.yml` (steps actionlint + zizmor), `zizmor.yml` (config/ignores justificados), `quality-baseline.json` (+`zizmorFindings {down}` — começar advisory, congelar baseline, depois bloquear).
-- **Approach:** actionlint = pass/fail imediato (sintaxe não tem "legado aceitável"); zizmor = catraca `down` no padrão do motor (os findings atuais — provavelmente actions não-pinadas por SHA — são dívida congelada que decai).
-- **Acceptance:** workflow novo com `pull_request_target` + checkout de código do PR falha; action não-pinada NOVA sobe o count e falha.
-
-### Task 20 — License compliance (allowlist SPDX) ➕ *adicionada pela auditoria 6A*
-- **Tool:** license-compliance ou @onebeyond/license-checker (ambos OSS, npm). Projeto é **MIT** — deps com copyleft forte (GPL/AGPL) em produção são risco de compliance para os usuários do proxy.
-- **Files:** `scripts/check/check-licenses.mjs`, `.license-allowlist.json` (SPDX permitidas: MIT, Apache-2.0, BSD-2/3, ISC, 0BSD, …), `ci.yml` (lint job), `dependency-allowlist.json`.
-- **Approach:** rodar sobre as `dependencies` de produção (devDependencies = relatório advisory); licença fora da allowlist → fail com o caminho da dep. Exceções pontuais (dual-license, LGPL avaliada) entram na allowlist por **pacote** com justificativa — pareia com o `check-deps` da Fase 2 (lá controla O QUE entra; aqui, SOB QUAL licença).
-- **Acceptance:** dep GPL-3.0 sintética em fixture falha; árvore atual passa com a allowlist calibrada.
-
----
-
-## Wiring & CI (resumo)
-- **lint job:** check-lockfile, check-cognitive-complexity (rápido?), check-type-coverage, **check-licenses (Task 20)**, **actionlint (Task 19)**.
-- **quality-gate job (paralelo):** check-vuln-ratchet, check-dead-code, check-codeql-ratchet, check-cognitive-complexity (se lento), check-bundle-size, **check-secrets (Task 18)**, **zizmor (Task 19)**.
-- **pr-test-policy job:** check-pr-evidence.
-- **sonarqube job:** Clean-as-You-Code + `qualitygate.wait`.
-- **NIGHTLY job (novo):** Stryker (mutação), semcheck (advisory), a11y full.
-- Todas as métricas numéricas → `quality-baseline.json` (motor da Fase 1, com `eps`/`tightenSlack` da 6A.5). Toda dep nova → `dependency-allowlist.json`. Toda allowlist nova nasce com o stale-enforcement da 6A.3.
-
-## Self-Review
-- **Cobertura do spec:** 7 gates sugeridos = Task 1-3 (segurança), 4 (knip), 9 (coverage por módulo), 10 (evidence), 11 (mutação), 12 (bundle), 13 (a11y). "Todas as ferramentas discutidas" = Tasks 1-8, 11-17 (Sonar/osv/CodeQL/knip/sonarjs/type-coverage/lockfile/dpdm/stryker/size-limit/axe/semcheck/agent-lsp/Qlty). Auditoria 6A (2026-06-09) acrescentou Tasks 18-20 (gitleaks, actionlint+zizmor, license compliance). ✓
-- **Community/OSS only:** confirmado — Sonar Community Build, todos os demais OSS (gitleaks MIT, zizmor/actionlint OSS, license-compliance npm), zero SaaS pago. ✓
-- **Sem flag-day:** toda catraca é só-regressão, calibrada do estado atual (zizmor/gitleaks começam advisory→baseline→bloqueio). ✓
-- **Consistência:** todas as métricas usam o formato `{value, direction}` do motor da Fase 1; deps novas passam pelo `check-deps`+`dependency-allowlist.json`. ✓
-- **Não-sobreposição com a 6A:** a 6A conserta/endurece o EXISTENTE (runners, stale-allowlists, escopos); a Fase 7 só adiciona ferramenta nova. gitleaks (Task 18) complementa — não substitui — o check-public-creds expandido pela 6A.8. ✓
-
-## Handoff (na ativação, 2026-06-16+)
-**Ordem: Fase 6A primeiro** ([`PLANO-QUALITY-GATES-FASE6A.md`](./PLANO-QUALITY-GATES-FASE6A.md)) — consertar os runners (testes órfãos + vitest no CI) e endurecer as catracas existentes muda os baselines (cobertura recalibrada) sobre os quais várias tasks daqui (1, 9, 11) calibram. Depois: começar pela **Task 1-3 (catraca de segurança)**, **Task 4 (knip)** e **Task 19 (zizmor — protege os release-workflows)** — maior retorno. Cada Task vira um sub-plano `writing-plans` bite-sized próprio. Recomendado: Subagent-Driven, 1 subagente por Task, com auditoria (trust-but-verify) e o ratchet `eslintWarnings`/`check-deps` validando que cada adição não regride o que já temos.
diff --git a/PLANO-QUALITY-GATES-FASE8.md b/PLANO-QUALITY-GATES-FASE8.md
deleted file mode 100644
index 4a4bf70bef..0000000000
--- a/PLANO-QUALITY-GATES-FASE8.md
+++ /dev/null
@@ -1,91 +0,0 @@
-# Fase 8 — Quality Gates: Supply-Chain, Contratos, Resiliência & Segurança-LLM
-
-> **Status:** PLANO APROVADO PARA DEBATE/EXECUÇÃO (owner aprovou os 4 blocos em 2026-06-13).
-> **Pré-requisito:** Fases 6A + 7 completas (branch `feat/quality-gates-phase6a-7`).
-> **Origem:** pesquisa de estado-da-arte 2026 (2 agentes, 25+ buscas web, claims verificados). Tudo OSS/self-hostable.
-
-Cada task segue o padrão consolidado: gate determinístico → métrica (catraca) ou policy (pass/fail) → wire no CI. Catracas nascem com baseline congelado do estado real. Ferramental externo entra advisory até o 1º run verde, como na Fase 7.
-
----
-
-## BLOCO A — Supply-Chain & Artefato (o maior buraco atual; baixo esforço)
-
-### A.1 — npm/SLSA provenance
-- **Ferramenta:** `actions/attest-build-provenance` (MIT, OIDC do GitHub, zero chaves).
-- **O quê:** attestation SLSA Build L2 no publish do npm; verificável via `npm audit signatures`.
-- **Artefatos:** step no `npm-publish.yml` (`NPM_CONFIG_PROVENANCE: true` + attest). **Policy.**
-
-### A.2 — SBOM (CycloneDX)
-- **Ferramenta:** `@cyclonedx/cyclonedx-npm` + `anchore/syft` (Apache-2.0).
-- **O quê:** gerar SBOM CycloneDX por release; arquivar como artefato; alimenta o scan do Grype.
-- **Artefatos:** `scripts/build/generate-sbom.mjs`, step no release. **Artefato.**
-
-### A.3 — Container scan da imagem Docker
-- **Ferramenta:** `aquasecurity/trivy-action` (Apache-2.0).
-- **O quê:** scan da imagem publicada (CVEs/secrets/misconfig) com `--severity HIGH,CRITICAL`.
-- **Artefatos:** job no `docker-publish.yml`. **Policy** (advisory→bloqueante).
-
-### A.4 — OpenSSF Scorecard
-- **Ferramenta:** `ossf/scorecard-action` (Apache-2.0).
-- **O quê:** 20+ checks de postura (pinned-deps, token-permissions, dangerous-workflow). Casa com o zizmor da Fase 7.
-- **Artefatos:** workflow `scorecard.yml` (scheduled + push). **Catraca** (score 0–10).
-
----
-
-## BLOCO B — Contratos & Correção (alto ROI para um proxy de formatos)
-
-### B.1 — Property-based testing (`fast-check`, MIT)
-- **O quê:** invariantes em combo-routing (round-robin não repete provider consecutivo), translators (round-trip preserva `model`), sanitizers, parser SSE. Captura edge cases que testes por-exemplo perdem.
-- **Artefatos:** devDep `fast-check`; `tests/unit/**/*.property.test.ts`. **Catraca CI** (property violada = falha). Capturar seed em CI para reprodução.
-
-### B.2 — Golden-file/contract snapshots dos translators
-- **Ferramenta:** `node:assert.snapshot` (nativo Node 22+, zero dep).
-- **O quê:** congelar output de `translateRequest`/`translateResponse` (OpenAI↔Claude↔Gemini) como JSON versionado; drift de formato dispara o gate.
-- **Artefatos:** `tests/snapshots/translator-*.json`. **Catraca CI.**
-
-### B.3 — SSE-correctness gates (lacuna sem OSS pronto → helpers custom)
-- **Building blocks:** `eventsource-parser` + `undici` (nativo).
-- **O quê:** stream fecha após `[DONE]` (sem socket pendurado); abort propaga ao upstream; nenhum listener vazado; sub-streams de combo cancelados no abort; zero dup de texto no fim (bug já documentado no CLAUDE.md).
-- **Artefatos:** `tests/integration/sse-correctness.test.ts`. **Integração.** ⭐ maior ROI do bloco (proxies de LLM sofrem socket exhaustion/leak).
-
-### B.4 (opcional) — API fuzzing + breaking-change
-- `schemathesis` (lê o `openapi.yaml`, gera centenas de casos; nightly) + `oasdiff` (breaking-change no spec, pre-commit). Ambos maduros 2026.
-
----
-
-## BLOCO C — Resiliência Runtime (testa o coração do OmniRoute)
-
-### C.1 — Chaos/fault-injection (`Shopify/toxiproxy`, maduro)
-- **O quê:** valida circuit-breaker + connection-cooldown + model-lockout com latência/timeout/reset REAIS (não mocks). Toxics: `latency`, `timeout`, `slicer`+`reset_peer` (SSE no meio).
-- **Artefatos:** `docker-compose.test.yml` (toxiproxy), `tests/integration/resilience-chaos.test.ts`. **Nightly/integração.** ⭐ maior ROI (3 mecanismos de resiliência são o núcleo).
-
-### C.2 — Heap-growth gate (nativo, `--expose-gc`)
-- **O quê:** após N requests, `heapUsed` não cresce além de um teto (pega SSE que não fecha, listeners de combo, caches sem TTL). O projeto já teve OOM (#3069).
-- **Artefatos:** `tests/integration/heap-growth.test.ts`. **Nightly soak.** (clinic.js está descontinuado — usar nativo.)
-
-### C.3 (opcional) — Load/soak (`k6`, Grafana)
-- Thresholds p95/TTFT como gate; soak nightly de 30min detecta degradação entre releases.
-
----
-
-## BLOCO D — Segurança LLM (emergente 2026)
-
-### D.1 — LLM eval/red-team (`promptfoo`, MIT)
-- **O quê:** `apiBaseUrl: localhost:20128/v1` → testa de ponta a ponta. (a) regressão de qualidade de saída via llm-rubric; (b) valida que os guardrails de prompt-injection (`src/lib/guardrails/`) REALMENTE bloqueiam. Presets `owasp:llm`.
-- **Artefatos:** `promptfooconfig.yaml`, job nightly. **Policy** (nightly, custo por run).
-
-### D.2 — LLM vuln-scan (`NVIDIA/garak`, ativo)
-- **O quê:** 37+ probes (jailbreak, PII-leak, system-prompt-exfil) contra o proxy. Python, job separado.
-- **Artefatos:** job nightly/release. **Nightly.**
-
----
-
-## Descartados na pesquisa (transparência)
-`clinic.js` (heap, descontinuado 2023) → nativo. `apiaryio/dredd` (contract, arquivado nov/2024) → schemathesis/golden-files. `jazzer.js` (fuzzing coverage-guided, nicho) → fast-check cobre o ROI. Reproducible builds npm (sem tool madura; Next.js chunk IDs não-determinísticos) → SLSA provenance substitui. AI code provenance/watermarking (research-only). Tracetest (observabilidade-gate; exige OTel instrumentado primeiro) → diferir.
-
----
-
-## Sequenciamento sugerido (a debater)
-1. **Quick wins (baixo esforço, alto retorno):** A.1 provenance, A.3 Trivy, A.4 Scorecard, B.1 fast-check, B.2 golden-files.
-2. **Estrutural:** B.3 SSE-correctness, C.1 toxiproxy, C.2 heap, A.2 SBOM.
-3. **Nightly:** D.1 promptfoo, D.2 garak, C.3 k6, B.4 schemathesis.
diff --git a/PLANO-QUALITY-GATES.md b/PLANO-QUALITY-GATES.md
deleted file mode 100644
index c2727665db..0000000000
--- a/PLANO-QUALITY-GATES.md
+++ /dev/null
@@ -1,688 +0,0 @@
-# Plano de Implementação — Quality Gates & Catraca Anti-Alucinação
-
-> **Para workers agênticos:** SUB-SKILL OBRIGATÓRIA: use `superpowers:subagent-driven-development` (recomendado) ou `superpowers:executing-plans` para executar este plano tarefa-a-tarefa. Os passos usam checkbox (`- [ ]`) para rastreio. **Cada fix de bug obedece à Hard Rule #18** (teste falha→passa OU validação ao vivo no VPS). Não burle Husky (`--no-verify`) sem aprovação. Veja o diagnóstico completo em [`RELATORIO-QUALITY-GATES.md`](./RELATORIO-QUALITY-GATES.md).
-
-**Goal:** Generalizar a catraca de qualidade do OmniRoute (hoje só para `any`) para todas as métricas relevantes — cobertura, duplicação, tamanho de arquivo, complexidade — e adicionar gates determinísticos anti-alucinação, tudo no padrão `check-*.mjs` já existente, sem SaaS novo.
-
-**Architecture:** Camadas incrementais. (0) reativa/reconcilia o que já existe; (1) constrói o **motor de catraca** (`quality-baseline.json` commitado + coletor + comparador genérico que clona o `check-t11-any-budget.mjs`); (2) adiciona gates determinísticos que matam os ímãs de alucinação (provider-consistency, fetch-targets, openapi-routes); (3) catraca de duplicação+tamanho; (4) catraca de cobertura + anti test-masking; (5) skill `/babysit` + evidência. Toda catraca é **só-regressão** (baseline congelado) — nunca um piso absoluto que exija limpeza flag-day.
-
-**Tech Stack:** Node ≥20 ESM (`.mjs`/`.ts` via `tsx`), ESLint 9 flat config, c8, jscpd v5, eslint-plugin-sonarjs v4, GitHub Actions, Node native test runner (`node --import tsx --test`), `gh` CLI + GraphQL.
-
-**Escopo / sub-planos (scope-check):** Fases 0, 1 e 2 estão totalmente bite-sized aqui. Fases 3, 4 e 5 são subsistemas independentes — cada uma deve ser **expandida no próprio sub-plano** (via `writing-plans`) no momento da execução, a partir das specs/critérios de aceitação definidos aqui. Cada fase entrega software funcional e testável por si só.
-
----
-
-## Mapa de arquivos (o que será criado/modificado)
-
-| Arquivo | Responsabilidade |
-|---------|------------------|
-| `quality-baseline.json` (criar, **commitar**) | Baseline congelado: por métrica `{value, direction}` (`down`=menor-é-melhor, `up`=maior-é-melhor) |
-| `scripts/quality/collect-metrics.mjs` (criar) | Roda os coletores → emite `quality-metrics.json` |
-| `scripts/quality/check-quality-ratchet.mjs` (criar) | Comparador genérico: falha em qualquer regressão; com `--update` ratcheta o baseline |
-| `scripts/check/check-fetch-targets.mjs` (criar) | Todo `fetch("/api/...")` do dashboard resolve para um `route.ts` real |
-| `scripts/check/check-provider-consistency.ts` (criar) | ids de provider batem entre `providers.ts` ↔ `providerRegistry.ts` ↔ `validation.ts` |
-| `scripts/check/check-openapi-routes.mjs` (criar) | Toda `path` do `openapi.yaml` ↔ `route.ts` real (bidirecional) |
-| `scripts/check/check-deps.mjs` (criar) | Anti-slopsquatting: allowlist + existência no registry + age-cooldown |
-| `.github/workflows/ci.yml` (modificar) | Novo job `quality-gate`; reconciliar gate de cobertura; escalonar audit; plugar scripts órfãos |
-| `.husky/pre-commit` (modificar) | Reativar a parte barata |
-| `package.json` (modificar) | Novos scripts `check:*` / `quality:*` |
-| `eslint.config.mjs` (modificar, Fase 3) | `max-lines`, `max-lines-per-function`, `complexity`, `sonarjs/cognitive-complexity` (warn) |
-| `.claude/skills/babysit/SKILL.md` (criar, Fase 5) | Skill `/babysit` |
-| `tests/unit/quality-ratchet.test.ts` etc. (criar) | Testes TDD de cada gate |
-
----
-
-# FASE 0 — Reativar & Reconciliar (quick wins, sem tooling novo)
-
-### Task 0.1: Reconciliar o gate de cobertura do CI (40 → baseline real)
-
-**Contexto:** `ci.yml:377` gata em `40/40/40/40`; local gata `60`; comentário renderiza `60`; baseline real ≈ 79–82%. O 40 torna o gate quase banguela.
-
-**Files:**
-- Modify: `.github/workflows/ci.yml:376-377`
-
-- [ ] **Step 1: Confirmar o baseline real de cobertura**
-
-Run:
-```bash
-npm run test:coverage 2>&1 | tail -20
-node -e "const c=require('./coverage/coverage-summary.json').total; console.log(c.statements.pct,c.lines.pct,c.functions.pct,c.branches.pct)"
-```
-Expected: 4 números (ex.: `79.8 79.8 82.2 75.2`). Anote-os.
-
-- [ ] **Step 2: Subir o gate do CI para `baseline_real - 2` (headroom anti-flake)**
-
-Em `.github/workflows/ci.yml`, troque a linha `--statements 40 --lines 40 --functions 40 --branches 40` pelos valores `(real-2)` de cada métrica (ex.: `--statements 77 --lines 77 --functions 80 --branches 73`). Mantenha como **piso**; a catraca da Fase 4 cuidará do "não-cair".
-
-- [ ] **Step 3: Alinhar o script local e o display do comentário** para os mesmos números (procure `60` em `package.json` `test:coverage` e em `scripts/check/test-report-summary.mjs`).
-
-- [ ] **Step 4: Verificar que o CI não quebra** — abrir um PR de teste (ou rodar `act`/push numa branch) e confirmar que o job `test-coverage` fica verde com o novo piso.
-
-- [ ] **Step 5: Commit**
-```bash
-git add .github/workflows/ci.yml package.json scripts/check/test-report-summary.mjs
-git commit -m "fix(ci): reconcile coverage gate to real baseline (40->~78) across CI/local/report"
-```
-
-### Task 0.2: Escalonar `npm audit` (critical=bloqueia / high=avisa)
-
-**Files:** Modify: `package.json:112`
-
-- [ ] **Step 1: Trocar o script `audit:deps`** de `npm audit --audit-level=moderate && npm run audit:electron` para:
-```json
-"audit:deps": "npm audit --audit-level=critical && (npm audit --audit-level=high || echo '::warning::high-severity advisories present (non-blocking)') && npm run audit:electron",
-```
-
-- [ ] **Step 2: Rodar e observar o comportamento**
-```bash
-npm run audit:deps; echo "exit=$?"
-```
-Expected: `exit=0` se não houver critical; mensagem de warning se houver high.
-
-- [ ] **Step 3: Commit**
-```bash
-git add package.json && git commit -m "chore(ci): tier npm audit (critical blocks, high warns)"
-```
-
-### Task 0.3: Plugar os 3 scripts órfãos no CI
-
-**Contexto:** `check:cli-i18n`, `check:openapi-coverage`, `check:openapi-security-tiers` existem, dão `exit 1`, mas não rodam em lugar nenhum (Hard Rules #15/#17).
-
-**Files:** Modify: `.github/workflows/ci.yml` (job `docs-sync-strict` ou `lint`)
-
-- [ ] **Step 1: Rodar os 3 localmente para confirmar verde no estado atual**
-```bash
-npm run check:cli-i18n && npm run check:openapi-coverage && npm run check:openapi-security-tiers; echo "exit=$?"
-```
-Expected: `exit=0` (se algum falhar, corrija a deriva antes de plugar).
-
-- [ ] **Step 2: Adicionar os 3 como steps** no job `docs-sync-strict` do `ci.yml`, após `check:docs-all`.
-
-- [ ] **Step 3: Commit**
-```bash
-git add .github/workflows/ci.yml && git commit -m "ci: wire orphaned gates (cli-i18n, openapi-coverage, openapi-security-tiers)"
-```
-
-### Task 0.4: Reativar a parte barata do pre-commit do Husky
-
-**Files:** Modify: `.husky/pre-commit`
-
-- [ ] **Step 1: Descomentar SÓ as 3 linhas baratas e determinísticas:**
-```sh
-npx lint-staged
-node scripts/check/check-docs-sync.mjs
-npm run check:any-budget:t11
-```
-(Deixe i18n/openapi comentados por enquanto — eles são mais lentos; rodam no CI.)
-
-- [ ] **Step 2: Testar o hook** com um commit trivial e medir o tempo:
-```bash
-time git commit --allow-empty -m "chore: test pre-commit hook"
-git reset --soft HEAD~1
-```
-Expected: hook roda lint-staged + 2 checks em poucos segundos.
-
-- [ ] **Step 3: Commit**
-```bash
-git add .husky/pre-commit && git commit -m "chore(husky): re-enable cheap pre-commit gates (lint-staged, docs-sync, any-budget)"
-```
-
----
-
-# FASE 1 — Motor de Catraca (o coração) ⭐
-
-> Generaliza o `check-t11-any-budget.mjs` (catraca de `any` por arquivo) para um motor de catraca genérico, multi-métrica, que lê um baseline commitado e falha em qualquer regressão. Começa com 2 métricas (warnings de ESLint + cobertura) e é estendido nas fases seguintes.
-
-### Task 1.1: Comparador genérico de catraca (TDD)
-
-**Files:**
-- Create: `scripts/quality/check-quality-ratchet.mjs`
-- Test: `tests/unit/quality-ratchet.test.ts`
-
-- [ ] **Step 1: Escrever o teste que falha**
-```ts
-// tests/unit/quality-ratchet.test.ts
-import { test } from "node:test";
-import assert from "node:assert";
-import { execFileSync } from "node:child_process";
-import fs from "node:fs";
-import os from "node:os";
-import path from "node:path";
-
-const SCRIPT = path.resolve("scripts/quality/check-quality-ratchet.mjs");
-
-function run(baseline, metrics, extraArgs = []) {
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), "ratchet-"));
- const bPath = path.join(dir, "baseline.json");
- const mPath = path.join(dir, "metrics.json");
- fs.writeFileSync(bPath, JSON.stringify(baseline));
- fs.writeFileSync(mPath, JSON.stringify(metrics));
- try {
- const out = execFileSync("node", [SCRIPT, "--baseline", bPath, "--metrics", mPath, ...extraArgs], { encoding: "utf8" });
- return { code: 0, out, dir, bPath };
- } catch (e) {
- return { code: e.status, out: (e.stdout || "") + (e.stderr || ""), dir, bPath };
- }
-}
-
-test("passes when metrics equal baseline", () => {
- const b = { metrics: { eslintWarnings: { value: 100, direction: "down" }, "coverage.lines": { value: 80, direction: "up" } } };
- assert.equal(run(b, { eslintWarnings: 100, "coverage.lines": 80 }).code, 0);
-});
-
-test("fails when a 'down' metric regresses (more warnings)", () => {
- const b = { metrics: { eslintWarnings: { value: 100, direction: "down" } } };
- const r = run(b, { eslintWarnings: 101 });
- assert.equal(r.code, 1);
- assert.match(r.out, /eslintWarnings/);
-});
-
-test("fails when an 'up' metric regresses (coverage drops)", () => {
- const b = { metrics: { "coverage.lines": { value: 80, direction: "up" } } };
- assert.equal(run(b, { "coverage.lines": 79 }).code, 1);
-});
-
-test("passes on improvement; --update ratchets the baseline", () => {
- const b = { metrics: { eslintWarnings: { value: 100, direction: "down" } } };
- const r = run(b, { eslintWarnings: 90 }, ["--update"]);
- assert.equal(r.code, 0);
- const updated = JSON.parse(fs.readFileSync(r.bPath, "utf8"));
- assert.equal(updated.metrics.eslintWarnings.value, 90);
-});
-
-test("fails (code 2) when a baseline metric is missing from collected metrics", () => {
- const b = { metrics: { eslintWarnings: { value: 100, direction: "down" } } };
- assert.equal(run(b, {}).code, 1);
-});
-```
-
-- [ ] **Step 2: Rodar o teste e ver falhar**
-```bash
-node --import tsx --test tests/unit/quality-ratchet.test.ts
-```
-Expected: FAIL (script não existe ainda).
-
-- [ ] **Step 3: Implementar o comparador**
-```js
-#!/usr/bin/env node
-// scripts/quality/check-quality-ratchet.mjs
-// Catraca genérica multi-métrica. Clona o espírito de check-t11-any-budget.mjs:
-// um baseline congelado por métrica; falha em qualquer regressão; só anda num sentido.
-import fs from "node:fs";
-import path from "node:path";
-
-const cwd = process.cwd();
-function getArg(name, fallback) {
- const i = process.argv.indexOf(name);
- return i >= 0 && process.argv[i + 1] ? process.argv[i + 1] : fallback;
-}
-const BASELINE = path.resolve(getArg("--baseline", path.join(cwd, "quality-baseline.json")));
-const METRICS = path.resolve(getArg("--metrics", path.join(cwd, "quality-metrics.json")));
-const SUMMARY = getArg("--summary", null);
-const UPDATE = process.argv.includes("--update");
-const EPS = 0.01;
-
-function load(p) {
- if (!fs.existsSync(p)) { console.error(`[quality-ratchet] arquivo ausente: ${p}`); process.exit(2); }
- return JSON.parse(fs.readFileSync(p, "utf8"));
-}
-
-const baseline = load(BASELINE);
-const metrics = load(METRICS);
-const failures = [];
-const improvements = [];
-const rows = [];
-
-for (const [key, spec] of Object.entries(baseline.metrics)) {
- const current = metrics[key];
- const base = spec.value;
- const dir = spec.direction; // "down" = menor-é-melhor | "up" = maior-é-melhor
- if (current === undefined) { failures.push(`métrica "${key}" ausente em ${path.basename(METRICS)}`); rows.push([key, base, "—", "MISSING"]); continue; }
- let status = "ok";
- if (dir === "down") {
- if (current > base + EPS) { failures.push(`${key}: ${current} > baseline ${base} (não pode aumentar)`); status = "REGRESSÃO"; }
- else if (current < base - EPS) { improvements.push([key, current]); status = "↑ melhorou"; }
- } else {
- if (current < base - EPS) { failures.push(`${key}: ${current} < baseline ${base} (não pode cair)`); status = "REGRESSÃO"; }
- else if (current > base + EPS) { improvements.push([key, current]); status = "↑ melhorou"; }
- }
- rows.push([key, base, current, status]);
-}
-
-if (SUMMARY) {
- const md = ["# Quality Ratchet", "", "| Métrica | Baseline | Atual | Status |", "|---|---|---|---|",
- ...rows.map(([k, b, c, s]) => `| ${k} | ${b} | ${c} | ${s} |`), "",
- failures.length ? `**${failures.length} regressão(ões) — gate BLOQUEADO.**` : "**Sem regressões — gate OK.**"].join("\n");
- fs.mkdirSync(path.dirname(SUMMARY), { recursive: true });
- fs.writeFileSync(SUMMARY, md + "\n");
-}
-
-if (UPDATE && failures.length === 0 && improvements.length) {
- for (const [key, val] of improvements) baseline.metrics[key].value = val;
- fs.writeFileSync(BASELINE, JSON.stringify(baseline, null, 2) + "\n");
- console.log(`[quality-ratchet] baseline ratcheado: ${improvements.length} métrica(s) melhoraram`);
-}
-
-if (failures.length) { console.error("[quality-ratchet] FALHOU:\n" + failures.map((f) => " ✗ " + f).join("\n")); process.exit(1); }
-console.log(`[quality-ratchet] OK (${rows.length} métricas, ${improvements.length} melhoraram)`);
-```
-
-- [ ] **Step 4: Rodar o teste e ver passar**
-```bash
-node --import tsx --test tests/unit/quality-ratchet.test.ts
-```
-Expected: PASS (5/5).
-
-- [ ] **Step 5: Commit**
-```bash
-git add scripts/quality/check-quality-ratchet.mjs tests/unit/quality-ratchet.test.ts
-git commit -m "feat(quality): generic ratchet comparator (multi-metric, regression-only)"
-```
-
-### Task 1.2: Coletor de métricas (ESLint warnings + cobertura)
-
-**Files:** Create: `scripts/quality/collect-metrics.mjs`
-
-- [ ] **Step 1: Implementar o coletor**
-```js
-#!/usr/bin/env node
-// scripts/quality/collect-metrics.mjs — emite quality-metrics.json
-import fs from "node:fs";
-import path from "node:path";
-import { execFileSync } from "node:child_process";
-
-const cwd = process.cwd();
-const out = {};
-
-// 1) ESLint: contagem de warnings (errors devem ser 0; o lint já gata isso)
-function eslintCounts() {
- let stdout;
- try {
- stdout = execFileSync("npx", ["eslint", ".", "--format", "json"], { encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
- } catch (e) { stdout = e.stdout?.toString() || "[]"; } // eslint sai !=0 quando há errors
- const results = JSON.parse(stdout);
- out.eslintWarnings = results.reduce((n, r) => n + (r.warningCount || 0), 0);
- out.eslintErrors = results.reduce((n, r) => n + (r.errorCount || 0), 0);
-}
-
-// 2) Cobertura: lê coverage/coverage-summary.json se existir
-function coverage() {
- const p = path.join(cwd, "coverage", "coverage-summary.json");
- if (!fs.existsSync(p)) return;
- const t = JSON.parse(fs.readFileSync(p, "utf8")).total;
- out["coverage.statements"] = t.statements.pct;
- out["coverage.lines"] = t.lines.pct;
- out["coverage.functions"] = t.functions.pct;
- out["coverage.branches"] = t.branches.pct;
-}
-
-eslintCounts();
-coverage();
-fs.writeFileSync(path.join(cwd, "quality-metrics.json"), JSON.stringify(out, null, 2) + "\n");
-console.log("[collect-metrics]", JSON.stringify(out));
-```
-
-- [ ] **Step 2: Rodar e inspecionar a saída**
-```bash
-node scripts/quality/collect-metrics.mjs && cat quality-metrics.json
-```
-Expected: JSON com `eslintWarnings`, `eslintErrors` e (se houver coverage) os 4 `coverage.*`. **Anote `eslintWarnings`.**
-
-- [ ] **Step 3: Commit**
-```bash
-git add scripts/quality/collect-metrics.mjs && echo "quality-metrics.json" >> .gitignore
-git add .gitignore && git commit -m "feat(quality): metrics collector (eslint warnings + coverage)"
-```
-
-### Task 1.3: Congelar o baseline inicial
-
-**Files:** Create: `quality-baseline.json` (**commitado**)
-
-- [ ] **Step 1: Gerar o baseline a partir das métricas reais** (use os números anotados):
-```json
-{
- "_comment": "Catraca: 'down' nao pode aumentar, 'up' nao pode cair. Atualize via 'npm run quality:ratchet -- --update' (so em melhora).",
- "metrics": {
- "eslintWarnings": { "value": , "direction": "down" },
- "coverage.statements": { "value": , "direction": "up" },
- "coverage.lines": { "value": , "direction": "up" },
- "coverage.functions": { "value": , "direction": "up" },
- "coverage.branches": { "value": , "direction": "up" }
- }
-}
-```
-
-- [ ] **Step 2: Validar a catraca contra si mesma**
-```bash
-node scripts/quality/collect-metrics.mjs
-node scripts/quality/check-quality-ratchet.mjs; echo "exit=$?"
-```
-Expected: `[quality-ratchet] OK` e `exit=0`.
-
-- [ ] **Step 3: Provar que pega regressão** (teste manual): edite `quality-metrics.json` somando 1 a `eslintWarnings`, rode o comparador, confirme `exit=1`, depois descarte a edição.
-
-- [ ] **Step 4: Adicionar scripts npm**
-```json
-"quality:collect": "node scripts/quality/collect-metrics.mjs",
-"quality:ratchet": "node scripts/quality/check-quality-ratchet.mjs",
-"quality:gate": "npm run quality:collect && npm run quality:ratchet"
-```
-
-- [ ] **Step 5: Commit**
-```bash
-git add quality-baseline.json package.json
-git commit -m "feat(quality): freeze initial quality baseline (eslint warnings + coverage)"
-```
-
-### Task 1.4: Wire no CI (job + artefato + comentário no PR)
-
-**Files:** Modify: `.github/workflows/ci.yml`
-
-- [ ] **Step 1: Adicionar job `quality-gate`** (depois de `test-coverage`, para reusar `coverage/coverage-summary.json`):
-```yaml
- quality-gate:
- name: Quality Ratchet
- runs-on: ubuntu-latest
- needs: test-coverage
- if: ${{ always() && needs.test-coverage.result == 'success' }}
- steps:
- - uses: actions/checkout@v4
- - uses: actions/setup-node@v4
- with: { node-version: '24', cache: 'npm' }
- - run: npm ci
- - uses: actions/download-artifact@v4
- with: { name: coverage-report, path: coverage/ }
- - run: npm run quality:collect
- - run: node scripts/quality/check-quality-ratchet.mjs --summary .artifacts/quality-ratchet.md
- - if: always()
- run: cat .artifacts/quality-ratchet.md >> "$GITHUB_STEP_SUMMARY"
- - if: always()
- uses: actions/upload-artifact@v4
- with: { name: quality-ratchet, path: .artifacts/quality-ratchet.md }
-```
-
-- [ ] **Step 2: Adicionar comentário no PR** clonando o job `coverage-pr-comment` (marcador ``, lê `.artifacts/quality-ratchet.md`). Reuse o mesmo `github-script` de upsert de comentário.
-
-- [ ] **Step 3: Validar num PR de teste** — confirmar que o job aparece, o step summary mostra a tabela, e o comentário é postado.
-
-- [ ] **Step 4: Commit**
-```bash
-git add .github/workflows/ci.yml
-git commit -m "ci(quality): add quality-ratchet job with PR comment + artifact"
-```
-
----
-
-# FASE 2 — Gates Determinísticos Anti-Alucinação
-
-> Cada gate ataca um ímã de alucinação específico (ver §2.4 do relatório). Todos no padrão `check-*.mjs`. **`check-fetch-targets` vem com código completo** (parsing determinístico de arquivos, sem risco de inventar nomes de export). **`check-provider-consistency` e `check-openapi-routes` começam com um Step 0 de verificação dos nomes reais** — propositalmente, porque fabricar a forma do import/spec seria o exato anti-padrão que este plano combate.
-
-### Task 2.1: `check-fetch-targets.mjs` — toda rota chamada pelo dashboard existe (TDD)
-
-**Ataca:** ímã nº2 (300 paths `fetch("/api/...")` sem ligação com as rotas).
-
-**Files:**
-- Create: `scripts/check/check-fetch-targets.mjs`
-- Test: `tests/unit/check-fetch-targets.test.ts`
-
-- [ ] **Step 1: Escrever o teste que falha**
-```ts
-// tests/unit/check-fetch-targets.test.ts
-import { test } from "node:test";
-import assert from "node:assert";
-import { resolveApiPathToRoute } from "../../scripts/check/check-fetch-targets.mjs";
-
-test("matches a static route file", () => {
- const files = new Set(["src/app/api/usage/route.ts"]);
- assert.equal(resolveApiPathToRoute("/api/usage", files), true);
-});
-
-test("matches a dynamic [param] segment", () => {
- const files = new Set(["src/app/api/providers/[id]/models/route.ts"]);
- assert.equal(resolveApiPathToRoute("/api/providers/abc-123/models", files), true);
-});
-
-test("rejects a hallucinated route", () => {
- const files = new Set(["src/app/api/usage/route.ts"]);
- assert.equal(resolveApiPathToRoute("/api/providers/refresh", files), false);
-});
-```
-
-- [ ] **Step 2: Rodar e ver falhar**
-```bash
-node --import tsx --test tests/unit/check-fetch-targets.test.ts
-```
-Expected: FAIL (módulo não existe).
-
-- [ ] **Step 3: Implementar o gate**
-```js
-#!/usr/bin/env node
-// scripts/check/check-fetch-targets.mjs
-// Todo fetch("/api/...") em src/app/(dashboard) deve resolver para um route.ts real.
-import fs from "node:fs";
-import path from "node:path";
-
-const cwd = process.cwd();
-const DASH = path.join(cwd, "src/app/(dashboard)");
-const API = path.join(cwd, "src/app/api");
-
-// allowlist de paths dinâmicos/externos que o checker não consegue resolver estaticamente
-const IGNORE = [/^\/api\/v1\//, /\$\{/, /` \+/]; // /v1 é a superfície OpenAI-compat; templates
-
-function walk(dir, acc = []) {
- if (!fs.existsSync(dir)) return acc;
- for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
- const p = path.join(dir, e.name);
- if (e.isDirectory()) walk(p, acc);
- else if (/\.(ts|tsx)$/.test(e.name)) acc.push(p);
- }
- return acc;
-}
-
-function collectRouteFiles() {
- return new Set(walk(API).filter((p) => /route\.tsx?$/.test(p)).map((p) => path.relative(cwd, p).replace(/\\/g, "/")));
-}
-
-export function resolveApiPathToRoute(apiPath, routeFiles) {
- // apiPath ex.: /api/providers/abc/models → src/app/api/providers/[id]/models/route.ts
- const segs = apiPath.replace(/^\//, "").replace(/[?#].*$/, "").split("/"); // ["api","providers","abc","models"]
- for (const rf of routeFiles) {
- const rsegs = rf.replace(/^src\/app\//, "").replace(/\/route\.tsx?$/, "").split("/"); // ["api","providers","[id]","models"]
- if (rsegs.length !== segs.length) continue;
- const ok = rsegs.every((rs, i) => rs === segs[i] || /^\[.*\]$/.test(rs));
- if (ok) return true;
- }
- return false;
-}
-
-function extractFetchPaths(file) {
- const src = fs.readFileSync(file, "utf8");
- const re = /(?:fetch|fetchJson|apiFetch)\(\s*["'`](\/api\/[^"'`?]+)/g;
- const out = [];
- let m;
- while ((m = re.exec(src))) out.push(m[1]);
- return out;
-}
-
-function main() {
- const routeFiles = collectRouteFiles();
- const misses = [];
- for (const f of walk(DASH)) {
- for (const apiPath of extractFetchPaths(f)) {
- if (IGNORE.some((rx) => rx.test(apiPath))) continue;
- if (!resolveApiPathToRoute(apiPath, routeFiles)) misses.push(`${path.relative(cwd, f)} → ${apiPath}`);
- }
- }
- if (misses.length) {
- console.error(`[check-fetch-targets] ${misses.length} fetch(es) para rota inexistente:\n` + misses.map((m) => " ✗ " + m).join("\n"));
- process.exit(1);
- }
- console.log(`[check-fetch-targets] OK (${routeFiles.size} rotas conhecidas)`);
-}
-
-if (import.meta.url === `file://${process.argv[1]}`) main();
-```
-
-- [ ] **Step 4: Rodar o teste unitário e ver passar**
-```bash
-node --import tsx --test tests/unit/check-fetch-targets.test.ts
-```
-Expected: PASS (3/3).
-
-- [ ] **Step 5: Rodar o gate no repo real** (modo descoberta — pode encontrar misses legítimos)
-```bash
-node scripts/check/check-fetch-targets.mjs; echo "exit=$?"
-```
-Se encontrar misses reais: triagem — ou são rotas faltantes (bug), ou paths dinâmicos a adicionar ao `IGNORE`. **Não** mascare; documente cada exceção no `IGNORE` com comentário.
-
-- [ ] **Step 6: Adicionar script + commit**
-```json
-"check:fetch-targets": "node scripts/check/check-fetch-targets.mjs"
-```
-```bash
-git add scripts/check/check-fetch-targets.mjs tests/unit/check-fetch-targets.test.ts package.json
-git commit -m "feat(check): gate that every dashboard fetch() targets a real API route"
-```
-
-### Task 2.2: `check-provider-consistency.ts` — ids batem entre os 3 arquivos
-
-**Ataca:** ímã nº1 (split de provider; 229 vs 155 vs N já divergem).
-
-**Files:** Create: `scripts/check/check-provider-consistency.ts` (rodado via `tsx`); Test: `tests/unit/check-provider-consistency.test.ts`
-
-- [ ] **Step 0 (VERIFICAÇÃO — obrigatório antes de codar):** descobrir os nomes reais de export, para **não inventar**:
-```bash
-grep -nE "export (const|function) " src/shared/constants/providers.ts | grep -iE "provider|section" | head
-grep -nE "getOrCreateAiProviders|_PROVIDER_SECTIONS|AI_PROVIDERS" src/shared/constants/providers.ts | head
-grep -nE "baseUrl|^\s*['\"][a-z0-9-]+['\"]\s*:" open-sse/config/providerRegistry.ts | head
-grep -nE "case ['\"]|=== ['\"]|SPECIALTY_VALIDATORS" src/lib/providers/validation.ts | head
-```
-Anote: como enumerar os ids canônicos em runtime, a forma das chaves do `providerRegistry.ts`, e onde `validation.ts` lista os providers cobertos.
-
-- [ ] **Step 1: Escrever o teste** com fixtures sintéticas (3 conjuntos de ids), afirmando que o diff detecta um id presente em um e ausente em outro. (Implemente a lógica como função pura `diffProviderSets(canonical, registry, validators)` que retorna `{missingInRegistry, missingInValidators, orphanInRegistry}`.)
-
-- [ ] **Step 2: Rodar e ver falhar.**
-
-- [ ] **Step 3: Implementar** importando os ids reais (descobertos no Step 0) e cruzando-os. Reusar o padrão de `scripts/check/check-docs-counts-sync.mjs` (que **já conta** executors/oauth/strategies vs docs — é o template direto). Para ids que legitimamente vivem só num lugar, manter um `KNOWN_EXCEPTIONS` allowlist com comentário por entrada.
-
-- [ ] **Step 4: Rodar no repo real**, triar as divergências reais (as contagens já divergem — algumas serão bugs de registro pela metade, outras exceções legítimas).
-
-- [ ] **Step 5: Commit** (`feat(check): provider-id consistency gate across providers.ts/registry/validation`).
-
-**Critério de aceitação:** o gate falha quando um id existe em `providers.ts` mas não no `providerRegistry.ts` (ou vice-versa), e quando um model no registry referencia um provider id desconhecido.
-
-### Task 2.3: `check-openapi-routes.mjs` — spec ↔ rotas (bidirecional)
-
-**Ataca:** docs-hallucination (endpoint inventado no `openapi.yaml`).
-
-**Files:** Create: `scripts/check/check-openapi-routes.mjs`; Test correspondente.
-
-- [ ] **Step 0 (VERIFICAÇÃO):** confirmar o caminho e a forma do spec:
-```bash
-ls docs/reference/openapi.yaml && grep -nE "^\s{2,4}/[a-z]" docs/reference/openapi.yaml | head
-```
-Confirmar como `check-openapi-coverage.mjs` já parseia o YAML (reusar o parser dele).
-
-- [ ] **Step 1–5 (TDD):** teste com spec sintética → implementar: toda `path` do spec resolve para um `route.ts` (reusando `resolveApiPathToRoute` da Task 2.1) e toda rota não-interna aparece no spec (com allowlist para rotas LOCAL_ONLY/internas). Commit.
-
-### Task 2.4: `check-deps.mjs` — anti-slopsquatting
-
-**Ataca:** pacotes alucinados (CSA: 19,7% das amostras IA).
-
-**Files:** Create: `scripts/check/check-deps.mjs`; Test.
-
-- [ ] **Step 0 (VERIFICAÇÃO):** decidir política — allowlist = todas as deps atuais do `package.json`/lockfile como baseline; novas deps exigem (a) existir no registry, (b) idade ≥ 72h, (c) entrada explícita.
-- [ ] **Step 1–5 (TDD):** teste com `package.json` sintético adicionando uma dep nova → gate falha se a dep não está no baseline E (não existe no registry OU foi publicada há <72h). Usar `npm view time.created` para idade. Reusar o padrão diff-base...HEAD de `check-pr-test-policy.mjs`. Commit.
-
-### Task 2.5 (opcional): lints Rule #11/#12 via `no-restricted-syntax`
-
-- [ ] Estender o `eslint.config.mjs` (bloco `no-restricted-syntax` já usado p/ a regra de busca turca) para sinalizar `NextResponse.json({ error: "" })` cru (exigir `buildErrorBody()`) e literais de `clientIdDefault`/`clientSecretDefault` no `providerRegistry.ts` (exigir `resolvePublicCred()`). Ratchet via contagem (adoção 11% → sobe). TDD com fixtures de ESLint.
-
-**Plugar tudo da Fase 2 no CI:** adicionar `check:fetch-targets`, `check:provider-consistency`, `check:openapi-routes`, `check:deps` ao job `lint` (ou `docs-sync-strict`) do `ci.yml`, e ao pre-commit barato quando rápidos o suficiente.
-
----
-
-# FASE 3 — Catraca de Duplicação + Tamanho (mata-slop) — *expandir em sub-plano*
-
-**Justificativa:** GitClear mostra duplicação 4–8× na era IA; é a assinatura nº1 de slop. Nenhum gate hoje (Sonar CPD excluído).
-
-**Specs (criar sub-plano `writing-plans` a partir daqui):**
-
-1. **Adicionar `jscpd` v5** como devDependency. Rodar `jscpd --reporters json src open-sse` → `quality-metrics.json` ganha `duplication.pct`. **[verificar no install]** o schema JSON do v5 antes de parsear (ver ressalva no relatório §4.2).
-2. **Estender `collect-metrics.mjs`** com um coletor de duplicação (lê o JSON do jscpd) e um coletor de **tamanho de arquivo** (conta LOC de cada arquivo em `src`+`open-sse`, emite `fileSize.` para os arquivos já acima de um teto, ex. >700 LOC — congelando os 64 atuais).
-3. **Adicionar regras ESLint** em `eslint.config.mjs` como `warn`: `max-lines` (700), `max-lines-per-function` (80), `complexity` (15), `sonarjs/cognitive-complexity` (15). Coletor adiciona `eslintWarnings` por categoria (a contagem já está na catraca da Fase 1; opcionalmente segregar `complexityWarnings`).
-4. **Estender `quality-baseline.json`** com `duplication.pct` (`down`) e os `fileSize.*` dos arquivos grandes (`down` — só podem encolher; arquivos novos têm teto absoluto).
-5. **Teto absoluto para arquivos novos:** o gate falha se um arquivo **não** presente no baseline nasce acima do teto (ex. 700 LOC) — impede o próximo god-component.
-
-**Critérios de aceitação:** PR que aumenta a duplicação % falha; PR que cresce qualquer um dos 64 arquivos grandes falha; PR que cria arquivo novo >700 LOC falha; refator que encolhe um arquivo grande ratcheta o baseline para baixo (via `--update`).
-
----
-
-# FASE 4 — Catraca de Cobertura + Anti Test-Masking — *expandir em sub-plano*
-
-**Specs:**
-
-1. **`check-coverage-ratchet`** já é coberto pelo motor da Fase 1 (as 4 métricas `coverage.*` com `direction: "up"`). Confirmar que o job `quality-gate` roda **após** o merge dos 8 shards de cobertura no CI (não localmente, onde a cobertura é parcial). Adicionar **epsilon** maior (ex. 0.5) para `coverage.branches` por causa do não-determinismo do v8.
-2. **Pisos por módulo crítico:** estender `collect-metrics.mjs` para emitir `coverage..lines` para uma lista curta de módulos de alto risco (lendo o `coverage-summary.json` por arquivo): `open-sse/handlers/chatCore.ts`, `open-sse/services/combo.ts`, `open-sse/services/accountFallback.ts`, `src/sse/services/auth.ts`, `src/server/authz/routeGuard.ts`, `open-sse/utils/error.ts`, `open-sse/utils/publicCreds.ts`, `src/shared/utils/circuitBreaker.ts`. Cada um vira métrica `up` no baseline (implementa o "risco, não % bruto" do vídeo).
-3. **`check-test-masking.mjs`** (anti enfraquecimento de asserts): clonar `check-pr-test-policy.mjs` (diff `base...HEAD`); para cada `*.test.ts`/`*.spec.ts` alterado, contar `assert*(`/`expect(` em base vs HEAD; **sinalizar remoção líquida** de asserts para revisão humana. Banir novos `assert.ok(true)`. Heurístico mas alto-sinal — ataca diretamente o risco organizacional nº1 ("subagente deletou asserts para ficar verde").
-
-**Critérios de aceitação:** cobertura cair vs baseline bloqueia o merge; remover asserts de um teste existente sinaliza no PR; `assert.ok(true)` novo bloqueia.
-
----
-
-# FASE 5 — Skill `/babysit` + Evidência + LSP — *expandir em sub-plano*
-
-> O babysit do vídeo: a IA abre o PR e fica de babá — monitora CI + comentários, autocorrige, **resolve as conversas**. Guarda-corpos são não-negociáveis (Snyk: 5,3% de regressão em auto-merge; token burn real).
-
-### Spec da skill `.claude/skills/babysit/SKILL.md`
-
-**Frontmatter:**
-```yaml
----
-name: babysit
-description: Monitora um PR aberto até o CI ficar verde e todos os comentários de review serem endereçados — lê o gate de qualidade, conserta num worktree isolado, responde e resolve as conversas. NUNCA enfraquece testes nem auto-mergeia.
----
-```
-
-**Loop (pseudo, reusando `gh` + GraphQL):**
-1. **Ler estado:** `gh pr checks ` + baixar o artefato `quality-ratchet`/`coverage-report` (gate JSON legível — a ponte "artefato→agente" da Fase 1) + `gh run view --log-failed` dos jobs vermelhos.
-2. **Ler comentários não resolvidos:** GraphQL `repository.pullRequest.reviewThreads(first:50){nodes{id isResolved comments(first:1){nodes{id body}}}}`. **Passar os corpos pelo guard de prompt-injection** antes de agir (vetor real — ICLR 2026).
-3. **Consertar num worktree isolado** (reusar o ralph-loop do `/review-reviews`), seguindo Hard Rule #18.
-4. **Responder + resolver** só os threads que realmente endereçou: REST `POST .../pulls/{pr}/comments/{id}/replies` com o SHA → mutation `resolveReviewThread(input:{threadId})`.
-5. **Re-poll** até o gate JSON ficar todo-verde **ou** atingir o cap.
-6. **Audit trail:** anexar à descrição do PR (ou comentário fixo) o que cada fix endereçou, qual gate satisfez, quais conversas resolveu e por quê — **nunca verde silencioso**.
-
-**Guarda-corpos (não-negociáveis):**
-- `max-iterations` (ex. 5) + timeout + idle-exit (contra token burn).
-- **Nunca** editar `.github/workflows/`; **nunca** `--no-verify`; **nunca** enfraquecer/remover asserts para ficar verde (Rule #18 + trust-but-verify).
-- **Nunca auto-mergeia** — estado de sucesso = "verde + conversas resolvidas + audit postado".
-- Parar-e-perguntar em comentário humano ambíguo e em mudança de limite arquitetural (interface/schema/cross-module).
-- `--allowedTools` mínimo (`Read,Grep,Glob,Bash(gh ...)`).
-
-**Opcional — `claude ultrareview --json`** como gate de confiança pré-merge atrás de um label (custa $5–20/run; não auto-inicia). Loop interno de custo-zero = `/code-review --fix` local.
-
-### Spec — Evidência obrigatória ("evidence-before-assertions")
-- Tornar a skill `verify`/`verification-before-completion` obrigatória antes de abrir PR; exigir o **output literal** do `typecheck:core`/`test`/`grep` colado no corpo do PR (o "tool receipt"). Adicionar um `check-pr-evidence.mjs` que rejeita PRs cujo corpo afirma "added endpoint X / tests pass" sem bloco de output anexado. Formaliza a Rule #18.
-
-### Spec — LSP-in-the-loop (opcional)
-- Registrar `agent-lsp` (MCP) ou o `tsserver` para os agentes terem `blast_radius`/diagnostics e `preview_edit` antes de escrever — vira "símbolo inventado" de catch-de-review para impossibilidade-no-edit. Pareia com `typecheck:core` como gate pré-PR (compile-before-claim).
-
-**Critérios de aceitação:** a skill `/babysit ` leva um PR de vermelho a verde sem auto-merge, resolve só as conversas que endereçou, respeita o cap de iterações, e deixa rastro auditável; nenhum assert é enfraquecido.
-
----
-
-## Self-Review (checklist do autor)
-
-- **Cobertura do spec:** Fases 0–2 mapeiam 1:1 com as recomendações do relatório §5; Fases 3–5 cobrem duplicação/tamanho, cobertura/masking e babysit/evidência. ✓
-- **Sem placeholders nos passos bite-sized:** Fases 0/1/2.1 têm comandos e código reais. As Fases 2.2–2.4 usam um Step-0 de verificação **deliberado** (não placeholder) para não fabricar nomes de export — coerente com o objetivo anti-alucinação. ✓
-- **Consistência de tipos:** `resolveApiPathToRoute` (Task 2.1) é reusada na Task 2.3; o comparador usa o mesmo formato `{value, direction}` em todas as fases; `quality-metrics.json` é a interface única coletor↔comparador. ✓
-- **Ordem de dependência:** Fase 1 (motor) precede 3/4 (que só adicionam métricas ao baseline); Fase 0 destrava o resto. ✓
-
-## Handoff de Execução
-
-**Plano salvo em `PLANO-QUALITY-GATES.md`.** Duas opções:
-
-1. **Subagent-Driven (recomendado)** — um subagente fresco por task, review entre tasks. SUB-SKILL: `superpowers:subagent-driven-development`.
-2. **Inline** — executar nesta sessão com checkpoints. SUB-SKILL: `superpowers:executing-plans`.
-
-**Recomendação:** começar pela **Fase 0** (quick wins, baixo risco) e **Fase 1** (motor de catraca) numa branch `feat/quality-ratchet`, validar num PR de teste, e só então abrir as fases anti-alucinação. Fases 3–5 viram sub-planos próprios.
diff --git a/RELATORIO-QUALITY-GATES.md b/RELATORIO-QUALITY-GATES.md
deleted file mode 100644
index 6ffb3b86e3..0000000000
--- a/RELATORIO-QUALITY-GATES.md
+++ /dev/null
@@ -1,219 +0,0 @@
-# Relatório — Quality Gates, Catraca & Anti-Alucinação no OmniRoute
-
-> **Data:** 2026-06-09
-> **Origem:** Auditoria do projeto (5 subagentes Opus em paralelo mapeando todas as pastas exceto `node_modules`/`_references`/`dist`) + análise da transcrição do vídeo *"Qualidade de código"* (Stupid Button Club, 2026-05-04) + pesquisa web 2026 (4 frentes, últimos ~3 meses).
-> **Companheiro:** Veja [`PLANO-QUALITY-GATES.md`](./PLANO-QUALITY-GATES.md) para o plano de implementação bite-sized (TDD).
-
----
-
-## 0. TL;DR
-
-1. **O OmniRoute já é muito mais maduro** que o projeto "Strawberry" do vídeo: tem CI com 20 jobs, gate de cobertura, ESLint 9 flat, SonarQube, 14 scripts `check-*.mjs` e **uma catraca real já funcionando** (`check-t11-any-budget.mjs` — orçamento de `any` por arquivo que só pode encolher). O vídeo descreve onde queremos chegar; nós já estamos a meio caminho.
-2. **Mas faltam exatamente as catracas que o vídeo prega.** Não há baseline congelado de métricas, nem gate de **duplicação**, nem de **tamanho de arquivo**, e o gate de cobertura é um **piso fixo** — não uma catraca "nunca piorar".
-3. **Há derivas (drifts) reais que pegamos na auditoria:** o gate de cobertura **no CI é `40/40/40/40`** (ci.yml:377), não os `60/60/60/60` que o CLAUDE.md anuncia (esse é só o script local). O Husky está **100% comentado** (zero gate local). O SonarQube tem `coverage` e `cpd` **excluídos** (`sonar-project.properties:9-10`) — as duas métricas mais úteis contra "slop" estão desligadas. E 3 scripts `check-*` existem mas **não rodam em lugar nenhum**.
-4. **Os maiores ímãs de alucinação são estruturais:** o split de provider em 3 arquivos gigantes em 2 workspaces (`providers.ts` ↔ `providerRegistry.ts` ↔ `validation.ts`, com contagens que já divergem: 229 ids vs 155 blocos vs N validadores), os 300 paths `fetch("/api/...")` hardcoded sem ligação de compilação com as rotas, e o arquivo de **12.760 linhas** (`providers/[id]/page.tsx`) que nenhum agente consegue segurar em contexto.
-5. **2026 confirma a tese do vídeo com dados:** GitClear (211M linhas) mostra duplicação crescendo 4–8× na era da IA; o paper SlopCodeBench prova que *instrução de prompt sozinha não impede a degradação* — só gates determinísticos seguram. O ecossistema 2026 tem ferramentas maduras para cada métrica (jscpd v5, knip v6, eslint-plugin-sonarjs v4, osv-scanner, Qlty, Sonar "Clean as You Code"/"AI Code Assurance").
-6. **A jogada é em camadas:** (a) reativar/reconciliar o que já existe; (b) construir o **motor de catraca** (baseline.json + coletor + comparador, clonando o `any-budget`); (c) adicionar **gates determinísticos anti-alucinação** (provider-consistency, fetch-target, openapi-routes); (d) catraca de **duplicação + tamanho**; (e) catraca de **cobertura** + detecção de test-masking; (f) skill **`/babysit`** com guarda-corpos. Detalhe no plano.
-
----
-
-## 1. O que o vídeo ensina (insights destilados)
-
-O vídeo é uma fala sem roteiro sobre *qualidade de código no mundo em que a IA escreve ~100% do código*. Pontos centrais:
-
-| # | Insight | Citação/essência |
-|---|---------|------------------|
-| 1 | **O humano virou o gargalo.** | "Eu acabei virando o gargalo da IA. Fazer o babysit das coisas do request básicas é o gargalo. Não consigo entregar 4 tarefas ao mesmo tempo se preciso ler 10.000 linhas/dia." |
-| 2 | **Quality Gate = portão que a IA tem que passar.** | Todo PR passa por um portão; a IA fica em loop se autocorrigindo até ficar verde, em vez de o humano revisar e pedir refação. |
-| 3 | **Baseline + Catraca (ratchet).** | "Tu congela o baseline e o repositório só pode melhorar a partir dali ou empatar." A catraca anda num sentido só. |
-| 4 | **Regra de ouro.** | "Cada PR pode adicionar código, mas não pode aumentar nenhuma das métricas — nem por uma violação, nem por uma linha, nem por 0,1 ponto percentual." |
-| 5 | **Métricas do baseline.** | Violações de ESLint (483 em 120 arquivos), duplicação de código (2,2% via JSCPD), cobertura (%), arquivos acima do limite de tamanho (19 arquivos; o maior com 4.600 linhas). |
-| 6 | **Pipeline de CI.** | `npm ci` → `npm audit` (critical=bloqueia / high=avisa) → `npm run lint` → `test:coverage` → **script node de quality gate** que compara métricas atuais vs `baseline.json` e falha em qualquer regressão → comentário no PR + **upload de artefatos** que o agente lê para se autocorrigir. |
-| 7 | **Artefatos legíveis pelo agente.** | "Não adianta cuspir isso no PR. O agente precisa ter acesso ao que está dando errado." |
-| 8 | **Babysit skill.** | "Recomendo criar uma skill de babysit": a IA monitora o CI + comentários dos revisores, endereça os comentários e **resolve as conversas** para dar rastreabilidade no GitHub. |
-| 9 | **Comentários perto do código (legibilidade p/ agente).** | Mudou de ideia: antes era contra comentários ("o código é a documentação"); agora, no mundo de agentes, comentário explicando *o quê* e *por quê* perto do código vale mais que um MD gigante, porque o harness faz `grep` no arquivo e lê o comentário junto. |
-| 10 | **É "só" colar ferramentas.** | "Não é nada excepcional. Eu só estou colando um monte de ferramentas e chamando de quality gate." Pode-se usar SonarQube ou GitHub Code Quality no lugar do script caseiro. |
-| 11 | **Por que a IA não faz certo de primeira.** | Modelos top já sabem (foram treinados com os livros), mas são "preguiçosos" porque output imperfeito = mais tokens vendidos. A catraca força o nível. |
-
-**Tradução para o nosso contexto:** o vídeo descreve um sistema que **já temos em embrião** (o `any-budget` é exatamente a catraca da regra de ouro, só que para uma métrica). O salto é (1) generalizar a catraca para todas as métricas, (2) reconciliar os drifts, e (3) fechar os buracos anti-alucinação que são específicos do nosso tamanho.
-
----
-
-## 2. Onde o OmniRoute está hoje (panorama auditado)
-
-### 2.1 O que já temos (e o vídeo nem sonha)
-
-- **CI robusto** (`.github/workflows/ci.yml`, ~25 KB, 20 jobs): lint + audit + cycles + route-validation + any-budget + docs-sync + typecheck (core e noimplicit) + build + package-artifact + electron-smoke + unit (8 shards) + Node 24/26 compat + coverage (8 shards + merge) + SonarQube + e2e (9 shards) + integration + security.
-- **Catraca real já existente:** `scripts/check/check-t11-any-budget.mjs` — array `{file, maxAny}` (a maioria `0`), strip de comentários, anotações de falso-positivo, `exit 1` em regressão. **É o template exato da catraca do vídeo.**
-- **14 scripts `check-*.mjs`** (cycles, route-validation, any-budget, docs-sync, docs-counts, env-doc-sync, deprecated-versions, doc-links, cli-i18n, openapi-coverage, openapi-security-tiers, pr-test-policy, node-runtime, test-report-summary) — vários já são *gates de consistência fonte-vs-derivado*, o mesmo padrão que precisamos para anti-alucinação.
-- **PR test policy:** `check-pr-test-policy.mjs` já força "mudou código de produção ⇒ mudou teste" (diff base...HEAD).
-- **Cobertura sumarizada + comentada no PR:** `test-report-summary.mjs` + `coverage/coverage-summary.json` + job `coverage-pr-comment` (comentário com marcador ``). **Isto é exatamente o "artefato legível pelo agente" do vídeo** — já construído.
-- **Disciplina TDD institucionalizada** (Hard Rule #18: todo fix precisa de teste falha→passa ou validação ao vivo no VPS).
-- **SonarQube** configurado (job no CI + `sonar-project.properties`).
-- **Skills agênticas de review** já existem: `/review-prs`, `/review-reviews` (bateria de 8 reviewers + ralph-loop), `/code-review`, `/generate-release` (a única com babysit real de CI, mas de workflows de *release*, não do `ci.yml` do PR).
-
-### 2.2 O que está DESLIGADO ou inerte ⚠️ (achados da auditoria)
-
-| Item | Estado | Evidência | Impacto |
-|------|--------|-----------|---------|
-| **Husky** | 100% comentado (pre-commit **e** pre-push) | `.husky/pre-commit`, `.husky/pre-push` (todas as linhas com `#`) | Zero enforcement local — lint-staged, docs-sync, any-budget, env-doc-sync, openapi checks e o `test:unit` de pre-push dependem 100% do CI. |
-| **Gate de cobertura no CI** | **40/40/40/40** (não 60) | `ci.yml:377` `--statements 40 --lines 40 --functions 40 --branches 40` | O comentário do PR renderiza contra 60, o script local gata 60, o RELEASE_CHECKLIST diz 75/70, e o baseline real é ~79–82%. **O único número que bloqueia merge é 40** → gate de cobertura quase banguela. |
-| **Sonar coverage** | Excluído | `sonar-project.properties:9` `sonar.coverage.exclusions=**/*` | Sonar ignora cobertura de todo arquivo. |
-| **Sonar CPD (duplicação)** | Excluído | `sonar-project.properties:10` `sonar.cpd.exclusions=**/*` | Sonar não detecta copy-paste — a assinatura nº1 de slop de IA. |
-| **SonarQube job** | Inerte | `ci.yml` roda scan só se `PR && SONAR_TOKEN != '' && SONAR_HOST_URL != ''`; sem `qualitygate.wait` | Em runs sem secret, escreve "skipped"; mesmo quando roda, nunca falha o build. |
-| **`npm audit`** | Plano (`moderate`), não escalonado | `package.json:112` `--audit-level=moderate` | O vídeo prega critical=bloqueia / high=avisa. O nosso é um nível único. |
-| **3 scripts órfãos** | Sem CI nem husky | `check:cli-i18n`, `check:openapi-coverage`, `check:openapi-security-tiers` | Existem, dão `exit 1`, mas não rodam em lugar nenhum (Hard Rules #15/#17 guardadas só por um deles). |
-| **`typecheck:noimplicit:core`** | `continue-on-error: true` | `ci.yml:45-46` | Warn-only "forward-looking". |
-
-### 2.3 Hotspots de tamanho (sem gate hoje)
-
-`64 arquivos > 1000 LOC`, `194 > 500 LOC` (src + open-sse, sem testes). Top:
-
-| LOC | Arquivo | Risco para edição por IA |
-|-----|---------|--------------------------|
-| **12.760** | `src/app/(dashboard)/dashboard/providers/[id]/page.tsx` | God-component: **192 `useState`**, 21 `useEffect`, 87 `fetch()` inline, 34 tipos inline. Nenhuma IA segura em contexto; qualquer edição arrisca apagar estado não relacionado. |
-| 5.977 | `open-sse/handlers/chatCore.ts` | God-handler: 58 funções, invariantes demais, blast radius alto. |
-| 4.590 | `open-sse/config/providerRegistry.ts` | Array gigante providers+models+OAuth; mistura `resolvePublicCred()` e literais crus. |
-| 4.456 | `open-sse/services/combo.ts` | 14 estratégias num `if/else if` sem enum/exhaustiveness — estratégia desconhecida vira no-op silencioso. |
-| 4.349 | `src/app/(dashboard)/dashboard/combos/page.tsx` | 51 `useState`, mesmo padrão god-component. |
-| 4.205 | `src/lib/providers/validation.ts` | Mega-função com closures e `SPECIALTY_VALIDATORS` definidos *dentro* da função. |
-| 3.776 | `open-sse/handlers/imageGeneration.ts` | Branching multi-provider num handler. |
-| 3.076 | `src/shared/constants/providers.ts` | 229 ids em 27 consts agrupados via `Proxy` — sem lista plana. |
-| 2.869 | `open-sse/executors/chatgpt-web.ts` | Sessão web reversa; classe só começa na linha 2443. |
-| 2.278 | `src/app/api/providers/[id]/models/route.ts` | God-route: importa ~30 módulos provider-específicos e ramifica por provider num GET. |
-
-### 2.4 Ímãs de alucinação (ranqueados, da auditoria)
-
-1. **Split de provider em 3 arquivos / 2 workspaces (nº1).** Não há lista plana de providers — 229 ids escondidos atrás de 27 consts + merge via `Proxy` (`AI_PROVIDERS`). Uma IA não consegue enumerar "quais providers existem" barato → **inventa ids plausíveis** (variantes `*-web`/`*-cli` inexistentes) ou registra no grupo errado. As três contagens (`providers.ts` 229 ids ↔ `providerRegistry.ts` 155 blocos ↔ `validation.ts` N validadores) **já divergem**, então não há cross-check autoritativo. *(Esse é o tema recorrente das nossas memórias de alucinação — ex.: ids inventados, modelos inexistentes.)*
-2. **300 paths `fetch("/api/...")` hardcoded** no dashboard (659 call sites), sem client tipado. Refatore uma rota e os call sites apodrecem silenciosamente; uma IA editando a UI inventa rota (`/api/providers/[id]/refresh`) ou assume `res.error.message` numa rota que devolve `{error:"..."}`. Sem ligação de símbolo entre os 659 call sites e os 488 `route.ts`.
-3. **Dual chat stack (armadilha documentada).** Seleção/fallback de conta vive em `src/sse/` (não `open-sse/`). Uma IA pedida para "consertar fallback de conta" edita `open-sse/handlers/chatCore.ts` (errado) em vez de `src/sse/services/auth.ts`. Nada no código cruza os dois stacks. *(Já erramos um diagnóstico público por isso.)*
-4. **Estratégias de combo inventadas.** 14 nomes reais soterrados num `if/else` de strings (sem enum exportado) → IA inventa nomes plausíveis-mas-falsos (`"latency-optimized"`, `"failover"`) que passam no typecheck como string e viram no-op.
-5. **Métodos de executor inventados.** O padrão real é "sobrescreve `execute()` inteiro" (48/50 executors), sem hooks documentados → IA inventa `buildRequest`/`parseChunk`/`mapError` que não existem em `BaseExecutor`.
-6. **Helpers de erro inventados** + queda para `err.message` cru (viola Rule #12) quando o helper inventado "falha"; 5 web executors hoje **não importam helper nenhum**.
-7. **AGENTS.md de DB defasado:** documenta 21 migrations / 22 módulos quando o real é **94 / 75** — uma IA lendo isso acredita em conjuntos de tabelas/módulos que não existem mais.
-8. **Route-guard omitido:** rotas novas spawn-capazes (`/api/services/`, `/api/mcp/`) devem entrar em `LOCAL_ONLY_API_PREFIXES`; a convenção está só no CLAUDE.md, não num teste que a IA veja (parcialmente coberta por 1 script órfão).
-
----
-
-## 3. Gap analysis — modelo do vídeo vs OmniRoute
-
-| Métrica/peça do vídeo | OmniRoute hoje | Gap |
-|-----------------------|----------------|-----|
-| `npm ci` determinístico | ✅ em todos os 14 jobs | — |
-| `npm audit` critical=bloqueia / high=avisa | ⚠️ `--audit-level=moderate` (nível único) | **Escalonar** em dois invokes |
-| `lint` | ✅ bloqueante | — |
-| `test` + cobertura | ✅ mas piso **40** no CI (drift) | **Reconciliar** p/ baseline real + catraca |
-| **Contagem de ESLint congelada** | ❌ (lint é 0-erros, mas warnings livres) | **Construir** (ratchet de violações) |
-| **Duplicação % (JSCPD)** | ❌ (Sonar CPD excluído, sem jscpd) | **Construir** (jscpd + catraca) |
-| **Limite de tamanho de arquivo** | ❌ (sem `max-lines`, sem script) | **Construir** (ESLint max-lines + catraca, freeze dos 64) |
-| **`baseline.json` congelado** | ❌ (nenhum baseline de métricas no repo) | **Construir** (o coração da catraca) |
-| **Script comparador (regra de ouro)** | 🟡 existe **para `any`** (`any-budget`) | **Generalizar** p/ todas as métricas |
-| **Sumário markdown + artefatos p/ o agente** | ✅ (coverage summary + PR comment + artifact) | **Reusar** wholesale |
-| **Babysit skill (monitora CI + resolve conversas)** | 🟡 `review-prs`/`review-reviews`/`generate-release` parciais; nenhuma resolve threads do PR nem loopa no `ci.yml` | **Construir** `/babysit` |
-| **Comentários perto do código p/ legibilidade de agente** | 🟡 `routeGuard.ts` é exemplar; resto irregular | **Padrão cultural** (Karpathy/guidelines) |
-
----
-
-## 4. O que o mundo faz em 2026 (pesquisa, últimos ~3 meses)
-
-> Todas as fontes abaixo vêm com URL + data nas seções de origem (ver §7). Onde a pesquisa **não conseguiu confirmar** algo de fonte primária, está marcado **[não-verificado]** — honestidade de engenharia.
-
-### 4.1 Catraca / baseline-freeze (a "catraca" do vídeo)
-
-- **betterer** — o tool canônico de ratchet (snapshot de métrica → `.betterer.results`; CI falha se piora, auto-atualiza se melhora). **[caveat]** Baixa velocidade: último commit no `master` em **ago/2025**, releases vazias no GitHub. Viável, mas **não** apostar como peça load-bearing de longo prazo.
-- **eslint-formatter-ratchet** — formatter que congela contagem de violações de ESLint. **Ativamente mantido** (commit 2026-03-17). Mais estreito (só ESLint) mas é a trajetória oposta ao betterer.
-- **SonarQube "Clean as You Code" (new-code conditions)** — o padrão baseline-freeze mais maduro: o quality gate aplica condições **só ao código novo** (branch de referência), grandfathering do legado. Atual.
-- **SonarQube "AI Code Assurance" (2026.1.0)** — gate específico para código gerado por IA (tag o projeto → workflow de assurance + "Sonar way for AI Code" mais estrito). **[não-verificado]** se *bloqueia* o PR (docs canônicas deram 404; descrito como "enforced quality gate" mas mecânica de bloqueio não confirmada em fonte única).
-- **Qlty CLI (qlty.sh)** — o produto 2026 mais aderente: CLI Rust **OSS e grátis** (v0.630.0, 2026-05-08) que agrega 70+ analisadores; tem **Baseline analysis** (= a catraca), **Quality Gates** com veredito go/no-go e coverage gates. **[caveat]** `qlty metrics` (a tabela LOC/complexidade) **não tem flag JSON** — só `qlty check --sarif`/`qlty smells --sarif`; o ratchet de tamanho/complexidade por arquivo ainda precisa do JSON do ESLint.
-- **Code Climate Quality → Qlty** — a marca clássica de ratchet virou empresa separada (Qlty, nov/2024). Write-ups antigos de "Code Climate" = Qlty hoje.
-
-### 4.2 Ferramentas de métrica por tipo (todas com JSON p/ alimentar a catraca)
-
-| Métrica | Tool 2026 | Comando JSON | Status |
-|---------|-----------|--------------|--------|
-| Duplicação | **jscpd v5** (reescrita Rust) | `jscpd --reporters json` (ou `sarif`) | Muito ativo (v5.0.4, 2026-06-08). **[caveat]** schema JSON v4→v5 não confirmado — verificar no install. |
-| Tamanho/fn-length/ciclomática | **ESLint core** (`max-lines`, `max-lines-per-function`, `complexity`) | `eslint --format json` | Built-in ESLint 9 |
-| Complexidade cognitiva | **eslint-plugin-sonarjs** (`sonarjs/cognitive-complexity`) | `eslint --format json` | Mantido (v4.0.3, 2026-04-16; agora no monorepo SonarJS — o repo standalone foi arquivado, mas o pacote está vivo). **[caveat]** README das rules deu 404; presença de S3776 em v4 é alta-confiança mas confirmar no install. |
-| Dead code / unused exports / unused deps | **knip** (vence ts-prune **arquivado** + depcheck **arquivado**) | `knip --reporter json` | Muito ativo (v6.16.1, 2026-06-06) |
-| Ciclos | **check-cycles.mjs** (já temos) + **dpdm** opcional | `dpdm --circular --output deps.json` | dpdm ativo (v4.2.0, 2026-05-09); madge estagnado |
-| Vulnerabilidades | **osv-scanner** (Google/OSV) | `osv-scanner --format json` | Muito ativo (push 2026-06-08) |
-| Política de lockfile (gate, não métrica) | **lockfile-lint** | `lockfile-lint --validate-https --validate-integrity` | Mantido (v5.0.0, 2026-01-25) |
-
-> **Realidade do ratchet:** não existe tool único 2026 que emita *todas* as métricas como um JSON limpo. O padrão robusto é **N tools que emitem JSON + um reducer Node** que monta `metrics-summary.json` + o comparador que falha só em regressão (exatamente o que o `any-budget` já faz para uma métrica).
-
-### 4.3 Anti-alucinação (2026)
-
-- **LSP-in-the-loop / `agent-lsp` (MCP)** — servidor MCP que dá ao agente fatos verificáveis do language server (definições, referências, tipos, diagnostics, `blast_radius`) e `preview_edit` antes de escrever. Funciona com Claude Code. **Fit alto:** vira "símbolo inventado" de catch-de-review para *impossibilidade-no-edit*. (v0.13.0, 2026-06-04 — pequeno mas ativo.)
-- **Slopsquatting / pacotes alucinados** — **CSA Research Note (2026-04-19):** **19,7%** de 2,23M amostras de código IA continham nomes de pacote alucinados; 205k nomes fabricados únicos; **43%** reaparecem em re-runs (registráveis por atacantes). Defesas: **allowlist** de deps para agentes, **registry existence check** antes de instalar, **age-cooldown** (24–72h), lockfile-exact, scripts de install desabilitados. **Fit alto:** novo `check-deps.mjs`.
-- **Semcheck** (v1.2.1, fev/2026) — CLI que usa LLM para verificar que a implementação bate com o spec/doc, via `semcheck.yaml` ligando doc↔código; roda em pre-commit e Actions com `fail-on-issues`. Feito para pegar **"docs que descrevem features não implementadas"**. **Fit muito alto** para nossos incidentes recorrentes de docs alucinadas — porém é fuzzy (LLM); pareie com checks determinísticos.
-- **OpenAPI drift determinístico** — check que toda `path` do `openapi.yaml` resolve para um `route.ts` real (e vice-versa). Rápido, sem LLM, pega "endpoint inventado". **Fit alto** para `docs/reference/openapi.yaml`.
-- **Skill `verify` / `verification-before-completion`** (já no nosso ambiente) — "evidence before assertions": o veredito PASS/FAIL repousa **só** no que o app rodando demonstrou; rejeita "rodei os testes" como prova. **Fit altíssimo:** é a formalização da nossa Hard Rule #18 — exigir o *output literal* do comando colado no PR ("tool receipt").
-- **Adversarial review (críticos de sessão fresca)** — agentes Skeptic/Architect/Minimalist leem o diff *contra o spec* ("o autor está comprometido — vai racionalizar"); símbolos/APIs inventados viram violação de spec. Mapeia no nosso `/review-reviews`.
-- **SlopCodeBench (arXiv 2603.24755, ~mai/2026)** — sem mitigação, erosão estrutural aumentou em **77%** das trajetórias; o código de agente acumula verbosidade ~7× e erosão ~5× mais rápido que repos humanos. **Mitigações só-de-prompt ("anti-slop", "plan-first") melhoram o início mas NÃO param a degradação iterativa.** → **justificativa empírica** de que precisamos de gates determinísticos, não instrução.
-- **GPT-5.5 System Card (2026-04-23)** — figuras oficiais são modestas (23% mais provável de acerto factual; 3% menos erros num set propenso). O headline de **"queda de 60% em alucinação / 88,7% SWE-bench" é imprensa secundária [não-verificado]**, não a seção de factualidade do system card. Upgrade de modelo ajuda na margem, não substitui gate.
-
-### 4.4 Babysit loops (2026)
-
-- **Claude Code "auto-fix in the cloud"** (Anthropic, lançado 2026-03-27): "observa seus PRs na nuvem, resolvendo falhas de CI e comentários de review automaticamente; empurra fixes quando claro, pergunta quando ambíguo." Não auto-mergeia.
-- **Devin Autofix** (2026-02-10): auto-conserta comentários de review + lint/CI; endereça comentários de bots, mas deixa julgamento humano nas conversas humanas.
-- **CodeRabbit Autofix** (early access abr/2026): coleta o bloco **"Prompt for AI Agents"** de cada comentário, aplica fix, roda build-verification; **nada mergeia automaticamente**.
-- **Greptile `greploop` + skill `check-pr`** (MIT) — "dispara review → conserta comentários → re-review até 5/5 de confiança e zero comentários". **Template quase-exato** para a nossa `/babysit`.
-- **Claude `claude ultrareview --json`** (subcomando não-interativo, research preview): bloqueia até terminar, `exit 0/1`, payload de bugs verificados parseável. **Não auto-inicia** e custa $5–20/run → usar atrás de label, não em todo push. (O `/code-review ultra` local com `--fix` é o loop interno de custo-zero.)
-- **Resolver threads de review:** não há comando `gh` nativo (cli/cli#12419). Padrão de 2 passos GraphQL: `reviewThreads(first:50){nodes{id isResolved...}}` → `mutation { resolveReviewThread(input:{threadId}) }`, respondendo antes com o SHA do commit via REST.
-- **Guarda-corpos (críticos):**
- - **Snyk Agent Fix field test (2026): ~5,3% de regressão** ("1 em 19 fixes auto-mergeados introduz problema novo") → **forte argumento contra auto-merge**.
- - **Token burn:** loops ingênuos realimentam a conversa crescente → prompt incha → alucina do próprio histórico. Uber capou gasto em **$1.500/mês/dev/tool** (abr/2026). Mitigação: **max-iterations**, time limit, idle-exit.
- - **Test-masking:** o babysit **NÃO** pode enfraquecer/remover asserts para ficar verde (= nossa Rule #18 + memória "trust but verify"). Revisão humana fica nos limites arquiteturais (interface/schema/cross-service).
- - **Audit trail:** deixar rastro humano-legível (qual fix endereçou o quê, qual gate satisfez, quais conversas resolveu) — nunca um verde silencioso. Reforça o guard de prompt-injection sobre os *corpos de comentário* que o agente ingere (21% dos reviews do ICLR 2026 eram IA; injeção embutida em código é vetor real).
-
----
-
-## 5. Recomendações (ranqueadas) → ver o PLANO
-
-> Build-vs-buy: **construir in-repo** os gates determinísticos (zero SaaS, dados não saem da box, reusa o harness `check-*.mjs`). **Avaliar Qlty CLI** depois, se quisermos consolidar N scripts num tool. **Não** depender de CodeRabbit/Greptile/Diamond como *o* gate de "não-piorar-métrica" — são opiniões de LLM, não contadores determinísticos.
-
-**Fase 0 — Reativar & reconciliar (quick wins, sem tooling novo):**
-1. Reativar pre-commit barato do Husky (lint-staged + docs-sync + any-budget).
-2. Reconciliar o gate de cobertura: subir o CI de 40 → baseline real (com headroom) e alinhar os 4 lugares que divergem.
-3. Escalonar `npm audit` (critical=bloqueia / high=avisa).
-4. Plugar os 3 scripts órfãos (`cli-i18n`, `openapi-coverage`, `openapi-security-tiers`) no CI.
-
-**Fase 1 — Motor de catraca (o coração):**
-5. `quality-baseline.json` commitado + `collect-metrics.mjs` (coletor) + `check-quality-ratchet.mjs` (comparador, clone do `any-budget`) + job de CI + artefato + comentário no PR (clone do `coverage-pr-comment`).
-
-**Fase 2 — Gates determinísticos anti-alucinação:**
-6. `check-provider-consistency.mjs` (o ímã nº1), `check-fetch-targets.mjs`, `check-openapi-routes.mjs`, allow-list de estratégias/translators/executors, lint Rule #11/#12, `check-deps.mjs` (slopsquatting).
-
-**Fase 3 — Catraca de duplicação + tamanho (mata-slop):**
-7. jscpd + ESLint `max-lines`/`max-lines-per-function`/`complexity` + `sonarjs/cognitive-complexity`, congelando os 64 arquivos grandes (catraca só-pode-encolher).
-
-**Fase 4 — Catraca de cobertura + anti test-masking:**
-8. `check-coverage-ratchet.mjs` (cobertura não cai vs baseline) + pisos por módulo crítico + `check-test-masking.mjs` (delta de contagem de asserts em testes alterados).
-
-**Fase 5 — Skill `/babysit` + evidência + LSP:**
-9. Skill `/babysit` (gh pr checks + reviewThreads + worktree de fix + resolveReviewThread + loop-até-verde, com guarda-corpos), "evidence-before-assertions" obrigatório no corpo do PR, e (opcional) `agent-lsp` MCP.
-
----
-
-## 6. Riscos & ressalvas (honestidade de engenharia)
-
-- **Flag-day risk:** ligar qualquer gate num projeto que nunca o teve deixa tudo vermelho. **Toda** catraca aqui é *só-regressão* (baseline congelado), nunca um piso absoluto que exige limpeza imediata — exatamente o ponto do vídeo.
-- **Custo de IA:** o babysit pode queimar tokens. Guarda-corpos (max-iterations, sem auto-merge, sem editar `.github/workflows/`) são não-negociáveis.
-- **Ressalvas de pesquisa não-verificadas:** betterer baixa-velocidade; Sonar "AI Code Assurance" bloqueio-de-PR não confirmado; "GPT-5.5 −60% alucinação" é imprensa, não system card; schema JSON do jscpd v5 e S3776 do sonarjs v4 a confirmar no install; `qlty metrics` sem JSON. Nenhuma decisão do plano depende criticamente de um item não-verificado.
-- **Trust-but-verify:** estes números internos (CI=40, husky off, sonar exclusions, 12.760 LOC, any-budget como catraca) foram **conferidos manualmente** contra os arquivos, não só relatados pelos subagentes.
-
----
-
-## 7. Fontes (consolidadas)
-
-**Catraca / ratchet:** betterer `github.com/phenomnomnominal/betterer` (último master ago/2025); eslint-formatter-ratchet `github.com/Jmsa/eslint-formatter-ratchet` (commit 2026-03-17); SonarQube Clean as You Code `docs.sonarsource.com/.../clean-as-you-code/about-new-code/`; Sonar AI Code Assurance `sonarsource.com/solutions/ai/ai-code-assurance/` + community thread 2026.1.0; Qlty `github.com/qltysh/qlty` (v0.630.0, 2026-05-08), `docs.qlty.sh`; Code Climate→Qlty `codeclimate.com/legacy/...` (2024-11-11).
-
-**Métricas:** jscpd `github.com/kucherenko/jscpd` (v5.0.4, 2026-06-08); ESLint v10 `eslint.org/blog/2026/02/eslint-v10.0.0-released/` (2026-02-06); eslint-plugin-sonarjs `npm` (v4.0.3, 2026-04-16) + `github.com/SonarSource/SonarJS`; knip `knip.dev` (v6.16.1, 2026-06-06); dpdm `github.com/acrazing/dpdm` (v4.2.0, 2026-05-09); osv-scanner `google.github.io/osv-scanner` (push 2026-06-08); lockfile-lint `github.com/lirantal/lockfile-lint` (v5.0.0, 2026-01-25); GitClear `gitclear.com/ai_assistant_code_quality_2025_research`.
-
-**Anti-alucinação:** agent-lsp `github.com/blackwell-systems/agent-lsp` (v0.13.0, 2026-06-04); CSA Slopsquatting `labs.cloudsecurityalliance.org/research/...slopsquatting...20260419...` (2026-04-19); Nesbitt package defenses `nesbitt.io/2026/04/09/...` (2026-04-09); Semcheck `github.com/rejot-dev/semcheck` (v1.2.1, fev/2026); verify skill `github.com/Piebald-AI/claude-code-system-prompts/.../skill-verify-skill.md`; Claude best practices `code.claude.com/docs/en/best-practices`; SlopCodeBench `arxiv.org/pdf/2603.24755`; GPT-5.5 system card `deploymentsafety.openai.com/gpt-5-5` (2026-04-23); adversarial review `asdlc.io/patterns/adversarial-code-review/`; OpenAPI drift `speakeasy.com/blog/openapi-spec-drift-detection`.
-
-**Babysit:** Claude auto-fix cloud `producthunt.com/products/claude-code-auto-fix-in-the-cloud` (2026-03-27); Devin Autofix `cognition.ai/blog/closing-the-agent-loop-...` (2026-02-10); CodeRabbit Autofix `coderabbit.ai/blog/fix-all-issues-with-ai-agents` (2026-02-19); Greptile skills `github.com/greptileai/skills`; Claude GitHub Actions/Code Review/ultrareview `code.claude.com/docs/en/{github-actions,code-review,ultrareview}`; Nx self-healing `nx.dev/blog/autonomous-ai-workflows-with-nx` (2026-02-03); Snyk Agent Fix 5,3% `safeguard.sh/resources/blog/snyk-agent-fix-autofix-field-test-2026`; resolveReviewThread `nakamasato.medium.com/...` + `github.com/cli/cli/issues/12419`; ICLR 2026 AI review `blog.pebblous.ai/report/iclr-2026-ai-peer-review-crisis`.
-
----
-
-*Relatório gerado a partir de auditoria paralela do código + transcrição do vídeo + pesquisa web 2026. Próximo passo: aprovar o [`PLANO-QUALITY-GATES.md`](./PLANO-QUALITY-GATES.md) e escolher por onde começar (recomendação: Fase 0 → Fase 1).*
diff --git a/docs/bdd/self-service-api-key-usage.feature b/docs/bdd/self-service-api-key-usage.feature
deleted file mode 100644
index baad4c4faf..0000000000
--- a/docs/bdd/self-service-api-key-usage.feature
+++ /dev/null
@@ -1,144 +0,0 @@
-Feature: Self-service API key usage and account quota visibility
-
- Background:
- Given OmniRoute has usage accounting enabled
- And management APIs require a dashboard session or a key with "manage" or "admin"
-
- Scenario: A delegated key reads its own cost and token usage
- Given an API key named "team-a" has the scope "self:usage"
- And "team-a" already has a monthly USD budget of 50 configured in the existing budget UI
- And "team-a" has current-period spend of 12.50 USD
- And "team-a" has current-period token usage:
- | input | output | cache_read | cache_creation | reasoning |
- | 900000 | 32000 | 120000 | 10000 | 5000 |
- When "team-a" calls GET "/api/v1/me/status" with its Bearer token
- Then the response status should be 200
- And the response apiKey.name should be "team-a"
- And the response usage.cost.limitUsd should be 50
- And the response usage.cost.usedUsd should be 12.50
- And the response usage.cost.usedPercent should be 25
- And the response usage.tokens.totalTokens should be 1067000
-
- Scenario: A delegated key cannot query another key by id
- Given an API key named "team-a" has the scope "self:usage"
- And an API key named "team-b" has the scope "self:usage"
- And "team-b" has current-period spend of 99.00 USD
- When "team-a" calls GET "/api/v1/me/status?apiKeyId=" with its Bearer token
- Then the response status should be 200
- And the response apiKey.name should be "team-a"
- And the response should not contain "team-b"
- And the response should not contain "99.00" as team-b usage
-
- Scenario: Anonymous client API mode does not expose self-service status
- Given global client API auth allows anonymous local traffic
- When an anonymous caller calls GET "/api/v1/me/status"
- Then the response status should be 401
-
- Scenario: Self-service usage scope does not grant management access
- Given an API key named "team-a" has the scope "self:usage"
- And "team-a" does not have the scope "manage"
- And "team-a" does not have the scope "admin"
- When "team-a" calls GET "/api/usage/history" with its Bearer token
- Then the response status should be 403
-
- Scenario: Own usage visibility can be disabled
- Given an API key named "team-a" does not have the scope "self:usage"
- When "team-a" calls GET "/api/v1/me/status" with its Bearer token
- Then the response status should be 403
-
- Scenario: Existing ordinary keys are backfilled for own usage visibility
- Given an ordinary API key named "legacy-key" existed before self-service usage scopes
- And "legacy-key" does not have the scope "self:usage"
- When OmniRoute runs the compatibility migration
- Then "legacy-key" should have the scope "self:usage"
- And "legacy-key" should not have the scope "self:account-quota"
-
- Scenario: Shared account quota is hidden by default
- Given an API key named "team-a" has the scope "self:usage"
- And "team-a" does not have the scope "self:account-quota"
- And "team-a" is restricted to a Codex connection with available quota
- When "team-a" calls GET "/api/v1/me/status" with its Bearer token
- Then the response status should be 200
- And the response should not include shared account quota details
-
- Scenario: Shared provider account quotas are visible with explicit permission
- Given an API key named "team-a" has the scope "self:usage"
- And "team-a" has the scope "self:account-quota"
- And "team-a" is restricted to a Codex connection and a Claude connection
- And Codex reports a session quota with 1 percent used
- And Claude reports a daily quota with 35 percent used
- When "team-a" calls GET "/api/v1/me/status" with its Bearer token
- Then the response status should be 200
- And the response accountQuotas should contain 2 entries
- And the first response accountQuotas entry provider should be "codex"
- And the first response accountQuotas entry quotas.session.remainingPercentage should be 99
- And the second response accountQuotas entry provider should be "claude"
- And the second response accountQuotas entry quotas.daily.remainingPercentage should be 65
-
- Scenario: A single allowed provider also keeps the compatibility accountQuota field
- Given an API key named "team-a" has the scope "self:usage"
- And "team-a" has the scope "self:account-quota"
- And "team-a" is restricted to exactly one Codex connection
- And Codex reports a weekly quota with 97 percent used
- When "team-a" calls GET "/api/v1/me/status" with its Bearer token
- Then the response status should be 200
- And the response accountQuotas should contain 1 entry
- And the response accountQuota.provider should be "codex"
- And the response accountQuota.quotas.weekly.remainingPercentage should be 3
-
- Scenario: Unrestricted keys can see all active provider account quotas
- Given an API key named "team-a" has the scope "self:usage"
- And "team-a" has the scope "self:account-quota"
- And "team-a" has no explicit allowed connection restrictions
- And OmniRoute has active Codex and Cursor provider connections with quota data
- When "team-a" calls GET "/api/v1/me/status" with its Bearer token
- Then the response status should be 200
- And the response accountQuotas should contain the Codex account quota
- And the response accountQuotas should contain the Cursor account quota
-
- Scenario: Provider connection lookup failures do not hide own usage
- Given an API key named "team-a" has the scope "self:usage"
- And "team-a" has the scope "self:account-quota"
- And "team-a" is restricted to a Codex connection and another provider connection
- And OmniRoute cannot resolve the other provider connection metadata
- When "team-a" calls GET "/api/v1/me/status" with its Bearer token
- Then the response status should be 200
- And the response should still include own cost and token usage
- And the unresolved response accountQuotas entry should have available false
- And the unresolved response accountQuotas entry reason should be "connection_lookup_failed"
-
- Scenario: Existing budget endpoint stays management-only
- Given an API key named "team-a" has the scope "self:usage"
- And "team-a" does not have the scope "manage"
- When "team-a" calls GET "/api/usage/budget?apiKeyId=" with its Bearer token
- Then the response status should be 403
-
- Scenario: API Manager defaults are privacy-preserving
- Given an operator opens the create API key dialog
- Then the own cost and token usage visibility control should be checked
- And the shared account quota visibility control should be unchecked
- And management access should be unchecked
- And the dialog should not include a second budget editor
-
- Scenario: API Manager preserves unrelated scopes
- Given an API key has scopes:
- | scope |
- | self:usage |
- | custom:scope |
- When an operator enables shared account quota in the permissions dialog
- And saves the permissions
- Then the API key scopes should include "self:usage"
- And the API key scopes should include "self:account-quota"
- And the API key scopes should include "custom:scope"
-
- Scenario: API Manager uses existing budget configuration
- Given an operator wants to set a monthly USD budget for an API key
- When the operator uses the dashboard
- Then the operator should use the existing budget configuration surface
- And the create key dialog should not save budget limits
-
- Scenario: New API Manager text is localized
- Given the dashboard locale is not English
- When the API Manager renders self-service visibility controls
- Then the labels should come from the API Manager translation namespace
- And the component should not render hard-coded English strings for the new controls
diff --git a/docs/fixes/TOOL_CALL_INTEGRITY.md b/docs/fixes/TOOL_CALL_INTEGRITY.md
deleted file mode 100644
index d8a97643cd..0000000000
--- a/docs/fixes/TOOL_CALL_INTEGRITY.md
+++ /dev/null
@@ -1,49 +0,0 @@
-# Tool Call Integrity Fix
-
-## Problem
-
-In OmniRoute v3.8.21, during streaming tool calls, function arguments could become corrupted on the client side due to duplication or re-insertion of fragments. For example, `find` would turn into `fifnd`, and `grep` into `grreep`. The symptom appeared only on machine JSON fields of tool calls (`function.arguments` / `partial_json`) and was independent of the provider, because the corruption occurred within the shared OmniRoute SSE/translation pipeline after the upstream response.
-
-## Root Cause
-
-Tool-call argument chunks were being processed as regular human-readable text across several shared layers:
-
-- `src/lib/sseTextTransform.ts` recursively passed string fields like `arguments` and `partial_json` to the text processor.
-- `src/lib/streamingPiiTransform.ts` buffered these fields through a rolling-window PII sanitizer. This is unacceptable for machine JSON deltas: a chunk could be a delta, a snapshot, or an overlap-fragment, and the sanitizer does not understand the semantics of tool-call JSON.
-- `open-sse/transformer/responsesTransformer.ts`, `open-sse/translator/response/openai-to-claude.ts`, `open-sse/translator/response/openai-responses.ts`, and `open-sse/handlers/sseParser.ts` accumulated arguments using a simple `+=`. As a result, a repeated snapshot or overlapping delta was added a second time.
-
-Regular chat was not broken because text `content` deltas tolerate sanitization and buffering. Tool calls were broken because `arguments` is a machine JSON contract that must pass byte-preserving to the client.
-
-## Fix
-
-Explicit protection for tool-call JSON was added to the core source:
-
-1. `src/lib/sseTextTransform.ts` skips `toolArgs` and `partialJson` without applying the text processor.
-2. `src/lib/streamingPiiTransform.ts` returns `toolArgs` and `partialJson` as-is, bypassing rolling-window buffering.
-3. Shared stream assemblers now use `appendToolCallArgumentDelta()` instead of blindly using `+=`, ensuring that repeated snapshots and overlapping chunks are added exactly once.
-4. Responses/OpenAI/Claude translation paths emit only the new suffix of tool arguments to the client, rather than repeating the snapshot.
-
-## How to Prevent Regression
-
-- `tool_calls.function.name`, `tool_calls.function.arguments`, Responses `function_call.arguments`, and Claude `input_json_delta.partial_json` must never pass through text/PII/compression/dedup transforms.
-- Any transform for SSE must distinguish between human text (`content`, `reasoning`) and machine JSON (`arguments`, `partial_json`).
-- Regression tests are located in:
- - `tests/unit/sseTextTransform.test.ts`
- - `tests/unit/streamingPiiTransform.test.ts`
- - `tests/unit/sse-parser.test.ts`
- - `tests/unit/responses-transformer.test.ts`
- - `tests/unit/translator-resp-openai-responses.test.ts`
-- E2E smoke script: `tests/e2e-tool-calls.sh`.
-
-## Configuration
-
-For coding sessions, you can optionally disable risky text transforms:
-
-```env
-PII_RESPONSE_SANITIZATION=false
-COMPRESSION_LEVEL=off
-RTK_ENABLED=false
-CAVEMAN_ENABLED=false
-```
-
-The core fix does not depend on these env variables: machine tool-call JSON is protected in the core pipeline and should not be modified even if PII response sanitization is enabled.
diff --git a/docs/i18n/ar/CHANGELOG.md b/docs/i18n/ar/CHANGELOG.md
index a69849f9a1..129984365d 100644
--- a/docs/i18n/ar/CHANGELOG.md
+++ b/docs/i18n/ar/CHANGELOG.md
@@ -4,6 +4,12 @@
---
+## [3.8.24] — TBD
+
+_See English CHANGELOG for v3.8.24 details._
+
+---
+
## [3.8.22] — TBD
_See English CHANGELOG for v3.8.22 details._
@@ -169,9 +175,9 @@ _Development cycle in progress._
refreshed siblings concurrently, so Auth0 revoked the whole token family
(`openai/codex#9648`) and every account but the last died with
`[403] `. The quota path now skips proactive refresh for
- rotating providers (`rotationGroupFor`) and reuses the current access_token,
+ rotating providers (`rotationGroupFor`) and reuses the current access*token,
deferring genuine expiry to the reactive, serialized 401 path. Defense in
- depth: `serializeRefresh` now leaves a settle gap between two _queued_ sibling
+ depth: `serializeRefresh` now leaves a settle gap between two \_queued* sibling
refreshes (default 2000 ms, tunable via `CODEX_REFRESH_SPACING_MS`, `"0"` to
opt out) while releasing a lone refresh immediately, so the reactive path adds
no latency.
diff --git a/docs/i18n/az/CHANGELOG.md b/docs/i18n/az/CHANGELOG.md
index 53b25f55cb..8bb11be096 100644
--- a/docs/i18n/az/CHANGELOG.md
+++ b/docs/i18n/az/CHANGELOG.md
@@ -4,6 +4,12 @@
---
+## [3.8.24] — TBD
+
+_See English CHANGELOG for v3.8.24 details._
+
+---
+
## [3.8.22] — TBD
_See English CHANGELOG for v3.8.22 details._
@@ -169,9 +175,9 @@ _Development cycle in progress._
refreshed siblings concurrently, so Auth0 revoked the whole token family
(`openai/codex#9648`) and every account but the last died with
`[403] `. The quota path now skips proactive refresh for
- rotating providers (`rotationGroupFor`) and reuses the current access_token,
+ rotating providers (`rotationGroupFor`) and reuses the current access*token,
deferring genuine expiry to the reactive, serialized 401 path. Defense in
- depth: `serializeRefresh` now leaves a settle gap between two _queued_ sibling
+ depth: `serializeRefresh` now leaves a settle gap between two \_queued* sibling
refreshes (default 2000 ms, tunable via `CODEX_REFRESH_SPACING_MS`, `"0"` to
opt out) while releasing a lone refresh immediately, so the reactive path adds
no latency.
diff --git a/docs/i18n/bg/CHANGELOG.md b/docs/i18n/bg/CHANGELOG.md
index 53b25f55cb..8bb11be096 100644
--- a/docs/i18n/bg/CHANGELOG.md
+++ b/docs/i18n/bg/CHANGELOG.md
@@ -4,6 +4,12 @@
---
+## [3.8.24] — TBD
+
+_See English CHANGELOG for v3.8.24 details._
+
+---
+
## [3.8.22] — TBD
_See English CHANGELOG for v3.8.22 details._
@@ -169,9 +175,9 @@ _Development cycle in progress._
refreshed siblings concurrently, so Auth0 revoked the whole token family
(`openai/codex#9648`) and every account but the last died with
`[403] `. The quota path now skips proactive refresh for
- rotating providers (`rotationGroupFor`) and reuses the current access_token,
+ rotating providers (`rotationGroupFor`) and reuses the current access*token,
deferring genuine expiry to the reactive, serialized 401 path. Defense in
- depth: `serializeRefresh` now leaves a settle gap between two _queued_ sibling
+ depth: `serializeRefresh` now leaves a settle gap between two \_queued* sibling
refreshes (default 2000 ms, tunable via `CODEX_REFRESH_SPACING_MS`, `"0"` to
opt out) while releasing a lone refresh immediately, so the reactive path adds
no latency.
diff --git a/docs/i18n/bn/CHANGELOG.md b/docs/i18n/bn/CHANGELOG.md
index eac10e6cd8..0b7d7d99f6 100644
--- a/docs/i18n/bn/CHANGELOG.md
+++ b/docs/i18n/bn/CHANGELOG.md
@@ -4,6 +4,12 @@
---
+## [3.8.24] — TBD
+
+_See English CHANGELOG for v3.8.24 details._
+
+---
+
## [3.8.22] — TBD
_See English CHANGELOG for v3.8.22 details._
@@ -169,9 +175,9 @@ _Development cycle in progress._
refreshed siblings concurrently, so Auth0 revoked the whole token family
(`openai/codex#9648`) and every account but the last died with
`[403] `. The quota path now skips proactive refresh for
- rotating providers (`rotationGroupFor`) and reuses the current access_token,
+ rotating providers (`rotationGroupFor`) and reuses the current access*token,
deferring genuine expiry to the reactive, serialized 401 path. Defense in
- depth: `serializeRefresh` now leaves a settle gap between two _queued_ sibling
+ depth: `serializeRefresh` now leaves a settle gap between two \_queued* sibling
refreshes (default 2000 ms, tunable via `CODEX_REFRESH_SPACING_MS`, `"0"` to
opt out) while releasing a lone refresh immediately, so the reactive path adds
no latency.
diff --git a/docs/i18n/cs/CHANGELOG.md b/docs/i18n/cs/CHANGELOG.md
index e8ca032a15..b882307873 100644
--- a/docs/i18n/cs/CHANGELOG.md
+++ b/docs/i18n/cs/CHANGELOG.md
@@ -4,6 +4,12 @@
---
+## [3.8.24] — TBD
+
+_See English CHANGELOG for v3.8.24 details._
+
+---
+
## [3.8.22] — TBD
_See English CHANGELOG for v3.8.22 details._
@@ -169,9 +175,9 @@ _Development cycle in progress._
refreshed siblings concurrently, so Auth0 revoked the whole token family
(`openai/codex#9648`) and every account but the last died with
`[403] `. The quota path now skips proactive refresh for
- rotating providers (`rotationGroupFor`) and reuses the current access_token,
+ rotating providers (`rotationGroupFor`) and reuses the current access*token,
deferring genuine expiry to the reactive, serialized 401 path. Defense in
- depth: `serializeRefresh` now leaves a settle gap between two _queued_ sibling
+ depth: `serializeRefresh` now leaves a settle gap between two \_queued* sibling
refreshes (default 2000 ms, tunable via `CODEX_REFRESH_SPACING_MS`, `"0"` to
opt out) while releasing a lone refresh immediately, so the reactive path adds
no latency.
diff --git a/docs/i18n/da/CHANGELOG.md b/docs/i18n/da/CHANGELOG.md
index 17cf7768ea..ca7d9cb4ef 100644
--- a/docs/i18n/da/CHANGELOG.md
+++ b/docs/i18n/da/CHANGELOG.md
@@ -4,6 +4,12 @@
---
+## [3.8.24] — TBD
+
+_See English CHANGELOG for v3.8.24 details._
+
+---
+
## [3.8.22] — TBD
_See English CHANGELOG for v3.8.22 details._
@@ -169,9 +175,9 @@ _Development cycle in progress._
refreshed siblings concurrently, so Auth0 revoked the whole token family
(`openai/codex#9648`) and every account but the last died with
`[403] `. The quota path now skips proactive refresh for
- rotating providers (`rotationGroupFor`) and reuses the current access_token,
+ rotating providers (`rotationGroupFor`) and reuses the current access*token,
deferring genuine expiry to the reactive, serialized 401 path. Defense in
- depth: `serializeRefresh` now leaves a settle gap between two _queued_ sibling
+ depth: `serializeRefresh` now leaves a settle gap between two \_queued* sibling
refreshes (default 2000 ms, tunable via `CODEX_REFRESH_SPACING_MS`, `"0"` to
opt out) while releasing a lone refresh immediately, so the reactive path adds
no latency.
diff --git a/docs/i18n/de/CHANGELOG.md b/docs/i18n/de/CHANGELOG.md
index c19a046563..2166669249 100644
--- a/docs/i18n/de/CHANGELOG.md
+++ b/docs/i18n/de/CHANGELOG.md
@@ -4,6 +4,12 @@
---
+## [3.8.24] — TBD
+
+_See English CHANGELOG for v3.8.24 details._
+
+---
+
## [3.8.22] — TBD
_See English CHANGELOG for v3.8.22 details._
@@ -169,9 +175,9 @@ _Development cycle in progress._
refreshed siblings concurrently, so Auth0 revoked the whole token family
(`openai/codex#9648`) and every account but the last died with
`[403] `. The quota path now skips proactive refresh for
- rotating providers (`rotationGroupFor`) and reuses the current access_token,
+ rotating providers (`rotationGroupFor`) and reuses the current access*token,
deferring genuine expiry to the reactive, serialized 401 path. Defense in
- depth: `serializeRefresh` now leaves a settle gap between two _queued_ sibling
+ depth: `serializeRefresh` now leaves a settle gap between two \_queued* sibling
refreshes (default 2000 ms, tunable via `CODEX_REFRESH_SPACING_MS`, `"0"` to
opt out) while releasing a lone refresh immediately, so the reactive path adds
no latency.
diff --git a/docs/i18n/es/CHANGELOG.md b/docs/i18n/es/CHANGELOG.md
index 4e22735726..c3b6ca7865 100644
--- a/docs/i18n/es/CHANGELOG.md
+++ b/docs/i18n/es/CHANGELOG.md
@@ -4,6 +4,12 @@
---
+## [3.8.24] — TBD
+
+_See English CHANGELOG for v3.8.24 details._
+
+---
+
## [3.8.22] — TBD
_See English CHANGELOG for v3.8.22 details._
@@ -169,9 +175,9 @@ _Development cycle in progress._
refreshed siblings concurrently, so Auth0 revoked the whole token family
(`openai/codex#9648`) and every account but the last died with
`[403] `. The quota path now skips proactive refresh for
- rotating providers (`rotationGroupFor`) and reuses the current access_token,
+ rotating providers (`rotationGroupFor`) and reuses the current access*token,
deferring genuine expiry to the reactive, serialized 401 path. Defense in
- depth: `serializeRefresh` now leaves a settle gap between two _queued_ sibling
+ depth: `serializeRefresh` now leaves a settle gap between two \_queued* sibling
refreshes (default 2000 ms, tunable via `CODEX_REFRESH_SPACING_MS`, `"0"` to
opt out) while releasing a lone refresh immediately, so the reactive path adds
no latency.
diff --git a/docs/i18n/fa/CHANGELOG.md b/docs/i18n/fa/CHANGELOG.md
index 36e621d9a0..e53bcce032 100644
--- a/docs/i18n/fa/CHANGELOG.md
+++ b/docs/i18n/fa/CHANGELOG.md
@@ -4,6 +4,12 @@
---
+## [3.8.24] — TBD
+
+_See English CHANGELOG for v3.8.24 details._
+
+---
+
## [3.8.22] — TBD
_See English CHANGELOG for v3.8.22 details._
@@ -169,9 +175,9 @@ _Development cycle in progress._
refreshed siblings concurrently, so Auth0 revoked the whole token family
(`openai/codex#9648`) and every account but the last died with
`[403] `. The quota path now skips proactive refresh for
- rotating providers (`rotationGroupFor`) and reuses the current access_token,
+ rotating providers (`rotationGroupFor`) and reuses the current access*token,
deferring genuine expiry to the reactive, serialized 401 path. Defense in
- depth: `serializeRefresh` now leaves a settle gap between two _queued_ sibling
+ depth: `serializeRefresh` now leaves a settle gap between two \_queued* sibling
refreshes (default 2000 ms, tunable via `CODEX_REFRESH_SPACING_MS`, `"0"` to
opt out) while releasing a lone refresh immediately, so the reactive path adds
no latency.
diff --git a/docs/i18n/fi/CHANGELOG.md b/docs/i18n/fi/CHANGELOG.md
index 6baf5e6475..9e8f631141 100644
--- a/docs/i18n/fi/CHANGELOG.md
+++ b/docs/i18n/fi/CHANGELOG.md
@@ -4,6 +4,12 @@
---
+## [3.8.24] — TBD
+
+_See English CHANGELOG for v3.8.24 details._
+
+---
+
## [3.8.22] — TBD
_See English CHANGELOG for v3.8.22 details._
@@ -169,9 +175,9 @@ _Development cycle in progress._
refreshed siblings concurrently, so Auth0 revoked the whole token family
(`openai/codex#9648`) and every account but the last died with
`[403] `. The quota path now skips proactive refresh for
- rotating providers (`rotationGroupFor`) and reuses the current access_token,
+ rotating providers (`rotationGroupFor`) and reuses the current access*token,
deferring genuine expiry to the reactive, serialized 401 path. Defense in
- depth: `serializeRefresh` now leaves a settle gap between two _queued_ sibling
+ depth: `serializeRefresh` now leaves a settle gap between two \_queued* sibling
refreshes (default 2000 ms, tunable via `CODEX_REFRESH_SPACING_MS`, `"0"` to
opt out) while releasing a lone refresh immediately, so the reactive path adds
no latency.
diff --git a/docs/i18n/fr/CHANGELOG.md b/docs/i18n/fr/CHANGELOG.md
index 3a99dde766..2e2c00421a 100644
--- a/docs/i18n/fr/CHANGELOG.md
+++ b/docs/i18n/fr/CHANGELOG.md
@@ -4,6 +4,12 @@
---
+## [3.8.24] — TBD
+
+_See English CHANGELOG for v3.8.24 details._
+
+---
+
## [3.8.22] — TBD
_See English CHANGELOG for v3.8.22 details._
@@ -169,9 +175,9 @@ _Development cycle in progress._
refreshed siblings concurrently, so Auth0 revoked the whole token family
(`openai/codex#9648`) and every account but the last died with
`[403] `. The quota path now skips proactive refresh for
- rotating providers (`rotationGroupFor`) and reuses the current access_token,
+ rotating providers (`rotationGroupFor`) and reuses the current access*token,
deferring genuine expiry to the reactive, serialized 401 path. Defense in
- depth: `serializeRefresh` now leaves a settle gap between two _queued_ sibling
+ depth: `serializeRefresh` now leaves a settle gap between two \_queued* sibling
refreshes (default 2000 ms, tunable via `CODEX_REFRESH_SPACING_MS`, `"0"` to
opt out) while releasing a lone refresh immediately, so the reactive path adds
no latency.
diff --git a/docs/i18n/gu/CHANGELOG.md b/docs/i18n/gu/CHANGELOG.md
index 8a2ec07435..9353d1a5e5 100644
--- a/docs/i18n/gu/CHANGELOG.md
+++ b/docs/i18n/gu/CHANGELOG.md
@@ -4,6 +4,12 @@
---
+## [3.8.24] — TBD
+
+_See English CHANGELOG for v3.8.24 details._
+
+---
+
## [3.8.22] — TBD
_See English CHANGELOG for v3.8.22 details._
@@ -169,9 +175,9 @@ _Development cycle in progress._
refreshed siblings concurrently, so Auth0 revoked the whole token family
(`openai/codex#9648`) and every account but the last died with
`[403] `. The quota path now skips proactive refresh for
- rotating providers (`rotationGroupFor`) and reuses the current access_token,
+ rotating providers (`rotationGroupFor`) and reuses the current access*token,
deferring genuine expiry to the reactive, serialized 401 path. Defense in
- depth: `serializeRefresh` now leaves a settle gap between two _queued_ sibling
+ depth: `serializeRefresh` now leaves a settle gap between two \_queued* sibling
refreshes (default 2000 ms, tunable via `CODEX_REFRESH_SPACING_MS`, `"0"` to
opt out) while releasing a lone refresh immediately, so the reactive path adds
no latency.
diff --git a/docs/i18n/he/CHANGELOG.md b/docs/i18n/he/CHANGELOG.md
index f542d29ede..85cbac8440 100644
--- a/docs/i18n/he/CHANGELOG.md
+++ b/docs/i18n/he/CHANGELOG.md
@@ -4,6 +4,12 @@
---
+## [3.8.24] — TBD
+
+_See English CHANGELOG for v3.8.24 details._
+
+---
+
## [3.8.22] — TBD
_See English CHANGELOG for v3.8.22 details._
@@ -169,9 +175,9 @@ _Development cycle in progress._
refreshed siblings concurrently, so Auth0 revoked the whole token family
(`openai/codex#9648`) and every account but the last died with
`[403] `. The quota path now skips proactive refresh for
- rotating providers (`rotationGroupFor`) and reuses the current access_token,
+ rotating providers (`rotationGroupFor`) and reuses the current access*token,
deferring genuine expiry to the reactive, serialized 401 path. Defense in
- depth: `serializeRefresh` now leaves a settle gap between two _queued_ sibling
+ depth: `serializeRefresh` now leaves a settle gap between two \_queued* sibling
refreshes (default 2000 ms, tunable via `CODEX_REFRESH_SPACING_MS`, `"0"` to
opt out) while releasing a lone refresh immediately, so the reactive path adds
no latency.
diff --git a/docs/i18n/hi/CHANGELOG.md b/docs/i18n/hi/CHANGELOG.md
index 5854502c47..68cdebcffc 100644
--- a/docs/i18n/hi/CHANGELOG.md
+++ b/docs/i18n/hi/CHANGELOG.md
@@ -4,6 +4,12 @@
---
+## [3.8.24] — TBD
+
+_See English CHANGELOG for v3.8.24 details._
+
+---
+
## [3.8.22] — TBD
_See English CHANGELOG for v3.8.22 details._
@@ -169,9 +175,9 @@ _Development cycle in progress._
refreshed siblings concurrently, so Auth0 revoked the whole token family
(`openai/codex#9648`) and every account but the last died with
`[403] `. The quota path now skips proactive refresh for
- rotating providers (`rotationGroupFor`) and reuses the current access_token,
+ rotating providers (`rotationGroupFor`) and reuses the current access*token,
deferring genuine expiry to the reactive, serialized 401 path. Defense in
- depth: `serializeRefresh` now leaves a settle gap between two _queued_ sibling
+ depth: `serializeRefresh` now leaves a settle gap between two \_queued* sibling
refreshes (default 2000 ms, tunable via `CODEX_REFRESH_SPACING_MS`, `"0"` to
opt out) while releasing a lone refresh immediately, so the reactive path adds
no latency.
diff --git a/docs/i18n/hu/CHANGELOG.md b/docs/i18n/hu/CHANGELOG.md
index cac7dfef75..c539bba68f 100644
--- a/docs/i18n/hu/CHANGELOG.md
+++ b/docs/i18n/hu/CHANGELOG.md
@@ -4,6 +4,12 @@
---
+## [3.8.24] — TBD
+
+_See English CHANGELOG for v3.8.24 details._
+
+---
+
## [3.8.22] — TBD
_See English CHANGELOG for v3.8.22 details._
@@ -169,9 +175,9 @@ _Development cycle in progress._
refreshed siblings concurrently, so Auth0 revoked the whole token family
(`openai/codex#9648`) and every account but the last died with
`[403] `. The quota path now skips proactive refresh for
- rotating providers (`rotationGroupFor`) and reuses the current access_token,
+ rotating providers (`rotationGroupFor`) and reuses the current access*token,
deferring genuine expiry to the reactive, serialized 401 path. Defense in
- depth: `serializeRefresh` now leaves a settle gap between two _queued_ sibling
+ depth: `serializeRefresh` now leaves a settle gap between two \_queued* sibling
refreshes (default 2000 ms, tunable via `CODEX_REFRESH_SPACING_MS`, `"0"` to
opt out) while releasing a lone refresh immediately, so the reactive path adds
no latency.
diff --git a/docs/i18n/id/CHANGELOG.md b/docs/i18n/id/CHANGELOG.md
index efbd66686b..286a249523 100644
--- a/docs/i18n/id/CHANGELOG.md
+++ b/docs/i18n/id/CHANGELOG.md
@@ -4,6 +4,12 @@
---
+## [3.8.24] — TBD
+
+_See English CHANGELOG for v3.8.24 details._
+
+---
+
## [3.8.22] — TBD
_See English CHANGELOG for v3.8.22 details._
@@ -169,9 +175,9 @@ _Development cycle in progress._
refreshed siblings concurrently, so Auth0 revoked the whole token family
(`openai/codex#9648`) and every account but the last died with
`[403] `. The quota path now skips proactive refresh for
- rotating providers (`rotationGroupFor`) and reuses the current access_token,
+ rotating providers (`rotationGroupFor`) and reuses the current access*token,
deferring genuine expiry to the reactive, serialized 401 path. Defense in
- depth: `serializeRefresh` now leaves a settle gap between two _queued_ sibling
+ depth: `serializeRefresh` now leaves a settle gap between two \_queued* sibling
refreshes (default 2000 ms, tunable via `CODEX_REFRESH_SPACING_MS`, `"0"` to
opt out) while releasing a lone refresh immediately, so the reactive path adds
no latency.
diff --git a/docs/i18n/in/CHANGELOG.md b/docs/i18n/in/CHANGELOG.md
index f4b5f6aedd..d96b6ceabe 100644
--- a/docs/i18n/in/CHANGELOG.md
+++ b/docs/i18n/in/CHANGELOG.md
@@ -4,6 +4,12 @@
---
+## [3.8.24] — TBD
+
+_See English CHANGELOG for v3.8.24 details._
+
+---
+
## [3.8.22] — TBD
_See English CHANGELOG for v3.8.22 details._
@@ -169,9 +175,9 @@ _Development cycle in progress._
refreshed siblings concurrently, so Auth0 revoked the whole token family
(`openai/codex#9648`) and every account but the last died with
`[403] `. The quota path now skips proactive refresh for
- rotating providers (`rotationGroupFor`) and reuses the current access_token,
+ rotating providers (`rotationGroupFor`) and reuses the current access*token,
deferring genuine expiry to the reactive, serialized 401 path. Defense in
- depth: `serializeRefresh` now leaves a settle gap between two _queued_ sibling
+ depth: `serializeRefresh` now leaves a settle gap between two \_queued* sibling
refreshes (default 2000 ms, tunable via `CODEX_REFRESH_SPACING_MS`, `"0"` to
opt out) while releasing a lone refresh immediately, so the reactive path adds
no latency.
diff --git a/docs/i18n/it/CHANGELOG.md b/docs/i18n/it/CHANGELOG.md
index 4293c8264f..c1f63a816e 100644
--- a/docs/i18n/it/CHANGELOG.md
+++ b/docs/i18n/it/CHANGELOG.md
@@ -4,6 +4,12 @@
---
+## [3.8.24] — TBD
+
+_See English CHANGELOG for v3.8.24 details._
+
+---
+
## [3.8.22] — TBD
_See English CHANGELOG for v3.8.22 details._
@@ -169,9 +175,9 @@ _Development cycle in progress._
refreshed siblings concurrently, so Auth0 revoked the whole token family
(`openai/codex#9648`) and every account but the last died with
`[403] `. The quota path now skips proactive refresh for
- rotating providers (`rotationGroupFor`) and reuses the current access_token,
+ rotating providers (`rotationGroupFor`) and reuses the current access*token,
deferring genuine expiry to the reactive, serialized 401 path. Defense in
- depth: `serializeRefresh` now leaves a settle gap between two _queued_ sibling
+ depth: `serializeRefresh` now leaves a settle gap between two \_queued* sibling
refreshes (default 2000 ms, tunable via `CODEX_REFRESH_SPACING_MS`, `"0"` to
opt out) while releasing a lone refresh immediately, so the reactive path adds
no latency.
diff --git a/docs/i18n/ja/CHANGELOG.md b/docs/i18n/ja/CHANGELOG.md
index 84e747513d..e78f31bd76 100644
--- a/docs/i18n/ja/CHANGELOG.md
+++ b/docs/i18n/ja/CHANGELOG.md
@@ -4,6 +4,12 @@
---
+## [3.8.24] — TBD
+
+_See English CHANGELOG for v3.8.24 details._
+
+---
+
## [3.8.22] — TBD
_See English CHANGELOG for v3.8.22 details._
@@ -169,9 +175,9 @@ _Development cycle in progress._
refreshed siblings concurrently, so Auth0 revoked the whole token family
(`openai/codex#9648`) and every account but the last died with
`[403] `. The quota path now skips proactive refresh for
- rotating providers (`rotationGroupFor`) and reuses the current access_token,
+ rotating providers (`rotationGroupFor`) and reuses the current access*token,
deferring genuine expiry to the reactive, serialized 401 path. Defense in
- depth: `serializeRefresh` now leaves a settle gap between two _queued_ sibling
+ depth: `serializeRefresh` now leaves a settle gap between two \_queued* sibling
refreshes (default 2000 ms, tunable via `CODEX_REFRESH_SPACING_MS`, `"0"` to
opt out) while releasing a lone refresh immediately, so the reactive path adds
no latency.
diff --git a/docs/i18n/ko/CHANGELOG.md b/docs/i18n/ko/CHANGELOG.md
index 7e05fe336c..676d8800c4 100644
--- a/docs/i18n/ko/CHANGELOG.md
+++ b/docs/i18n/ko/CHANGELOG.md
@@ -4,6 +4,12 @@
---
+## [3.8.24] — TBD
+
+_See English CHANGELOG for v3.8.24 details._
+
+---
+
## [3.8.22] — TBD
_See English CHANGELOG for v3.8.22 details._
@@ -169,9 +175,9 @@ _Development cycle in progress._
refreshed siblings concurrently, so Auth0 revoked the whole token family
(`openai/codex#9648`) and every account but the last died with
`[403] `. The quota path now skips proactive refresh for
- rotating providers (`rotationGroupFor`) and reuses the current access_token,
+ rotating providers (`rotationGroupFor`) and reuses the current access*token,
deferring genuine expiry to the reactive, serialized 401 path. Defense in
- depth: `serializeRefresh` now leaves a settle gap between two _queued_ sibling
+ depth: `serializeRefresh` now leaves a settle gap between two \_queued* sibling
refreshes (default 2000 ms, tunable via `CODEX_REFRESH_SPACING_MS`, `"0"` to
opt out) while releasing a lone refresh immediately, so the reactive path adds
no latency.
diff --git a/docs/i18n/mr/CHANGELOG.md b/docs/i18n/mr/CHANGELOG.md
index e6ea8faf0e..aecd70d7a0 100644
--- a/docs/i18n/mr/CHANGELOG.md
+++ b/docs/i18n/mr/CHANGELOG.md
@@ -4,6 +4,12 @@
---
+## [3.8.24] — TBD
+
+_See English CHANGELOG for v3.8.24 details._
+
+---
+
## [3.8.22] — TBD
_See English CHANGELOG for v3.8.22 details._
@@ -169,9 +175,9 @@ _Development cycle in progress._
refreshed siblings concurrently, so Auth0 revoked the whole token family
(`openai/codex#9648`) and every account but the last died with
`[403] `. The quota path now skips proactive refresh for
- rotating providers (`rotationGroupFor`) and reuses the current access_token,
+ rotating providers (`rotationGroupFor`) and reuses the current access*token,
deferring genuine expiry to the reactive, serialized 401 path. Defense in
- depth: `serializeRefresh` now leaves a settle gap between two _queued_ sibling
+ depth: `serializeRefresh` now leaves a settle gap between two \_queued* sibling
refreshes (default 2000 ms, tunable via `CODEX_REFRESH_SPACING_MS`, `"0"` to
opt out) while releasing a lone refresh immediately, so the reactive path adds
no latency.
diff --git a/docs/i18n/ms/CHANGELOG.md b/docs/i18n/ms/CHANGELOG.md
index 12b6491553..5711170380 100644
--- a/docs/i18n/ms/CHANGELOG.md
+++ b/docs/i18n/ms/CHANGELOG.md
@@ -4,6 +4,12 @@
---
+## [3.8.24] — TBD
+
+_See English CHANGELOG for v3.8.24 details._
+
+---
+
## [3.8.22] — TBD
_See English CHANGELOG for v3.8.22 details._
@@ -169,9 +175,9 @@ _Development cycle in progress._
refreshed siblings concurrently, so Auth0 revoked the whole token family
(`openai/codex#9648`) and every account but the last died with
`[403] `. The quota path now skips proactive refresh for
- rotating providers (`rotationGroupFor`) and reuses the current access_token,
+ rotating providers (`rotationGroupFor`) and reuses the current access*token,
deferring genuine expiry to the reactive, serialized 401 path. Defense in
- depth: `serializeRefresh` now leaves a settle gap between two _queued_ sibling
+ depth: `serializeRefresh` now leaves a settle gap between two \_queued* sibling
refreshes (default 2000 ms, tunable via `CODEX_REFRESH_SPACING_MS`, `"0"` to
opt out) while releasing a lone refresh immediately, so the reactive path adds
no latency.
diff --git a/docs/i18n/nl/CHANGELOG.md b/docs/i18n/nl/CHANGELOG.md
index 87ee99cd32..fb51eca85b 100644
--- a/docs/i18n/nl/CHANGELOG.md
+++ b/docs/i18n/nl/CHANGELOG.md
@@ -4,6 +4,12 @@
---
+## [3.8.24] — TBD
+
+_See English CHANGELOG for v3.8.24 details._
+
+---
+
## [3.8.22] — TBD
_See English CHANGELOG for v3.8.22 details._
@@ -169,9 +175,9 @@ _Development cycle in progress._
refreshed siblings concurrently, so Auth0 revoked the whole token family
(`openai/codex#9648`) and every account but the last died with
`[403] `. The quota path now skips proactive refresh for
- rotating providers (`rotationGroupFor`) and reuses the current access_token,
+ rotating providers (`rotationGroupFor`) and reuses the current access*token,
deferring genuine expiry to the reactive, serialized 401 path. Defense in
- depth: `serializeRefresh` now leaves a settle gap between two _queued_ sibling
+ depth: `serializeRefresh` now leaves a settle gap between two \_queued* sibling
refreshes (default 2000 ms, tunable via `CODEX_REFRESH_SPACING_MS`, `"0"` to
opt out) while releasing a lone refresh immediately, so the reactive path adds
no latency.
diff --git a/docs/i18n/no/CHANGELOG.md b/docs/i18n/no/CHANGELOG.md
index 48336b783e..4ce01fc5bb 100644
--- a/docs/i18n/no/CHANGELOG.md
+++ b/docs/i18n/no/CHANGELOG.md
@@ -4,6 +4,12 @@
---
+## [3.8.24] — TBD
+
+_See English CHANGELOG for v3.8.24 details._
+
+---
+
## [3.8.22] — TBD
_See English CHANGELOG for v3.8.22 details._
@@ -169,9 +175,9 @@ _Development cycle in progress._
refreshed siblings concurrently, so Auth0 revoked the whole token family
(`openai/codex#9648`) and every account but the last died with
`[403] `. The quota path now skips proactive refresh for
- rotating providers (`rotationGroupFor`) and reuses the current access_token,
+ rotating providers (`rotationGroupFor`) and reuses the current access*token,
deferring genuine expiry to the reactive, serialized 401 path. Defense in
- depth: `serializeRefresh` now leaves a settle gap between two _queued_ sibling
+ depth: `serializeRefresh` now leaves a settle gap between two \_queued* sibling
refreshes (default 2000 ms, tunable via `CODEX_REFRESH_SPACING_MS`, `"0"` to
opt out) while releasing a lone refresh immediately, so the reactive path adds
no latency.
diff --git a/docs/i18n/phi/CHANGELOG.md b/docs/i18n/phi/CHANGELOG.md
index b0e72d3e66..b0a4da238f 100644
--- a/docs/i18n/phi/CHANGELOG.md
+++ b/docs/i18n/phi/CHANGELOG.md
@@ -4,6 +4,12 @@
---
+## [3.8.24] — TBD
+
+_See English CHANGELOG for v3.8.24 details._
+
+---
+
## [3.8.22] — TBD
_See English CHANGELOG for v3.8.22 details._
@@ -169,9 +175,9 @@ _Development cycle in progress._
refreshed siblings concurrently, so Auth0 revoked the whole token family
(`openai/codex#9648`) and every account but the last died with
`[403] `. The quota path now skips proactive refresh for
- rotating providers (`rotationGroupFor`) and reuses the current access_token,
+ rotating providers (`rotationGroupFor`) and reuses the current access*token,
deferring genuine expiry to the reactive, serialized 401 path. Defense in
- depth: `serializeRefresh` now leaves a settle gap between two _queued_ sibling
+ depth: `serializeRefresh` now leaves a settle gap between two \_queued* sibling
refreshes (default 2000 ms, tunable via `CODEX_REFRESH_SPACING_MS`, `"0"` to
opt out) while releasing a lone refresh immediately, so the reactive path adds
no latency.
diff --git a/docs/i18n/pl/CHANGELOG.md b/docs/i18n/pl/CHANGELOG.md
index 985f01dfc0..ec27815482 100644
--- a/docs/i18n/pl/CHANGELOG.md
+++ b/docs/i18n/pl/CHANGELOG.md
@@ -4,6 +4,12 @@
---
+## [3.8.24] — TBD
+
+_See English CHANGELOG for v3.8.24 details._
+
+---
+
## [3.8.22] — TBD
_See English CHANGELOG for v3.8.22 details._
@@ -169,9 +175,9 @@ _Development cycle in progress._
refreshed siblings concurrently, so Auth0 revoked the whole token family
(`openai/codex#9648`) and every account but the last died with
`[403] `. The quota path now skips proactive refresh for
- rotating providers (`rotationGroupFor`) and reuses the current access_token,
+ rotating providers (`rotationGroupFor`) and reuses the current access*token,
deferring genuine expiry to the reactive, serialized 401 path. Defense in
- depth: `serializeRefresh` now leaves a settle gap between two _queued_ sibling
+ depth: `serializeRefresh` now leaves a settle gap between two \_queued* sibling
refreshes (default 2000 ms, tunable via `CODEX_REFRESH_SPACING_MS`, `"0"` to
opt out) while releasing a lone refresh immediately, so the reactive path adds
no latency.
diff --git a/docs/i18n/pt-BR/CHANGELOG.md b/docs/i18n/pt-BR/CHANGELOG.md
index fc2a5cadbe..5b369c2fb4 100644
--- a/docs/i18n/pt-BR/CHANGELOG.md
+++ b/docs/i18n/pt-BR/CHANGELOG.md
@@ -4,6 +4,12 @@
---
+## [3.8.24] — TBD
+
+_See English CHANGELOG for v3.8.24 details._
+
+---
+
## [3.8.22] — TBD
_See English CHANGELOG for v3.8.22 details._
@@ -169,9 +175,9 @@ _Development cycle in progress._
refreshed siblings concurrently, so Auth0 revoked the whole token family
(`openai/codex#9648`) and every account but the last died with
`[403] `. The quota path now skips proactive refresh for
- rotating providers (`rotationGroupFor`) and reuses the current access_token,
+ rotating providers (`rotationGroupFor`) and reuses the current access*token,
deferring genuine expiry to the reactive, serialized 401 path. Defense in
- depth: `serializeRefresh` now leaves a settle gap between two _queued_ sibling
+ depth: `serializeRefresh` now leaves a settle gap between two \_queued* sibling
refreshes (default 2000 ms, tunable via `CODEX_REFRESH_SPACING_MS`, `"0"` to
opt out) while releasing a lone refresh immediately, so the reactive path adds
no latency.
diff --git a/docs/i18n/pt/CHANGELOG.md b/docs/i18n/pt/CHANGELOG.md
index c1e556f9c7..becfc6b895 100644
--- a/docs/i18n/pt/CHANGELOG.md
+++ b/docs/i18n/pt/CHANGELOG.md
@@ -4,6 +4,12 @@
---
+## [3.8.24] — TBD
+
+_See English CHANGELOG for v3.8.24 details._
+
+---
+
## [3.8.22] — TBD
_See English CHANGELOG for v3.8.22 details._
@@ -169,9 +175,9 @@ _Development cycle in progress._
refreshed siblings concurrently, so Auth0 revoked the whole token family
(`openai/codex#9648`) and every account but the last died with
`[403] `. The quota path now skips proactive refresh for
- rotating providers (`rotationGroupFor`) and reuses the current access_token,
+ rotating providers (`rotationGroupFor`) and reuses the current access*token,
deferring genuine expiry to the reactive, serialized 401 path. Defense in
- depth: `serializeRefresh` now leaves a settle gap between two _queued_ sibling
+ depth: `serializeRefresh` now leaves a settle gap between two \_queued* sibling
refreshes (default 2000 ms, tunable via `CODEX_REFRESH_SPACING_MS`, `"0"` to
opt out) while releasing a lone refresh immediately, so the reactive path adds
no latency.
diff --git a/docs/i18n/ro/CHANGELOG.md b/docs/i18n/ro/CHANGELOG.md
index 7515cb8c77..4c38f59743 100644
--- a/docs/i18n/ro/CHANGELOG.md
+++ b/docs/i18n/ro/CHANGELOG.md
@@ -4,6 +4,12 @@
---
+## [3.8.24] — TBD
+
+_See English CHANGELOG for v3.8.24 details._
+
+---
+
## [3.8.22] — TBD
_See English CHANGELOG for v3.8.22 details._
@@ -169,9 +175,9 @@ _Development cycle in progress._
refreshed siblings concurrently, so Auth0 revoked the whole token family
(`openai/codex#9648`) and every account but the last died with
`[403] `. The quota path now skips proactive refresh for
- rotating providers (`rotationGroupFor`) and reuses the current access_token,
+ rotating providers (`rotationGroupFor`) and reuses the current access*token,
deferring genuine expiry to the reactive, serialized 401 path. Defense in
- depth: `serializeRefresh` now leaves a settle gap between two _queued_ sibling
+ depth: `serializeRefresh` now leaves a settle gap between two \_queued* sibling
refreshes (default 2000 ms, tunable via `CODEX_REFRESH_SPACING_MS`, `"0"` to
opt out) while releasing a lone refresh immediately, so the reactive path adds
no latency.
diff --git a/docs/i18n/ru/CHANGELOG.md b/docs/i18n/ru/CHANGELOG.md
index 8602274b67..64c1dfc2c3 100644
--- a/docs/i18n/ru/CHANGELOG.md
+++ b/docs/i18n/ru/CHANGELOG.md
@@ -4,6 +4,12 @@
---
+## [3.8.24] — TBD
+
+_See English CHANGELOG for v3.8.24 details._
+
+---
+
## [3.8.22] — TBD
_See English CHANGELOG for v3.8.22 details._
@@ -169,9 +175,9 @@ _Development cycle in progress._
refreshed siblings concurrently, so Auth0 revoked the whole token family
(`openai/codex#9648`) and every account but the last died with
`[403] `. The quota path now skips proactive refresh for
- rotating providers (`rotationGroupFor`) and reuses the current access_token,
+ rotating providers (`rotationGroupFor`) and reuses the current access*token,
deferring genuine expiry to the reactive, serialized 401 path. Defense in
- depth: `serializeRefresh` now leaves a settle gap between two _queued_ sibling
+ depth: `serializeRefresh` now leaves a settle gap between two \_queued* sibling
refreshes (default 2000 ms, tunable via `CODEX_REFRESH_SPACING_MS`, `"0"` to
opt out) while releasing a lone refresh immediately, so the reactive path adds
no latency.
diff --git a/docs/i18n/sk/CHANGELOG.md b/docs/i18n/sk/CHANGELOG.md
index e03ef3ee6d..4444fd7fde 100644
--- a/docs/i18n/sk/CHANGELOG.md
+++ b/docs/i18n/sk/CHANGELOG.md
@@ -4,6 +4,12 @@
---
+## [3.8.24] — TBD
+
+_See English CHANGELOG for v3.8.24 details._
+
+---
+
## [3.8.22] — TBD
_See English CHANGELOG for v3.8.22 details._
@@ -169,9 +175,9 @@ _Development cycle in progress._
refreshed siblings concurrently, so Auth0 revoked the whole token family
(`openai/codex#9648`) and every account but the last died with
`[403] `. The quota path now skips proactive refresh for
- rotating providers (`rotationGroupFor`) and reuses the current access_token,
+ rotating providers (`rotationGroupFor`) and reuses the current access*token,
deferring genuine expiry to the reactive, serialized 401 path. Defense in
- depth: `serializeRefresh` now leaves a settle gap between two _queued_ sibling
+ depth: `serializeRefresh` now leaves a settle gap between two \_queued* sibling
refreshes (default 2000 ms, tunable via `CODEX_REFRESH_SPACING_MS`, `"0"` to
opt out) while releasing a lone refresh immediately, so the reactive path adds
no latency.
diff --git a/docs/i18n/sv/CHANGELOG.md b/docs/i18n/sv/CHANGELOG.md
index 4467eb15ed..0298620e40 100644
--- a/docs/i18n/sv/CHANGELOG.md
+++ b/docs/i18n/sv/CHANGELOG.md
@@ -4,6 +4,12 @@
---
+## [3.8.24] — TBD
+
+_See English CHANGELOG for v3.8.24 details._
+
+---
+
## [3.8.22] — TBD
_See English CHANGELOG for v3.8.22 details._
@@ -169,9 +175,9 @@ _Development cycle in progress._
refreshed siblings concurrently, so Auth0 revoked the whole token family
(`openai/codex#9648`) and every account but the last died with
`[403] `. The quota path now skips proactive refresh for
- rotating providers (`rotationGroupFor`) and reuses the current access_token,
+ rotating providers (`rotationGroupFor`) and reuses the current access*token,
deferring genuine expiry to the reactive, serialized 401 path. Defense in
- depth: `serializeRefresh` now leaves a settle gap between two _queued_ sibling
+ depth: `serializeRefresh` now leaves a settle gap between two \_queued* sibling
refreshes (default 2000 ms, tunable via `CODEX_REFRESH_SPACING_MS`, `"0"` to
opt out) while releasing a lone refresh immediately, so the reactive path adds
no latency.
diff --git a/docs/i18n/sw/CHANGELOG.md b/docs/i18n/sw/CHANGELOG.md
index c85af1c131..f8555fff1b 100644
--- a/docs/i18n/sw/CHANGELOG.md
+++ b/docs/i18n/sw/CHANGELOG.md
@@ -4,6 +4,12 @@
---
+## [3.8.24] — TBD
+
+_See English CHANGELOG for v3.8.24 details._
+
+---
+
## [3.8.22] — TBD
_See English CHANGELOG for v3.8.22 details._
@@ -169,9 +175,9 @@ _Development cycle in progress._
refreshed siblings concurrently, so Auth0 revoked the whole token family
(`openai/codex#9648`) and every account but the last died with
`[403] `. The quota path now skips proactive refresh for
- rotating providers (`rotationGroupFor`) and reuses the current access_token,
+ rotating providers (`rotationGroupFor`) and reuses the current access*token,
deferring genuine expiry to the reactive, serialized 401 path. Defense in
- depth: `serializeRefresh` now leaves a settle gap between two _queued_ sibling
+ depth: `serializeRefresh` now leaves a settle gap between two \_queued* sibling
refreshes (default 2000 ms, tunable via `CODEX_REFRESH_SPACING_MS`, `"0"` to
opt out) while releasing a lone refresh immediately, so the reactive path adds
no latency.
diff --git a/docs/i18n/ta/CHANGELOG.md b/docs/i18n/ta/CHANGELOG.md
index 30f309ffd8..e1d0296193 100644
--- a/docs/i18n/ta/CHANGELOG.md
+++ b/docs/i18n/ta/CHANGELOG.md
@@ -4,6 +4,12 @@
---
+## [3.8.24] — TBD
+
+_See English CHANGELOG for v3.8.24 details._
+
+---
+
## [3.8.22] — TBD
_See English CHANGELOG for v3.8.22 details._
@@ -169,9 +175,9 @@ _Development cycle in progress._
refreshed siblings concurrently, so Auth0 revoked the whole token family
(`openai/codex#9648`) and every account but the last died with
`[403] `. The quota path now skips proactive refresh for
- rotating providers (`rotationGroupFor`) and reuses the current access_token,
+ rotating providers (`rotationGroupFor`) and reuses the current access*token,
deferring genuine expiry to the reactive, serialized 401 path. Defense in
- depth: `serializeRefresh` now leaves a settle gap between two _queued_ sibling
+ depth: `serializeRefresh` now leaves a settle gap between two \_queued* sibling
refreshes (default 2000 ms, tunable via `CODEX_REFRESH_SPACING_MS`, `"0"` to
opt out) while releasing a lone refresh immediately, so the reactive path adds
no latency.
diff --git a/docs/i18n/te/CHANGELOG.md b/docs/i18n/te/CHANGELOG.md
index 09ea46c7b1..3368b46a57 100644
--- a/docs/i18n/te/CHANGELOG.md
+++ b/docs/i18n/te/CHANGELOG.md
@@ -4,6 +4,12 @@
---
+## [3.8.24] — TBD
+
+_See English CHANGELOG for v3.8.24 details._
+
+---
+
## [3.8.22] — TBD
_See English CHANGELOG for v3.8.22 details._
@@ -169,9 +175,9 @@ _Development cycle in progress._
refreshed siblings concurrently, so Auth0 revoked the whole token family
(`openai/codex#9648`) and every account but the last died with
`[403] `. The quota path now skips proactive refresh for
- rotating providers (`rotationGroupFor`) and reuses the current access_token,
+ rotating providers (`rotationGroupFor`) and reuses the current access*token,
deferring genuine expiry to the reactive, serialized 401 path. Defense in
- depth: `serializeRefresh` now leaves a settle gap between two _queued_ sibling
+ depth: `serializeRefresh` now leaves a settle gap between two \_queued* sibling
refreshes (default 2000 ms, tunable via `CODEX_REFRESH_SPACING_MS`, `"0"` to
opt out) while releasing a lone refresh immediately, so the reactive path adds
no latency.
diff --git a/docs/i18n/th/CHANGELOG.md b/docs/i18n/th/CHANGELOG.md
index 9d785a7add..34e57315c8 100644
--- a/docs/i18n/th/CHANGELOG.md
+++ b/docs/i18n/th/CHANGELOG.md
@@ -4,6 +4,12 @@
---
+## [3.8.24] — TBD
+
+_See English CHANGELOG for v3.8.24 details._
+
+---
+
## [3.8.22] — TBD
_See English CHANGELOG for v3.8.22 details._
@@ -169,9 +175,9 @@ _Development cycle in progress._
refreshed siblings concurrently, so Auth0 revoked the whole token family
(`openai/codex#9648`) and every account but the last died with
`[403] `. The quota path now skips proactive refresh for
- rotating providers (`rotationGroupFor`) and reuses the current access_token,
+ rotating providers (`rotationGroupFor`) and reuses the current access*token,
deferring genuine expiry to the reactive, serialized 401 path. Defense in
- depth: `serializeRefresh` now leaves a settle gap between two _queued_ sibling
+ depth: `serializeRefresh` now leaves a settle gap between two \_queued* sibling
refreshes (default 2000 ms, tunable via `CODEX_REFRESH_SPACING_MS`, `"0"` to
opt out) while releasing a lone refresh immediately, so the reactive path adds
no latency.
diff --git a/docs/i18n/tr/CHANGELOG.md b/docs/i18n/tr/CHANGELOG.md
index f59f8e1a7e..755a2d7508 100644
--- a/docs/i18n/tr/CHANGELOG.md
+++ b/docs/i18n/tr/CHANGELOG.md
@@ -4,6 +4,12 @@
---
+## [3.8.24] — TBD
+
+_See English CHANGELOG for v3.8.24 details._
+
+---
+
## [3.8.22] — TBD
_See English CHANGELOG for v3.8.22 details._
@@ -169,9 +175,9 @@ _Development cycle in progress._
refreshed siblings concurrently, so Auth0 revoked the whole token family
(`openai/codex#9648`) and every account but the last died with
`[403] `. The quota path now skips proactive refresh for
- rotating providers (`rotationGroupFor`) and reuses the current access_token,
+ rotating providers (`rotationGroupFor`) and reuses the current access*token,
deferring genuine expiry to the reactive, serialized 401 path. Defense in
- depth: `serializeRefresh` now leaves a settle gap between two _queued_ sibling
+ depth: `serializeRefresh` now leaves a settle gap between two \_queued* sibling
refreshes (default 2000 ms, tunable via `CODEX_REFRESH_SPACING_MS`, `"0"` to
opt out) while releasing a lone refresh immediately, so the reactive path adds
no latency.
diff --git a/docs/i18n/uk-UA/CHANGELOG.md b/docs/i18n/uk-UA/CHANGELOG.md
index 1583ff85f7..fedda0bbdd 100644
--- a/docs/i18n/uk-UA/CHANGELOG.md
+++ b/docs/i18n/uk-UA/CHANGELOG.md
@@ -4,6 +4,12 @@
---
+## [3.8.24] — TBD
+
+_See English CHANGELOG for v3.8.24 details._
+
+---
+
## [3.8.22] — TBD
_See English CHANGELOG for v3.8.22 details._
@@ -169,9 +175,9 @@ _Development cycle in progress._
refreshed siblings concurrently, so Auth0 revoked the whole token family
(`openai/codex#9648`) and every account but the last died with
`[403] `. The quota path now skips proactive refresh for
- rotating providers (`rotationGroupFor`) and reuses the current access_token,
+ rotating providers (`rotationGroupFor`) and reuses the current access*token,
deferring genuine expiry to the reactive, serialized 401 path. Defense in
- depth: `serializeRefresh` now leaves a settle gap between two _queued_ sibling
+ depth: `serializeRefresh` now leaves a settle gap between two \_queued* sibling
refreshes (default 2000 ms, tunable via `CODEX_REFRESH_SPACING_MS`, `"0"` to
opt out) while releasing a lone refresh immediately, so the reactive path adds
no latency.
diff --git a/docs/i18n/ur/CHANGELOG.md b/docs/i18n/ur/CHANGELOG.md
index 31a19aecf5..64ffa0d06c 100644
--- a/docs/i18n/ur/CHANGELOG.md
+++ b/docs/i18n/ur/CHANGELOG.md
@@ -4,6 +4,12 @@
---
+## [3.8.24] — TBD
+
+_See English CHANGELOG for v3.8.24 details._
+
+---
+
## [3.8.22] — TBD
_See English CHANGELOG for v3.8.22 details._
@@ -169,9 +175,9 @@ _Development cycle in progress._
refreshed siblings concurrently, so Auth0 revoked the whole token family
(`openai/codex#9648`) and every account but the last died with
`[403] `. The quota path now skips proactive refresh for
- rotating providers (`rotationGroupFor`) and reuses the current access_token,
+ rotating providers (`rotationGroupFor`) and reuses the current access*token,
deferring genuine expiry to the reactive, serialized 401 path. Defense in
- depth: `serializeRefresh` now leaves a settle gap between two _queued_ sibling
+ depth: `serializeRefresh` now leaves a settle gap between two \_queued* sibling
refreshes (default 2000 ms, tunable via `CODEX_REFRESH_SPACING_MS`, `"0"` to
opt out) while releasing a lone refresh immediately, so the reactive path adds
no latency.
diff --git a/docs/i18n/vi/CHANGELOG.md b/docs/i18n/vi/CHANGELOG.md
index 739c7af600..6db1b0d984 100644
--- a/docs/i18n/vi/CHANGELOG.md
+++ b/docs/i18n/vi/CHANGELOG.md
@@ -4,6 +4,12 @@
---
+## [3.8.24] — TBD
+
+_See English CHANGELOG for v3.8.24 details._
+
+---
+
## [3.8.22] — TBD
_See English CHANGELOG for v3.8.22 details._
@@ -169,9 +175,9 @@ _Development cycle in progress._
refreshed siblings concurrently, so Auth0 revoked the whole token family
(`openai/codex#9648`) and every account but the last died with
`[403] `. The quota path now skips proactive refresh for
- rotating providers (`rotationGroupFor`) and reuses the current access_token,
+ rotating providers (`rotationGroupFor`) and reuses the current access*token,
deferring genuine expiry to the reactive, serialized 401 path. Defense in
- depth: `serializeRefresh` now leaves a settle gap between two _queued_ sibling
+ depth: `serializeRefresh` now leaves a settle gap between two \_queued* sibling
refreshes (default 2000 ms, tunable via `CODEX_REFRESH_SPACING_MS`, `"0"` to
opt out) while releasing a lone refresh immediately, so the reactive path adds
no latency.
diff --git a/docs/i18n/zh-CN/CHANGELOG.md b/docs/i18n/zh-CN/CHANGELOG.md
index 6cc1a5fbe3..e8a7a239af 100644
--- a/docs/i18n/zh-CN/CHANGELOG.md
+++ b/docs/i18n/zh-CN/CHANGELOG.md
@@ -4,6 +4,12 @@
---
+## [3.8.24] — TBD
+
+_See English CHANGELOG for v3.8.24 details._
+
+---
+
## [3.8.22] — TBD
_See English CHANGELOG for v3.8.22 details._
@@ -169,9 +175,9 @@ _Development cycle in progress._
refreshed siblings concurrently, so Auth0 revoked the whole token family
(`openai/codex#9648`) and every account but the last died with
`[403] `. The quota path now skips proactive refresh for
- rotating providers (`rotationGroupFor`) and reuses the current access_token,
+ rotating providers (`rotationGroupFor`) and reuses the current access*token,
deferring genuine expiry to the reactive, serialized 401 path. Defense in
- depth: `serializeRefresh` now leaves a settle gap between two _queued_ sibling
+ depth: `serializeRefresh` now leaves a settle gap between two \_queued* sibling
refreshes (default 2000 ms, tunable via `CODEX_REFRESH_SPACING_MS`, `"0"` to
opt out) while releasing a lone refresh immediately, so the reactive path adds
no latency.
diff --git a/docs/openspec/changes/self-service-api-key-usage/proposal.md b/docs/openspec/changes/self-service-api-key-usage/proposal.md
deleted file mode 100644
index 1c9a36e164..0000000000
--- a/docs/openspec/changes/self-service-api-key-usage/proposal.md
+++ /dev/null
@@ -1,65 +0,0 @@
-# Change: Self-Service API Key Usage and Quota Visibility
-
-## Summary
-
-Add a client-facing self-service status endpoint and dashboard controls that let each OmniRoute API key inspect its own USD usage, token usage, and percent used against its existing USD budget configuration. Optionally expose shared upstream account quota when an operator grants a dedicated per-key scope.
-
-## Motivation
-
-OmniRoute can route multiple delegated API keys through one upstream coding account. Operators need per-key accountability without giving every delegated key management access. Existing management usage APIs are too broad for delegated clients because they can expose other keys and operational state.
-
-This change creates a narrow own-key API and UI controls:
-
-- Own cost and token usage are visible by default for ordinary new keys.
-- Shared account quota remains opt-in because it is account-level and sensitive.
-- USD budgets remain the enforcement mechanism; token totals are reporting only.
-
-## Scope
-
-In scope:
-
-- New `GET /api/v1/me/status` endpoint authenticated by normal Bearer API key.
-- New self-service API key scopes: `self:usage` and `self:account-quota`.
-- Per-key cost and token aggregation for the calling key.
-- Optional normalized provider account quotas for all provider-limit connections the key may use.
-- API Manager create/edit controls for visibility scopes.
-- Reuse the existing budget configuration surface for USD limits.
-- i18n message keys for all new dashboard text.
-- Tests and docs for the new behavior.
-
-Out of scope:
-
-- Token quota enforcement.
-- Cross-key reporting through the self-service endpoint.
-- Changing management usage APIs.
-- Changing provider routing or quota preflight behavior.
-- Raw upstream quota payload exposure.
-- A second budget editor inside key creation or permissions dialogs.
-
-## Compatibility
-
-Existing keys should continue to work. A migration or first-start normalization step should backfill `self:usage` onto existing ordinary keys so they receive the same default own-usage visibility as newly created keys. Existing keys must not receive shared account quota visibility unless `self:account-quota` is explicitly granted.
-
-The new scopes must not grant management access. Only `manage` and `admin` remain management-grade.
-
-## Risks
-
-- Scope editing in the current dashboard can collapse scopes to only management access; implementation must preserve unrelated scopes.
-- Shared account quota can reveal account exhaustion; it must remain disabled by default.
-- Unrestricted keys can use all active provider connections, so account quota visibility enumerates all active provider-limit connections when explicitly permitted.
-- Backfill must be idempotent so upgrades do not repeatedly rewrite API keys or re-enable a permission an operator later disabled.
-- New UI text can regress non-English dashboards if translation keys are not added consistently.
-- The current scope validation cap is 16 entries; adding self-service scopes may require raising that cap.
-- The current `/api/usage/budget` route relies on route-level authz rather than handler-level `requireManagementAuth()`, so the PR should harden it or explicitly test the proxy guard.
-
-## Rollout
-
-1. Add constants, validation, and helper tests.
-2. Raise or otherwise adapt scope validation limits.
-3. Add idempotent existing-key backfill for `self:usage`.
-4. Harden `/api/usage/budget` with handler-level management auth or add explicit proxy-guard tests.
-5. Add self-service status endpoint.
-6. Add create/edit dashboard controls.
-7. Add i18n message keys for dashboard text.
-8. Add API/reference docs.
-9. Verify against release branch used for upstream PR.
diff --git a/docs/openspec/changes/self-service-api-key-usage/specs/api-key-self-service-usage/spec.md b/docs/openspec/changes/self-service-api-key-usage/specs/api-key-self-service-usage/spec.md
deleted file mode 100644
index f704c08130..0000000000
--- a/docs/openspec/changes/self-service-api-key-usage/specs/api-key-self-service-usage/spec.md
+++ /dev/null
@@ -1,216 +0,0 @@
-# Specification: API Key Self-Service Usage
-
-## ADDED Requirements
-
-### Requirement: Self-service status endpoint
-
-OmniRoute SHALL provide `GET /api/v1/me/status` for a valid Bearer API key to retrieve status for that same API key.
-
-#### Scenario: Valid key reads own status
-
-- GIVEN a valid API key with own-usage visibility
-- WHEN it calls `GET /api/v1/me/status`
-- THEN the response status SHALL be `200`
-- AND the response SHALL include the API key id and name
-- AND the response SHALL include cost usage for that key
-- AND the response SHALL include token usage for that key
-
-#### Scenario: Invalid key is rejected
-
-- GIVEN a missing or invalid Bearer token
-- WHEN the caller calls `GET /api/v1/me/status`
-- THEN the response status SHALL be `401`
-
-#### Scenario: Anonymous client API mode does not bypass self-service auth
-
-- GIVEN global client API auth allows anonymous local traffic
-- WHEN a caller without a Bearer API key calls `GET /api/v1/me/status`
-- THEN the response status SHALL be `401`
-
-#### Scenario: Environment management key is not a self-service key
-
-- GIVEN the deployment has an environment management key
-- WHEN that key calls `GET /api/v1/me/status`
-- THEN the response SHALL NOT expose delegated API key usage
-
-### Requirement: Own-key isolation
-
-The self-service endpoint SHALL derive the API key id from the authenticated Bearer key and SHALL NOT accept caller-supplied key ids for lookup.
-
-#### Scenario: Caller tries to query another key
-
-- GIVEN API key A and API key B both have usage
-- WHEN API key A calls `GET /api/v1/me/status?apiKeyId=`
-- THEN the response SHALL contain only API key A identity and usage
-- AND the response SHALL NOT contain API key B usage
-
-### Requirement: USD budget status
-
-The self-service endpoint SHALL report per-key USD budget usage using the existing budget system.
-
-#### Scenario: Key has an active monthly budget
-
-- GIVEN an API key has a monthly USD budget of `50`
-- AND the key has current-period cost of `12.50`
-- WHEN the key calls the self-service status endpoint
-- THEN `usage.cost.limitUsd` SHALL be `50`
-- AND `usage.cost.usedUsd` SHALL be `12.50`
-- AND `usage.cost.usedPercent` SHALL be `25`
-- AND `usage.cost.remainingUsd` SHALL be `37.50`
-
-#### Scenario: Key has no budget
-
-- GIVEN an API key has no configured budget
-- WHEN the key calls the self-service status endpoint
-- THEN `usage.cost.limitUsd` SHALL be `null`
-- AND `usage.cost.usedPercent` SHALL be `null`
-- AND cost and token totals SHALL still be returned for the default display period
-
-### Requirement: Token usage reporting
-
-The self-service endpoint SHALL report token totals from `usage_history` for the authenticated API key and selected reporting period.
-
-#### Scenario: Token totals include all tracked categories
-
-- GIVEN an API key has usage rows with input, output, cache read, cache creation, and reasoning tokens
-- WHEN the key calls the self-service status endpoint
-- THEN the response SHALL include each token category total
-- AND `totalTokens` SHALL include all reported token categories
-
-### Requirement: Self-service scopes
-
-OmniRoute SHALL support `self:usage` and `self:account-quota` API key scopes. These scopes SHALL NOT grant management API access.
-
-#### Scenario: Self-service scope is not management
-
-- GIVEN an API key has `self:usage`
-- AND it does not have `manage` or `admin`
-- WHEN it calls a management usage endpoint
-- THEN the response SHALL be forbidden
-
-#### Scenario: New key defaults
-
-- GIVEN an operator opens the create API key UI
-- THEN own cost and token usage visibility SHALL be enabled by default
-- AND shared account quota visibility SHALL be disabled by default
-
-#### Scenario: Existing keys receive own-usage visibility on upgrade
-
-- GIVEN an ordinary API key existed before this feature
-- AND it does not have `self:usage`
-- WHEN the compatibility migration or startup normalization runs
-- THEN the API key SHALL have `self:usage`
-- AND the API key SHALL NOT have `self:account-quota`
-
-#### Scenario: Key without own-usage scope is denied
-
-- GIVEN a valid API key does not have `self:usage`
-- WHEN it calls `GET /api/v1/me/status`
-- THEN the response status SHALL be `403`
-
-### Requirement: Shared account quota permission
-
-The self-service endpoint SHALL include shared account quota only when the authenticated key has `self:account-quota`.
-
-#### Scenario: Account quota hidden by default
-
-- GIVEN a valid API key has own-usage visibility
-- AND it does not have `self:account-quota`
-- WHEN it calls the self-service endpoint
-- THEN the response SHALL NOT include shared account quota details
-
-#### Scenario: Allowed provider quotas shown with explicit permission
-
-- GIVEN a valid API key has `self:account-quota`
-- AND it is allowed to use Codex and Claude provider-limit connections
-- AND quota data is available for both connections
-- WHEN it calls the self-service endpoint
-- THEN the response SHALL include an `accountQuotas` entry for each allowed provider-limit connection
-- AND each window SHALL include used percentage, remaining percentage, and reset timestamp when known
-
-#### Scenario: Single connection compatibility field
-
-- GIVEN a valid API key has `self:account-quota`
-- AND it is allowed to use exactly one provider-limit connection
-- WHEN it calls the self-service endpoint
-- THEN the response SHALL include exactly one `accountQuotas` entry
-- AND the response SHALL also include `accountQuota` with the same entry for backwards compatibility
-
-#### Scenario: Unrestricted connections include active provider-limit connections
-
-- GIVEN a valid API key has `self:account-quota`
-- AND its `allowedConnections` list is empty, meaning all connections are allowed
-- WHEN it calls the self-service endpoint
-- THEN the response SHALL include `accountQuotas` entries for active provider-limit connections
-
-#### Scenario: Per-connection quota failure is isolated
-
-- GIVEN a valid API key has `self:account-quota`
-- AND it is allowed to use two provider-limit connections
-- AND one provider quota fetch fails
-- WHEN it calls the self-service endpoint
-- THEN the successful provider SHALL remain in `accountQuotas`
-- AND the failed provider SHALL be represented with `available: false` and `reason: "fetch_failed"`
-
-#### Scenario: Provider connection lookup failure is isolated
-
-- GIVEN a valid API key has `self:account-quota`
-- AND it is explicitly allowed to use two provider-limit connections
-- AND one provider connection lookup fails before quota fetching
-- WHEN it calls the self-service endpoint
-- THEN the successful provider SHALL remain in `accountQuotas`
-- AND the unresolved connection SHALL be represented with `available: false` and `reason: "connection_lookup_failed"`
-- AND the response SHALL still include the key's own cost and token usage
-
-### Requirement: Dashboard configuration
-
-The API Manager SHALL allow operators to configure self-service visibility and SHALL reuse the existing budget configuration surface for USD limits.
-
-#### Scenario: Edit preserves unrelated scopes
-
-- GIVEN an API key has scopes `["self:usage", "custom:scope"]`
-- WHEN an operator enables shared account quota in the permissions UI
-- THEN the saved scopes SHALL include `self:usage`
-- AND the saved scopes SHALL include `self:account-quota`
-- AND the saved scopes SHALL still include `custom:scope`
-
-#### Scenario: Budget editing remains in existing budget UI
-
-- GIVEN an operator wants to change a key's USD budget limit
-- WHEN they use the dashboard
-- THEN OmniRoute SHALL direct them to the existing budget configuration surface
-- AND the create-key dialog SHALL NOT introduce a second budget editor
-
-#### Scenario: No budget is displayed as not configured
-
-- GIVEN an API key has no configured budget
-- WHEN the API Manager shows self-service usage for that key
-- THEN the UI SHALL show usage and token totals
-- AND the budget limit, remaining amount, and percent SHALL be shown as not configured
-
-### Requirement: Dashboard internationalization
-
-All new API Manager text for self-service usage visibility, shared account quota visibility, and no-budget display SHALL use OmniRoute's existing i18n message system.
-
-#### Scenario: New UI strings use translation keys
-
-- GIVEN the API Manager renders the new self-service controls
-- THEN labels, descriptions, tooltips, empty states, and errors SHALL come from translation keys
-- AND no new user-visible dashboard text SHALL be hard-coded in the component
-
-#### Scenario: Locale files stay structurally compatible
-
-- GIVEN new API Manager translation keys are added
-- WHEN the translation consistency check runs
-- THEN supported locale message files SHALL have compatible key structure
-
-### Requirement: Existing budget management remains protected
-
-The existing `/api/usage/budget` management endpoint SHALL NOT become an own-key self-service data source.
-
-#### Scenario: Self-service key cannot read arbitrary budget endpoint
-
-- GIVEN an API key has `self:usage`
-- AND it does not have `manage` or `admin`
-- WHEN it calls `/api/usage/budget?apiKeyId=`
-- THEN the response SHALL be rejected by management auth
diff --git a/docs/openspec/changes/self-service-api-key-usage/tasks.md b/docs/openspec/changes/self-service-api-key-usage/tasks.md
deleted file mode 100644
index 21f0848897..0000000000
--- a/docs/openspec/changes/self-service-api-key-usage/tasks.md
+++ /dev/null
@@ -1,72 +0,0 @@
-# Tasks
-
-## 1. Scope and Validation
-
-- [ ] Add `self:usage` and `self:account-quota` constants outside management scopes.
-- [ ] Extend key creation validation to accept self-service scopes.
-- [ ] Raise or replace the current 16-scope validation cap so new scopes do not break existing custom/MCP-heavy keys.
-- [ ] Add an idempotent compatibility migration or startup normalization for existing keys.
-- [ ] Add tests proving self-service scopes do not satisfy management auth.
-
-## 2. Usage Aggregation
-
-- [ ] Add helper to derive self-service status from authenticated API key metadata.
-- [ ] Aggregate cost through existing `getCostSummary()` and `checkBudget()`.
-- [ ] Aggregate token totals from `usage_history` by `api_key_id` and period start.
-- [ ] Add tests for missing budget, configured budget, and token totals.
-
-## 3. Account Quota
-
-- [ ] Resolve account quota only when the key has `self:account-quota`.
-- [ ] Enumerate all explicit allowed connections, or all active connections when `allowedConnections` is empty.
-- [ ] Normalize quota windows for every provider-limit connection that returns quota data.
-- [ ] Preserve the legacy `accountQuota` field when exactly one quota entry is returned.
-- [ ] Add tests for no scope, one connection, multiple connections, unrestricted connections, unsupported provider, and fetch failure.
-
-## 4. API Endpoint
-
-- [ ] Add `GET /api/v1/me/status`.
-- [ ] Authenticate in the handler using a normal Bearer API key and derive the API key id from DB metadata.
-- [ ] Reject anonymous access even when global client API auth would allow anonymous local traffic.
-- [ ] Reject env-only management keys for this own-key endpoint.
-- [ ] Reject missing/invalid keys with `401`.
-- [ ] Reject keys without `self:usage` with `403` after compatibility backfill has run.
-- [ ] Ignore any caller-supplied `apiKeyId`.
-- [ ] Add route tests for isolation and response shape.
-
-## 5. Dashboard
-
-- [ ] Add create-key controls for own usage visibility and shared account quota visibility.
-- [ ] Add edit-permissions controls for self-service visibility.
-- [ ] Reuse the existing budget configuration surface for USD limit editing.
-- [ ] Preserve unrelated scopes when editing permissions.
-- [ ] Show per-key budget percent and token totals in the key details experience.
-- [ ] Show no-budget state as not configured while still showing usage.
-- [ ] Add UI tests for defaults and scope preservation.
-
-## 6. Internationalization
-
-- [ ] Add translation keys under the existing API Manager namespace for all new UI text.
-- [ ] Update default and generated locale message files according to the repo's i18n workflow.
-- [ ] Add or run a translation key consistency check.
-- [ ] Run `npm run i18n:sync-ui:dry`.
-- [ ] Run `npm run i18n:check-ui-coverage`.
-
-## 7. Budget Endpoint Hardening
-
-- [ ] Add handler-level management auth to `/api/usage/budget` GET and POST, or document and test why proxy-only protection is intentional.
-- [ ] Add a regression test proving ordinary self-service keys cannot use `/api/usage/budget?apiKeyId=...` to read arbitrary keys.
-
-## 8. Documentation
-
-- [ ] Add API reference entry for `/api/v1/me/status`.
-- [ ] Update user guide/API manager docs.
-- [ ] Document privacy behavior for shared account quota.
-- [ ] Add migration/compatibility note for existing keys.
-
-## 9. Verification
-
-- [ ] Run lint.
-- [ ] Run typecheck.
-- [ ] Run focused unit/API/UI tests.
-- [ ] Run coverage or the repo-required validation command before PR.
diff --git a/docs/specs/2026-05-16-adaptive-stream-readiness-design.md b/docs/specs/2026-05-16-adaptive-stream-readiness-design.md
deleted file mode 100644
index 4bec818db4..0000000000
--- a/docs/specs/2026-05-16-adaptive-stream-readiness-design.md
+++ /dev/null
@@ -1,91 +0,0 @@
-# Adaptive Stream Readiness Timeout
-
-## Problem
-
-Long Codex continue sessions can produce very large Responses API payloads (hundreds of messages, 20 tools, and large cached input). OmniRoute currently uses a fixed `STREAM_READINESS_TIMEOUT_MS` default of 30 seconds for the first useful SSE event. That fixed threshold is too short for some large Codex requests, even when the upstream later completes successfully.
-
-Recent local evidence showed:
-
-- Small/medium Codex requests confirm readiness in roughly 0.8-2.5 seconds.
-- Large Codex requests can run 60+ seconds and still complete successfully.
-- A fixed 30 second readiness timeout can therefore create false failures: `Stream produced no useful content within 30000ms`.
-
-The solution should avoid a blanket manual timeout increase, because that would slow fallback for genuinely dead streams.
-
-## Goals
-
-- Keep small requests fast to fail when the upstream stream is dead.
-- Give large/tool-heavy Codex Responses requests more time to produce first useful content.
-- Make timeout decisions visible in logs for future debugging.
-- Preserve the existing default behavior unless the request shape justifies extra budget.
-- Keep a hard upper bound so zombie streams cannot hang indefinitely.
-
-## Non-Goals
-
-- Do not change provider fallback ordering in this spec.
-- Do not alter account health/rate-limit policy.
-- Do not change post-readiness stream idle behavior.
-- Do not implement compression or summarization in this change.
-
-## Design
-
-Add a small policy helper that computes the readiness timeout from request shape:
-
-`open-sse/utils/streamReadinessPolicy.ts`
-
-The helper accepts:
-
-- `baseTimeoutMs`
-- `provider`
-- `model`
-- `body`
-
-It returns:
-
-- `timeoutMs`
-- `reasons`
-
-The initial heuristic is intentionally conservative:
-
-- Start with the configured base timeout, usually 30 seconds.
-- Add budget for large input arrays or message arrays.
-- Add budget for tool-heavy requests.
-- Add budget for Codex GPT-5.5 Responses requests, because local evidence shows these can take longer on large sessions.
-- Cap the result at 120 seconds by default.
-
-This is adaptive, not purely provider-based: Codex only receives the extra budget when the payload is large/tool-heavy enough to justify it.
-
-## Integration
-
-In `open-sse/handlers/chatCore.ts`, replace the direct use of `STREAM_READINESS_TIMEOUT_MS` in `ensureStreamReadiness` with the policy result.
-
-Log the chosen timeout and reasons when it differs from the base timeout, for example:
-
-```text
-[sse] stream readiness timeout=90000ms base=30000ms reason=codex,gpt-5.5,large_input,tool_heavy
-```
-
-## Failure Behavior
-
-If no useful stream content appears before the adaptive timeout, OmniRoute should continue using the existing failure path and return `STREAM_READINESS_TIMEOUT`. This change only changes the budget, not the fallback/error semantics.
-
-## Testing
-
-Add unit tests for the policy helper:
-
-- Small request keeps the base timeout.
-- Large message array increases timeout.
-- Tool-heavy request increases timeout.
-- Codex GPT-5.5 large request receives a larger timeout.
-- Timeout is capped at the maximum.
-- Zero/disabled base timeout remains zero so readiness checks can still be disabled by config.
-
-Add or update a handler-level test only if needed after unit coverage.
-
-## Acceptance Criteria
-
-- Large Codex continue sessions get an adaptive readiness timeout above 30 seconds.
-- Small requests still use 30 seconds by default.
-- The max adaptive timeout cannot exceed 120 seconds unless explicitly changed in code later.
-- Unit tests cover the policy and pass.
-- Existing pre-commit checks pass.
diff --git a/docs/specs/2026-05-29-self-service-api-key-usage-design.md b/docs/specs/2026-05-29-self-service-api-key-usage-design.md
deleted file mode 100644
index 64522ed1b8..0000000000
--- a/docs/specs/2026-05-29-self-service-api-key-usage-design.md
+++ /dev/null
@@ -1,409 +0,0 @@
-# Self-Service API Key Usage and Quota Visibility
-
-## Problem
-
-Operators often share one upstream coding account, such as Codex, across multiple OmniRoute API keys. OmniRoute already records per-key usage and supports per-key USD budgets, but a normal client API key cannot query its own spend or token totals. The existing usage APIs are management endpoints, so exposing them to each API key would disclose other keys, account metadata, and operational settings.
-
-Operators also need a way to decide whether a key may see shared upstream account quotas. For Codex this includes the short session window and weekly window fetched from ChatGPT usage APIs, and other subscription providers can expose their own normalized provider-limit windows. That quota is account-level state, not key-level state, so it should not be visible by default.
-
-The goal is to add a small self-service status API and matching dashboard controls so a delegated API key can see:
-
-- Its own USD usage against its configured budget.
-- Its own token usage totals.
-- The percent used toward its own USD budget limit.
-- Optionally, shared upstream account quota remaining when explicitly permitted.
-
-## Baseline
-
-This design was written after comparing the official source and a live deployment:
-
-- Official checkout: `origin/main` at `dc3915a4`, package version `3.8.5`.
-- Live deployment: package version `3.8.3`, installed under `/usr/lib/node_modules/omniroute/app`.
-- Contributor guide: PRs currently target `release/v3.8.3`, so implementation should start from the release branch even though the source survey used current `main`.
-
-Relevant current implementation:
-
-- API key creation is in `src/app/api/keys/route.ts`; `createKeySchema` currently accepts `name`, `noLog`, and `scopes`.
-- API key metadata is stored in `api_keys`, including `scopes`, `allowed_connections`, model restrictions, request rate limits, and lifecycle fields.
-- Management auth treats `manage` and `admin` as management scopes in `src/shared/constants/managementScopes.ts`.
-- `/api/v1/*` routes are public from the route classifier perspective, but individual handlers still validate Bearer API keys.
-- Per-key USD budgets already exist through `domain_budgets`, `domain_cost_history`, `getCostSummary(apiKeyId)`, and `checkBudget(apiKeyId)`.
-- Token usage is already recorded per key in `usage_history.api_key_id` with input, output, cache read, cache creation, and reasoning token columns.
-- Provider quota data is fetched through `src/lib/usage/providerLimits.ts` and provider usage support in `open-sse/services/usage.ts`.
-- The API Manager UI currently has a management-access toggle on create/edit and sends `scopes: ["manage"]` or `[]`; the edit modal must be changed before adding more scope types so it does not discard unrelated scopes.
-
-## Goals
-
-- Add an authenticated self-service endpoint for the calling API key's own usage.
-- Keep management endpoints protected exactly as they are today.
-- Use USD budgets for enforcement and percentage reporting.
-- Include token totals as reporting data only, not as quota enforcement.
-- Make account quota visibility opt-in per API key.
-- Add create/edit UI controls for self-service visibility while reusing the existing budget configuration flow for USD limits.
-- Add all new dashboard text through OmniRoute's i18n message system.
-- Preserve arbitrary existing scopes when the dashboard edits permissions.
-- Provide a design that can become an upstream-quality PR with tests and docs.
-
-## Non-Goals
-
-- Do not expose other API keys' usage through the self-service endpoint.
-- Do not add token-based quota enforcement in this change.
-- Do not change provider routing, fallback, or quota preflight behavior.
-- Do not disclose upstream access tokens, workspace IDs, emails, or connection secrets.
-- Do not make shared account quota visible by default.
-- Do not replace the existing management usage dashboards.
-
-## Proposed API
-
-Add:
-
-```text
-GET /api/v1/me/status
-Authorization: Bearer
-```
-
-The route is under `/api/v1` so it follows the client API surface, but the handler must explicitly validate the Bearer API key and load its metadata. It must not use `requireManagementAuth()`.
-
-The handler must not rely only on the global `CLIENT_API` authz policy. In the current source, `clientApiPolicy` can allow anonymous traffic when `REQUIRE_API_KEY` is not `"true"`, and some `/api/v1` helper code assumes the middleware already made that decision. This endpoint is more sensitive, so it must perform handler-local validation:
-
-- Require an `Authorization: Bearer ` credential.
-- Call `validateApiKey()` / `getApiKeyMetadata()` or an equivalent DB-backed helper.
-- Reject anonymous, dashboard-session-only, invalid, expired, revoked, inactive, and env-only management keys for this self-service response.
-- Derive the returned API key id from metadata, never from request parameters.
-
-The response contains only the caller's own API key identity, budget usage, token usage, and optional account quota:
-
-```json
-{
- "apiKey": {
- "id": "key_123",
- "name": "team-a"
- },
- "usage": {
- "cost": {
- "period": "monthly",
- "currency": "USD",
- "usedUsd": 12.34,
- "limitUsd": 50,
- "remainingUsd": 37.66,
- "usedPercent": 24.68,
- "warningThreshold": 0.8,
- "resetAt": "2026-06-01T00:00:00.000Z",
- "periodStartAt": "2026-05-01T00:00:00.000Z"
- },
- "tokens": {
- "periodStartAt": "2026-05-01T00:00:00.000Z",
- "inputTokens": 900000,
- "outputTokens": 32000,
- "cacheReadTokens": 120000,
- "cacheCreationTokens": 10000,
- "reasoningTokens": 5000,
- "totalTokens": 1067000
- }
- },
- "accountQuotas": [
- {
- "provider": "codex",
- "connectionId": "conn_123",
- "shared": true,
- "plan": "ChatGPT Plus",
- "quotas": {
- "session": {
- "remainingPercentage": 99,
- "usedPercentage": 1,
- "resetAt": "2026-05-29T18:11:44.000Z"
- },
- "weekly": {
- "remainingPercentage": 3,
- "usedPercentage": 97,
- "resetAt": "2026-05-31T01:23:38.000Z"
- }
- }
- },
- {
- "provider": "claude",
- "connectionId": "conn_456",
- "shared": true,
- "plan": "Claude Max",
- "quotas": {
- "daily": {
- "remainingPercentage": 65,
- "usedPercentage": 35,
- "resetAt": "2026-05-30T00:00:00.000Z"
- }
- }
- }
- ],
- "accountQuota": {
- "provider": "codex",
- "connectionId": "conn_123",
- "shared": true,
- "plan": "ChatGPT Plus",
- "quotas": {
- "weekly": {
- "remainingPercentage": 3,
- "usedPercentage": 97,
- "resetAt": "2026-05-31T01:23:38.000Z"
- }
- }
- }
-}
-```
-
-`accountQuotas` is omitted unless the key has the account quota scope. `accountQuota` is retained as a compatibility alias only when exactly one account quota entry is returned. If a specific allowed connection cannot fetch quota data, include a per-connection unavailable entry:
-
-```json
-{
- "accountQuotas": [
- {
- "provider": "cursor",
- "connectionId": "conn_789",
- "shared": true,
- "available": false,
- "reason": "fetch_failed"
- }
- ],
- "accountQuota": {
- "provider": "cursor",
- "connectionId": "conn_789",
- "shared": true,
- "available": false,
- "reason": "fetch_failed"
- }
-}
-```
-
-If explicit connection metadata lookup fails before the provider is known, return the unresolved connection as unavailable without failing the whole status response:
-
-```json
-{
- "accountQuotas": [
- {
- "provider": "unknown",
- "connectionId": "conn_789",
- "shared": true,
- "available": false,
- "reason": "connection_lookup_failed"
- }
- ]
-}
-```
-
-Use stable reason strings: `not_supported`, `not_available`, `fetch_failed`, and `connection_lookup_failed`.
-
-## Scopes
-
-Add self-service scopes that do not grant management access:
-
-- `self:usage`: allows a key to query its own spend, budget percent, and token totals.
-- `self:account-quota`: allows a key to see shared upstream account quotas for provider-limit connections it may use.
-
-`self:usage` should be enabled by default for newly created ordinary API keys. The UI should show it checked by default and persist the scope when the control is enabled. For backwards compatibility, the implementation should backfill `self:usage` onto existing ordinary keys during migration or first startup after upgrade. After that compatibility step, absence of `self:usage` means own-usage visibility is disabled and the self-service endpoint returns `403`.
-
-`self:account-quota` must be disabled by default. The dashboard should require an explicit opt-in when creating or editing a key.
-
-These scopes must not be added to `MANAGEMENT_API_KEY_SCOPES`. `manage` and `admin` remain the only management-grade scopes.
-
-## Budget Semantics
-
-The existing USD budget system remains authoritative:
-
-- `getCostSummary(apiKeyId)` provides current period cost, active USD limit, reset interval, reset time, and period boundaries.
-- `checkBudget(apiKeyId)` remains the enforcement check used by request handling.
-- The self-service endpoint reports budget percentage as `usedUsd / limitUsd * 100`.
-- When no budget is configured, return `limitUsd: null`, `remainingUsd: null`, and `usedPercent: null`.
-
-The endpoint should report the active period from the budget window when configured. If a key has no budget, use the current calendar month for display-only usage aggregation so the API still returns useful cost and token totals.
-
-## Token Usage Semantics
-
-Add a small aggregation helper over `usage_history` scoped by `api_key_id` and time window:
-
-```sql
-SELECT
- COALESCE(SUM(tokens_input), 0) AS inputTokens,
- COALESCE(SUM(tokens_output), 0) AS outputTokens,
- COALESCE(SUM(tokens_cache_read), 0) AS cacheReadTokens,
- COALESCE(SUM(tokens_cache_creation), 0) AS cacheCreationTokens,
- COALESCE(SUM(tokens_reasoning), 0) AS reasoningTokens
-FROM usage_history
-WHERE api_key_id = ?
- AND timestamp >= ?
-```
-
-`totalTokens` should include all reported token categories. Token totals are informational and should not affect budget enforcement.
-
-## Account Quota Resolution
-
-Account quota is shared provider state. The self-service endpoint may include it only when:
-
-- The API key has `self:account-quota`.
-- Provider connections can be resolved from the key's allowed connection policy.
-- The providers support quota fetching through the provider limits path.
-
-Connection resolution must follow the source semantics for `allowedConnections`: an empty array means unrestricted access to all connections, not "no connections".
-
-- If explicit allowed connection ids are present, fetch quota data for those active provider-limit connections.
-- If `allowedConnections` is empty, fetch quota data for all active provider-limit connections because the key may use all of them.
-- If an allowed connection is inactive, missing, or unsupported, skip it or return a per-connection `not_supported` entry when the connection identity is known.
-- If explicit connection lookup fails, keep the rest of the response and return that connection as `available: false` with `connection_lookup_failed`.
-- If unrestricted connection listing fails, keep the rest of the response and return an empty `accountQuotas` array because no allowed connection identities can be resolved.
-- If a provider quota fetch fails, keep the rest of the response and return that connection as `available: false` with `fetch_failed`.
-
-This rule matches routing permissions: the endpoint exposes only account quotas for connections the key is allowed to use, and only when the operator explicitly grants `self:account-quota`.
-
-Reuse the existing provider limits path. Normalize every returned quota window to used/remaining percentages plus reset timestamps. Do not return raw upstream payloads.
-
-## Dashboard UX
-
-API Manager should expose these controls during key creation and editing.
-
-Create key modal:
-
-- Management access remains a separate, off-by-default toggle.
-- Add "Self-service visibility":
- - "Own cost and token usage" checked by default.
- - "Shared account quota" unchecked by default.
-- Do not add budget limit fields here. Per-key USD budgets already have a dedicated configuration surface, and this feature should link to or surface the existing budget state instead of creating a second configuration path.
-
-Editing permissions:
-
-- Keep existing model, endpoint, connection, schedule, and rate-limit controls.
-- Add the same self-service visibility toggles.
-- Preserve all existing scopes when toggling one permission. The current edit flow must not rebuild scopes as only `["manage"]` or `[]`.
-- Do not move budget editing into the permissions modal. The permissions modal may show a read-only hint or link to the existing budget configuration area.
-
-Usage display:
-
-- In the key list or details panel, show USD used, active USD limit, and used percent when a budget exists.
-- Show token totals in a compact details view.
-- Show shared account quotas only for keys with `self:account-quota`, clearly labeled as shared account quota, not per-key quota.
-- When no USD budget is configured, show usage normally and render the limit, remaining amount, and percent as unset/not configured rather than `0%`.
-
-## Internationalization
-
-OmniRoute's dashboard is localized through `src/i18n/messages/*.json` and components use `useTranslations()`. All new API Manager labels, descriptions, tooltips, empty states, and error messages must use translation keys instead of hard-coded UI strings.
-
-Implementation should:
-
-- Add new keys under the existing `apiManager` namespace for self-service visibility labels, shared account quota labels, and unset-budget display text.
-- Update the default source locale and keep other locale files structurally compatible with the repo's i18n workflow.
-- Avoid concatenating translated fragments for dynamic text; use complete translation strings with variables where needed.
-- Run the repo's UI i18n checks, especially `npm run i18n:sync-ui:dry` and `npm run i18n:check-ui-coverage`, so missing translations are caught before PR.
-- If the implementation touches the existing budget page for links or hints, localize any new budget-page strings as well. The existing `BudgetTab` still has some hard-coded labels, so do not add more hard-coded user-facing text there.
-
-## Validation and Storage Changes
-
-Extend `createKeySchema` to accept:
-
-- `scopes` containing the new self-service scope names.
-
-`createKeySchema` and `updateKeyPermissionsSchema` currently cap `scopes` at 16 entries. Adding two self-service scopes can make legitimate keys exceed that limit when they already carry management or MCP/custom scopes. The implementation should either raise the cap to a documented value such as 32 or validate against named scope families instead of keeping the current 16-entry limit.
-
-Do not extend key creation with a budget object in this change. Budget limits are already configured through the existing budget APIs and UI. The self-service endpoint should read those existing limits and display `null` limit/percent fields when none are configured.
-
-Add a compatibility migration or startup normalization step:
-
-- Existing ordinary keys receive `self:usage`.
-- Existing keys do not receive `self:account-quota`.
-- Existing management keys keep their current management scopes and may also receive `self:usage` if they are expected to use the self-service endpoint.
-- The backfill is one-time and guarded by the repo's existing migration/version mechanism so it cannot re-enable `self:usage` after an operator later disables it.
-- After that one-time backfill, missing `self:usage` is an explicit denial for the self-service endpoint.
-
-The key creation route should:
-
-1. Validate the request.
-2. Normalize scopes by preserving known custom scopes and adding `self:usage` when omitted by the UI default.
-3. Create the key.
-4. Return the created key metadata.
-
-The update permissions route should support the same scope preservation behavior. Scope mutation should be set-based:
-
-- Start from existing scopes.
-- Add or remove only the scopes represented by the UI controls.
-- Leave unknown or unrelated scopes intact.
-
-The current `PermissionsModal` calls `onSave(..., manageEnabled ? ["manage"] : [], ...)`, which would discard any new self-service or custom scope. This must be changed before the self-service toggles are added.
-
-## Existing Budget Endpoint Guard
-
-The global authz proxy classifies `/api/usage/budget` as a management API, but unlike `/api/usage/budget/bulk`, the current route handler does not call `requireManagementAuth()` directly. The self-service design must not reuse `/api/usage/budget?apiKeyId=...` because that endpoint accepts arbitrary key ids.
-
-For defense in depth and easier direct route testing, the implementation PR should either:
-
-- Add handler-level `requireManagementAuth()` to `/api/usage/budget` GET and POST, matching the bulk route; or
-- Include an explicit note and tests proving the proxy is the only intended guard.
-
-The preferred upstream-quality fix is to add handler-level management auth to `/api/usage/budget` while adding the separate own-key `/api/v1/me/status` endpoint.
-
-## Security and Privacy
-
-The self-service handler must be own-key only. It should derive `apiKeyId` from the presented Bearer key and never accept an `apiKeyId` query parameter.
-
-Never include:
-
-- Full API key value.
-- Upstream access tokens or refresh tokens.
-- Provider account email unless that email is already visible to this key through another client API.
-- Other keys' spend, token totals, names, or budgets.
-- Raw upstream usage payloads.
-
-Account quota should be treated as sensitive because it lets delegated users infer shared account exhaustion. The default remains off.
-
-## Error Handling
-
-- Missing or invalid Bearer key: `401` with a generic auth error.
-- Valid key without `self:usage`: `403`.
-- Budget missing: `200` with null limit and percent fields.
-- Usage aggregation failure: `500` with generic message; log server-side details.
-- Quota fetch unsupported or unavailable: `200` with per-connection unavailable entries in `accountQuotas`.
-- Quota fetch auth failure: do not leak provider auth details; return `not_available` or `fetch_failed` and log details server-side.
-
-## Testing
-
-Add focused tests:
-
-- Self-service endpoint rejects missing and invalid Bearer keys.
-- Self-service endpoint rejects anonymous access even when `REQUIRE_API_KEY` is not `"true"`.
-- Self-service endpoint rejects env-only management keys or any key without DB metadata suitable for own-key usage.
-- A normal key with `self:usage` can query its own cost and token totals without `manage`.
-- The endpoint never accepts an `apiKeyId` override.
-- Key A cannot see Key B usage.
-- A key without account quota scope does not receive `accountQuotas`.
-- A key with account quota scope and one allowed Codex connection receives normalized session and weekly quota plus the compatibility `accountQuota` field.
-- A key with account quota scope and multiple allowed provider-limit connections receives multiple `accountQuotas` entries.
-- A key with account quota scope and unrestricted connection access receives all active provider-limit connection quotas.
-- A failed provider quota fetch returns an unavailable entry without hiding successful provider quota entries.
-- Create UI defaults own usage on and shared quota off.
-- Edit UI preserves unrelated scopes.
-- UI renders the no-budget state as not configured, with usage and token totals still visible.
-- New dashboard strings are covered by i18n keys.
-- `/api/usage/budget` remains management-only and is not usable as an own-key data escape hatch.
-
-## Implementation Notes
-
-Recommended new files:
-
-- `src/shared/constants/selfServiceScopes.ts`
-- `src/lib/usage/apiKeySelfService.ts`
-- `src/app/api/v1/me/status/route.ts`
-
-Recommended modified files:
-
-- `src/shared/validation/schemas.ts`
-- `src/app/api/keys/route.ts`
-- `src/app/api/keys/[id]/route.ts`
-- `src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx`
-- `src/i18n/messages/*.json`
-- API reference docs after implementation.
-
-## Acceptance Criteria
-
-- Delegated keys can see their own USD usage, budget percentage, and token usage.
-- Shared account quota is hidden unless explicitly enabled per key.
-- The dashboard can configure self-service visibility during create/edit.
-- The dashboard continues to use the existing budget configuration surface for USD limits.
-- New UI text is localized through existing i18n files.
-- Existing management usage APIs remain management-only.
-- Scope edits do not discard unrelated scopes.
-- Tests cover API, helper logic, and UI scope defaults.
diff --git a/docs/superpowers/plans/2026-05-29-windsurf-login-hotfix.md b/docs/superpowers/plans/2026-05-29-windsurf-login-hotfix.md
deleted file mode 100644
index 0df2ad2932..0000000000
--- a/docs/superpowers/plans/2026-05-29-windsurf-login-hotfix.md
+++ /dev/null
@@ -1,740 +0,0 @@
-# Windsurf Login Hotfix (Phase 1) Implementation Plan
-
-> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
-
-**Goal:** Hapus PKCE OAuth flow yang rusak (`app.devin.ai/editor/signin` → 404) untuk provider `windsurf` & `devin-cli`, jadikan import-token sebagai satu-satunya path login.
-
-**Architecture:** Single-branch hotfix di `fix/windsurf-login-2026-05-29`. Modifikasi 5 file source + 1 file test, tidak ada migrasi DB, tidak ada credential baru. Existing connections (api_key tersimpan) tetap jalan tanpa perubahan.
-
-**Tech Stack:** TypeScript 5.9, Next.js 16 App Router, Node.js test runner (`node --import tsx/esm --test`), Zod validation, React 19.
-
-**Spec:** `docs/superpowers/specs/2026-05-29-windsurf-login-fix-design.md` (Phase 1 section).
-
----
-
-## File Structure
-
-| File | Type | Responsibility |
-|---|---|---|
-| `src/lib/oauth/providers/windsurf.ts` | Modify | Drop PKCE handlers (`buildAuthUrl`, `flowType: "authorization_code_pkce"`). Set `flowType: "import_token"`. Keep `mapTokens` + token validation. |
-| `src/lib/oauth/constants/oauth.ts` | Modify | Comment out PKCE-only fields. Keep `inferenceUrl`, `showAuthTokenUrl`, `firebaseApiKey`, `ideName`. |
-| `src/lib/oauth/providers/index.ts` | No change | Re-export already correct (`windsurf` + alias `devin-cli`). |
-| `src/app/api/oauth/[provider]/[action]/route.ts` | Modify | Remove `windsurf` & `devin-cli` from `PKCE_CALLBACK_PROVIDERS`. Return HTTP 410 Gone for `authorize` & `start-callback-server` actions. |
-| `src/shared/components/OAuthModal.tsx` | Modify | Hide PKCE buttons for windsurf/devin-cli, show paste-token panel + "Get token" link. |
-| `tests/unit/windsurf-devin-executors.test.ts` | Modify | Add tests asserting PKCE disabled + import-token still works. |
-
-**Out of scope (Phase 2):** Firebase OAuth, RegisterUser, refresh worker, `WindsurfLoginModal.tsx`, DB migration. See spec Phase 2 section.
-
----
-
-## Pre-flight
-
-- [ ] **Step 0.1: Verify branch + identity**
-
-Run:
-```bash
-git branch --show-current
-git config user.email
-```
-Expected: branch is `fix/windsurf-login-2026-05-29`, email is set (any value, just non-empty).
-
-- [ ] **Step 0.2: Verify spec is committed**
-
-Run:
-```bash
-git log --oneline -3
-```
-Expected: top commit is `docs(oauth): add Windsurf login fix design ...`.
-
----
-
-## Task 1: Test — PKCE auth URL generation throws / returns disabled
-
-**Files:**
-- Test: `tests/unit/windsurf-devin-executors.test.ts` (modify — add test cases at end of describe block)
-
-- [ ] **Step 1.1: Read existing test structure**
-
-Run:
-```bash
-head -30 tests/unit/windsurf-devin-executors.test.ts
-```
-Note the existing imports + describe blocks. Match style.
-
-- [ ] **Step 1.2: Add failing test for PKCE-disabled auth URL**
-
-Append to `tests/unit/windsurf-devin-executors.test.ts` (inside the existing top-level `describe` or as a new describe at the end):
-
-```typescript
-import { test } from "node:test";
-import assert from "node:assert/strict";
-import { generateAuthData, getProvider } from "@/lib/oauth/providers";
-
-test("windsurf provider: flowType is import_token (PKCE disabled post-rebrand)", () => {
- const provider = getProvider("windsurf");
- assert.equal(provider.flowType, "import_token");
-});
-
-test("devin-cli provider: flowType is import_token (shares windsurf config)", () => {
- const provider = getProvider("devin-cli");
- assert.equal(provider.flowType, "import_token");
-});
-
-test("windsurf provider: generateAuthData returns no authUrl (PKCE flow disabled)", () => {
- const data = generateAuthData("windsurf", "http://localhost:0/auth/callback");
- assert.equal(data.authUrl, undefined);
- assert.equal(data.supported, false);
- assert.match(data.error ?? "", /import-token|disabled|app\.devin\.ai/i);
-});
-
-test("devin-cli provider: generateAuthData returns no authUrl", () => {
- const data = generateAuthData("devin-cli", "http://localhost:0/auth/callback");
- assert.equal(data.authUrl, undefined);
- assert.equal(data.supported, false);
-});
-```
-
-If file already imports `test` and `assert`, do not duplicate; reuse.
-
-- [ ] **Step 1.3: Run test — confirm failure**
-
-Run:
-```bash
-node --import tsx/esm --test tests/unit/windsurf-devin-executors.test.ts 2>&1 | tail -30
-```
-Expected: 4 new tests FAIL — `flowType` is `"authorization_code_pkce"` not `"import_token"`, and `generateAuthData` still returns a real URL.
-
----
-
-## Task 2: Implementation — switch windsurf provider to import_token flow
-
-**Files:**
-- Modify: `src/lib/oauth/providers/windsurf.ts` (full file rewrite — small file)
-
-- [ ] **Step 2.1: Read full current file**
-
-Run:
-```bash
-wc -l src/lib/oauth/providers/windsurf.ts
-```
-Confirm file size before rewriting. Should be < 200 lines.
-
-- [ ] **Step 2.2: Replace file content**
-
-Write `src/lib/oauth/providers/windsurf.ts`:
-
-```typescript
-import { WINDSURF_CONFIG } from "../constants/oauth";
-
-/**
- * Windsurf / Devin CLI OAuth Provider — import-token only (Phase 1 hotfix, 2026-05-29).
- *
- * The previous PKCE Authorization Code flow targeting `https://app.devin.ai/editor/signin`
- * stopped working post-rebrand: that endpoint now returns 404. Until Phase 2 ports the
- * Firebase OAuth + RegisterUser flow (see docs/superpowers/specs/2026-05-29-windsurf-login-fix-design.md),
- * the only supported login path is import-token:
- *
- * 1. User opens https://windsurf.com/show-auth-token in a browser
- * 2. Copies the displayed Windsurf API key (`sk-ws-...` style)
- * 3. Pastes it into OmniRoute via /api/oauth/windsurf/import-token
- *
- * The pasted token is stored as `accessToken` and used directly by `WindsurfExecutor`
- * (open-sse/executors/windsurf.ts) as the `Authorization: Bearer ...` header against
- * the inference server (`server.self-serve.windsurf.com`).
- */
-export const windsurf = {
- config: WINDSURF_CONFIG,
- flowType: "import_token" as const,
-
- /**
- * Validate a pasted Windsurf API key. Accepts the `sk-ws-...` format issued by
- * windsurf.com/show-auth-token and the legacy raw-token format. Empty or
- * whitespace-only tokens are rejected.
- */
- validateImportToken(token: string): { valid: boolean; reason?: string } {
- const trimmed = (token ?? "").trim();
- if (!trimmed) {
- return { valid: false, reason: "Token is empty" };
- }
- if (trimmed.length < 16) {
- return { valid: false, reason: "Token is too short" };
- }
- return { valid: true };
- },
-
- /**
- * Map a pasted import token onto the connection record. The token IS the
- * Windsurf API key; there is no exchange step.
- */
- mapTokens(token: string) {
- return {
- accessToken: token,
- refreshToken: null,
- expiresAt: null,
- };
- },
-};
-```
-
-- [ ] **Step 2.3: Re-run Task 1 tests**
-
-Run:
-```bash
-node --import tsx/esm --test tests/unit/windsurf-devin-executors.test.ts 2>&1 | tail -30
-```
-Expected: 4 new tests now FAIL only on `generateAuthData` — `flowType` tests should PASS. `generateAuthData` still depends on the route handler / provider dispatcher, which Task 3 wires.
-
-If `flowType` tests still fail with `flowType: "authorization_code_pkce"`, re-check that `windsurf.ts` was overwritten.
-
----
-
-## Task 3: Make `generateAuthData` return disabled stub for windsurf/devin-cli
-
-**Files:**
-- Modify: `src/lib/oauth/providers/index.ts` (add helper export — small)
-- OR: Modify the dispatcher inside `src/lib/oauth/providers/` that owns `generateAuthData` (find it)
-
-- [ ] **Step 3.1: Locate `generateAuthData` definition**
-
-Run:
-```bash
-grep -rn "export function generateAuthData\|export const generateAuthData" src/lib/oauth/
-```
-Expected: one hit. Note the file path — call it ``.
-
-- [ ] **Step 3.2: Read the dispatcher**
-
-Read ``. Identify the branch where `provider.flowType === "authorization_code_pkce"` builds the auth URL via `provider.buildAuthUrl(...)`.
-
-- [ ] **Step 3.3: Add early-return for `import_token` flowType**
-
-In ``, modify `generateAuthData` so that when `provider.flowType === "import_token"` it returns:
-
-```typescript
-if (provider.flowType === "import_token") {
- return {
- authUrl: undefined,
- codeVerifier: undefined,
- state: undefined,
- supported: false,
- error:
- provider === windsurf || providerKey === "windsurf" || providerKey === "devin-cli"
- ? "Browser login disabled — paste token from https://windsurf.com/show-auth-token instead. Phase 2 will restore Firebase OAuth."
- : "This provider only supports import-token flow.",
- };
-}
-```
-
-Match the ``'s actual function signature — if it receives `providerKey: string` and `redirectUri: string`, use those names. The exact field set returned must match what `OAuthModal.tsx` and the `/authorize` route already consume (see Step 1.2 expected fields). Keep `supported: false` and a non-empty `error` string; both are checked by the test.
-
-- [ ] **Step 3.4: Run all Task 1 tests — must pass now**
-
-Run:
-```bash
-node --import tsx/esm --test tests/unit/windsurf-devin-executors.test.ts 2>&1 | tail -30
-```
-Expected: all 4 new tests PASS.
-
-- [ ] **Step 3.5: Commit progress**
-
-Run:
-```bash
-git add src/lib/oauth/providers/windsurf.ts src/lib/oauth/providers/index.ts tests/unit/windsurf-devin-executors.test.ts
-# Plus the dispatcher file from Step 3.1 if different
-git status --short
-git commit -m "fix(oauth): switch windsurf provider to import_token flow
-
-The PKCE auth URL targeting app.devin.ai/editor/signin returns 404
-post-rebrand. Until Phase 2 ports Firebase OAuth + RegisterUser, the
-only supported path is import-token via windsurf.com/show-auth-token.
-
-- windsurf.ts: drop buildAuthUrl, set flowType=import_token
-- generateAuthData returns supported:false + helpful error for windsurf/devin-cli
-- tests: assert flowType + disabled stub"
-```
-
----
-
-## Task 4: Test — `start-callback-server` returns 410 Gone for windsurf/devin-cli
-
-**Files:**
-- Test: `tests/unit/windsurf-devin-executors.test.ts` (extend)
-
-- [ ] **Step 4.1: Add failing test**
-
-Append to `tests/unit/windsurf-devin-executors.test.ts`:
-
-```typescript
-import { GET as oauthGet } from "@/app/api/oauth/[provider]/[action]/route";
-
-test("OAuth route: windsurf/start-callback-server returns 410 Gone", async () => {
- const url = "http://localhost:20128/api/oauth/windsurf/start-callback-server";
- const request = new Request(url, { method: "GET" });
- const response = await oauthGet(request, {
- params: Promise.resolve({ provider: "windsurf", action: "start-callback-server" }),
- } as never);
- assert.equal(response.status, 410);
- const body = await response.json();
- assert.match(body.error, /import-token|disabled|410/i);
-});
-
-test("OAuth route: devin-cli/authorize returns 410 Gone", async () => {
- const url = "http://localhost:20128/api/oauth/devin-cli/authorize";
- const request = new Request(url, { method: "GET" });
- const response = await oauthGet(request, {
- params: Promise.resolve({ provider: "devin-cli", action: "authorize" }),
- } as never);
- assert.equal(response.status, 410);
-});
-```
-
-- [ ] **Step 4.2: Run — confirm failure**
-
-Run:
-```bash
-node --import tsx/esm --test tests/unit/windsurf-devin-executors.test.ts 2>&1 | tail -30
-```
-Expected: 2 new tests FAIL — current handler probably returns 400 or 200.
-
----
-
-## Task 5: Implementation — return 410 Gone for disabled PKCE actions
-
-**Files:**
-- Modify: `src/app/api/oauth/[provider]/[action]/route.ts:75-160` (`GET` handler)
-
-- [ ] **Step 5.1: Read existing handler shape**
-
-Run:
-```bash
-sed -n '40,50p;75,165p' src/app/api/oauth/[provider]/[action]/route.ts
-```
-Confirm `PKCE_CALLBACK_PROVIDERS` set definition + the `if (action === "authorize")` branch.
-
-- [ ] **Step 5.2: Remove `windsurf` and `devin-cli` from `PKCE_CALLBACK_PROVIDERS`**
-
-In `src/app/api/oauth/[provider]/[action]/route.ts`, find:
-
-```typescript
-const PKCE_CALLBACK_PROVIDERS = new Set(["codex", "windsurf", "devin-cli"]);
-```
-
-Replace with:
-
-```typescript
-// windsurf & devin-cli removed 2026-05-29 — PKCE endpoint app.devin.ai/editor/signin
-// returns 404 post-rebrand. Phase 2 will reintroduce browser login via Firebase OAuth.
-const PKCE_CALLBACK_PROVIDERS = new Set(["codex"]);
-
-// Providers whose PKCE flow has been retired but whose import-token path is still
-// active. The route returns 410 Gone for `authorize` / `start-callback-server`
-// and points users at /import-token.
-const RETIRED_PKCE_PROVIDERS = new Set(["windsurf", "devin-cli"]);
-```
-
-- [ ] **Step 5.3: Add 410 short-circuit at top of `GET` handler body**
-
-Inside `GET`, immediately after `const { provider, action } = await params;` (and before `if (action === "authorize")`), insert:
-
-```typescript
-if (
- RETIRED_PKCE_PROVIDERS.has(provider) &&
- (action === "authorize" || action === "start-callback-server" || action === "poll-callback")
-) {
- return NextResponse.json(
- {
- error:
- "Browser OAuth disabled for this provider — use import-token via /api/oauth/" +
- provider +
- "/import-token. See https://windsurf.com/show-auth-token to obtain a token.",
- },
- { status: 410 }
- );
-}
-```
-
-- [ ] **Step 5.4: Run tests — must pass**
-
-Run:
-```bash
-node --import tsx/esm --test tests/unit/windsurf-devin-executors.test.ts 2>&1 | tail -30
-```
-Expected: all 6 new tests PASS.
-
-- [ ] **Step 5.5: Run full unit suite — no regressions**
-
-Run:
-```bash
-npm run test:unit 2>&1 | tail -30
-```
-Expected: green. If a Codex-specific test now fails because `PKCE_CALLBACK_PROVIDERS.has("windsurf")` was assumed, fix that test (Codex behaviour didn't change; the assertion did).
-
-- [ ] **Step 5.6: Commit**
-
-Run:
-```bash
-git add src/app/api/oauth/[provider]/[action]/route.ts tests/unit/windsurf-devin-executors.test.ts
-git commit -m "fix(oauth): return 410 Gone for retired windsurf/devin-cli PKCE actions
-
-start-callback-server, authorize, and poll-callback now return 410
-with a pointer to /import-token. Codex PKCE flow unchanged."
-```
-
----
-
-## Task 6: Clean up `WINDSURF_CONFIG` — annotate PKCE fields as retired
-
-**Files:**
-- Modify: `src/lib/oauth/constants/oauth.ts:328-365`
-
-- [ ] **Step 6.1: Read full block**
-
-Run:
-```bash
-sed -n '328,375p' src/lib/oauth/constants/oauth.ts
-```
-
-- [ ] **Step 6.2: Update header comment block**
-
-Replace the comment block above `export const WINDSURF_CONFIG = {` with:
-
-```typescript
-// Windsurf / Devin CLI Configuration
-//
-// 2026-05-29 (Phase 1 hotfix):
-// The browser PKCE flow targeting https://app.devin.ai/editor/signin returned
-// 404 post-rebrand. PKCE-only fields (`authorizeUrl`, `codeChallengeMethod`,
-// `callbackPort`, `callbackPath`, `apiServerUrl`, `exchangePath`) are kept
-// below for archival reference but are NO LONGER consumed by any code path —
-// the provider exports flowType="import_token" only.
-//
-// Phase 2 will reintroduce browser login via Firebase OAuth + RegisterUser.
-// Spec: docs/superpowers/specs/2026-05-29-windsurf-login-fix-design.md.
-//
-// Active fields:
-// - inferenceUrl → used by WindsurfExecutor (open-sse/executors/windsurf.ts)
-// - showAuthTokenUrl → linked from OAuthModal "Get token" button
-// - firebaseApiKey → reserved for Phase 2
-// - ideName → sent in extension headers
-```
-
-Inline-annotate the retired fields (`authorizeUrl`, `codeChallengeMethod`, `callbackPort`, `callbackPath`, `apiServerUrl`, `exchangePath`) with `// retired 2026-05-29` comments. Do **not** delete them — that would break any downstream import; just mark them.
-
-- [ ] **Step 6.3: Verify typecheck still passes**
-
-Run:
-```bash
-npm run typecheck:core 2>&1 | tail -10
-```
-Expected: no errors.
-
-- [ ] **Step 6.4: Commit**
-
-Run:
-```bash
-git add src/lib/oauth/constants/oauth.ts
-git commit -m "refactor(oauth): annotate retired PKCE fields in WINDSURF_CONFIG
-
-No behaviour change — comment-only update documenting that authorizeUrl,
-codeChallengeMethod, callbackPort, callbackPath, apiServerUrl, and
-exchangePath are no longer consumed."
-```
-
----
-
-## Task 7: UI — `OAuthModal.tsx` skips PKCE buttons for windsurf/devin-cli
-
-**Files:**
-- Modify: `src/shared/components/OAuthModal.tsx`
-
-- [ ] **Step 7.1: Read modal**
-
-Run:
-```bash
-grep -n "windsurf\|devin-cli\|flowType\|authorize\|start-callback\|import-token\|showAuthTokenUrl" src/shared/components/OAuthModal.tsx | head -40
-```
-
-Identify the rendering branch that picks PKCE vs import-token UI based on `provider.flowType` or provider key.
-
-- [ ] **Step 7.2: Add windsurf/devin-cli explicit branch**
-
-In the modal render, find where it switches on flow type. Add — at the top of the render-decision logic — a check:
-
-```typescript
-// Phase 1 hotfix: PKCE flow for windsurf/devin-cli is retired (app.devin.ai 404).
-// Force import-token panel + "Get your token" link to windsurf.com/show-auth-token.
-const isWindsurfFamily = providerKey === "windsurf" || providerKey === "devin-cli";
-
-if (isWindsurfFamily) {
- return (
-
- );
-}
-```
-
-If `ImportTokenPanel` doesn't exist as a separate component, render the equivalent inline using the same JSX structure already used for other `IMPORT_TOKEN_PROVIDERS` (claude alternate-flow, e.g.). The key UX requirements:
-1. No "Sign in with browser" button visible
-2. Single textarea/input for token paste
-3. A button labeled "Get your Windsurf API token" that calls `window.open("https://windsurf.com/show-auth-token", "_blank", "noopener,noreferrer")`
-4. Submit button validates non-empty + calls existing `/api/oauth/windsurf/import-token` POST
-
-- [ ] **Step 7.3: Manual smoke (developer-side)**
-
-Run:
-```bash
-npm run dev &
-sleep 8
-curl -s -o /dev/null -w "%{http_code}\n" http://localhost:20128/api/oauth/windsurf/start-callback-server
-curl -s -o /dev/null -w "%{http_code}\n" http://localhost:20128/api/oauth/devin-cli/authorize
-kill %1
-```
-Expected: both `410`.
-
-- [ ] **Step 7.4: Lint + typecheck**
-
-Run:
-```bash
-npm run lint 2>&1 | tail -5
-npm run typecheck:core 2>&1 | tail -5
-```
-Expected: 0 errors. Pre-existing warnings OK.
-
-- [ ] **Step 7.5: Commit**
-
-Run:
-```bash
-git add src/shared/components/OAuthModal.tsx
-git commit -m "fix(dashboard): force import-token panel for windsurf/devin-cli
-
-PKCE 'Sign in with browser' button is hidden for these providers.
-Single 'Get your Windsurf API token' link opens windsurf.com/show-auth-token,
-user pastes the returned token into the form."
-```
-
----
-
-## Task 8: i18n — update Windsurf login guide steps
-
-**Files:**
-- Modify: i18n keys for windsurf onboarding guide (39 languages)
-
-- [ ] **Step 8.1: Locate i18n keys**
-
-Run:
-```bash
-grep -rln "windsurf" src/locales/ public/locales/ docs/i18n/ 2>/dev/null | head -10
-grep -rln "show-auth-token\|app.devin.ai\|editor/signin" . --include="*.json" --include="*.ts" 2>/dev/null | grep -v node_modules | grep -v ".next" | head -10
-```
-Note the i18n root directory — call it ``.
-
-- [ ] **Step 8.2: Find the Windsurf step key**
-
-Run:
-```bash
-grep -rln "docs.windsurf.steps\|windsurfGuideSteps\|windsurf_steps\|Sign in to Windsurf"
-```
-Identify the canonical key (e.g. `docs.windsurf.steps` or `onboarding.windsurf.steps`).
-
-- [ ] **Step 8.3: Update English (`en.json` or equivalent) first**
-
-Replace any string mentioning "Sign in", "browser login", or `app.devin.ai` with the import-token equivalent. Target string (rewrite per file's existing key shape):
-
-```
-1. Open https://windsurf.com/show-auth-token in your browser
-2. Sign in to your Windsurf account if prompted
-3. Copy the API token displayed on the page
-4. Paste it into the OmniRoute Windsurf connection form
-```
-
-- [ ] **Step 8.4: Sync the other 38 languages**
-
-If the repo has a sync script, run it:
-
-```bash
-npm run docs:i18n-sync 2>&1 | tail -10
-# or
-npm run check:docs-all 2>&1 | tail -20
-```
-
-If no script: replace the same key in every locale file with the English text wrapped in a `// TODO translate` marker, OR leave language-specific versions untouched if they already don't mention `app.devin.ai`. The CI gate (`check-docs-sync` in pre-commit) will tell you which files are out of sync.
-
-- [ ] **Step 8.5: Run docs gate**
-
-Run:
-```bash
-npm run check:docs-all 2>&1 | tail -20
-```
-Expected: PASS or only pre-existing warnings.
-
-- [ ] **Step 8.6: Commit**
-
-Run:
-```bash
-git add
-git commit -m "docs(i18n): update Windsurf onboarding to import-token flow
-
-Replace 'Sign in via browser' steps with the windsurf.com/show-auth-token
-copy-paste flow across all locales."
-```
-
----
-
-## Task 9: Full verification
-
-- [ ] **Step 9.1: Full unit suite**
-
-Run:
-```bash
-npm run test:unit 2>&1 | tail -20
-```
-Expected: green.
-
-- [ ] **Step 9.2: Coverage gate**
-
-Run:
-```bash
-npm run test:coverage 2>&1 | tail -20
-```
-Expected: 75/75/75/70 thresholds met.
-
-- [ ] **Step 9.3: Lint + typecheck**
-
-Run:
-```bash
-npm run lint 2>&1 | tail -5
-npm run typecheck:core 2>&1 | tail -5
-npm run typecheck:noimplicit:core 2>&1 | tail -5
-```
-Expected: 0 errors.
-
-- [ ] **Step 9.4: Combined check**
-
-Run:
-```bash
-npm run check 2>&1 | tail -10
-```
-Expected: green.
-
-- [ ] **Step 9.5: Manual smoke — local server**
-
-Run:
-```bash
-npm run dev &
-sleep 10
-# 1. Disabled PKCE actions return 410
-curl -s -o /dev/null -w "GET /windsurf/authorize: %{http_code}\n" \
- http://localhost:20128/api/oauth/windsurf/authorize
-curl -s -o /dev/null -w "GET /windsurf/start-callback-server: %{http_code}\n" \
- http://localhost:20128/api/oauth/windsurf/start-callback-server
-curl -s -o /dev/null -w "GET /devin-cli/authorize: %{http_code}\n" \
- http://localhost:20128/api/oauth/devin-cli/authorize
-# 2. Codex still works (regression check)
-curl -s -o /dev/null -w "GET /codex/authorize: %{http_code}\n" \
- http://localhost:20128/api/oauth/codex/authorize
-kill %1
-```
-Expected: windsurf/devin-cli all `410`. Codex `200` (or whatever it returned before — must be unchanged).
-
-- [ ] **Step 9.6: Manual smoke — paste valid token**
-
-Manual steps in browser at `http://localhost:20128/dashboard/providers`:
-1. Click "Connect" on Windsurf provider
-2. Verify modal does NOT show "Sign in with browser"
-3. Verify "Get your Windsurf API token" link is present
-4. Click link → opens new tab to `windsurf.com/show-auth-token` (or warns if not logged in)
-5. Paste a valid token, submit
-6. Connection saves, status shows "Active"
-7. Open chat playground, send 1 request to `swe-1`, verify a response is returned
-
-If you do not have a valid Windsurf token, document this in the PR as "blocked on tester with credentials" and note that automated tests cover the negative path.
-
----
-
-## Task 10: Push branch + open PR
-
-- [ ] **Step 10.1: Verify clean tree**
-
-Run:
-```bash
-git status --short
-```
-Expected: empty (everything committed).
-
-- [ ] **Step 10.2: Push branch**
-
-Run:
-```bash
-git push -u origin fix/windsurf-login-2026-05-29
-```
-
-- [ ] **Step 10.3: Open PR**
-
-Run:
-```bash
-gh pr create \
- --title "fix(oauth): hotfix Windsurf login — drop dead PKCE flow, promote import-token" \
- --body "$(cat <<'EOF'
-## Summary
-
-The Windsurf provider's PKCE OAuth URL (`https://app.devin.ai/editor/signin`) returns
-404 post-Cognition rebrand, leaving users unable to log in. This PR retires the dead
-flow and makes import-token (from `https://windsurf.com/show-auth-token`) the only
-supported login path. Phase 2 (Firebase OAuth + RegisterUser, ported from
-`fendoushaonian/WindSurf-gRPC-API`) will follow in a separate PR.
-
-Spec: \`docs/superpowers/specs/2026-05-29-windsurf-login-fix-design.md\`.
-
-## Changes
-
-- \`src/lib/oauth/providers/windsurf.ts\` — drop \`buildAuthUrl\`, set \`flowType: "import_token"\`
-- \`src/app/api/oauth/[provider]/[action]/route.ts\` — return 410 Gone for retired actions (\`authorize\`, \`start-callback-server\`, \`poll-callback\`) on \`windsurf\` & \`devin-cli\`. Codex unchanged.
-- \`src/lib/oauth/constants/oauth.ts\` — annotate retired PKCE fields, no behaviour change
-- \`src/shared/components/OAuthModal.tsx\` — hide "Sign in with browser" button for windsurf/devin-cli, show import-token panel + \`windsurf.com/show-auth-token\` link
-- i18n — update onboarding steps in all locales
-- Tests — new assertions covering disabled flowType, 410 responses, import-token still functional
-
-No DB migration. Existing connections (\`accessToken\` already saved) continue working.
-
-## Test plan
-
-- [x] \`npm run test:unit\` — green
-- [x] \`npm run test:coverage\` — 75/75/75/70 met
-- [x] \`npm run lint\` + \`typecheck:core\` — 0 errors
-- [x] \`curl GET /api/oauth/windsurf/authorize\` → 410
-- [x] \`curl GET /api/oauth/devin-cli/start-callback-server\` → 410
-- [x] \`curl GET /api/oauth/codex/authorize\` → unchanged
-- [ ] Manual: paste valid token, send 1 chat request to \`swe-1\` (blocked on tester with credentials)
-
-## Rollback
-
-Revert this single PR. No schema changes; existing tokens unaffected.
-EOF
-)" \
- --base main \
- --head fix/windsurf-login-2026-05-29
-```
-
-- [ ] **Step 10.4: Confirm CI starts**
-
-Run:
-```bash
-gh pr checks 2>&1 | tail -10
-```
-Expected: workflows queued. If failing, address in follow-up commits on the same branch.
-
----
-
-## Self-Review Notes
-
-- **Spec coverage**: All Phase 1 items in `docs/superpowers/specs/2026-05-29-windsurf-login-fix-design.md` mapped to tasks 1-8. Phase 2 explicitly out of scope.
-- **Type consistency**: `flowType: "import_token"` used consistently. `RETIRED_PKCE_PROVIDERS` & `PKCE_CALLBACK_PROVIDERS` kept distinct.
-- **Placeholder scan**: Task 8 has one `` placeholder that the engineer resolves via Step 8.1 grep — necessary because i18n location varies. Task 3 has `` — resolved via Step 3.1 grep.
-- **Frequent commits**: 6 commits across the implementation (Tasks 3, 5, 6, 7, 8 each commit; Task 1+2 share commit at Step 3.5).
-- **TDD**: Tasks 1, 4 write tests first; Tasks 2, 5 make them pass.
diff --git a/docs/superpowers/specs/2026-05-29-windsurf-login-fix-design.md b/docs/superpowers/specs/2026-05-29-windsurf-login-fix-design.md
deleted file mode 100644
index 73bbd280d2..0000000000
--- a/docs/superpowers/specs/2026-05-29-windsurf-login-fix-design.md
+++ /dev/null
@@ -1,272 +0,0 @@
-# Windsurf Login Fix — Design
-
-**Date:** 2026-05-29
-**Status:** Draft (awaiting approval)
-**Owner:** OmniRoute team
-**Related:** `src/lib/oauth/providers/windsurf.ts`, `src/lib/oauth/constants/oauth.ts`, `open-sse/executors/windsurf.ts`
-
----
-
-## Problem
-
-OAuth URL yang di-generate untuk Windsurf provider kembali **404** saat user buka di browser:
-
-```
-https://app.devin.ai/editor/signin?response_type=code&redirect_uri=...&code_challenge=...
-```
-
-Akibatnya user tidak bisa login ke Windsurf via OmniRoute. Provider effectively broken.
-
-### Root cause
-
-1. URL `app.devin.ai/editor/signin` di-extract dari binary Devin CLI lama dan di-hardcode di `src/lib/oauth/constants/oauth.ts:343`. Endpoint ini sudah dihapus pasca rebrand Cognition/Windsurf.
-2. Reference document yang awalnya disediakan (`dwgx/WindsurfAPI/docs/analysis-v1.9.5.md`) tidak membahas login flow — hanya proxy/protobuf architecture.
-3. Reverse-engineering ulang dari `language_server_linux_x64` (Go-stripped, 188 MB) terlalu mahal.
-4. Repo `fendoushaonian/WindSurf-gRPC-API` sudah RE flow login Windsurf yang aktual: **Firebase OAuth + RegisterUser**, bukan PKCE ke `app.devin.ai`.
-
-### Actual Windsurf login flow (per fendoushaonian/WindSurf-gRPC-API)
-
-```
-1. User auth dengan Google/GitHub/Microsoft OAuth atau email/password
-2. POST identitytoolkit.googleapis.com/v1/accounts:signInWithIdp?key=
- body: { postBody: "id_token=&providerId=google.com",
- requestUri: "https://windsurf.com/login",
- returnSecureToken: true }
- → returns { idToken, refreshToken, email, localId }
-3. POST register.windsurf.com/exa.seat_management_pb.SeatManagementService/RegisterUser
- body: { firebase_id_token: idToken }
- → returns { api_key: "sk-ws-..." } ← ini WINDSURF_API_KEY untuk inference
-4. Refresh: POST securetoken.googleapis.com/v1/token (grant_type=refresh_token)
-```
-
-Konstanta publik yang harus di-embed (extracted from Windsurf extension.js, public Firebase Web key — bukan secret):
-- `FIREBASE_API_KEY = ""`
-- `GOOGLE_CLIENT_ID = "957777847521-egrk5uakal87pjkqctk89fe7b7qtd1dq.apps.googleusercontent.com"`
-- `REGISTER_SERVER = "https://register.windsurf.com"`
-
----
-
-## Solution Overview
-
-Two-phase delivery:
-
-| Phase | Goal | Path |
-|---|---|---|
-| **1. Hotfix** | Hentikan 404, restore minimum-viable login | Hapus PKCE flow, promote import-token sebagai primary path |
-| **2. Automation** | Restore browser-based login experience | Port Firebase OAuth + RegisterUser flow dari `fendoushaonian/WindSurf-gRPC-API` |
-
-Phase 1 ship dulu sebagai single PR untuk un-block user. Phase 2 ship di PR terpisah behind feature flag.
-
----
-
-## Phase 1 — Hotfix (Option B: import-token only)
-
-### Architecture
-
-Hapus seluruh PKCE browser flow yang broken. Promosikan **import-token** sebagai satu-satunya path resmi:
-- User klik "Connect Windsurf" → modal langsung tampil paste-token
-- Tombol "Get token" buka tab baru ke `https://windsurf.com/show-auth-token` (URL ini masih hidup, return HTTP 200)
-- User copy token, paste, OmniRoute test connection, simpan
-
-### Files touched
-
-| File | Change |
-|---|---|
-| `src/lib/oauth/providers/windsurf.ts` | Hapus `buildAuthUrl`, `flowType: "authorization_code_pkce"`, `callbackPath`, `callbackPort`. Sisakan `config` + `validateImportToken` helper. Set `flowType: "import_token"`. |
-| `src/lib/oauth/constants/oauth.ts` | Comment out / hapus field obsolete: `authorizeUrl`, `codeChallengeMethod`, `callbackPort`, `callbackPath`, `apiServerUrl`, `exchangePath`. Tambah comment block: `// PKCE OAuth flow disabled 2026-05-29 — app.devin.ai/editor/signin returns 404 post-rebrand. Use import-token from showAuthTokenUrl. Phase 2 will restore Firebase OAuth.` Keep `inferenceUrl`, `showAuthTokenUrl`, `firebaseApiKey`, `ideName`. |
-| `src/lib/oauth/providers/index.ts` | Drop `"windsurf"` & `"devin-cli"` dari `PKCE_CALLBACK_PROVIDERS`. Keep di `IMPORT_TOKEN_PROVIDERS`. |
-| `src/app/api/oauth/[provider]/[action]/route.ts` | Saat `provider === "windsurf"` atau `"devin-cli"` masuk ke `start-callback-server` / `authorize` action: return HTTP 410 `Gone` dengan body `{ error: "PKCE OAuth disabled for windsurf — use import-token via /api/oauth//import-token" }`. Routing ke `import-token` action tidak berubah. |
-| `src/shared/components/OAuthModal.tsx` | Saat provider `"windsurf"` / `"devin-cli"`: hide tombol "Sign in with browser" + "Use device code". Show single panel: paste-token textarea + tombol "Get your token" (`window.open(showAuthTokenUrl)`). |
-| Docs i18n keys | Update `docs.windsurf.steps` di 39 bahasa: hapus mention "click Sign In", ganti dengan "open windsurf.com/show-auth-token, copy your API token, paste it here". |
-| `tests/unit/windsurf-devin-executors.test.ts` | Tambah test `generateAuthData("windsurf")` returns error / throws — PKCE explicitly disabled. Tambah test untuk import-token happy path + invalid-token rejection. |
-| (No DB migration needed) | Connection schema tidak berubah. Existing connections tetap jalan. |
-
-### Behavior change
-
-| Before | After |
-|---|---|
-| User klik "Sign in" → browser → 404 | User klik "Connect Windsurf" → modal paste-token → "Get token" button buka windsurf.com/show-auth-token tab → user paste → connection saved |
-| `generateAuthData("windsurf")` returns auth URL | `generateAuthData("windsurf")` throws `OAuthFlowDisabledError` |
-| `/api/oauth/windsurf/start-callback-server` 200 | `/api/oauth/windsurf/start-callback-server` 410 Gone |
-
-### Validation plan
-
-- Unit: `tests/unit/windsurf-devin-executors.test.ts` covers PKCE-disabled path + import-token happy path
-- Integration: 1 manual smoke — paste valid token from `windsurf.com/show-auth-token`, send 1 chat request via `swe-1`, expect 200
-- Regression: existing connections (api_key already saved) MUST continue to work — no schema migration, no re-auth required
-- Coverage gate: stays ≥75/75/75/70 (Phase 1 mostly removes code, doesn't add untested logic)
-
-### Risk & rollback
-
-- **Risk**: low. Cuma hapus dead path + UI copy update.
-- **Rollback**: revert single commit. Existing connections tidak terpengaruh karena schema tidak berubah.
-
-### Success criteria Phase 1
-
-- [ ] `npm run test:unit` pass (semua test windsurf-devin-executors)
-- [ ] `npm run test:coverage` ≥ 75/75/75/70
-- [ ] `npm run typecheck:core` clean
-- [ ] Manual: paste valid token → connection saved → 1 chat request lewat swe-1 berhasil
-- [ ] Manual: GET `/api/oauth/windsurf/start-callback-server` → 410 Gone
-- [ ] UI: WindsurfModal tidak menampilkan tombol "Sign in with browser" lagi
-
----
-
-## Phase 2 — Firebase OAuth integration (port `fendoushaonian/WindSurf-gRPC-API`)
-
-### Goal
-
-Restore browser-based login automation. User klik tombol Google/GitHub/Microsoft/email → OmniRoute auto-dapat `sk-ws-...` API key + Firebase refresh token. Schedule auto-refresh sebelum token expire.
-
-### Reference implementation
-
-- Repo: `https://github.com/fendoushaonian/WindSurf-gRPC-API`
-- Files studied: `windsurf_api/auth.py`, `windsurf_api/services/seat_management.py` (RegisterUser), `windsurf_api/client.py`
-- Approach: port logic-nya ke TypeScript, follow OmniRoute conventions (Zod schemas, `buildErrorBody`, `resolvePublicCred`, AES-256-GCM at-rest)
-
-### Architecture
-
-```
-┌─────────────────────────────────────────────────────────────────────┐
-│ User flow │
-└─────────────────────────────────────────────────────────────────────┘
-
-[Google/GitHub/MS] [Email/password]
- │ │
- │ User klik tombol di WindsurfLoginModal │
- ▼ ▼
-┌──────────────────────────────┐ ┌──────────────────────┐
-│ POST /api/oauth/windsurf/ │ │ POST /api/oauth/ │
-│ firebase │ │ windsurf/firebase │
-│ body: { method: "google", │ │ body: { method: │
-│ credential: │ │ "email_login", │
-│ } │ │ credential: {...}} │
-└────────────┬─────────────────┘ └─────────┬────────────┘
- │ │
- ▼ ▼
- ┌─────────────────────────────────────────────────────────────────┐
- │ open-sse/services/windsurfFirebase.ts │
- │ - signInWithIdp(provider, oauth_token) → Firebase id_token │
- │ - signInWithPassword(email, password) → Firebase id_token │
- │ - signUp(email, password) → Firebase id_token │
- │ - refreshIdToken(refresh_token) → new id_token │
- └────────────────────────┬────────────────────────────────────────┘
- │ Firebase id_token
- ▼
- ┌─────────────────────────────────────────────────────────────────┐
- │ open-sse/services/windsurfRegister.ts │
- │ POST register.windsurf.com/.../RegisterUser │
- │ body: { firebase_id_token } │
- │ → { api_key: "sk-ws-...", user_id } │
- └────────────────────────┬────────────────────────────────────────┘
- │
- ▼
- ┌─────────────────────────────────────────────────────────────────┐
- │ Persist: │
- │ accessToken = api_key (encrypted) │
- │ firebaseRefreshToken = refresh_token (encrypted) │
- │ firebaseExpiresAt = now + 3600s │
- │ Schedule refresh worker (50min interval) │
- └─────────────────────────────────────────────────────────────────┘
-```
-
-### Files baru
-
-| File | Purpose | Lines |
-|---|---|---|
-| `src/lib/oauth/providers/windsurfFirebase.ts` | New OAuth provider entry. `flowType: "firebase_oauth"`. Methods: `loginGoogle`, `loginGithub`, `loginMicrosoft`, `loginEmail`, `signUpEmail`. | ~150 |
-| `open-sse/services/windsurfFirebase.ts` | Firebase Identity Toolkit wrapper. `signInWithIdp`, `signInWithPassword`, `signUp`, `refreshIdToken`. Uses `resolvePublicCred()` for Firebase Web key. | ~200 |
-| `open-sse/services/windsurfRegister.ts` | `register.windsurf.com` Connect-JSON client. `RegisterUser(firebase_id_token) → api_key`. Errors via `buildErrorBody()`. | ~100 |
-| `src/app/api/oauth/windsurf/firebase/route.ts` | `POST /api/oauth/windsurf/firebase` — body `{ method, credential }`. Calls Firebase + RegisterUser, creates connection. Rate-limited 5/5min/IP. | ~150 |
-| `src/lib/oauth/utils/windsurfRefresh.ts` | Background refresh worker. When token < 10min from expiry, refresh via securetoken endpoint. Re-register if api_key needs renewal. | ~120 |
-| `src/shared/components/WindsurfLoginModal.tsx` | UI: 4 tombol (Google/GitHub/Microsoft/Email). Email mode shows email+password+isSignup form. | ~200 |
-| `tests/unit/windsurfFirebase.test.ts` | Mock Firebase Identity Toolkit + register.windsurf.com responses. Cover all 5 methods + refresh flow + error sanitization assertion. | ~300 |
-| `tests/unit/windsurfRegister.test.ts` | Mock RegisterUser endpoint. Test happy path + 401 + malformed response. | ~150 |
-| `docs/frameworks/WINDSURF-LOGIN.md` | Document Firebase OAuth flow + diagram. | ~200 |
-
-### Files updated
-
-| File | Change |
-|---|---|
-| `src/lib/oauth/constants/oauth.ts` | Add `WINDSURF_FIREBASE_CONFIG` block: `firebaseApiKey` via `resolvePublicCred("windsurf_fb", "WINDSURF_FIREBASE_API_KEY")`, `googleClientId` via `resolvePublicCred("windsurf_google", "WINDSURF_GOOGLE_CLIENT_ID")`, `registerUrl: "https://register.windsurf.com"`, `firebaseAuthUrl`, `firebaseTokenUrl`, OAuth redirect uri `https://windsurf.com/login`. |
-| `open-sse/utils/publicCreds.ts` | Add embedded defaults: `windsurf_fb: ""`, `windsurf_google: "957777847521-egrk5uakal87pjkqctk89fe7b7qtd1dq.apps.googleusercontent.com"`. Both are public Web keys/client_ids — non-sensitive but must follow rule #11 pattern. |
-| `src/lib/oauth/providers/index.ts` | Register `"windsurf-firebase"` provider entry. |
-| `src/lib/db/migrations/_windsurf_firebase.sql` | Add columns to `connections`: `firebase_refresh_token TEXT NULL` (encrypted), `firebase_expires_at INTEGER NULL`. Idempotent (`IF NOT EXISTS` pattern). |
-| `src/lib/db/connections.ts` | Update CRUD to handle new optional columns. Encrypt/decrypt via existing `connectionEncryption` helpers. |
-| `src/shared/constants/providers.ts` | Mark windsurf supports both `import_token` AND `firebase_oauth`. |
-| `docs/security/PUBLIC_CREDS.md` | Add windsurf entries (Firebase Web key, Google client_id) ke registered list. |
-| `docs/reference/PROVIDER_REFERENCE.md` | Regenerate via `npm run gen:provider-reference`. |
-
-### Security
-
-- **Rule #11** (public creds): Firebase Web key + Google client_id WAJIB via `resolvePublicCred()`. Test asserts shape:
- ```ts
- expect(creds.windsurf_fb).toMatch(/^AIza[A-Za-z0-9_-]{35}$/)
- expect(creds.windsurf_google).toMatch(/^\d+-[a-z0-9]+\.apps\.googleusercontent\.com$/)
- ```
-- **Rule #12** (error sanitization): Semua response error (Firebase + RegisterUser) WAJIB lewat `buildErrorBody()`. Firebase error sering leak `email` / `localId` di message — strip explicitly. Test asserts:
- ```ts
- expect(body.error.message).not.toContain(testEmail)
- expect(body.error.message).not.toMatch(/at\s+\//) // no stack
- ```
-- **Refresh token at rest**: encrypt pakai `connectionEncryption` helper (AES-256-GCM, existing pattern). Never log refresh_token.
-- **Rate limit**: `/api/oauth/windsurf/firebase` rate-limited 5 attempt / 5min / IP via existing `src/lib/rateLimit/` middleware. Abuse Firebase tidak gratis.
-- **Feature flag**: `OMNIROUTE_WINDSURF_FIREBASE_AUTH=1` (default OFF). Beta testing, then default ON setelah stable.
-- **Dual flow**: import-token (Phase 1) tetap available. Kalau Firebase API key di-rotate Windsurf, user fall back ke import-token via UI banner.
-
-### Behavior change
-
-- WindsurfLoginModal punya 5 path: Google/GitHub/Microsoft/Email/import-token.
-- Connection schema gain optional `firebase_refresh_token`, `firebase_expires_at`. Old connections (token-only, dari Phase 1) tetap jalan tanpa migrasi.
-- Auto-refresh: api_key Windsurf TTL ±1 jam (mengikuti Firebase ID token). Worker re-register tiap 50 menit.
-- New env vars: `WINDSURF_FIREBASE_API_KEY` (override), `WINDSURF_GOOGLE_CLIENT_ID` (override), `OMNIROUTE_WINDSURF_FIREBASE_AUTH` (feature flag).
-
-### Validation plan
-
-- **Unit**: 100% Firebase + RegisterUser path coverage. Mock both endpoints, assert payload shape, error sanitization, refresh logic.
-- **Integration**: 1 E2E test gated by `RUN_WINDSURF_INT=1` — uses real test account. Spawn server, hit `/api/oauth/windsurf/firebase`, assert connection saved + chat request lewat.
-- **Manual smoke**: 4 login methods (Google/GitHub/MS/Email) end-to-end di staging. Verify auto-refresh trigger setelah 50min.
-- **Coverage gate**: ≥75/75/75/70.
-
-### Risk & rollback
-
-- **Risk: medium**. Firebase Web key bisa dirotate Windsurf — kalau itu terjadi, semua user broken. Mitigation:
- - Env override `WINDSURF_FIREBASE_API_KEY` (bisa di-update tanpa redeploy)
- - Monitoring `auth/invalid-api-key` error → auto-banner di UI: "Browser login broken, please use import-token (Phase 1 fallback)"
- - Phase 1 import-token tetap aktif — user tidak total stuck
-- **Rollback**: feature flag `OMNIROUTE_WINDSURF_FIREBASE_AUTH=0`. Migration tidak di-revert (kolom optional, NULL-safe). Phase 1 path tetap jalan.
-
-### Success criteria Phase 2
-
-- [ ] All 5 login methods work end-to-end (Google/GitHub/MS/Email login/Email signup)
-- [ ] Auto-refresh terjadi sebelum 1-hour expiry, no user-visible disruption
-- [ ] Error messages tidak leak email / localId / stack
-- [ ] CodeQL + Secret-Scanning pass (Firebase key terdeteksi false positive — dismiss with reference ke `docs/security/PUBLIC_CREDS.md`)
-- [ ] Coverage gate pass ≥75/75/75/70
-- [ ] Feature flag default OFF di first PR; default ON setelah 2 minggu beta tanpa critical issue
-
----
-
-## Migration path
-
-| Step | When | What |
-|---|---|---|
-| 1 | T+0 | Phase 1 PR merged → released → users un-blocked via import-token |
-| 2 | T+1d to T+1w | Phase 2 PR opened, behind `OMNIROUTE_WINDSURF_FIREBASE_AUTH=1` flag |
-| 3 | T+1w to T+3w | Beta testing dengan opt-in users |
-| 4 | T+3w | Flag default ON, import-token tetap available sebagai fallback |
-| 5 | T+3m | Evaluate: kalau Firebase OAuth stable + 0 critical issue, deprecate import-token UI option (keep API endpoint untuk backward compat) |
-
----
-
-## Out of scope
-
-- Migration tool untuk auto-upgrade existing import-token connections ke Firebase OAuth (user can manually re-connect kalau mau auto-refresh)
-- SAML / Enterprise SSO (not in `fendoushaonian/WindSurf-gRPC-API` reference)
-- Devin CLI specific flow (currently shares config dengan windsurf — Phase 1 hotfix covers both, Phase 2 evaluate apakah Devin CLI butuh path terpisah)
-
----
-
-## Open questions
-
-None blocking. Implementation can proceed.