From 74452cde4fba20344efe002041d4277c8b87190a Mon Sep 17 00:00:00 2001 From: Francisco Solla Date: Mon, 24 Aug 2026 17:05:18 -0300 Subject: [PATCH] =?UTF-8?q?fix(OPS94):=20fila=20de=20claim=20n=C3=A3o=20mo?= =?UTF-8?q?rre=20mais=20em=20issue=20ready=20sem=20frontmatter=20id=20(ski?= =?UTF-8?q?p=20+=20aviso;=20#659=20ganha=20id=20B195-F1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - buildClaimQueue exclui Issues ready sem frontmatter id (não viram worktree por construção) e withoutIdReadyIssues() expõe as puladas - agent:claim / worktree next / pnpm issue next avisam com os números em vez de morrer — uma Issue quebrada não trava mais a fila - pin novo em agentPoolEligibility.unit.spec.ts Closes #864 --- docs/changelog/2026-08-24-ops94.md | 3 +++ scripts/agent-claim.mjs | 13 +++++++++++++ scripts/issue.mjs | 10 ++++++++-- scripts/lib/agent-forgejo.mjs | 18 ++++++++++++++++-- scripts/worktree.mjs | 10 ++++++++-- tests/unit/agentPoolEligibility.unit.spec.ts | 14 +++++++++++++- 6 files changed, 61 insertions(+), 7 deletions(-) create mode 100644 docs/changelog/2026-08-24-ops94.md diff --git a/docs/changelog/2026-08-24-ops94.md b/docs/changelog/2026-08-24-ops94.md new file mode 100644 index 00000000..c61caf48 --- /dev/null +++ b/docs/changelog/2026-08-24-ops94.md @@ -0,0 +1,3 @@ +# OPS94 — Fila de claim: Issue `ready` sem frontmatter id sai da fila com aviso (2026-08-24) + +**OPS94 (Issue #864) — `worktree next` morria com "Issue sem frontmatter id" e a Issue travava a fila inteira (2026-08-24):** a limpeza manual do tracker deu `ready` a débitos antigos e a #659 (B195/D11, wizard "Registrar pedido") — única `ready` sem bloco frontmatter — **envenenava a fila**: todo pick sem `--issue` caía nela e morria antes do flip de labels (o design já morre antes do claim — "fail loudly, never invent" — mas trava a fila atrás dela indefinidamente). **Correção em duas camadas.** _Dados:_ #659 ganhou frontmatter (`id: B195-F1`, `depends: []`, `priority: P2`, `kind: defect`), título renomeado para `B195-F1 — …` e label `prio:P2` — volta a claimar normalmente (ao vivo: `agent:claim --dry-run` lista `#659 [prio:P2] B195-F1`). _Código:_ `buildClaimQueue` agora **exclui** Issues `ready` sem id (não viram worktree por construção — `branchNameForIssue` falha por contrato); novo `withoutIdReadyIssues()` expõe as puladas; `agent:claim` / `worktree next` / `pnpm issue next` **avisam com os números** em vez de morrer — uma Issue quebrada nunca mais trava a fila. _Teste:_ pin novo em `agentPoolEligibility.unit.spec.ts` (exclusão da fila + lista de puladas). Verificação: tsc limpo, lint limpo, suíte unit completa 2458/2458 verde. diff --git a/scripts/agent-claim.mjs b/scripts/agent-claim.mjs index 7db59db4..ae1d9e3b 100644 --- a/scripts/agent-claim.mjs +++ b/scripts/agent-claim.mjs @@ -21,6 +21,7 @@ import { dieAgent, issuesById, parseArgs, + withoutIdReadyIssues, } from './lib/agent-forgejo.mjs' import { githubApi as api } from './lib/github-api.mjs' @@ -33,12 +34,24 @@ const openReady = await api.listIssues({ state: 'open', labels: 'ready', limit: // identical ordering/filtering, pinned by agentPoolEligibility.unit.spec.ts). const queue = buildClaimQueue(openReady, await issuesById()) +const skippedWithoutId = withoutIdReadyIssues(openReady) + +if (skippedWithoutId.length > 0) { + console.warn( + `[agent:claim] ${skippedWithoutId.length} issue(s) \`ready\` sem frontmatter id — fora da fila (não viram worktree): ` + + `${skippedWithoutId.map(({ issue }) => `#${issue.number}`).join(', ')} — adicione o bloco frontmatter (id/depends/...) e re-role.`, + ) +} + if (flags['dry-run'] || flags['dryrun']) { console.log(`[agent:claim] ${queue.length} unblocked ready issue(s):`) for (const entry of queue) { const model = entry.meta.model ? ` model:${entry.meta.model}` : '' console.log(` #${entry.issue.number} [${entry.priority}]${model} ${entry.issue.title}`) } + for (const { issue } of skippedWithoutId) { + console.log(` #${issue.number} [SKIP — sem frontmatter id] ${issue.title}`) + } process.exit(0) } diff --git a/scripts/issue.mjs b/scripts/issue.mjs index 177e3e7b..932ccaa6 100644 --- a/scripts/issue.mjs +++ b/scripts/issue.mjs @@ -19,9 +19,15 @@ import { githubApi as api } from './lib/github-api.mjs' const die = dieAgent('issue') const cmdNext = async () => { - const pick = await nextClaimableIssue() + const { entry: pick, skippedWithoutId } = await nextClaimableIssue() if (!pick) { - die('Fila vazia — nada `ready` desbloqueado. Rode `pnpm issue all` para a overview.') + const skipped = + skippedWithoutId.length > 0 + ? ` Issues \`ready\` sem frontmatter id fora da fila: ${skippedWithoutId.map(({ issue }) => `#${issue.number}`).join(', ')}.` + : '' + die( + `Fila vazia — nada \`ready\` desbloqueado. Rode \`pnpm issue all\` para a overview.${skipped}`, + ) } const { issue, meta, priority, satisfiedWithoutIssue } = pick const id = typeof meta.id === 'string' ? meta.id : null diff --git a/scripts/lib/agent-forgejo.mjs b/scripts/lib/agent-forgejo.mjs index 23941a69..068cb7cc 100644 --- a/scripts/lib/agent-forgejo.mjs +++ b/scripts/lib/agent-forgejo.mjs @@ -100,6 +100,18 @@ const entryForIssue = (issue, byId, doneIds) => { } } +/** Ready issues without a frontmatter `id` — unworkable: `branchNameForIssue` + * fails loudly and they can never become a worktree, so they stay out of the + * queue instead of poisoning `worktree next` forever. + * + * @param {Array} openReady + * @returns {Array<{ issue: any, meta: Record }>} + */ +export const withoutIdReadyIssues = (openReady) => + openReady + .map((issue) => ({ issue, meta: parseFrontmatter(issue.body).meta })) + .filter(({ meta }) => typeof meta.id !== 'string' || meta.id.length === 0) + /** * Claim queue shared by `agent:claim` and the agent pool: open `ready` issues * filtered to UNBLOCKED (every frontmatter `depends` id is closed or labeled @@ -115,15 +127,17 @@ const entryForIssue = (issue, byId, doneIds) => { export const buildClaimQueue = (openReady, byId) => { const doneIds = doneIdsOf(byId) - return openReady + const queue = openReady .map((issue) => entryForIssue(issue, byId, doneIds)) .filter((entry) => entry.blockedBy.length === 0) + .filter((entry) => typeof entry.meta.id === 'string' && entry.meta.id.length > 0) .sort((a, b) => { const rank = priorityRank(a.priority.replace('prio:', '')) - priorityRank(b.priority.replace('prio:', '')) return rank !== 0 ? rank : a.issue.createdAt.localeCompare(b.issue.createdAt) }) + return queue } /** @@ -225,7 +239,7 @@ export const setLabels = (number, { add = [], remove = [] }) => export const nextClaimableIssue = async () => { const openReady = await api.listIssues({ state: 'open', labels: 'ready', limit: 200 }) const queue = buildClaimQueue(openReady, await issuesById()) - return queue[0] ?? null + return { entry: queue[0] ?? null, skippedWithoutId: withoutIdReadyIssues(openReady) } } export const parseArgs = (argv, flagsWithValue) => { diff --git a/scripts/worktree.mjs b/scripts/worktree.mjs index 97eec544..dc4029c0 100644 --- a/scripts/worktree.mjs +++ b/scripts/worktree.mjs @@ -466,9 +466,15 @@ const pickNextIssue = async ({ requestedIssueNumber, die }) => { return { entry, reopened: false, directed: true } } - const pick = await nextClaimableIssue() + const { entry: pick, skippedWithoutId } = await nextClaimableIssue() if (!pick) { - die('Fila vazia — nada `ready` desbloqueado. Rode `pnpm agent:status` para ver a fila.') + const skipped = + skippedWithoutId.length > 0 + ? ` Issues \`ready\` sem frontmatter id fora da fila (corrija antes de re-rodar): ${skippedWithoutId.map(({ issue }) => `#${issue.number}`).join(', ')}.` + : '' + die( + `Fila vazia — nada \`ready\` desbloqueado. Rode \`pnpm agent:status\` para ver a fila.${skipped}`, + ) } return { entry: pick, reopened: false, directed: false } } diff --git a/tests/unit/agentPoolEligibility.unit.spec.ts b/tests/unit/agentPoolEligibility.unit.spec.ts index 34ee0e37..abde9959 100644 --- a/tests/unit/agentPoolEligibility.unit.spec.ts +++ b/tests/unit/agentPoolEligibility.unit.spec.ts @@ -6,6 +6,7 @@ import { buildClaimQueue, claimQueueEntry, claimTargetVerdict, + withoutIdReadyIssues, } from '../../scripts/lib/agent-forgejo.mjs' import { buildPoolQueue, @@ -167,8 +168,9 @@ describe('buildClaimQueue parity (shared with agent:claim)', () => { number: 2, createdAt: '2026-07-03T00:00:00Z', labels: [{ name: 'ready' }, { name: 'prio:P0' }], + body: fm([]), }) - const p2New = issue({ number: 3, createdAt: '2026-07-02T00:00:00Z' }) + const p2New = issue({ number: 3, createdAt: '2026-07-02T00:00:00Z', body: fm([]) }) const queue = buildClaimQueue([p2New, p2Old, p0], byId) expect(queue.map((item) => item.issue.number)).toEqual([2, 1, 3]) expect(queue[1]!.satisfiedWithoutIssue).toEqual(['B9']) @@ -180,6 +182,16 @@ describe('buildClaimQueue parity (shared with agent:claim)', () => { const queue = buildClaimQueue([blocked, free], byId) expect(queue.map((item) => item.issue.number)).toEqual([2]) }) + + it('excludes ready issues without a frontmatter id and surfaces them via withoutIdReadyIssues', () => { + const withId = issue({ number: 1, body: fm([]) }) + const withoutId = issue({ number: 2, body: 'defeito sem frontmatter' }) + const queue = buildClaimQueue([withoutId, withId], byId) + expect(queue.map((item) => item.issue.number)).toEqual([1]) + expect( + withoutIdReadyIssues([withoutId, withId]).map(({ issue: skipped }) => skipped.number), + ).toEqual([2]) + }) }) describe('claimQueueEntry (single-issue shape for `worktree next --issue` reopen)', () => {