PBI-50 F3: nieuwe MCP-tools voor SPRINT_IMPLEMENTATION-flow

Vier nieuwe tools + propagateStatusUpwards uitbreiding:

T1 — verify_sprint_task (src/tools/verify-sprint-task.ts):
  Execution-aware verify met frozen plan_snapshot. Input: execution_id +
  worktree_path + optionele summary (voor PARTIAL/DIVERGENT-rationale).
  Vult base_sha dynamisch voor task[1..N] op basis van vorige DONE-execution's
  head_sha. Schrijft verify_result + verify_summary op execution-row.
  Returns { result, reasoning, base_sha, allowed_for_done, reason? } —
  allowed_for_done via standaard checkVerifyGate met snapshot-velden.

T2 — update_task_execution (src/tools/update-task-execution.ts):
  Lifecycle-tool voor SprintTaskExecution: PENDING/RUNNING/DONE/FAILED/SKIPPED
  + base_sha/head_sha/skip_reason. Idempotent. Token-check via
  execution.sprint_job.claimed_by_token_id. started_at/finished_at automatisch.

T3 — job_heartbeat (src/tools/job-heartbeat.ts):
  Verlengt ClaudeJob.lease_until met 5 min via atomic conditional UPDATE
  (token-check + status-check in WHERE). Voor SPRINT-jobs: response bevat
  sprint_run_status + sprint_run_pause_reason zodat worker op UI-side cancel
  of MERGE_CONFLICT-pause kan breken zonder extra query.

T4 — update_task_status sprint_run_id-arg + token-coupling
  (src/tools/update-task-status.ts):
  Optionele sprint_run_id-arg voor expliciete cascade. Validaties: SprintRun
  bestaat + actief, task in deze sprint, current token heeft een actieve
  ClaudeJob in deze run geclaimd (403 anders). Response uitgebreid met
  sprint_run_status_change.

T5 — propagateStatusUpwards sprintRunId-param
  (src/lib/tasks-status-update.ts):
  Optionele sprintRunId-parameter. Resolve-volgorde: expliciete arg →
  ClaudeJob.task_id-lookup → Story → Sprint → SprintRun.findFirst({active}).
  De derde fallback dekt SPRINT_IMPLEMENTATION (geen task_id-koppeling) én
  handmatige task-statuswijzigingen via UI. cancelExceptJobId voor
  sibling-cancel; null voor SPRINT-job betekent geen siblings te cancellen.

src/index.ts: drie nieuwe tools geregistreerd.

Tests: 31 files, 243 passing (geen tests voor nieuwe tools nog — F5).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Madhura68 2026-05-07 12:40:18 +02:00
parent 35601e8e4b
commit 25ab68073a
6 changed files with 471 additions and 16 deletions

View file

@ -0,0 +1,110 @@
// PBI-50 F3-T2: update_task_execution
//
// SPRINT_IMPLEMENTATION-flow lifecycle-tool. Worker roept dit aan voor elke
// task in de batch om de SprintTaskExecution-row te muteren:
// PENDING → RUNNING → DONE/FAILED/SKIPPED
// Idempotent: dezelfde call kan veilig herhaald worden.
import { z } from 'zod'
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { prisma } from '../prisma.js'
import { requireWriteAccess } from '../auth.js'
import { toolError, toolJson, withToolErrors } from '../errors.js'
const inputSchema = z.object({
execution_id: z.string().min(1),
status: z.enum(['PENDING', 'RUNNING', 'DONE', 'FAILED', 'SKIPPED']),
base_sha: z.string().optional(),
head_sha: z.string().optional(),
skip_reason: z.string().max(2000).optional(),
})
export function registerUpdateTaskExecutionTool(server: McpServer) {
server.registerTool(
'update_task_execution',
{
title: 'Update SprintTaskExecution status',
description:
'Mutate a SprintTaskExecution row in a SPRINT_IMPLEMENTATION batch. ' +
'Status: PENDING|RUNNING|DONE|FAILED|SKIPPED. Worker calls this for each ' +
'task transition. Token must own the parent SPRINT_IMPLEMENTATION ClaudeJob. ' +
'Idempotent — safe to retry. Schrijft started_at (RUNNING) en finished_at ' +
'(DONE/FAILED/SKIPPED). Forbidden for demo accounts.',
inputSchema,
},
async ({ execution_id, status, base_sha, head_sha, skip_reason }) =>
withToolErrors(async () => {
const auth = await requireWriteAccess()
const execution = await prisma.sprintTaskExecution.findUnique({
where: { id: execution_id },
select: {
id: true,
sprint_job_id: true,
sprint_job: {
select: { claimed_by_token_id: true, status: true, kind: true },
},
},
})
if (!execution) {
return toolError(`SprintTaskExecution ${execution_id} not found`)
}
if (execution.sprint_job.kind !== 'SPRINT_IMPLEMENTATION') {
return toolError(
`Execution ${execution_id} hangs at job kind ${execution.sprint_job.kind}, expected SPRINT_IMPLEMENTATION`,
)
}
if (execution.sprint_job.claimed_by_token_id !== auth.tokenId) {
return toolError(
`Forbidden: token does not own SPRINT_IMPLEMENTATION job for execution ${execution_id}`,
)
}
if (
execution.sprint_job.status !== 'CLAIMED' &&
execution.sprint_job.status !== 'RUNNING'
) {
return toolError(
`Sprint job is in terminal state ${execution.sprint_job.status}`,
)
}
const now = new Date()
const updated = await prisma.sprintTaskExecution.update({
where: { id: execution_id },
data: {
status,
...(base_sha !== undefined ? { base_sha } : {}),
...(head_sha !== undefined ? { head_sha } : {}),
...(skip_reason !== undefined ? { skip_reason } : {}),
...(status === 'RUNNING' ? { started_at: now } : {}),
...(status === 'DONE' || status === 'FAILED' || status === 'SKIPPED'
? { finished_at: now }
: {}),
},
select: {
id: true,
status: true,
base_sha: true,
head_sha: true,
verify_result: true,
verify_summary: true,
skip_reason: true,
started_at: true,
finished_at: true,
},
})
return toolJson({
execution_id: updated.id,
status: updated.status,
base_sha: updated.base_sha,
head_sha: updated.head_sha,
verify_result: updated.verify_result,
verify_summary: updated.verify_summary,
skip_reason: updated.skip_reason,
started_at: updated.started_at?.toISOString() ?? null,
finished_at: updated.finished_at?.toISOString() ?? null,
})
}),
)
}