diff --git a/.agents/skills/plan-issue/ui-design-html.md b/.agents/skills/plan-issue/ui-design-html.md index bd3620de..4136aebd 100644 --- a/.agents/skills/plan-issue/ui-design-html.md +++ b/.agents/skills/plan-issue/ui-design-html.md @@ -25,6 +25,8 @@ Mesma doutrina para os dois papéis (fonte única — os prompts dos agentes apo O tier frontier (`designer` com pin `openai/gpt-5.6-sol`) é **recurso escasso e caro**: ele é despachado **apenas** nos dois modos de design — **Criar** ou **Criticar** o artefato — e **apenas** para item que **muda UI**. Todo trabalho que não é isso roda no **modelo padrão da sessão**, nunca no frontier. +**Provider `openai` reservado aos agentes de design.** O provider `openai` (OAuth ChatGPT Plus) existe **só** para o design: todo modelo `openai/*` — `gpt-5.6-sol`/`sol-fast`, `gpt-6-astra`, `gpt-5.6-luna`/`-fast`, `gpt-5.6-terra` — é usado **apenas** pelos agentes de design (`designer`, e `designer-campanha-solla` na própria sessão) e **apenas** para criar/criticar design. Nenhuma outra sessão ou agente (`build`, `explore`, `general`, skills, subagentes), nem `small_model`, compaction ou worker de orquestração, troca para `openai/*` via `/models`, `--model` ou config — esses rodam no modelo padrão (`deepseek/deepseek-flash`). A reserva cobre **todo** o consumo do provider, não só o dispatch do `designer`. + **Lista fechada do que NUNCA vai para o `designer` frontier:** - smoke/validação de `permission`, guard de escrita, frontmatter, `model:` ou config de agente — **a semântica de `permission` é do frontmatter, não do modelo**: valide estaticamente, ou rode o smoke com o `designer-degraded` (barato); o resultado do guard é o mesmo; diff --git a/AGENTS.md b/AGENTS.md index f92dc788..6d66e27d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,6 +16,7 @@ These are product/architecture constraints — not a ban on editing existing cod - Donations are NOT handled in this app — they run through QueroApoiar (`apoiar.me/jorgesolla`, TSE-homologated). The site only needs a "Doar" link/CTA out to that URL. - **Embasamento de produto/domínio:** `docs/research/` guarda o compêndio de literatura (campanha de DF na Bahia, geografia do voto, cartografia eleitoral) e o relatório de discovery aprovado (2026-07-21) com o playbook dado→decisão. Para métricas, análises territoriais, mapa e priorização em `/campanha`, consulte-o antes de inventar — o kernel: a disputa de DF é conta de quociente fragmentada; leitura útil é relativa e local (captura, LQ, quantis, cobertura de meta por pledges), nunca % estadual absoluto. +- **Provider `openai` reservado aos agentes de design (OPS123):** os modelos `openai/*` (`gpt-5.6-sol`/`sol-fast`, `gpt-6-astra`, `gpt-5.6-luna`/`-fast`, `gpt-5.6-terra`) existem no Teqo **só** para o design — `designer` (pin `openai/gpt-5.6-sol`; escalada `gpt-6-astra` via `/models`) e `designer-campanha-solla` (sem pin, na própria sessão) — e apenas para criar/criticar design. Nenhuma outra sessão, agente, `small_model`, compaction ou worker troca para `openai/*` por `/models`, `--model` ou config; o resto roda no modelo padrão (`deepseek/deepseek-flash`). Escopo e gate: `.agents/skills/plan-issue/ui-design-html.md` §Escopo de dispatch. **Real conventions already established in this codebase (follow these patterns; extend or edit the owner when the concern already exists — do not invent a parallel path):** diff --git a/docs/AGENT-OPS.md b/docs/AGENT-OPS.md index 1fd11a0b..96f30cfd 100644 --- a/docs/AGENT-OPS.md +++ b/docs/AGENT-OPS.md @@ -21,7 +21,7 @@ main → push dispara deploy.yml sozinho (ou workflow_dispatch manual) → prefl **Skills:** `plan-issue` (intenção + Issues) → `work-issue` (humano: Issue já claimada → impl plan → confirmação → execução) ou `agent-work-issue` (pool: já claimada → impl plan → execução sem pausa) → `/simplify` → `capture-review-debts` → PR `--base main` (GitHub) → `project-status`. `docs/roadmap.md` = legado congelado; fonte canônica = Issues do GitHub (`github.com/fsolla/teqo/issues`). -**Design hi-fi (OPS113):** item que muda UI passa pelo agente `designer` (`model: openai/gpt-5.6-sol`; crítica contestada/pixel-critical em `openai/gpt-6-astra`, mesmo agente via `/models`); a doutrina `.agents/skills/plan-issue/ui-design-html.md` é a fonte única do artefato `docs/plans/-ui-design.html` (+ `-ui-design-assets/*.svg`), que é a fonte de verdade do port — o implementador porta classe-a-classe. Indisponibilidade/quota desce a ladder — `opencode-go/deepseek-v4.1-flash` (agente `designer-degraded`) → `deepseek/deepseek-flash` (inline pelo orquestrador) — sempre gravando o tier no PR (`Design tier:`) — ou na Issue, no fluxo autônomo sem tier primário, que não abre PR e flipa para `blocked`; tier degradado marca `DEGRADED`, não certifica e para em sign-off humano (triggers no `work-issue`/`agent-work-issue`: OPS115). Ambos têm `permission` fail-closed: `edit` negado fora de `docs/plans/*-ui-design*` e `bash` com os **vetores de escrita enumerados** negados (redirecionamento, `sed -i`, `tee`, shells/interpretadores — OPS117), preservando leitura e o `prettier --write` do artefato; o resto do shell fica em `ask` (o guard cobre os vetores, não é sandbox de SO). **Escopo de dispatch fechado** (`ui-design-html.md` §Escopo de dispatch): o tier frontier só roda **Criar/Criticar** o artefato de item que muda UI — smoke/validação de `permission`/config, teste de visão, exploração e escrita de plano vão para o **modelo padrão da sessão** (o smoke do guard dos agentes de design roda no `designer-degraded`, barato, pois a semântica de `permission` é do frontmatter, não do modelo); prefira **uma** sessão para N itens UI a N sessões frias; agente de design sem pin (`designer-campanha-solla`) herda o modelo da sessão — dispare a sessão barata e só troque para frontier no design. +**Design hi-fi (OPS113):** item que muda UI passa pelo agente `designer` (`model: openai/gpt-5.6-sol`; crítica contestada/pixel-critical em `openai/gpt-6-astra`, mesmo agente via `/models`); a doutrina `.agents/skills/plan-issue/ui-design-html.md` é a fonte única do artefato `docs/plans/-ui-design.html` (+ `-ui-design-assets/*.svg`), que é a fonte de verdade do port — o implementador porta classe-a-classe. Indisponibilidade/quota desce a ladder — `opencode-go/deepseek-v4.1-flash` (agente `designer-degraded`) → `deepseek/deepseek-flash` (inline pelo orquestrador) — sempre gravando o tier no PR (`Design tier:`) — ou na Issue, no fluxo autônomo sem tier primário, que não abre PR e flipa para `blocked`; tier degradado marca `DEGRADED`, não certifica e para em sign-off humano (triggers no `work-issue`/`agent-work-issue`: OPS115). Ambos têm `permission` fail-closed: `edit` negado fora de `docs/plans/*-ui-design*` e `bash` com os **vetores de escrita enumerados** negados (redirecionamento, `sed -i`, `tee`, shells/interpretadores — OPS117), preservando leitura e o `prettier --write` do artefato; o resto do shell fica em `ask` (o guard cobre os vetores, não é sandbox de SO). **Escopo de dispatch fechado** (`ui-design-html.md` §Escopo de dispatch): o tier frontier só roda **Criar/Criticar** o artefato de item que muda UI — smoke/validação de `permission`/config, teste de visão, exploração e escrita de plano vão para o **modelo padrão da sessão** (o smoke do guard dos agentes de design roda no `designer-degraded`, barato, pois a semântica de `permission` é do frontmatter, não do modelo); prefira **uma** sessão para N itens UI a N sessões frias; agente de design sem pin (`designer-campanha-solla`) herda o modelo da sessão — dispare a sessão barata e só troque para frontier no design. **Provider `openai` reservado ao design:** nenhum outro agente/sessão — nem `small_model`, compaction ou worker de orquestração — troca para `openai/*` (`gpt-5.6-luna`/`-fast`, `sol-fast`, `terra` inclusive) por `/models`, `--model` ou config; o resto roda no modelo padrão (`deepseek/deepseek-flash`), e a reserva cobre todo o consumo do provider, não só o dispatch do `designer`. - **Agente faz sozinho:** claim (pool-supervisor no pool; humano via `pnpm agent:claim` fora da sessão — OPS33: `worktree next` claima) → implementa → **e2e local afetado** (OPS72, discricionário: roda os e2e que criou + os da mesma superfície — `pnpm test:e2e:affected`; ver skill) → **`pnpm push`** (origin = GitHub) → PR via `node scripts/github-pr.mjs` **Ready** (nunca draft) com `Closes #N` → o safety net `agent-pr-ready-automerge.yml` arma o **auto-merge nativo do GitHub** (`enablePullRequestAutoMerge`, rebase) — o servidor mergea quando o required check `CI (PR) / checks` fica verde. Regra always-on: `.agents/rules/agent-pr-workflow.mdc`. Em Cursor Cloud: `ManagePullRequest` com `draft: false` (o default draft da tool **não** vale neste repo) — o merge continua pelo safety net. - **Só humano:** aprovar o environment `production` no run (o único gate de publicação — OPS104 nunca auto-aprova; OPS107: aprove só o run escolhido e rejeite os demais — runs não aprovados não publicam nada), envs de produção no homeserver (`~/stack/teqo-1313.env`, fora do repo — o deploy as lê via BuildKit secrets), `GITHUB_TOKEN` local (PAT) para os scripts GitHub, secrets no repo GitHub, `pnpm configure:branch-protection` (idempotente; reaplicar se o drift voltar), runbook manual/rollback (`docs/ops/teqo-1313-deploy.md`). diff --git a/docs/changelog/2026-09-16-ops123.md b/docs/changelog/2026-09-16-ops123.md index 43e75953..e62aa2eb 100644 --- a/docs/changelog/2026-09-16-ops123.md +++ b/docs/changelog/2026-09-16-ops123.md @@ -1 +1 @@ -**OPS123 — escopo de dispatch do designer frontier: só design de fato (2026-09-16):** fecha o vazamento de cota do agente `designer` (`openai/gpt-5.6-sol`, OAuth ChatGPT Plus) que queimou a cota semanal do plano numa manhã — 12 das 14 sessões `openai` do dia 2026-09-16 foram smoke/validação não-design (guard de escrita OPS117, smoke OPS113, teste de visão) rodadas no frontier, além de 5 subagentes `designer` frios em paralelo para o lote do plan-issue-36. A regra de escopo passa a ter **um dono**: a doutrina `.agents/skills/plan-issue/ui-design-html.md` ganha a seção **§Escopo de dispatch** — o tier frontier só roda **Criar**/**Criticar** o artefato `docs/plans/-ui-design*` de item que muda UI; **lista fechada** do que nunca vai ao frontier (smoke/validação de `permission`/guard/frontmatter/`model:`/config de agente — a semântica de `permission` é do frontmatter, não do modelo; teste de visão/sanidade de imagem; `@explore`/`@general`/`@solla-comunicacao`; escrita/revisão de plano, impl plan, PR, changelog ou doc) e um **gate de 3 perguntas** antes de disparar (muda UI? Criar/Criticar? output é o artefato?). Os dispatchers **apontam para o dono** (sem twinning): `plan-issue/SKILL.md` (regra dura #4, `Quando` do sub-agente Designer e passo 3d) e `work-issue/execution-pipeline.md §Design` (bloco "Fora do `designer` frontier de vez" — proibição, não non-trigger). O próprio `.opencode/agent/designer.md` ganha o inegociável **"Só design de fato"** (recusa e devolve ao orquestrador fora do escopo); `designer-degraded.md` declara-se o tier certo para o smoke do guard dos agentes de design (mesmo ruleset de `permission`, barato); `designer-campanha-solla.md` (sem `model:` pin) avisa que herda o modelo da sessão — dispare a sessão barata e só troque para o frontier no design. **Anti fan-out:** prefira **uma** sessão `designer` para N itens UI do mesmo lote (cache reusado; o system prompt frio de ~25K tokens é pago uma vez) a N sessões frias em paralelo. Frontmatter/`permission` intactos (o guard OPS117 segue verde). `docs/AGENT-OPS.md` atualizado. Testes: `tests/unit/opencodeAgents.unit.spec.ts` ganha 6 asserções pinando o contrato (seção da doutrina, gate, recusa do agente, escopo do `designer-degraded`, referência nos dois skills). Sem migration, sem schema, sem UI, sem acesso/Consent. Issue #1091. +**OPS123 — escopo de dispatch do designer frontier: só design de fato (2026-09-16):** fecha o vazamento de cota do agente `designer` (`openai/gpt-5.6-sol`, OAuth ChatGPT Plus) que queimou a cota semanal do plano numa manhã — 12 das 14 sessões `openai` do dia 2026-09-16 foram smoke/validação não-design (guard de escrita OPS117, smoke OPS113, teste de visão) rodadas no frontier, além de 5 subagentes `designer` frios em paralelo para o lote do plan-issue-36. A regra de escopo passa a ter **um dono**: a doutrina `.agents/skills/plan-issue/ui-design-html.md` ganha a seção **§Escopo de dispatch** — o tier frontier só roda **Criar**/**Criticar** o artefato `docs/plans/-ui-design*` de item que muda UI; **lista fechada** do que nunca vai ao frontier (smoke/validação de `permission`/guard/frontmatter/`model:`/config de agente — a semântica de `permission` é do frontmatter, não do modelo; teste de visão/sanidade de imagem; `@explore`/`@general`/`@solla-comunicacao`; escrita/revisão de plano, impl plan, PR, changelog ou doc) e um **gate de 3 perguntas** antes de disparar (muda UI? Criar/Criticar? output é o artefato?). Os dispatchers **apontam para o dono** (sem twinning): `plan-issue/SKILL.md` (regra dura #4, `Quando` do sub-agente Designer e passo 3d) e `work-issue/execution-pipeline.md §Design` (bloco "Fora do `designer` frontier de vez" — proibição, não non-trigger). O próprio `.opencode/agent/designer.md` ganha o inegociável **"Só design de fato"** (recusa e devolve ao orquestrador fora do escopo); `designer-degraded.md` declara-se o tier certo para o smoke do guard dos agentes de design (mesmo ruleset de `permission`, barato); `designer-campanha-solla.md` (sem `model:` pin) avisa que herda o modelo da sessão — dispare a sessão barata e só troque para o frontier no design. **Anti fan-out:** prefira **uma** sessão `designer` para N itens UI do mesmo lote (cache reusado; o system prompt frio de ~25K tokens é pago uma vez) a N sessões frias em paralelo. Frontmatter/`permission` intactos (o guard OPS117 segue verde). `docs/AGENT-OPS.md` atualizado. **Provider `openai` reservado ao design:** a reserva não para no dispatch — nenhum outro agente/sessão, nem `small_model`, compaction ou worker de orquestração, troca para `openai/*` (`gpt-5.6-luna`/`-fast`, `sol-fast`, `terra` inclusive) por `/models`, `--model` ou config (cobre o uso de `gpt-5.6-luna` em sessões `build` que o relatório de uso apontou); a regra entra no `AGENTS.md` **always-on** e na doutrina/`AGENT-OPS`, e o novo `tests/unit/openaiProviderScope.unit.spec.ts` (5 casos) falha se qualquer agente fora do allow-list pinar `openai/*`, se um comando/config de projeto rotear modelo para `openai/*`, ou se a reserva sumir da doutrina/`AGENTS.md`. Testes: `tests/unit/opencodeAgents.unit.spec.ts` ganha 6 asserções pinando o contrato (seção da doutrina, gate, recusa do agente, escopo do `designer-degraded`, referência nos dois skills). Sem migration, sem schema, sem UI, sem acesso/Consent. Issue #1091. diff --git a/tests/unit/openaiProviderScope.unit.spec.ts b/tests/unit/openaiProviderScope.unit.spec.ts new file mode 100644 index 00000000..e049f6bc --- /dev/null +++ b/tests/unit/openaiProviderScope.unit.spec.ts @@ -0,0 +1,98 @@ +import { readdirSync, readFileSync } from 'node:fs' +import { resolve } from 'node:path' +import { describe, expect, it } from 'vitest' + +// OPS123: the OpenAI provider (ChatGPT Plus OAuth) is reserved to the design +// agents. A manual `/models` switch is covered by the always-on rule in +// `AGENTS.md`; this spec is the static half of the guard — no repo-controlled +// agent/command/config may pin an `openai/*` model outside the allow-list, and +// the doctrine keeps the reservation written down (so it cannot erode silently). +// +// The waste this pins against: non-design sessions (build/explore/general) +// running `gpt-5.6-luna`/`sol` and burning the shared weekly quota. + +const repoRoot = process.cwd() +const AGENT_DIR = resolve(repoRoot, '.opencode/agent') +const COMMAND_DIR = resolve(repoRoot, '.opencode/commands') + +/** Only these agents may use the `openai` provider — they are the design tier. */ +const ALLOWED_OPENAI_AGENTS = new Set(['designer']) + +const frontmatter = (file: string) => { + const content = readFileSync(file, 'utf8') + const match = content.match(/^---\n([\s\S]*?)\n---/) + return match ? match[1] : null +} + +const pinnedModel = (file: string) => { + const block = frontmatter(file) + if (!block) return null + const line = block.split('\n').find((entry) => entry.startsWith('model:')) + return line ? line.slice('model:'.length).trim() : null +} + +const mdFiles = (dir: string) => readdirSync(dir).filter((file) => file.endsWith('.md')) + +describe('the OpenAI provider stays reserved to the design agents (OPS123)', () => { + it('no agent outside the allow-list pins an openai/* model', () => { + const offenders = mdFiles(AGENT_DIR) + .map((file) => ({ + name: file.replace(/\.md$/, ''), + path: resolve(AGENT_DIR, file), + })) + .filter(({ name }) => !ALLOWED_OPENAI_AGENTS.has(name)) + .map(({ name, path }) => ({ name, model: pinnedModel(path) })) + .filter(({ model }) => Boolean(model?.startsWith('openai/'))) + .map(({ name, model }) => `${name} -> ${model}`) + + expect( + offenders, + `agentes fora do design não podem pinar openai/* (só ${[...ALLOWED_OPENAI_AGENTS].join(', ')} pode): ${offenders.join(', ')}`, + ).toEqual([]) + }) + + it('the designer keeps its frontier pin (the reservation is real, not empty)', () => { + expect(pinnedModel(resolve(AGENT_DIR, 'designer.md'))).toBe('openai/gpt-5.6-sol') + }) + + it('no command pins an openai model', () => { + const offenders = mdFiles(COMMAND_DIR) + .filter((file) => pinnedModel(resolve(COMMAND_DIR, file))?.startsWith('openai/')) + .map((file) => `commands/${file}`) + + expect(offenders, `comandos não pinam modelo (OPS101): ${offenders.join(', ')}`).toEqual([]) + }) + + it('no project config routes a model field to openai/*', () => { + const offenders: string[] = [] + for (const config of ['opencode.json', '.opencode/opencode.json']) { + let raw: string + try { + raw = readFileSync(resolve(repoRoot, config), 'utf8') + } catch { + continue + } + if (/"(?:model|small_model|smallModel)"\s*:\s*"openai\//.test(raw)) offenders.push(config) + } + + expect( + offenders, + `config de projeto não aponta modelo para openai/*: ${offenders.join(', ')}`, + ).toEqual([]) + }) + + it('the doctrine and AGENTS.md keep the reservation written down', () => { + const doctrine = readFileSync( + resolve(repoRoot, '.agents/skills/plan-issue/ui-design-html.md'), + 'utf8', + ) + const agents = readFileSync(resolve(repoRoot, 'AGENTS.md'), 'utf8') + + expect(doctrine, 'a doutrina deve reservar o provider openai ao design').toContain( + 'Provider `openai` reservado aos agentes de design', + ) + expect(agents, 'AGENTS.md (always-on) deve carregar a reserva').toContain( + 'Provider `openai` reservado aos agentes de design', + ) + }) +})