teqo-backup/docs/plans/agent-pool-orchestrator.md
Francisco Solla 9467e0f292 fix(campaign): C140 — manual dispatch in 18 forms prevents React 19 form reset on error
Fix manual dispatch em 18 formulários de /campanha que usavam <form action={submitAction}>: campos não controlados voltavam ao defaultValue após qualquer action assentada (sucesso ou erro) do React 19.

- WizardStepFormChrome alterado uma vez (cobre 3 wizard steps)
- 15 forms recebem dispatch manual arquivo a arquivo
- AdvisorPasswordResetButton (2 micro-forms sem campo digitável) ficou como está
- Unit spec novo campaignWizardStepFormChrome.unit.spec.tsx
- CHANGELOG-AGENTS.md cleaned (98 duplicates removed)

Also fixes:
- campaignPageActor writable gate only blocks advisors (was blocking coordinators too)
- campaignLeaderships e2e stability (waitForLoadState before attribute assertion)
2026-08-23 17:54:19 -03:00

517 lines
37 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
name: Agent pool orchestrator
overview: 'Supervisor remoto stateless (GitHub Actions tick) que mantém até 5 Cursor Cloud Agents rodando work-issue sobre Issues ready; claim coordenado pelo supervisor, modelo da Issue no spawn, estado em repo variables + derivação; promote stage→main continua humano.'
todos:
- id: fase0-spike
content: 'Spike API Cursor: GET /v1/models, spawn com model explícito, agentId idempotente, boot real'
status: pending
- id: fase1-fundacao
content: 'PR A: eligibility + state + CLI status/tick --dry-run + testes unit'
status: pending
- id: fase2-spawn
content: 'PR B: cursor client + prompt + models + claim coordenado + workflow dispatch'
status: pending
- id: fase3-ciclo
content: 'PR C: schedule + replenish + failure path + migration cap + pause + auto-stop'
status: pending
- id: fase4-ops
content: 'PR D: skill agent-pool + AGENT-OPS + CHANGELOG + smoke remoto'
status: pending
isProject: false
---
# Orquestrador de pool de agentes Cursor Cloud (Teqo)
## 1. Resumo executivo
Um **supervisor remoto stateless** mantém até **5 Cursor Cloud Agents** em paralelo, cada um
executando `work-issue` sobre uma GitHub Issue `ready` e elegível para autonomia. O supervisor
é um **workflow do GitHub Actions** (`.github/workflows/agent-pool.yml`) que roda
`node scripts/agent-pool.mjs tick` a cada 10 minutos, a cada merge em `stage`, e sob
`workflow_dispatch` — nenhum processo local, nenhum daemon, laptop fechado não importa.
A cada tick, um script Node determinístico (testável, sem deps npm): lê o estado do pool
(repo variables), deriva os workers ativos (API Cursor + labels das Issues), reconcilia falhas,
calcula `gap = maxSlots - ativos`, **claima ele mesmo** as próximas Issues elegíveis (flip
`ready→in-progress` com lock otimista — alocador único, sem race) e spawna workers via
`POST https://api.cursor.com/v1/agents` com o `model:` do frontmatter da Issue (fallback
`composer-2.5`). Para quando o humano mandar (`action=stop`) ou quando a fila drenar.
`agent:promote` nunca é chamado — promote `stage→main` continua humano.
Decisão central: o supervisor é **GitHub Actions**, não um Cursor Automation — o tick é lógica
determinística com testes unitários, e cada tick de Automation queimaria um slot de agente (de 8
do plano Pro) + tokens a cada 10 min. Automation fica documentado como plano B.
## 2. Arquitetura (supervisor remoto obrigatório)
### Três camadas
```mermaid
flowchart TB
subgraph humano [Humano]
Cmd["gh workflow run agent-pool.yml -f action=start|stop|status"]
end
subgraph sup [Supervisor remoto — GitHub Actions]
Trig["Triggers: schedule */10min · pull_request closed em stage · workflow_dispatch"]
Tick["pnpm agent:pool tick — scripts/agent-pool.mjs"]
Vars["Estado: repo variables POOL_*"]
Derive["Ativos derivados: Cursor API + labels GitHub"]
Trig --> Tick
Tick <--> Vars
Tick <--> Derive
end
subgraph workers [Workers — Cursor Cloud Agents, cap 5]
Spawn["POST /v1/agents — prompt work-issue + model da Issue"]
W["worker × N: claim pré-feito → implementa → PR base stage → watch CI até merge"]
Spawn --> W
end
Cmd -->|"workflow_dispatch (único canal)"| Trig
Tick -->|"claim coordenado + spawn até gap"| Spawn
W -->|"PR + auto-merge CI green"| Merge["merge em stage → issue-done-on-stage-merge.yml flip done"]
Merge -->|"trigger pull_request closed"| Trig
```
### Modelo de reconciliação: **stateless tick** (escolhido)
Cada tick é um job curto (< 2 min) e idempotente que executa, nesta ordem:
1. **Lê config** — repo variables: `POOL_ENABLED`, `POOL_MAX_SLOTS` (default 5), `POOL_PAUSED`,
`POOL_STARTED_AT`, `POOL_STARTED_BY`.
2. **Sai cedo** se `POOL_ENABLED != true` ("pool desligado") ou `POOL_PAUSED == true`
("pausado — audit solitário").
3. **Deriva workers do pool** — Issues `in-progress` cujo comentário de claim carrega o marcador
do pool (`pool-worker`) cruzadas com os runs da API Cursor (`GET /v1/agents/{id}/runs/{runId}`):
- run terminal **e** Issue `done` (CI flipou no merge) → slot livre; `POST /archive` no agente.
- run terminal **e** Issue `in-progress` com PR aberto → auto-merge em voo; slot **ocupado**,
re-avalia no próximo tick (grace 60 min → humano).
- run terminal **e** Issue `in-progress` sem PR, ou run `ERROR`/`CANCELLED`/`EXPIRED` →
**falha terminal documentada**: comentário na Issue, flip `in-progress→blocked`
(sai da fila, humano triage), archive do agente, circuit breaker (≥2 falhas na mesma Issue
→ nunca re-enfileira; conta via comentários do pool).
- run não-terminal → slot ocupado.
4. **Gargalo de migrations** — mesma query do job `migration-lock`
(`gh pr list --state open --json files`): se ≥1 PR aberto toca `src/migrations/` ou
`payload-types.ts`, Issues `needs:migration` / `serializes:[migrations]` saem da fila **deste
tick** (re-avaliadas no próximo).
5. **Fila elegível** — `isAutonomousClaimable` (seção 3) ordenada por prio → mais antiga
(paridade com `agent:claim`).
6. **Spawn** — `gap = maxSlots - ativos`; claima e spawna até `gap` workers (seção 4 e 5).
7. **Auto-stop** — fila elegível vazia **e** zero ativos → `POOL_ENABLED=false` + sumário
"pool drenado". Fila vazia com ativos → não spawna, continua tickando até drenar.
8. **Sumário** — escreve GHA job summary (markdown: slots, fila, spawns, falhas) — é a superfície
remota de status no browser.
Ticks são **serializados** por `concurrency: group: agent-pool, cancel-in-progress: false` —
invariante de alocador único. O código executado pelo tick é sempre o da **default branch**
(schedule e `pull_request` rodam sobre `main`); `workflow_dispatch` permite escolher branch
para teste.
Justificativa vs. "supervisor session" (um Cloud Agent 24/7 com loop interno): rejeitado —
timeout de sessão não documentado, queima 1 slot dos 8 permanentemente, LLM fazendo trabalho de
cron, e não há nada no loop que um tick de 30 s não faça.
### Onde persiste estado
| Dado | Onde | Por quê |
| --------------------------------------------------------- | ---------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `enabled`, `maxSlots`, `paused`, `startedAt`, `startedBy` | **Repo variables** (`gh variable get/set POOL_*`) | Escalares de config; API própria; legíveis por humano no browser (Settings → Variables) |
| `activeRuns: [{runId, issueNumber, claimedAt}]` | **Derivado, não persistido** | Estado persistido dessincroniza da realidade (crash entre spawn e write). As duas fontes de verdade — status do run na Cursor API e labels/comentários no GitHub — bastam; o comentário de claim na Issue (com a URL do agente `bc-…`) é o vínculo durável worker↔Issue |
| `lastTickAt`, histórico de ticks, último erro | **GHA runs + job summary** (`gh run list --workflow agent-pool.yml`) | O próprio run é o registro; não duplica |
| Reserva worker→Issue | **Comentário de claim na Issue** (marcador `pool-worker bc-<id> <agentUrl>`) | Visível a humanos, auditável, self-healing |
O step "set/get variables" usa `gh variable` com `GITHUB_TOKEN` e `permissions: actions: write`
(REST de Actions variables). Localmente, `pnpm agent:pool -- status` usa o `gh` auth do usuário.
### Canal canônico: **`workflow_dispatch`** (um só)
```bash
# start (qualquer máquina/phone com gh auth; também via browser Actions → Run workflow)
gh workflow run agent-pool.yml -f action=start # maxSlots default 5
gh workflow run agent-pool.yml -f action=start -f maxSlots=3
# stop (para de spawnar; ativos drenam naturalmente até o merge)
gh workflow run agent-pool.yml -f action=stop
# pausa para engineering-audit (e retomada)
gh workflow run agent-pool.yml -f action=pause
gh workflow run agent-pool.yml -f action=resume
# status / tick manual
gh workflow run agent-pool.yml -f action=status
gh workflow run agent-pool.yml -f action=tick
```
`pnpm agent:pool -- start|stop|status` local é apenas um wrapper que dispara o
`workflow_dispatch` ou lê estado — o canal continua único. Comentário `/pool start` em Issue e
trigger de Automation foram avaliados e rejeitados (seção 12): dispatch já cobre CLI, browser e
celular sem superfície nova.
## 3. Contrato de elegibilidade — `isAutonomousClaimable(issue)`
Função pura em `scripts/lib/agent-pool-eligibility.mjs`, testada por tabela. Entrada: Issue
(labels, body/frontmatter, state), `doneIds`, `migrationBusy` (bool do tick), `now`. Saída:
`{ ok: true } | { ok: false, reason }`.
| Condição | Decisão | Motivo |
| -------------------------------------------------------------------- | ---------------------------------------- | --------------------------------------------------------------------------------- |
| `state !== 'OPEN'` ou sem label `ready` | exclui (`not-ready`) | contrato básico do claim |
| label `in-progress` / `blocked` / `done` / `in-prod` | exclui (`state-label`) | estado é exclusivo |
| label `requirements-changed` | exclui (`needs-human`) | escopo mudou; gate humano pendente |
| label `needs:consent` | exclui (`needs-human`) | gate legal/produto — nunca autônomo |
| `depends:` com dep não satisfeita (não `done`/`in-prod`/fechada) | exclui (`blocked-by-deps`) | mesma regra de `agent-claim.mjs` (dep sem Issue = roadmap entregue, satisfeita) |
| `needs:migration` ou `serializes:[migrations]` **e** `migrationBusy` | exclui **neste tick** (`migration-busy`) | respeita o `migration-lock` (≤1 PR de schema); transitório, volta no próximo tick |
| sem link `docs/plans/` no body | **warn, não bloqueia** (v1) | preferir Issues com plano; loga no sumário |
| `POOL_PAUSED` (audit solitário) | pausa **global** no passo 2 do tick | engineering-audit é modo solitário (AGENT-OPS) |
| `model:` ausente/inválido | **não exclui** | fallback `composer-2.5` (seção 5) |
Issues que exigem gate de `plan-issue` (wireframes, confirmação de produto) já chegam `blocked`
ou sem `ready` — o predicado as exclui por construção, e isso é pinado em teste.
**Label nova `autonomy:confirmed`: rejeitada.** As regras sobre labels existentes cobrem o
contrato; uma label a mais é um passo manual a mais no `plan-issue` sem ganho de segurança
(as exclusões já são fail-closed).
A fila do pool é uma **extensão** da fila do `agent-claim` (mesma ordenação, predicados mais
estritos). O builder da fila é extraído para `scripts/lib/agent-github.mjs`
(`buildClaimQueue(openReady, byId)`) e compartilhado — `agent-claim.mjs` continua com
comportamento idêntico (humano pode claimar `needs:consent` conscientemente; o pool, não).
## 4. Estratégia anti-race de claim — **claim coordenado pelo supervisor**
O supervisor é o **único alocador** e o tick é serializado (concurrency group). Para cada Issue
escolhida, na mesma transação lógica do tick:
1. **Re-lê a Issue** (`gh issue view N --json labels,state`) — lock otimista idêntico ao de
`agent-claim.mjs` (recusa se `ready` sumiu ou `in-progress` apareceu — ex.: humano claimou).
2. **Flip `ready→in-progress` + comentário de claim do pool** (`Claimed by pool-worker —
worker será anunciado aqui`).
3. **Spawn** `POST /v1/agents` com `agentId` determinístico `bc-<uuid5(issueNumber, date)>`
(idempotência: re-POST do mesmo `agentId` retorna `409 agent_id_conflict` em vez de duplicar
— retry de tick crashado é seguro). Falha no spawn → rollback do flip (`in-progress→ready`)
- comentário.
4. **Edita o comentário de claim** com a URL do agente (vínculo durável usado pela derivação).
O prompt do worker diz: _"a Issue #N já está claimada para você (claim feito pelo
pool-supervisor); **não rode `pnpm agent:claim`** — comece no passo 1b da skill work-issue."_
O fluxo manual (`pnpm agent:claim` sem pool) fica intocado.
**Safety net no reconcile:** se dois agentes vivos mapearem para a mesma Issue (só possível via
erro humano no dashboard), o tick cancela o run mais novo (`POST …/runs/{runId}/cancel`) e loga.
**Por que não as alternativas:** _jitter escalonado_ reduz colisão mas não a elimina (dois
workers bootam juntos, leem a fila ao mesmo tempo) e adiciona latência; _pré-atribuição sem
flip no supervisor_ deixa a Issue `ready` visível para o próximo tick durante os ~2–5 min de
boot do worker — janela de dupla atribuição que exigiria um segundo mecanismo (comentário de
reserva com TTL). O flip síncrono no tick fecha a janela por construção, reusa o lock otimista
já testado e mantém `agent:status` verdadeiro desde o instante do spawn.
Parâmetros (só relevantes ao backoff de retry de tick): `spawnRetryBackoff = próximo tick`
(10 min) — sem retry intra-tick, falha de spawn vira warn no sumário.
## 5. Seleção de modelo
- Fonte: frontmatter `model:` da Issue (escrito por `agent:register --model`, tabela de slugs
em `.agents/skills/model-selection/SKILL.md`).
- Spawn: `model: { id: <slug>, params: [...] }` no body do `POST /v1/agents`. A API valida contra
`GET /v1/models` (ids + aliases + params, ex.: `composer-2.5` com param `fast`).
- **Namespace:** os slugs do repo (Task tool) e os da Cloud API são tabelas irmãs, não idênticas.
O spike (Fase 0) despeja `GET /v1/models` e codifica o mapeamento explícito em
`scripts/lib/agent-pool-models.mjs`: `Map<repoSlug, cloudModelId>` com entradas conhecidas
(`composer-2.5` → id composer 2.5 da API + **`fast=false`**,
`cursor-grok-4.5-{low,medium,high}` → `grok-4.5` + `effort` + **`fast=false`**,
`kimi-k3-low` → `kimi-k3` + `reasoning=low`; **sem** variantes `-fast`).
Omitir `fast` faz a API cair no default da variante (`fast=true` → usage `composer-2.5-fast`);
o resolver pina `fast=false` sempre que o modelo anuncia o param.
- **Fallback:** `model:` ausente, desconhecido ou rejeitado pela API → **`composer-2.5` + `fast=false`**
(pool "Cursor Models", custo incluído — regra 1 da model-selection). Warn no sumário do tick.
- Validação: o tick consulta `GET /v1/models` uma vez por tick (cache em memória do processo)
e nunca spawna com slug fora da lista — erro de validação não pode derrubar o tick.
## 6. API de controle
`pnpm agent:pool -- < comando >` → `scripts/agent-pool.mjs` (Node 24; `gh` CLI + `fetch` nativo,
mas com a dependência transitiva habitual de `scripts/lib/cli.mjs` → `dotenv` — o workflow instala
deps como os workflows irmãos; corrigido em 2026-07-31 após o ERR_MODULE_NOT_FOUND da primeira
ativação):
| Comando | Faz |
| ----------------------- | ------------------------------------------------------------------------------------------------------ |
| `start [--max-slots N]` | Wrapper: `gh workflow run agent-pool.yml -f action=start [-f maxSlots=N]` |
| `stop` | Idem `action=stop` (para de spawnar; ativos drenam) |
| `pause` / `resume` | Idem — modo audit solitário |
| `status` | Lê variables + Cursor API + Issues; imprime slots, Issues em voo, PRs, fila elegível, último tick/erro |
| `tick` | Executa a reconciliação (usado pelo workflow; local exige `--dry-run`) |
| `tick --dry-run` | Reconcilia **sem** claim/spawn/flip — imprime o plano do tick (default local) |
Flags de ambiente: `CURSOR_API_KEY` (obrigatória para spawn/archive/cancel; `status` degrada
para "Cursor API indisponível" sem ela), `GH_TOKEN`/`gh auth` para GitHub. Logs: stdout
estruturado por passo (`[agent:pool] …`), pt-BR nas mensagens; em GHA, o sumário markdown vai
para `GITHUB_STEP_SUMMARY`.
Secrets/permissões: `CURSOR_API_KEY` em repo secrets (só este workflow usa; nunca impressa;
rotação documentada no runbook). `GITHUB_TOKEN` com `issues: write` (flip/comentários),
`pull-requests: read` (migration cap), `contents: read`, `actions: write` (variables).
## 7. Mudanças no repo (lista de arquivos previstos)
| Arquivo | Ação | Fase |
| ---------------------------------------------- | --------------------------------------------------------------------- | --------------------------------------- |
| `scripts/lib/agent-pool-eligibility.mjs` | novo — `isAutonomousClaimable` + fila do pool | 1 |
| `scripts/lib/agent-pool-state.mjs` | novo — variables get/set + derivação de ativos | 1 |
| `scripts/agent-pool.mjs` | novo — CLI da seção 6 | 1 (status/tick dry) → 2/3 (spawn/ciclo) |
| `scripts/lib/agent-github.mjs` | extrai `buildClaimQueue()` (mesma lógica, hoje inline em agent-claim) | 1 |
| `scripts/agent-claim.mjs` | consome `buildClaimQueue()` — **comportamento idêntico** | 1 |
| `scripts/lib/agent-pool-cursor.mjs` | novo — client fino da API v1 (models/agents/runs/cancel/archive) | 2 |
| `scripts/lib/agent-pool-prompt.mjs` | novo — template do prompt do worker | 2 |
| `scripts/lib/agent-pool-models.mjs` | novo — mapeamento slug repo → `model.id` Cloud + fallback | 2 |
| `.github/workflows/agent-pool.yml` | novo — dispatch (fase 2) + schedule + `pull_request` closed (fase 3) | 2→3 |
| `package.json` | script `agent:pool` | 1 |
| `tests/unit/agentPoolEligibility.unit.spec.ts` | novo — tabela do predicado + ordenação + migration cap | 1 |
| `tests/unit/agentPoolState.unit.spec.ts` | novo — derivação de ativos, failure path, circuit breaker, gap | 1→3 |
| `tests/unit/agentPoolModels.unit.spec.ts` | novo — mapping/fallback/validação | 2 |
| `tests/unit/agentPoolPrompt.unit.spec.ts` | novo — contrato do prompt (issue N, sem claim, PR base stage) | 2 |
| `docs/plans/agent-pool-orchestrator.md` | este plano + resultados do spike | 0 |
| `.agents/skills/agent-pool/SKILL.md` | novo — runbook humano (start/stop/status/triage) | 4 |
| `docs/AGENT-OPS.md` | linha na tabela de comandos + seção curta de secrets | 4 |
| `docs/CHANGELOG-AGENTS.md` | uma entrada curta | 4 |
Nada em `src/` — sem migrations, sem gates de app (`tsc`/lint cobrem scripts; `knip`/`cycles`
não são afetados por `scripts/`, verificar no gate). `pnpm gate:fast` antes de cada push, como
sempre.
## 8. Fluxo operacional (runbook)
**Pré-requisitos (uma vez):**
1. `CURSOR_API_KEY` gerada em cursor.com/dashboard/api → repo secret
(Settings → Secrets → Actions).
2. Fila com Issues `ready` (ver `pnpm agent:status`); nenhum audit em curso.
3. Workflow mergeado em `main` — **schedule e trigger de PR só rodam da default branch**
(antes do promote, tudo funciona via `workflow_dispatch` na branch do PR B/C).
**Start remoto:** `gh workflow run agent-pool.yml -f action=start` (ou browser). Em ≤1 tick
(≤10 min; dispatch dispara na hora) os primeiros `min(maxSlots, fila)` workers aparecem em
cursor.com/agents com nome `pool-i<N>-…`.
**Monitorar:** `pnpm agent:pool status` local, ou Actions → último run → job summary, ou
cursor.com/agents. Workers abrem PR `--base stage` com `Closes #N`; auto-merge no CI green;
`issue-done-on-stage-merge.yml` flipa `done`; o tick seguinte repõe o slot.
**Stop/drain:** `action=stop` — próximo tick já não spawna; ativos seguem até o merge e o pool
marca `POOL_ENABLED=false` ao drenar. Matar worker travado: cursor.com/agents → archive (o tick
seguinte reconcilia a Issue como falha documentada) — ou `action=pause` antes para não repor.
**Agente travado / Issue envenenada:** falha terminal → Issue vai a `blocked` com comentário do
pool; humano lê o run, decide re-`ready` manual (circuit breaker impede 3ª tentativa automática)
ou deixar bloqueada.
**Audit solitário:** `action=pause` no início, `action=resume` no fim (a skill
`engineering-audit` ganha essa linha na Fase 4).
## 9. Testes
**Unit (vitest, `tests/unit/`, DATABASE_URL inválida como sempre):**
- `isAutonomousClaimable`: tabela completa label × decisão da seção 3, incl. `needs:consent` e
`requirements-changed` **nunca** claimáveis com qualquer combinação de outras labels
(fail-closed), dep sem Issue = satisfeita, `migration-busy` liga/desliga.
- Ordenação da fila: paridade com a ordem do `agent-claim --dry-run` (mesmo input → mesma ordem).
- Derivação de ativos (fixtures de Issues + runs): occupied / freed-by-done / waiting-auto-merge /
failure-path / circuit breaker (2ª falha → blocked permanente).
- Cálculo de `gap` e cap hard (nunca spawna além de `maxSlots` mesmo com fila longa).
- Model mapping: slug válido → id Cloud (+ effort/reasoning) **+ `fast=false` quando o param existe**; `-fast` **proibido** (strip + warn + pin `fast=false`);
ausente/inválido → `composer-2.5` + `fast=false`; slug fora do `GET /v1/models` → fallback + warn.
- Prompt: contém `#N`, instrução de não-claim, `gh pr create --base stage`, `Closes #N`.
- State: parse/serialize de variables; defaults.
**Smoke manual remoto (critérios de aceite):**
1. 2 Issues `kind:chore` triviais (sem `model:`) + `action=start maxSlots=2` → 2 agents em
≤15 min (boot real medido no spike; se ≤2 min não for físico — setup install — o número real
é documentado aqui e vira o critério).
2. Zero duplicata: cada worker claimou Issue diferente (comentários + labels).
3. Issues `blocked`/`requirements-changed`/`needs:consent` plantadas na fila: intactas.
4. Modelo: 1 Issue com `model: cursor-grok-4.5-high` → run criado com esse modelo (verificar no
dashboard/API); sem `model:` → composer.
5. Fila drenada → `POOL_ENABLED=false` sozinho.
6. `action=stop` → próximo tick sem spawns (≤10 min + margem).
7. `status` mostra slots/Issues/PRs/erros.
8. **Laptop desligado:** start pelo celular/browser → pool operando ≥30 min (trivialmente
satisfeito: schedule é GitHub-hosted; o limite real é o cap de 8 agentes do plano Pro).
9. Nenhuma dependência de sessão Desktop (verificável por construção: tudo é GHA + REST).
## 10. Fases de implementação (appetite)
- **Fase 0 — spike (30–45 min, antes de qualquer PR):** `GET /v1/me` + `GET /v1/models` (dump
commitado neste plano); spawn de 1 agente de teste no repo com `model.id` explícito e prompt
trivial; confirmar: boot via `.cursor/environment.json`, `gh` autenticado no worker, tempo real
de boot, formato exato do `agentId` cliente (`bc-…`) e o `409` no re-POST. Resultado: seção de
evidências atualizada + tabela de mapeamento de modelos.
- **Fase 1 — fundação read-only (appetite ~2–3h de sessão):** PR A — eligibility, state,
`buildClaimQueue` extraído, CLI `status`/`tick --dry-run`, testes. Nada spawna.
- **Fase 2 — spawn remoto (~3–4h):** PR B — cursor client, prompt, models, claim coordenado,
workflow com `workflow_dispatch` apenas. Smoke: 1 worker real numa Issue `kind:chore`.
- **Fase 3 — ciclo de vida (~3–4h):** PR C — schedule + trigger `pull_request` closed,
reposição, failure path + archive + circuit breaker, migration cap, pause/resume, auto-stop,
job summary rico.
- **Fase 4 — docs/ops (~1–2h):** PR D — skill `agent-pool`, AGENT-OPS, CHANGELOG, linha de pause
na skill engineering-audit, smoke remoto completo (seção 9) executado e registrado.
Cada PR vai `--base stage` com CI green (fluxo normal de PR, mas **sem** `agent:register`/Issue
de spec — ops infra). **Schedule só ativa após o promote humano a `main`.**
## 11. Fora de escopo
- `pnpm agent:promote` (promote `stage→main` é humano — o pool nunca chama).
- Qualquer `DATABASE_URL` de stage/prod no supervisor ou nos workers (Cloud usa
`.cursor/cloud-setup.sh` + seed mínimo; o workflow não recebe secrets de banco).
- Editar Issues `in-progress` de outros agentes/humanos (o pool só toca as que ele claimou).
- Substituir ou alterar o fluxo `plan-issue` → `work-issue` para Issues normais.
- Supervisor local (`/loop`, `while` em shell, daemon em VPS) como default — rejeitado.
- Cancelamento automático de workers no `stop` (v1: stop = parar de spawnar; kill é manual).
- Webhooks da API v0 da Cursor (v1 tem "webhooks coming soon"; o trigger `pull_request` closed
do GHA já cobre o evento que interessa).
- Multi-repo, self-hosted pools, fleet autoscaling.
## 12. Decisões travadas + opções rejeitadas
**D1 — Superfície do supervisor.**
Opções: GHA `schedule`+`workflow_dispatch` stateless / Cursor Automation cron / Cloud Agent
supervisor 24/7 / daemon em VPS.
**Recomendação: GHA stateless tick.** O tick é lógica determinística com testes unitários (a
task exige `isAutonomousClaimable` testável); custa zero slots de agente e zero tokens (Pro = 8
concorrentes — 5 workers + supervisor Automation deixariam 2 para humanos e queimariam LLM a
cada 10 min, ~144 ticks/dia); triggers nativos cobrem cron e merge; `workflow_dispatch` é remoto
por definição. Mantém o norte "só GitHub + Cursor" (Actions é GitHub) e a família de scripts
`agent:*`/`refresh-stage` (precedente direto de ops via dispatch).
Rejeitadas: Automation cron (plano B documentado — o mesmo `pnpm agent:pool tick` pode ser
embrulhado numa Automation depois, pois o script é agnóstico de superfície); supervisor 24/7
(timeout não documentado, slot permanente, LLM fazendo cron); VPS (fora do norte).
**D2 — Persistência de estado.**
Opções: repo variables + derivação / Issue dedicada "pool control" / arquivo em branch
`agent/pool-state`.
**Recomendação: repo variables para config + derivação para ativos.** Escalares em variables
(API simples, visível no browser); `activeRuns` derivado das duas fontes de verdade (Cursor API
- labels/comentários GitHub) — estado persistido de runs dessincroniza no primeiro crash.
Rejeitadas: Issue de controle (parsing por tick, ruído de comentários); branch de estado
(commit de bot por tick, histórico poluído).
**D3 — Canal de start/stop.**
**Recomendação: `workflow_dispatch` único** (CLI `gh`, browser, celular; wrapper `pnpm
agent:pool`). Rejeitadas: comentário `/pool` em Issue (segundo canal para policiar, parsing de
comentário); trigger de Automation (não é um canal de comando humano).
**D4 — Anti-race.**
**Recomendação: claim coordenado pelo supervisor** (flip síncrono no tick serializado +
`agentId` idempotente + lock otimista reusado). Rejeitadas: jitter escalonado (não elimina a
janela); pré-atribuição sem flip (janela de boot de 2–5 min com a Issue ainda `ready`);
advisory lock novo (complexidade onde o alocador único basta).
**D5 — Modelo.**
**Recomendação: mapeamento explícito `scripts/lib/agent-pool-models.mjs` validado contra
`GET /v1/models` no tick; fallback `composer-2.5`.** Rejeitada: passar o slug do repo cru sem
validação (namespaces diferem; falha de spawn por slug inválido derrubaria o ciclo).
**D6 — Label `autonomy:confirmed`.** Rejeitada — regras sobre labels existentes já são
fail-closed; label extra é passo manual sem ganho.
**D7 — PR do worker.** `autoCreatePR: false` no spawn — o worker segue a skill (`gh pr create
--base stage` + `Closes #N` + `gh pr merge --auto --merge` + watch). O auto-PR da API abriria
contra a base errada e sem o corpo do contrato. `startingRef: 'stage'` no spawn (diff mínimo
contra a base do PR).
**D8 — Slot ocupado até o merge.** Conforme a spec: ativo = do claim ao merge em `stage` (ou
falha documentada). Efeito: o throughput do pool inclui a espera do CI (~dezenas de min por PR);
com auto-merge isso é hands-off e correto por construção. Derivação: Issue `in-progress` com
marcador do pool = ocupado.
## 13. Rabbit holes
- **Spawn sem API oficial:** a API v1 existe e é pública (beta) — risco baixo, mas shapes podem
mudar; o client fica isolado em `agent-pool-cursor.mjs` e o spike pinna o contrato. Plano B se
a API regredir: Automation por worker (uma Automation por slot com prompt fixo) — documentado,
não implementado.
- **Custo 5× Cloud:** agents são cobrados a preço de API por modelo; default `composer-2.5`
(pool incluído) e `maxSlots` configurável são as rédeas; spend limit no dashboard Cursor é
pré-requisito operacional; `GET /v1/agents/{id}/usage` no `status` fica como follow-up.
- **Flake de CI com 5 PRs simultâneos:** `migration-lock` já serializa schema; e2e/unit por diff
limitam carga; risco real é fila de runners GHA — visível no job summary, fora do controle do
pool (documentado, não mitigado em v1).
- **Migration-lock starvation:** humano abre PR de schema fora do pool → pool respeita o cap e
para de spawnar migrations; o inverso (pool segurando o único slot de migration enquanto o
worker demora) é resolvido pelo drain natural + triage humano da Issue `blocked`.
- **Timeout GHA 6h vs cron:** irrelevante no modelo stateless (tick < 2 min). Schedules do
GitHub atrasam sob carga (minutos) — aceitável num tick de 10 min; o trigger
`pull_request closed` compensa o replenish pós-merge.
- **`agentId` idempotente:** formato exato (`bc-` + o quê?) e comportamento do `409` são
confirmados no spike; se não funcionar, o reconcile anti-duplicata (cancel do run mais novo)
cobre a janela.
- **Boot real do worker:** `environment.json install` (pnpm install + Postgres + migrate + seed)
leva minutos; environment builds/snapshots do Cursor aceleram boots subsequentes. O critério
"5 agents em ≤2 min" é medido no spike e re-ancorado com o número real.
- **Phantom agents no cap de concorrência:** bug conhecido (agentes terminados contando no
limite; corrigido em 2026-06-17) — o tick arquiva todo agente terminal, e o runbook inclui
"archive em cursor.com/agents" como remediação.
- **Pool × sessões humanas no mesmo GitHub:** humano claima a Issue que o tick acabou de
escolher → lock otimista do passo 4.1 recusa e o tick segue para a próxima da fila.
## 14. Evidências de plataforma (investigação)
1. **Spawn remoto com modelo existe — Cloud Agents API v1 (public beta).**
`POST https://api.cursor.com/v1/agents` com `{ prompt: { text }, model: { id, params },
repos: [{ url, startingRef }], name, agentId, autoCreatePR, envVars }`; auth Basic/Bearer com
API key de cursor.com/dashboard/api. Resposta traz `agent` + `run` iniciais.
`agentId` cliente → `409 agent_id_conflict` em re-POST (idempotência).
Fonte: <https://cursor.com/docs/cloud-agent/api/endpoints>.
2. **Fim de worker sem polling local:** `GET /v1/agents/{id}` → `latestRunId` →
`GET /v1/agents/{id}/runs/{runId}` → `status` ∈ `CREATING|RUNNING|FINISHED|ERROR|CANCELLED|
EXPIRED`, com `git.branches[].prUrl`. SSE stream existe mas não é necessária (tick amostra).
Webhooks v1: "coming soon" — o trigger `pull_request` closed do GHA cobre o evento de merge.
3. **Validação de modelos:** `GET /v1/models` retorna ids, aliases e `params` aceitos
(ex.: `composer-2` + `fast`). O mapeamento repo→API é codificado na Fase 0/2.
4. **Limites de concorrência:** plano Pro historicamente = **8 agentes simultâneos** (staff Cursor, forum
2026-04-16); múltiplos agentes no mesmo repo são permitidos; bug de agentes fantasma no
contador corrigido em 2026-06-17 — remediação: arquivar em cursor.com/agents (o tick já
arquiva terminais). Soft default do pool = 5; hard clamp `POOL_HARD_MAX_SLOTS` = **12**
(2026-08-02) — spawn falha fechado se o plano Cursor estiver abaixo.
Fonte: <https://forum.cursor.com/t/clarification-on-cloud-agent-limits-simultaneous-agents-vs-environments-repos/157584>.
5. **Branches por worker:** o agente Cloud trabalha em branch própria gerada a partir de
`startingRef` (`workOnCurrentBranch: false`, default) — exatamente o modelo `agent/<id>-<slug>`
da work-issue; colisões de branch entre workers não existem por construção.
6. **Cursor Automations** (cursor.com/automations, skill `/automate`): triggers cron/GitHub/
Slack/webhook/Linear; cada trigger = um run de cloud agent com instruções + MCPs + modelo;
cron "pode atrasar, nunca adianta". Avaliado como supervisor e rejeitado para v1 (D1); fica
como plano B de superfície.
Fonte: <https://cursor.com/docs/cloud-agent/automations>.
7. **Ambiente do worker:** repo-file managed (`.cursor/environment.json` presente → install
`pnpm install + cloud-setup.sh`: Postgres nativo + migrate + seed mínimo, sem secrets de
stage/prod). Confirmado pelo MCP `cursor-cloud-environment-info` deste próprio run.
8. **MCP `cursor-cloud`** (diagnóstico): `list-cloud-agents` com filtros de status/fonte,
`batch-fetch-details` (transcript/logs/diff), `get-events` — útil no `status` e em triage,
mas **não spawna** agentes: o spawn é sempre via REST v1.
## 14.1 Adendo do spike (2026-07-30, implementação)
Verificado neste run (Cloud Agent `bc-9cd1a18c-…`, repo fsolla/teqo):
- **Environment repo-file confirmado em produção:** o `environment-info` deste run reporta
`source: Repository` + `environmentJsonPath: .cursor/environment.json` — workers spawnados
pela API neste repo bootam com `pnpm install` + `cloud-setup.sh` (Postgres nativo + migrate +
seed mínimo), sem secrets de stage/prod. Item 7 acima confirmado empiricamente.
- **Formato de id:** runs usam `bc-<uuid>` (este run: `bc-9cd1a18c-3f2b-4661-b18b-401db63c1182`),
compatível com o `agentId` cliente do claim coordenado.
- **Tokens gh limitados degradam, não quebram:** o token deste run não lê repo variables nem
issues (`403 Resource not accessible by integration`) — `status`/dry-run degradam com avisos
por seção e o tick live permanece estrito (fail-closed). Em GHA o `GITHUB_TOKEN` com
`actions: write` é o caminho suportado.
- **Pendente (primeiro run com `CURSOR_API_KEY`):** a chave foi adicionada ao dashboard de
secrets após o início deste run — secrets só injetam em runs NOVOS. No primeiro run que a
receber: `pnpm agent:pool -- doctor` (valida `/v1/me` + despeja `/v1/models` para revisar o
mapeamento de slugs) e o smoke trivial de spawn + re-POST `agentId` → `409` (runbook na skill
`agent-pool`). Até lá, `agent-pool-models.mjs` resolve contra a tabela viva em tempo de tick
com fallback `composer-2.5` — nenhuma decisão depende de dados não verificados.