mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 15:13:07 +00:00
feat(ops): OPS17 — plan-issue blocked até plano em main
Register com --plan nasce blocked (não claimável); pnpm agent:ready promove após o plano em main. Gate da skill plan-issue endurecido. Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
This commit is contained in:
parent
ce0dcdb1cf
commit
d2cc1d2693
12 changed files with 330 additions and 18 deletions
|
|
@ -93,6 +93,8 @@ Os wrappers disparam `gh workflow run agent-pool.yml -f action=…`; acompanhe c
|
|||
|
||||
`ready` → (tick: claim coordenado, flip + marcador) → `in-progress` → worker spawnado com o `model:` da Issue (fallback `composer-2.5` + **`fast=false`** — omitir `fast` na API vira `composer-2.5-fast` no usage) → PR `--base main` com `Closes #N` → CI green → auto-merge → `done`+`in-prod` (flip determinístico do CI) → tick arquiva o agente e repõe o slot.
|
||||
|
||||
Issues nascidas de `plan-issue` com `--plan` **não** entram como `ready`: ficam `blocked` até o plano de intenção estar em `main` e `pnpm agent:ready` (OPS17). O tick não promove — só consome `ready`.
|
||||
|
||||
- **Falha terminal** (run ERROR/CANCELLED/EXPIRED ou fim sem PR): tick comenta, move a Issue para `blocked` e arquiva o agente. **Triage humana**: ler o run em cursor.com/agents, decidir — re-`ready` manual se transitório (o circuit breaker recusa a 3ª tentativa automática), corrigir a spec se sistêmico.
|
||||
- **Worker travado**: archive em cursor.com/agents → o próximo tick reconcilia como falha documentada.
|
||||
- **Duplicata**: impossível em condições normais (alocador único + lock otimista + `agentId` idempotente); o tick cancela runs extras se alguém spawnar manualmente.
|
||||
|
|
|
|||
|
|
@ -130,7 +130,7 @@ Inclua baldes **já_resolvido**, **descartar** e **defer** com uma linha de raci
|
|||
|
||||
Para cada lote com destino **registrar** ou **absorver**:
|
||||
|
||||
1. Lotes de engenharia/UX: `pnpm agent:register -- --id <ID> --title "<título>" --prio <P> --kind chore --plan docs/plans/<slug>.md` — **um lote mesclado, não um item por achado micro**. Inclua appetite e rabbit holes do lote no plano curto.
|
||||
1. Lotes de engenharia/UX: `pnpm agent:register -- --id <ID> --title "<título>" --prio <P> --kind chore --plan docs/plans/<slug>.md` — **um lote mesclado, não um item por achado micro**. Inclua appetite e rabbit holes do lote no plano curto. Com `--plan`, a Issue nasce `blocked` (OPS17) até o plano estar em `main` e `pnpm agent:ready -- --issue <N>` (no fechamento da sessão após o merge, ou via OPS18).
|
||||
2. Défice comportamental do fluxo de agentes (algo que o agente errou e a convenção não pegou): `pnpm agent:file-miss` (`kind:agent-miss`) — alimenta o harvest do `engineering-audit`.
|
||||
3. Se **absorver** num plano existente: edite o plano (nova fase/seção) — mesmo precedente "Gap vs 2022". **Se a Issue dona do plano estiver `in-progress`, NÃO a edite** — registra como Issue nova com `--depends <id-do-pai>` (destrava sozinha quando o pai flipar `done`).
|
||||
4. Se **defer**: anote o gatilho no plano-pai (Adiado com gatilho / Explicitamente fora) ou na mensagem de triage confirmada — não crie Issue só por isso.
|
||||
|
|
|
|||
|
|
@ -20,10 +20,10 @@ Esta skill transforma ideias soltas em: (1) um **plano de intenção** em `docs/
|
|||
```text
|
||||
rascunho local (Issue: —)
|
||||
→ GATE (Passo 5) + confirmação explícita do lote
|
||||
→ register com --plan → Issue NÃO claimável (blocked)
|
||||
→ register com --plan → Issue NÃO claimável (blocked; script enforça)
|
||||
→ commit + PR dos planos (Related #N, nunca Closes)
|
||||
→ merge em main
|
||||
→ promote blocked→ready (agente no Passo 6 + Action no merge — dual, idempotente)
|
||||
→ promote blocked→ready (`pnpm agent:ready` + Action OPS18 — dual, idempotente)
|
||||
→ fila / pool
|
||||
```
|
||||
|
||||
|
|
@ -31,8 +31,8 @@ rascunho local (Issue: —)
|
|||
|
||||
1. **Nada no GitHub antes do gate.** Antes da confirmação explícita do Passo 5: proibido `pnpm agent:register`, `gh issue create`, `gh pr create` / push de PR de planos. Planos locais (`Issue: —`) ok.
|
||||
2. **Confirmação = OK ao overview do lote** (ex. “confirma”, “pode registrar”). “Ok” ambíguo no meio da edição **não** dispara o Passo 6.
|
||||
3. **Register com `--plan` não nasce `ready`.** Use `--blocked` (ou o default futuro de `agent:register --plan`) até o plano estar em `main`. Sem `--plan` (chore body-only / `file-miss`), pode nascer `ready`.
|
||||
4. **Promote só depois do plano em `main`.** Nunca flipar para `ready` com o PR ainda aberto — isso recria a race de claim. Caminhos: (A) agente no fim do Passo 6 após merge; (B) Action determinística no merge que lê `Related #N` (OPS18). Ambos idempotentes.
|
||||
3. **Register com `--plan` não nasce `ready`.** O script aplica `blocked` automaticamente quando `--plan` está presente (`--blocked` explícito ainda vale para chores sem plano). Sem `--plan` (chore body-only / `file-miss`), pode nascer `ready`.
|
||||
4. **Promote só depois do plano em `main`.** Nunca flipar para `ready` com o PR ainda aberto — isso recria a race de claim. Caminhos: (A) `pnpm agent:ready -- --issue N` no fim do Passo 6 após merge; (B) Action determinística no merge que lê `Related #N` (OPS18). Ambos idempotentes.
|
||||
5. **Planos de Issues `in-progress` / `done` / `in-prod` são imutáveis.** Não editar `docs/plans/<slug>.md` nem o body de intenção dessas Issues. Refino → **plano + Issue novos** (sucessor; `depends` no pai se fizer sentido). Enquanto a Issue ainda é só `blocked`/`ready` (sem claim), editar o mesmo plano ainda é barato.
|
||||
|
||||
**Canvas UI (obrigatório se muda UI):** itens com superfície Impeccable **B / C / D** (ou qualquer mudança do que o usuário vê/toca) devem ter um **Cursor canvas** de rascunho UI/UX no gate — ver [ui-draft-canvas.md](ui-draft-canvas.md). Classe A / sem UI → sem canvas.
|
||||
|
|
@ -58,8 +58,13 @@ Aqui **não** se implementa código de produto, **não** se escreve plano de imp
|
|||
- [ ] 2. Reserva de IDs de uma vez por trilha (roadmap legacy + issuesById())
|
||||
- [ ] 3. Por item (ordem topológica): classificar → fatiar → explorar só o suficiente → intenção completa
|
||||
- [ ] 4. Sugestão de modelo × effort via model-selection (uma linha por item)
|
||||
<<<<<<< HEAD
|
||||
- [ ] 5. GATE: overview do lote + canvas UI/UX (se muda UI) + esboços de fluxo → confirmar/iterar (PARAR aqui até o humano confirmar)
|
||||
- [ ] 6. Registro: register --blocked (se --plan) → PR Related → merge → promote ready
|
||||
=======
|
||||
- [ ] 5. GATE: overview do lote + esboços de fluxo (B/C/D) → confirmar/iterar (PARAR aqui até o humano confirmar; sem Issue/PR)
|
||||
- [ ] 6. Registro: `agent:register` (`--plan` → `blocked`) → PR `Related #N` → merge → `pnpm agent:ready`
|
||||
>>>>>>> 8fb041f4 (feat(ops): OPS17 — plan-issue blocked até plano em main)
|
||||
```
|
||||
|
||||
## Passo 1 — Parse e dedup
|
||||
|
|
@ -132,26 +137,38 @@ Antes de criar Issues **ou** abrir PR de planos:
|
|||
- Esboço textual de fluxo só se ajudar; não substitui o canvas quando há UI
|
||||
- Perguntas acumuladas numa rodada, recomendação de produto primeiro
|
||||
|
||||
<<<<<<< HEAD
|
||||
**Pare e espere.** Itere (incluindo o canvas, se houver UI) até confirmação explícita do lote. Só então Passo 6.
|
||||
=======
|
||||
**Pare e espere.** Itere até confirmação explícita do lote (não basta um “ok” solto durante a edição). Só então Passo 6.
|
||||
>>>>>>> 8fb041f4 (feat(ops): OPS17 — plan-issue blocked até plano em main)
|
||||
|
||||
## Passo 6 — Registro (ordem fixa)
|
||||
## Passo 6 — Registro (não claimável até plano em `main`)
|
||||
|
||||
1. **Register** (com `--model`; com `--plan` → **`--blocked`** até o plano em `main`):
|
||||
Ordem obrigatória (OPS17):
|
||||
|
||||
1. **Register** (com `--model`; com `--plan` o script nasce `blocked` — não precisa `--blocked` extra):
|
||||
|
||||
```bash
|
||||
pnpm agent:register -- --id <ID> --title "<título>" --prio <P0..P3> \
|
||||
--depends <A,B> --kind <feature|chore|...> --plan docs/plans/<slug>.md \
|
||||
--model <slug> --blocked
|
||||
--model <slug>
|
||||
```
|
||||
|
||||
Sem `--plan`: omita `--blocked` se a Issue deve nascer claimável.
|
||||
Sem `--plan`: nasce `ready` (use `--blocked` só se quiser não-claimável sem plano).
|
||||
|
||||
2. Atualize `Issue: #N` (e status) no plano local.
|
||||
3. Commit + **`pnpm push`** + PR **Ready** `--base main` com **`Related #N`** (nunca `Closes #N` em PR só de `docs/plans/` — `plans-only-closes`).
|
||||
4. Auto-merge (`gh pr merge --auto --merge`); espere o merge em `main`.
|
||||
5. **Promote** `blocked`→`ready` na Issue (e comente o motivo). A Action de merge (OPS18) é safety net se este passo falhar — não pule o promote do agente no caminho feliz.
|
||||
5. **Promote** com o script (idempotente; só Issues `blocked` + link `docs/plans/`):
|
||||
|
||||
**NÃO faz:** editar `docs/roadmap.md`; implementar código; claim; escrever `*-impl.md`; editar plano de Issue `in-progress`/`done`/`in-prod`; marcar `ready` antes do plano em `main`.
|
||||
```bash
|
||||
pnpm agent:ready -- --issue <N[,N…]>
|
||||
```
|
||||
|
||||
A Action de merge (OPS18) é safety net se este passo falhar — não pule o promote do agente no caminho feliz.
|
||||
|
||||
**NÃO faz:** editar `docs/roadmap.md`; implementar código; claim; escrever `*-impl.md`; editar plano de Issue `in-progress`/`done`/`in-prod`; marcar `ready` antes do plano em `main`; registrar/abrir PR antes do gate.
|
||||
|
||||
## Resumo final
|
||||
|
||||
|
|
|
|||
|
|
@ -37,7 +37,8 @@ main → ci.yml full suite → vercel deploy --prod (se verde) → requeue se HE
|
|||
| Comando | Faz |
|
||||
| ------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
|
||||
| `pnpm agent:claim [-- --dry-run]` | Fila ready+unblocked por prio → `in-progress` + brief |
|
||||
| `pnpm agent:register -- --id X --title T [--prio P1] [--depends A,B] [--plan docs/plans/x.md] [--model slug]` | Cria Issue |
|
||||
| `pnpm agent:register -- --id X --title T [--prio P1] [--depends A,B] [--plan docs/plans/x.md] [--model slug]` | Cria Issue (`--plan` ⇒ `blocked` até `agent:ready`; sem plano ⇒ `ready`) |
|
||||
| `pnpm agent:ready -- --issue N[,N…]` | Pós-merge do plano: `blocked`→`ready` (só Issues com link `docs/plans/`) |
|
||||
| `pnpm agent:status` | Overview / fila / mermaid |
|
||||
| `pnpm agent:prioritize -- <issue> <P0..P3>` | Troca `prio:*` |
|
||||
| `pnpm agent:file-miss -- --title ...` | Issue `kind:agent-miss` |
|
||||
|
|
@ -48,6 +49,8 @@ main → ci.yml full suite → vercel deploy --prod (se verde) → requeue se HE
|
|||
|
||||
Labels: `ready|in-progress|blocked|done|in-prod`, `prio:*`, `kind:*`, `needs:*`.
|
||||
|
||||
**plan-issue (OPS17):** gate pós-overview fecha o lote antes de qualquer Issue/PR; register com `--plan` nasce `blocked` (não entra na fila do claim/pool); após o PR de planos mergear em `main` (`Related #N`), `pnpm agent:ready` promove a `ready`. Chores sem plano continuam nascendo `ready`.
|
||||
|
||||
## Contrato de PR
|
||||
|
||||
1. Schema → atualizar `db:seed:minimal` no mesmo PR (`needs:migration`).
|
||||
|
|
|
|||
85
docs/plans/plan-issue-lifecycle-ready-impl.md
Normal file
85
docs/plans/plan-issue-lifecycle-ready-impl.md
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
# Impl: Ciclo de vida do plan-issue — colaboração fechada antes de claim
|
||||
|
||||
Status: aprovado
|
||||
Atualizado em: 2026-08-02
|
||||
Issue: #292
|
||||
Intenção: docs/plans/plan-issue-lifecycle-ready.md
|
||||
Appetite restante: ~0,5d (scripts + skills + pins; sem UI; Action dual = OPS18)
|
||||
|
||||
Nota de execução: retomado após kill-all do worker anterior (`9ff0b84d`); cherry-pick em cima de `main` com resolução de conflito em `plan-issue/SKILL.md` (main já tinha o ciclo em prosa; esta entrega amarra o enforce em script + `agent:ready`).
|
||||
|
||||
## Leitura da intenção
|
||||
|
||||
- **Outcome:** colaborar no `/plan-issue` não cria Issue/PR até confirmação explícita pós-overview; Issue com plano só fica claimável (`ready`) depois do plano de intenção em `main`.
|
||||
- **O que NÃO negociar:** chores sem `--plan` nascem `ready`; não inventar segundo tracker; não exigir review humano no PR de planos; não verificar blob do plano a cada tick do pool.
|
||||
- **O que reavaliar:** hipótese “só skill soft” — insuficiente; contrato tem de viver em `agent:register` (default com `--plan`) + promote explícito. Action no merge = sucessor OPS18 (#296), fora deste item.
|
||||
|
||||
## Abordagem recomendada
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Gate["Passo 5 overview + OK lote"] --> Reg["agent:register --plan"]
|
||||
Reg -->|"estado blocked"| Issue["Issue #N"]
|
||||
Issue --> PlansPR["PR Related #N → main"]
|
||||
PlansPR --> Ready["agent:ready → ready"]
|
||||
Ready --> Pool["fila claim/pool"]
|
||||
```
|
||||
|
||||
**Opções consideradas:** A) só checklist na skill (soft) · B) `--plan` ⇒ `blocked` no register + `agent:ready` pós-merge (sessão) · C) novo label `needs:plan` + gate no pool
|
||||
**Recomendação:** **B** — reusa `blocked` (já exclui claim/pool); enforce no script (não depende do agente lembrar `--blocked`); promote determinístico na sessão após merge.
|
||||
**Rejeitadas:** A (já falhou na prática — race com pool); C (prolifera estados; intenção cortou rabbit hole).
|
||||
|
||||
### Decisões de engenharia
|
||||
|
||||
1. **Estado enquanto espera plano em `main`:** `blocked` (não `needs:plan`).
|
||||
- Opções: A `blocked` | B `needs:plan` | C sem Issue até merge
|
||||
- Recomendação: A — vocabulário existente; claim/pool já excluem.
|
||||
- Rejeitadas: B (label nova sem necessidade); C (quebra `Related #N`).
|
||||
|
||||
2. **Enforce no register vs só skill:** default `blocked` quando `--plan` presente.
|
||||
- Opções: A default no script | B skill manda `--blocked` | C pool checa arquivo em `main`
|
||||
- Recomendação: A — fail-closed no ponto de criação.
|
||||
- Rejeitadas: B (esquecível); C (IO no tick — rabbit hole da intenção).
|
||||
|
||||
3. **Promote v1:** `pnpm agent:ready -- --issue N[,N…]` na mesma sessão (Passo 6 pós-merge).
|
||||
- Opções: A script de sessão | B Action no merge | C humano sempre
|
||||
- Recomendação: A neste item; B = OPS18 (#296).
|
||||
- Rejeitadas: C (opaco); B fora de escopo (comentário na Issue #292).
|
||||
|
||||
4. **Nome do comando:** `agent:ready` (não reviver `agent:promote` do cutover stage→main).
|
||||
|
||||
### Componentes / mudanças
|
||||
|
||||
- **`scripts/lib/agent-plan-lifecycle.mjs`:** puro — `resolveRegisterStateLabel({ hasPlan, explicitBlocked })`, `canPromotePlanIssue(issue)` (blocked + link `docs/plans/` + open + sem in-progress/done/in-prod).
|
||||
- **`scripts/agent-register.mjs`:** usa o helper; com `--plan` nasce `blocked` (mesmo sem `--blocked`); sem `--plan` preserva `ready` / `--blocked`.
|
||||
- **`scripts/agent-ready.mjs` + `pnpm agent:ready`:** flip `blocked`→`ready` idempotente; comenta motivo; recusa se não for “aguardando plano”.
|
||||
- **`.cursor/skills/plan-issue/SKILL.md`:** Passo 5 = gate duro (sem Issue/PR até OK explícito ao lote); Passo 6 = register → PR `Related #N` → merge → `agent:ready`.
|
||||
- **`docs/AGENT-OPS.md` + skill `agent-pool` (menção curta) + nota em `capture-review-debts`:** contrato de labels / promote após plano em `main`.
|
||||
- **Tests:** `tests/unit/agentPlanLifecycle.unit.spec.ts` (register label + canPromote).
|
||||
- **Migration / Access / Consent / UI:** N/A (Impeccable A).
|
||||
|
||||
## Fases verificáveis
|
||||
|
||||
1. **Tracer** — helper puro + register + pins unit; `agent:ready` + skill/docs.
|
||||
2. **Gates** — `pnpm gate:fast`; entrega com `pnpm push`.
|
||||
|
||||
## Rabbit holes / Não escopo (engenharia)
|
||||
|
||||
- **Action dual no merge (OPS18 #296).**
|
||||
- **Checagem de blob do plano no tick do pool.**
|
||||
- **Label `needs:plan`.**
|
||||
- **Reescrever `capture-review-debts` para auto-promote** (só nota + defer para OPS18).
|
||||
- **Rename cosmético `canPromotePlanIssue` → `canMarkPlanIssueReady`** — defer; API estável para OPS18 Action.
|
||||
|
||||
## Riscos e mitigação
|
||||
|
||||
- **Issue com `--plan` fica `blocked` para sempre se Passo 6 pular promote** — mitiga OPS18; v1 documenta `agent:ready` obrigatório no Passo 6.
|
||||
- **Confundir `blocked` por dep de produto com “aguardando plano”** — `canPromotePlanIssue` exige link `docs/plans/` no body; promote sem link falha fechado.
|
||||
|
||||
## Aceite de engenharia
|
||||
|
||||
- [x] Aceite de produto da intenção ainda coberto
|
||||
- [x] Invariantes AGENTS/engineering-standards (N/A runtime app)
|
||||
- [x] Testes de domínio previstos (unit no lifecycle)
|
||||
|
||||
Self-score decision-quality: 5/5
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
# Ciclo de vida do plan-issue — colaboração fechada antes de claim
|
||||
|
||||
Status: registrado (blocked até plano em main)
|
||||
Status: em execução (impl em andamento)
|
||||
Atualizado em: 2026-08-02
|
||||
Issue: #292
|
||||
Priority: P1
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@
|
|||
"db:seed:minimal": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm --import=./scripts/seed-loader.mjs\" node scripts/seed-minimal.mjs",
|
||||
"agent:claim": "node scripts/agent-claim.mjs",
|
||||
"agent:register": "node scripts/agent-register.mjs",
|
||||
"agent:ready": "node scripts/agent-ready.mjs",
|
||||
"agent:prioritize": "node scripts/agent-prioritize.mjs",
|
||||
"agent:file-miss": "node scripts/agent-file-miss.mjs",
|
||||
"agent:pool": "node scripts/agent-pool.mjs",
|
||||
|
|
|
|||
60
scripts/agent-ready.mjs
Normal file
60
scripts/agent-ready.mjs
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
/**
|
||||
* `pnpm agent:ready` — flip plan-issue Issues from `blocked` → `ready` after
|
||||
* the intention plan is on `main` (OPS17 Passo 6). Idempotent per issue.
|
||||
*
|
||||
* pnpm agent:ready -- --issue 292
|
||||
* pnpm agent:ready -- --issue 292,293
|
||||
*
|
||||
* Fail-closed: only Issues that are open, labeled `blocked`, and link
|
||||
* `docs/plans/` in the body. Does not promote product-blocked Issues without
|
||||
* a plan link (those stay for human triage).
|
||||
*/
|
||||
|
||||
import { dieAgent, gh, ghJson, labelNames, parseArgs, setLabels } from './lib/agent-github.mjs'
|
||||
import { canPromotePlanIssue } from './lib/agent-plan-lifecycle.mjs'
|
||||
|
||||
const die = dieAgent('ready')
|
||||
const { flags } = parseArgs(process.argv.slice(2), new Set(['issue']))
|
||||
|
||||
if (!flags.issue) {
|
||||
die('Usage: pnpm agent:ready -- --issue <N[,N…]>')
|
||||
}
|
||||
|
||||
const numbers = String(flags.issue)
|
||||
.split(',')
|
||||
.map((value) => value.trim())
|
||||
.filter(Boolean)
|
||||
.map((value) => {
|
||||
if (!/^\d+$/.test(value)) die(`Invalid issue number "${value}".`)
|
||||
return Number(value)
|
||||
})
|
||||
|
||||
if (numbers.length === 0) die('Usage: pnpm agent:ready -- --issue <N[,N…]>')
|
||||
|
||||
for (const number of numbers) {
|
||||
const issue = ghJson([
|
||||
'issue',
|
||||
'view',
|
||||
String(number),
|
||||
'--json',
|
||||
'number,title,body,state,labels',
|
||||
])
|
||||
const verdict = canPromotePlanIssue(issue)
|
||||
if (!verdict.ok) {
|
||||
if (verdict.reason === 'not-blocked' && labelNames(issue).includes('ready')) {
|
||||
console.log(`[agent:ready] #${number} already ready — skip`)
|
||||
continue
|
||||
}
|
||||
die(`#${number} cannot mark ready (${verdict.reason}).`)
|
||||
}
|
||||
|
||||
setLabels(number, { add: ['ready'], remove: ['blocked'] })
|
||||
gh([
|
||||
'issue',
|
||||
'comment',
|
||||
String(number),
|
||||
'--body',
|
||||
'Promovido a `ready`: plano de intenção em `main` (ciclo OPS17 — `pnpm agent:ready`).',
|
||||
])
|
||||
console.log(`[agent:ready] #${number} blocked → ready`)
|
||||
}
|
||||
|
|
@ -5,10 +5,13 @@
|
|||
* --prio P1 --depends B79 --kind feature --plan docs/plans/x.md [--body "..."]
|
||||
*
|
||||
* Body = frontmatter (id/depends/priority) + spec (linked plan + --body text).
|
||||
* Starts life labeled `ready` (or `blocked` when --blocked).
|
||||
* Starts life labeled `ready`, or `blocked` when `--blocked` / `--plan`
|
||||
* (OPS17: plan-linked Issues are not claimable until the plan is on `main`
|
||||
* and `pnpm agent:ready` promotes them).
|
||||
*/
|
||||
|
||||
import { dieAgent, gh, issuesById, parseArgs, serializeFrontmatter } from './lib/agent-github.mjs'
|
||||
import { resolveRegisterStateLabel } from './lib/agent-plan-lifecycle.mjs'
|
||||
|
||||
const die = dieAgent('register')
|
||||
const { flags } = parseArgs(
|
||||
|
|
@ -46,8 +49,12 @@ const body = serializeFrontmatter(
|
|||
.join('\n\n'),
|
||||
)
|
||||
|
||||
const stateLabel = resolveRegisterStateLabel({
|
||||
hasPlan: Boolean(flags.plan),
|
||||
explicitBlocked: Boolean(flags.blocked),
|
||||
})
|
||||
const labels = [
|
||||
flags.blocked ? 'blocked' : 'ready',
|
||||
stateLabel,
|
||||
`prio:${priority}`,
|
||||
`kind:${flags.kind ?? 'feature'}`,
|
||||
...(flags.labels ?? '')
|
||||
|
|
@ -66,4 +73,8 @@ const url = gh([
|
|||
...labels.flatMap((label) => ['--label', label]),
|
||||
])
|
||||
|
||||
console.log(`[agent:register] created ${url}`)
|
||||
const hint =
|
||||
stateLabel === 'blocked' && flags.plan && !flags.blocked
|
||||
? ' (blocked até plano em main — depois: pnpm agent:ready -- --issue <N>)'
|
||||
: ''
|
||||
console.log(`[agent:register] created ${url}${hint}`)
|
||||
|
|
|
|||
43
scripts/lib/agent-plan-lifecycle.mjs
Normal file
43
scripts/lib/agent-plan-lifecycle.mjs
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
/**
|
||||
* Plan-issue lifecycle helpers (OPS17): register with `--plan` starts non-claimable;
|
||||
* promote to `ready` only after the intention plan is on `main`.
|
||||
*
|
||||
* Pure — no IO. Used by `agent-register` / `agent-ready` and pinned by unit tests.
|
||||
*/
|
||||
|
||||
import { HUMAN_GATE_LABELS, issueHasPlanLink } from './agent-pool-eligibility.mjs'
|
||||
import { labelNames } from './agent-github.mjs'
|
||||
|
||||
/**
|
||||
* Initial state label for a newly registered issue.
|
||||
*
|
||||
* @param {{ hasPlan: boolean, explicitBlocked?: boolean }} options
|
||||
* @returns {'ready' | 'blocked'}
|
||||
*/
|
||||
export const resolveRegisterStateLabel = ({ hasPlan, explicitBlocked = false }) => {
|
||||
if (explicitBlocked || hasPlan) return 'blocked'
|
||||
return 'ready'
|
||||
}
|
||||
|
||||
const TERMINAL_OR_ACTIVE = ['in-progress', 'done', 'in-prod']
|
||||
|
||||
/**
|
||||
* Whether an issue is waiting for its intention plan on `main` and may be
|
||||
* flipped `blocked` → `ready` by `agent:ready` (or the OPS18 Action).
|
||||
*
|
||||
* @param {{ state?: string, body?: string, labels?: Array<{ name: string }> }} issue
|
||||
* @returns {{ ok: true } | { ok: false, reason: string }}
|
||||
*/
|
||||
export const canPromotePlanIssue = (issue) => {
|
||||
if ((issue?.state ?? 'OPEN') !== 'OPEN') return { ok: false, reason: 'not-open' }
|
||||
const labels = labelNames(issue ?? { labels: [] })
|
||||
if (!labels.includes('blocked')) return { ok: false, reason: 'not-blocked' }
|
||||
if (TERMINAL_OR_ACTIVE.some((label) => labels.includes(label))) {
|
||||
return { ok: false, reason: 'state-label' }
|
||||
}
|
||||
if (HUMAN_GATE_LABELS.some((label) => labels.includes(label))) {
|
||||
return { ok: false, reason: 'needs-human' }
|
||||
}
|
||||
if (!issueHasPlanLink(issue)) return { ok: false, reason: 'no-plan-link' }
|
||||
return { ok: true }
|
||||
}
|
||||
|
|
@ -15,7 +15,7 @@ import { labelNames } from './agent-github.mjs'
|
|||
export const POOL_CIRCUIT_BREAKER_FAILURES = 2
|
||||
|
||||
const EXCLUDED_STATE_LABELS = ['in-progress', 'blocked', 'done', 'in-prod']
|
||||
const HUMAN_GATE_LABELS = ['requirements-changed', 'needs:consent']
|
||||
export const HUMAN_GATE_LABELS = ['requirements-changed', 'needs:consent']
|
||||
|
||||
/**
|
||||
* @param {ReturnType<import('./agent-github.mjs').buildClaimQueue>[number]} entry queue entry from buildClaimQueue
|
||||
|
|
@ -47,7 +47,8 @@ export const isAutonomousClaimable = (
|
|||
return { ok: true }
|
||||
}
|
||||
|
||||
/** Plan link is a preference, never a blocker (v1) — status surfaces it as a warn. */
|
||||
/** Plan link is a preference for claim/pool status (warn), never a claim blocker.
|
||||
* Promote-to-ready (`canPromotePlanIssue`) requires the link — different concern. */
|
||||
export const issueHasPlanLink = (issue) => /docs\/plans\//.test(issue?.body ?? '')
|
||||
|
||||
/**
|
||||
|
|
|
|||
89
tests/unit/agentPlanLifecycle.unit.spec.ts
Normal file
89
tests/unit/agentPlanLifecycle.unit.spec.ts
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
// @vitest-environment node
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
canPromotePlanIssue,
|
||||
resolveRegisterStateLabel,
|
||||
} from '../../scripts/lib/agent-plan-lifecycle.mjs'
|
||||
|
||||
type TestIssue = {
|
||||
number: number
|
||||
title: string
|
||||
body: string
|
||||
state: string
|
||||
labels: Array<{ name: string }>
|
||||
}
|
||||
|
||||
const issue = (over: Partial<TestIssue> = {}): TestIssue => ({
|
||||
number: 1,
|
||||
title: 'Issue de teste',
|
||||
body: 'Plano: [`docs/plans/x.md`](docs/plans/x.md)',
|
||||
state: 'OPEN',
|
||||
labels: [{ name: 'blocked' }, { name: 'prio:P2' }],
|
||||
...over,
|
||||
})
|
||||
|
||||
describe('resolveRegisterStateLabel', () => {
|
||||
it('keeps chores without a plan as ready', () => {
|
||||
expect(resolveRegisterStateLabel({ hasPlan: false })).toBe('ready')
|
||||
})
|
||||
|
||||
it('starts plan-linked issues as blocked (OPS17)', () => {
|
||||
expect(resolveRegisterStateLabel({ hasPlan: true })).toBe('blocked')
|
||||
})
|
||||
|
||||
it('honors explicit --blocked even without a plan', () => {
|
||||
expect(resolveRegisterStateLabel({ hasPlan: false, explicitBlocked: true })).toBe('blocked')
|
||||
})
|
||||
|
||||
it('stays blocked when both --plan and --blocked are set', () => {
|
||||
expect(resolveRegisterStateLabel({ hasPlan: true, explicitBlocked: true })).toBe('blocked')
|
||||
})
|
||||
})
|
||||
|
||||
describe('canPromotePlanIssue', () => {
|
||||
it('accepts an open blocked issue with a docs/plans/ link', () => {
|
||||
expect(canPromotePlanIssue(issue())).toEqual({ ok: true })
|
||||
})
|
||||
|
||||
it('rejects closed issues', () => {
|
||||
expect(canPromotePlanIssue(issue({ state: 'CLOSED' }))).toEqual({
|
||||
ok: false,
|
||||
reason: 'not-open',
|
||||
})
|
||||
})
|
||||
|
||||
it('rejects issues that are not blocked', () => {
|
||||
expect(canPromotePlanIssue(issue({ labels: [{ name: 'ready' }] }))).toEqual({
|
||||
ok: false,
|
||||
reason: 'not-blocked',
|
||||
})
|
||||
})
|
||||
|
||||
it.each(['in-progress', 'done', 'in-prod'])(
|
||||
'rejects when %s coexists with blocked',
|
||||
(stateLabel) => {
|
||||
expect(
|
||||
canPromotePlanIssue(issue({ labels: [{ name: 'blocked' }, { name: stateLabel }] })),
|
||||
).toEqual({ ok: false, reason: 'state-label' })
|
||||
},
|
||||
)
|
||||
|
||||
it('rejects human-gated blocked issues even when they link a plan', () => {
|
||||
expect(
|
||||
canPromotePlanIssue(
|
||||
issue({
|
||||
labels: [{ name: 'blocked' }, { name: 'needs:consent' }],
|
||||
}),
|
||||
),
|
||||
).toEqual({ ok: false, reason: 'needs-human' })
|
||||
})
|
||||
|
||||
it('rejects blocked issues without a plan link (product block, not awaiting plan)', () => {
|
||||
expect(canPromotePlanIssue(issue({ body: 'bloqueio jurídico' }))).toEqual({
|
||||
ok: false,
|
||||
reason: 'no-plan-link',
|
||||
})
|
||||
})
|
||||
})
|
||||
Loading…
Reference in a new issue