chore(OPS123): reserva o provider openai aos agentes de design (Closes #1091)

Nenhum modelo openai/* (luna/sol-fast/terra inclusive) fora dos agentes de design, por /models, --model, small_model ou config. Cobre o uso de gpt-5.6-luna em sessoes build. Regra always-on no AGENTS.md + doutrina/AGENT-OPS; guard estatico em tests/unit/openaiProviderScope.unit.spec.ts.
This commit is contained in:
Francisco Solla 2026-09-16 10:53:46 -03:00
parent a4a0e58add
commit f6a6d8e42b
5 changed files with 103 additions and 2 deletions

View file

@ -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;

View file

@ -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):**

View file

@ -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/<slug>-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/<slug>-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`).

View file

@ -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/<slug>-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/<slug>-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.

View file

@ -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',
)
})
})