diff --git a/actions/sprint-runs.ts b/actions/sprint-runs.ts index aa9a709..18a9f86 100644 --- a/actions/sprint-runs.ts +++ b/actions/sprint-runs.ts @@ -280,21 +280,39 @@ export async function resumePausedSprintRunAction( }) } - // RUNNING when there's still a claim active, otherwise QUEUED so the - // worker picks up the next job on its next claim. const activeClaims = await tx.claudeJob.count({ where: { sprint_run_id, status: { in: ['CLAIMED', 'RUNNING'] } }, }) + const queuedJobs = await tx.claudeJob.count({ + where: { sprint_run_id, status: 'QUEUED' }, + }) + + // PBI-49 P0: a STORY auto-merge MERGE_CONFLICT lands AFTER all tasks are + // already DONE (storyBecameDone fires the conflict). Going back to QUEUED + // would hang the SprintRun forever — there is no QUEUED job to claim. + // When the scope is fully complete, transition straight to DONE; the + // dev resolved the conflict manually and the PR is theirs to merge. + let nextStatus: 'RUNNING' | 'QUEUED' | 'DONE' + let finishedAt: Date | undefined + if (activeClaims === 0 && queuedJobs === 0) { + nextStatus = 'DONE' + finishedAt = new Date() + } else if (activeClaims > 0) { + nextStatus = 'RUNNING' + } else { + nextStatus = 'QUEUED' + } await tx.sprintRun.update({ where: { id: sprint_run_id }, data: { - status: activeClaims > 0 ? 'RUNNING' : 'QUEUED', + status: nextStatus, pause_context: Prisma.JsonNull, + ...(finishedAt ? { finished_at: finishedAt } : {}), }, }) - return { ok: true as const, sprint_id: run.sprint_id } + return { ok: true as const, sprint_id: run.sprint_id, finalStatus: nextStatus } }) if (result.ok && 'sprint_id' in result) {