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>
113 lines
4.5 KiB
TypeScript
113 lines
4.5 KiB
TypeScript
import { z } from 'zod'
|
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
import { prisma } from '../prisma.js'
|
|
import { requireWriteAccess } from '../auth.js'
|
|
import { userCanAccessTask } from '../access.js'
|
|
import { toolError, toolJson, withToolErrors } from '../errors.js'
|
|
import { TASK_STATUS_API_VALUES, taskStatusFromApi, taskStatusToApi } from '../status.js'
|
|
import { updateTaskStatusWithStoryPromotion } from '../lib/tasks-status-update.js'
|
|
|
|
const inputSchema = z.object({
|
|
task_id: z.string().min(1),
|
|
status: z.enum(TASK_STATUS_API_VALUES as [string, ...string[]]),
|
|
// PBI-50: optionele sprint_run_id voor SPRINT_IMPLEMENTATION-flow.
|
|
// Wanneer aanwezig: server valideert dat task in deze sprint zit, run
|
|
// actief is, en de huidige token een ClaudeJob in deze run heeft geclaimt.
|
|
sprint_run_id: z.string().min(1).optional(),
|
|
})
|
|
|
|
export function registerUpdateTaskStatusTool(server: McpServer) {
|
|
server.registerTool(
|
|
'update_task_status',
|
|
{
|
|
title: 'Update task status',
|
|
description:
|
|
'Set the status of a task. Allowed values: todo, in_progress, review, done, failed. ' +
|
|
'Optional sprint_run_id binds the update to a SPRINT_IMPLEMENTATION run for ' +
|
|
'cascade-propagation; the server validates that the task belongs to the sprint ' +
|
|
'and that the calling token has claimed a job in that run. ' +
|
|
'Forbidden for demo accounts.',
|
|
inputSchema,
|
|
},
|
|
async ({ task_id, status, sprint_run_id }) =>
|
|
withToolErrors(async () => {
|
|
const auth = await requireWriteAccess()
|
|
const dbStatus = taskStatusFromApi(status)
|
|
if (!dbStatus) {
|
|
return toolError(`Unknown status: ${status}`)
|
|
}
|
|
if (!(await userCanAccessTask(task_id, auth.userId))) {
|
|
return toolError(`Task ${task_id} not found or not accessible`)
|
|
}
|
|
|
|
// PBI-50: validate explicit sprint_run_id binding.
|
|
if (sprint_run_id) {
|
|
const sprintRun = await prisma.sprintRun.findUnique({
|
|
where: { id: sprint_run_id },
|
|
select: { id: true, status: true, sprint_id: true },
|
|
})
|
|
if (!sprintRun) {
|
|
return toolError(`SprintRun ${sprint_run_id} not found`)
|
|
}
|
|
if (
|
|
sprintRun.status !== 'QUEUED' &&
|
|
sprintRun.status !== 'RUNNING' &&
|
|
sprintRun.status !== 'PAUSED'
|
|
) {
|
|
return toolError(
|
|
`SprintRun ${sprint_run_id} is in terminal state ${sprintRun.status}; cannot update task status against it`,
|
|
)
|
|
}
|
|
|
|
// Task moet in deze sprint zitten
|
|
const task = await prisma.task.findUnique({
|
|
where: { id: task_id },
|
|
select: { story: { select: { sprint_id: true } } },
|
|
})
|
|
if (!task || task.story.sprint_id !== sprintRun.sprint_id) {
|
|
return toolError(
|
|
`Task ${task_id} is not in sprint ${sprintRun.sprint_id} (sprint_run ${sprint_run_id})`,
|
|
)
|
|
}
|
|
|
|
// Token-coupling: huidige token moet een actieve ClaudeJob in deze
|
|
// SprintRun hebben geclaimt (typisch de SPRINT_IMPLEMENTATION-job).
|
|
const tokenJob = await prisma.claudeJob.findFirst({
|
|
where: {
|
|
sprint_run_id,
|
|
claimed_by_token_id: auth.tokenId,
|
|
status: { in: ['CLAIMED', 'RUNNING'] },
|
|
},
|
|
select: { id: true },
|
|
})
|
|
if (!tokenJob) {
|
|
return toolError(
|
|
`Forbidden: current token has no active claim in sprint_run ${sprint_run_id}`,
|
|
)
|
|
}
|
|
}
|
|
|
|
const { task, storyStatusChange, sprintRunChanged } =
|
|
await updateTaskStatusWithStoryPromotion(task_id, dbStatus, undefined, sprint_run_id)
|
|
|
|
// Voor SPRINT-flow: stuur expliciete sprint_run_status mee zodat
|
|
// worker zijn loop kan breken bij FAILED/PAUSED zonder extra query.
|
|
let sprintRunStatusChange: string | null = null
|
|
if (sprintRunChanged && sprint_run_id) {
|
|
const updated = await prisma.sprintRun.findUnique({
|
|
where: { id: sprint_run_id },
|
|
select: { status: true },
|
|
})
|
|
sprintRunStatusChange = updated?.status ?? null
|
|
}
|
|
|
|
return toolJson({
|
|
id: task.id,
|
|
status: taskStatusToApi(task.status),
|
|
implementation_plan: task.implementation_plan,
|
|
story_status_change: storyStatusChange,
|
|
sprint_run_status_change: sprintRunStatusChange,
|
|
})
|
|
}),
|
|
)
|
|
}
|