feat(mcp): allow Done via update_task_status with worktree guard

External tasks finished outside a ClaudeDo run had no way to close out
their tracking task; update_task_status now permits Done alongside
Idle/Queued/Cancelled, refusing it when the task has an active
worktree so review_task stays the only path that merges.
This commit is contained in:
mika kuns
2026-07-29 08:58:34 +02:00
parent 24f999facd
commit d569313598
6 changed files with 78 additions and 10 deletions
+2 -2
View File
@@ -93,8 +93,8 @@ public sealed class BatchMcpTools
}
[McpServerTool, Description(
"Set the status of many tasks at once. status is 'Idle' (reset to editable) or " +
"'Queued' (enqueue for execution) only — same rule as update_task_status. " +
"Set the status of many tasks at once. status is 'Idle', 'Queued', 'Cancelled' or 'Done' only — " +
"same rule as update_task_status ('Done' is refused per-item for a task with an active worktree). " +
"Returns one result per id: { taskId, ok, error }. Max 100 ids.")]
public async Task<IReadOnlyList<BatchTaskResult>> BatchUpdateTaskStatus(
string[] taskIds, string status, CancellationToken cancellationToken)
+18 -2
View File
@@ -264,10 +264,12 @@ public sealed class ExternalMcpService
}
[McpServerTool, Description(
"Update a task's status. Only 'Idle', 'Queued' and 'Cancelled' are permitted externally — " +
"Update a task's status. Only 'Idle', 'Queued', 'Cancelled' and 'Done' are permitted externally — " +
"use run_task_now for execution control, and review_task to act on a WaitingForReview task. " +
"Settable: Idle (reset to editable), Queued (enqueue for execution), " +
"Cancelled (retire the task without deleting it; it can be reset to Idle later). " +
"Cancelled (retire the task without deleting it; it can be reset to Idle later), " +
"Done (mark complete; refused if the task has an active worktree — use review_task to approve " +
"and merge that worktree instead). " +
"Full lifecycle: Idle → Queued → Running → WaitingForReview → Done | Failed | Cancelled.")]
public async Task<TaskDto> UpdateTaskStatus(
string taskId,
@@ -300,6 +302,20 @@ public sealed class ExternalMcpService
throw new InvalidOperationException(cancelResult.Reason ?? "Cannot cancel task.");
break;
case TaskStatus.Done:
await using (var ctx = await _dbFactory.CreateDbContextAsync(cancellationToken))
{
var wt = await new WorktreeRepository(ctx).GetByTaskIdAsync(taskId, cancellationToken);
if (wt is not null && wt.State == WorktreeState.Active)
throw new InvalidOperationException(
"Task has an active worktree — use review_task to approve and merge instead.");
}
var doneResult = await _state.ForceSetStatusAsync(taskId, TaskStatus.Done, cancellationToken);
if (!doneResult.Ok)
throw new InvalidOperationException(doneResult.Reason ?? "Cannot set task to Done.");
break;
default:
throw new InvalidOperationException(
$"Status '{target}' is not settable externally. Use run_task_now or review_task.");