fix(cleanup): keepBranch + sprint-scope siblings voor SPRINT pr_strategy

Symptoom: in een sprint met pr_strategy=SPRINT (5 tasks, 3 stories)
werden de eerste twee tasks SKIPPED door Claude (werk al in main na een
externe PR). De derde task crashte op:

  git worktree add /home/agent/.scrum4me-agent-worktrees/<id> feat/sprint-uhrbtc8z
  fatal: invalid reference: feat/sprint-uhrbtc8z

Root cause: cleanupWorktreeForTerminalStatus checkte op active siblings
binnen dezelfde **story** + verwijderde de branch bij keepBranch=false.
Voor SPRINT pr_strategy delen alle stories in de sprint één branch
(feat/sprint-<id>). Eerste task SKIPPED, story ST-1304 had geen actieve
siblings meer (T-807 was ook al SKIPPED), branch werd verwijderd. T-808
in story ST-1305 wilde reuse'n maar branch bestond niet meer.

Fix:
1. Sibling-check verbreden voor SPRINT pr_strategy: kijk naar alle
   actieve jobs in dezelfde sprint_run_id (niet alleen story_id).
2. keepBranch=true voor SKIPPED bij SPRINT pr_strategy: andere stories
   in dezelfde sprint hebben de branch nog nodig.

Tests: 341 passed (38 files). Typecheck OK.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Madhura68 2026-05-09 16:18:20 +02:00
parent 7d217cf443
commit be2b5aef3d

View file

@ -71,31 +71,57 @@ export async function cleanupWorktreeForTerminalStatus(
return return
} }
// Branch-per-story: only remove the worktree if no sibling job in the same // Branch-shared check: bepaal welke siblings dezelfde branch reuse'n.
// story is still active. If siblings are queued/claimed/running they will // - SPRINT pr_strategy → alle TASK_IMPLEMENTATION jobs in dezelfde
// re-use this branch — destroying the worktree now wastes the next claim. // sprint_run delen feat/sprint-<id>.
// - STORY pr_strategy / legacy → alle TASK_IMPLEMENTATION jobs in
// dezelfde story delen feat/story-<id>.
// Bij active siblings: defer cleanup (en in elk geval keepBranch=true)
// zodat de volgende claim de branch kan reuse'n.
const job = await prisma.claudeJob.findUnique({ const job = await prisma.claudeJob.findUnique({
where: { id: jobId }, where: { id: jobId },
select: { task: { select: { story_id: true } } }, select: {
task: { select: { story_id: true } },
sprint_run_id: true,
sprint_run: { select: { pr_strategy: true } },
},
}) })
if (job?.task) {
const activeSiblings = await prisma.claudeJob.count({ let activeSiblings = 0
let scope = ''
if (job?.sprint_run && job.sprint_run.pr_strategy === 'SPRINT') {
activeSiblings = await prisma.claudeJob.count({
where: {
sprint_run_id: job.sprint_run_id,
status: { in: ['QUEUED', 'CLAIMED', 'RUNNING'] },
id: { not: jobId },
},
})
scope = `sprint_run ${job.sprint_run_id}`
} else if (job?.task) {
activeSiblings = await prisma.claudeJob.count({
where: { where: {
task: { story_id: job.task.story_id }, task: { story_id: job.task.story_id },
status: { in: ['QUEUED', 'CLAIMED', 'RUNNING'] }, status: { in: ['QUEUED', 'CLAIMED', 'RUNNING'] },
id: { not: jobId }, id: { not: jobId },
}, },
}) })
if (activeSiblings > 0) { scope = `story ${job.task.story_id}`
console.log(
`[update_job_status] cleanup deferred for job=${jobId}: ${activeSiblings} sibling(s) still active in story ${job.task.story_id}`,
)
return
}
} }
// Keep branch when job is done and a branch was reported (agent pushed) if (activeSiblings > 0) {
const keepBranch = status === 'done' && branch !== undefined console.log(
`[update_job_status] cleanup deferred for job=${jobId}: ${activeSiblings} sibling(s) still active in ${scope}`,
)
return
}
// Keep branch when:
// - job is done en agent rapporteerde push (branch !== undefined), of
// - SPRINT pr_strategy job is skipped — andere stories delen branch.
const keepBranch =
(status === 'done' && branch !== undefined) ||
(status === 'skipped' && job?.sprint_run?.pr_strategy === 'SPRINT')
try { try {
await removeWorktreeForJob({ repoRoot, jobId, keepBranch }) await removeWorktreeForJob({ repoRoot, jobId, keepBranch })
} catch (err) { } catch (err) {