diff --git a/__tests__/update-job-status-next-action.test.ts b/__tests__/update-job-status-next-action.test.ts new file mode 100644 index 0000000..3f1a870 --- /dev/null +++ b/__tests__/update-job-status-next-action.test.ts @@ -0,0 +1,25 @@ +import { describe, it, expect } from 'vitest' +import { resolveNextAction } from '../src/tools/update-job-status.js' + +describe('resolveNextAction', () => { + it('returns wait_for_job_again when queue has jobs after done', () => { + expect(resolveNextAction(3, 'done')).toBe('wait_for_job_again') + }) + + it('returns queue_empty when queue is empty after done', () => { + expect(resolveNextAction(0, 'done')).toBe('queue_empty') + }) + + it('returns wait_for_job_again when queue has jobs after failed', () => { + expect(resolveNextAction(1, 'failed')).toBe('wait_for_job_again') + }) + + it('returns queue_empty when queue is empty after failed', () => { + expect(resolveNextAction(0, 'failed')).toBe('queue_empty') + }) + + it('returns idle for running status regardless of queue count', () => { + expect(resolveNextAction(5, 'running')).toBe('idle') + expect(resolveNextAction(0, 'running')).toBe('idle') + }) +}) diff --git a/src/tools/update-job-status.ts b/src/tools/update-job-status.ts index fb5fe17..2d18940 100644 --- a/src/tools/update-job-status.ts +++ b/src/tools/update-job-status.ts @@ -97,6 +97,14 @@ const DB_STATUS_MAP = { failed: 'FAILED', } as const +export function resolveNextAction( + queueCount: number, + status: 'running' | 'done' | 'failed', +): 'wait_for_job_again' | 'queue_empty' | 'idle' { + if (status === 'running') return 'idle' + return queueCount > 0 ? 'wait_for_job_again' : 'queue_empty' +} + export async function maybeCreateAutoPr(opts: { jobId: string productId: string @@ -140,7 +148,8 @@ export function registerUpdateJobStatusTool(server: McpServer) { 'Report progress on a claimed ClaudeJob. Allowed transitions from CLAIMED/RUNNING: ' + 'running (start), done (finished), failed (error). ' + 'The Bearer token must match the token that claimed the job. ' + - 'Automatically emits an SSE event so the Scrum4Me UI updates in real time.', + 'Automatically emits an SSE event so the Scrum4Me UI updates in real time. ' + + 'Response includes next_action: when wait_for_job_again, immediately call wait_for_job again. When queue_empty, the agent batch is done.', inputSchema, }, async ({ job_id, status, branch, summary, error }) => @@ -262,6 +271,11 @@ export function registerUpdateJobStatusTool(server: McpServer) { await cleanupWorktreeForTerminalStatus(job.product_id, job_id, actualStatus, branchToWrite) } + const queueCount = await prisma.claudeJob.count({ + where: { user_id: userId, status: 'QUEUED' }, + }) + const nextAction = resolveNextAction(queueCount, actualStatus) + return toolJson({ job_id: updated.id, status: actualStatus, @@ -272,6 +286,7 @@ export function registerUpdateJobStatusTool(server: McpServer) { error: updated.error, started_at: updated.started_at?.toISOString() ?? null, finished_at: updated.finished_at?.toISOString() ?? null, + next_action: nextAction, }) }), )