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)
37 KiB
| name | overview | todos | isProject | |||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Agent pool orchestrator | 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. |
|
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
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:
- Lê config — repo variables:
POOL_ENABLED,POOL_MAX_SLOTS(default 5),POOL_PAUSED,POOL_STARTED_AT,POOL_STARTED_BY. - Sai cedo se
POOL_ENABLED != true("pool desligado") ouPOOL_PAUSED == true("pausado — audit solitário"). - Deriva workers do pool — Issues
in-progresscujo 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 /archiveno agente. - run terminal e Issue
in-progresscom PR aberto → auto-merge em voo; slot ocupado, re-avalia no próximo tick (grace 60 min → humano). - run terminal e Issue
in-progresssem PR, ou runERROR/CANCELLED/EXPIRED→ falha terminal documentada: comentário na Issue, flipin-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.
- run terminal e Issue
- Gargalo de migrations — mesma query do job
migration-lock(gh pr list --state open --json files): se ≥1 PR aberto tocasrc/migrations/oupayload-types.ts, Issuesneeds:migration/serializes:[migrations]saem da fila deste tick (re-avaliadas no próximo). - Fila elegível —
isAutonomousClaimable(seção 3) ordenada por prio → mais antiga (paridade comagent:claim). - Spawn —
gap = maxSlots - ativos; claima e spawna atégapworkers (seção 4 e 5). - 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. - 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ó)
# 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:
- Re-lê a Issue (
gh issue view N --json labels,state) — lock otimista idêntico ao deagent-claim.mjs(recusa sereadysumiu ouin-progressapareceu — ex.: humano claimou). - Flip
ready→in-progress+ comentário de claim do pool (Claimed by pool-worker — worker será anunciado aqui). - Spawn
POST /v1/agentscomagentIddeterminísticobc-<uuid5(issueNumber, date)>(idempotência: re-POST do mesmoagentIdretorna409 agent_id_conflictem vez de duplicar — retry de tick crashado é seguro). Falha no spawn → rollback do flip (in-progress→ready)- comentário.
- 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 poragent:register --model, tabela de slugs em.agents/skills/model-selection/SKILL.md). - Spawn:
model: { id: <slug>, params: [...] }no body doPOST /v1/agents. A API valida contraGET /v1/models(ids + aliases + params, ex.:composer-2.5com paramfast). - 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/modelse codifica o mapeamento explícito emscripts/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). Omitirfastfaz a API cair no default da variante (fast=true→ usagecomposer-2.5-fast); o resolver pinafast=falsesempre 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/modelsuma 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):
CURSOR_API_KEYgerada em cursor.com/dashboard/api → repo secret (Settings → Secrets → Actions).- Fila com Issues
ready(verpnpm agent:status); nenhum audit em curso. - Workflow mergeado em
main— schedule e trigger de PR só rodam da default branch (antes do promote, tudo funciona viaworkflow_dispatchna 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:consenterequirements-changednunca claimáveis com qualquer combinação de outras labels (fail-closed), dep sem Issue = satisfeita,migration-busyliga/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
gape cap hard (nunca spawna além demaxSlotsmesmo com fila longa). - Model mapping: slug válido → id Cloud (+ effort/reasoning) +
fast=falsequando o param existe;-fastproibido (strip + warn + pinfast=false); ausente/inválido →composer-2.5+fast=false; slug fora doGET /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):
- 2 Issues
kind:choretriviais (semmodel:) +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). - Zero duplicata: cada worker claimou Issue diferente (comentários + labels).
- Issues
blocked/requirements-changed/needs:consentplantadas na fila: intactas. - Modelo: 1 Issue com
model: cursor-grok-4.5-high→ run criado com esse modelo (verificar no dashboard/API); semmodel:→ composer. - Fila drenada →
POOL_ENABLED=falsesozinho. action=stop→ próximo tick sem spawns (≤10 min + margem).statusmostra slots/Issues/PRs/erros.- 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).
- 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 commodel.idexplícito e prompt trivial; confirmar: boot via.cursor/environment.json,ghautenticado no worker, tempo real de boot, formato exato doagentIdcliente (bc-…) e o409no 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,
buildClaimQueueextraído, CLIstatus/tick --dry-run, testes. Nada spawna. - Fase 2 — spawn remoto (~3–4h): PR B — cursor client, prompt, models, claim coordenado,
workflow com
workflow_dispatchapenas. Smoke: 1 worker real numa Issuekind:chore. - Fase 3 — ciclo de vida (~3–4h): PR C — schedule + trigger
pull_requestclosed, 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(promotestage→mainé humano — o pool nunca chama).- Qualquer
DATABASE_URLde 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-progressde outros agentes/humanos (o pool só toca as que ele claimou). - Substituir ou alterar o fluxo
plan-issue→work-issuepara Issues normais. - Supervisor local (
/loop,whileem 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_requestclosed 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.mjse 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) emaxSlotsconfigurável são as rédeas; spend limit no dashboard Cursor é pré-requisito operacional;GET /v1/agents/{id}/usagenostatusfica como follow-up. - Flake de CI com 5 PRs simultâneos:
migration-lockjá 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 closedcompensa o replenish pós-merge. agentIdidempotente: formato exato (bc-+ o quê?) e comportamento do409sã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)
- Spawn remoto com modelo existe — Cloud Agents API v1 (public beta).
POST https://api.cursor.com/v1/agentscom{ prompt: { text }, model: { id, params }, repos: [{ url, startingRef }], name, agentId, autoCreatePR, envVars }; auth Basic/Bearer com API key de cursor.com/dashboard/api. Resposta trazagent+runiniciais.agentIdcliente →409 agent_id_conflictem re-POST (idempotência). Fonte: https://cursor.com/docs/cloud-agent/api/endpoints. - Fim de worker sem polling local:
GET /v1/agents/{id}→latestRunId→GET /v1/agents/{id}/runs/{runId}→status∈CREATING|RUNNING|FINISHED|ERROR|CANCELLED| EXPIRED, comgit.branches[].prUrl. SSE stream existe mas não é necessária (tick amostra). Webhooks v1: "coming soon" — o triggerpull_requestclosed do GHA cobre o evento de merge. - Validação de modelos:
GET /v1/modelsretorna ids, aliases eparamsaceitos (ex.:composer-2+fast). O mapeamento repo→API é codificado na Fase 0/2. - 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. - Branches por worker: o agente Cloud trabalha em branch própria gerada a partir de
startingRef(workOnCurrentBranch: false, default) — exatamente o modeloagent/<id>-<slug>da work-issue; colisões de branch entre workers não existem por construção. - 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. - Ambiente do worker: repo-file managed (
.cursor/environment.jsonpresente → installpnpm install + cloud-setup.sh: Postgres nativo + migrate + seed mínimo, sem secrets de stage/prod). Confirmado pelo MCPcursor-cloud-environment-infodeste próprio run. - MCP
cursor-cloud(diagnóstico):list-cloud-agentscom filtros de status/fonte,batch-fetch-details(transcript/logs/diff),get-events— útil nostatuse 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-infodeste run reportasource: Repository+environmentJsonPath: .cursor/environment.json— workers spawnados pela API neste repo bootam compnpm 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 oagentIdcliente 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 oGITHUB_TOKENcomactions: 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/modelspara revisar o mapeamento de slugs) e o smoke trivial de spawn + re-POSTagentId→409(runbook na skillagent-pool). Até lá,agent-pool-models.mjsresolve contra a tabela viva em tempo de tick com fallbackcomposer-2.5— nenhuma decisão depende de dados não verificados.