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>
110 lines
4 KiB
TypeScript
110 lines
4 KiB
TypeScript
// 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,
|
|
})
|
|
}),
|
|
)
|
|
}
|