mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 15:13:07 +00:00
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)
517 lines
37 KiB
Markdown
517 lines
37 KiB
Markdown
---
|
||
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.
|