mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 06:43:06 +00:00
fix(OPS94): fila de claim não morre mais em issue ready sem frontmatter id (skip + aviso; #659 ganha id B195-F1)
- 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
This commit is contained in:
parent
87ffda56e7
commit
74452cde4f
6 changed files with 61 additions and 7 deletions
3
docs/changelog/2026-08-24-ops94.md
Normal file
3
docs/changelog/2026-08-24-ops94.md
Normal file
|
|
@ -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.
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<any>} openReady
|
||||
* @returns {Array<{ issue: any, meta: Record<string, any> }>}
|
||||
*/
|
||||
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) => {
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)', () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue