feat(mcp): update_task kann isManual setzen + List-Handler stellt seinen Task auf Review

Teil A: update_task bekommt einen optionalen isManual-Parameter (null =
unveraendert); TaskDto/TaskRefDto spiegeln IsManual zurueck.

Teil B: neuer submit_task_for_review MCP-Tool (LifecycleMcpTools) laesst
einen Handler-Task selbst auf WaitingForReview gehen -- fuer einen
worktree-losen Task wird HandlerHeadCommit vom aktuellen HEAD gestempelt,
sonst werden offene Aenderungen committet. Die Submit-Logik ist aus
WorkerHub.SubmitTaskForReview in InteractiveReviewSubmissionService
extrahiert; der Hub ruft sie jetzt nur noch auf. Der Merge-Prompt-Default
weist die Endzweige (merge ohne Rerun, merge_final) an, den eigenen
Handler-Task nach der Summary einzureichen.
This commit is contained in:
mika kuns
2026-08-21 16:51:18 +02:00
parent 36720d33ae
commit ea2271cc3e
9 changed files with 325 additions and 76 deletions
+7 -55
View File
@@ -227,6 +227,7 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
private readonly UsageSnapshotBuilder? _usageSnapshotBuilder;
private readonly ITranscriptUsageReader? _usageReader;
private readonly UsageMonitorService? _usageMonitor;
private readonly InteractiveReviewSubmissionService? _interactiveReviewSubmission;
public WorkerHub(
QueueService queue,
@@ -259,7 +260,8 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
Data.Git.GitService? git = null,
UsageSnapshotBuilder? usageSnapshotBuilder = null,
ITranscriptUsageReader? usageReader = null,
UsageMonitorService? usageMonitor = null)
UsageMonitorService? usageMonitor = null,
InteractiveReviewSubmissionService? interactiveReviewSubmission = null)
{
_queue = queue;
_waker = waker;
@@ -292,6 +294,7 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
_usageSnapshotBuilder = usageSnapshotBuilder;
_usageReader = usageReader;
_usageMonitor = usageMonitor;
_interactiveReviewSubmission = interactiveReviewSubmission;
}
// Persistence boundary for the session_skills JSON-array columns (task/list/global).
@@ -931,60 +934,9 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
// task status on its own.
public Task SubmitTaskForReview(string taskId) => HubGuard(async () =>
{
await using var ctx = await _dbFactory.CreateDbContextAsync();
var taskRepo = new TaskRepository(ctx);
var task = await taskRepo.GetByIdAsync(taskId, Context.ConnectionAborted)
?? throw new KeyNotFoundException();
if (task.Status is TaskStatus.Running or TaskStatus.Queued)
throw new InvalidOperationException("Can't submit a running or queued task — interrupt it first.");
if (task.Status is TaskStatus.WaitingForReview or TaskStatus.WaitingForChildren)
throw new InvalidOperationException("Task is already awaiting review.");
// SubmitInteractiveForReviewAsync below only accepts Idle/Failed -- check it up front too,
// before either mutation branch, so a Done or Cancelled task can't get committed / have its
// HandlerHeadCommit stamped and then be rejected, stranding the work on an orphaned branch.
if (task.Status is not (TaskStatus.Idle or TaskStatus.Failed))
throw new InvalidOperationException("Task must be Idle or Failed to submit for review.");
var worktree = await new WorktreeRepository(ctx).GetByTaskIdAsync(taskId, Context.ConnectionAborted);
if (worktree is not null)
{
if (_worktreeManager is null)
throw new InvalidOperationException("Worktree manager is not configured.");
if (worktree.State is not (WorktreeState.Active or WorktreeState.Kept))
throw new InvalidOperationException("This task has no active worktree to submit.");
if (!Directory.Exists(worktree.Path))
throw new InvalidOperationException("The task's worktree directory no longer exists.");
var list = await new ListRepository(ctx).GetByIdAsync(task.ListId, Context.ConnectionAborted)
?? throw new InvalidOperationException("Task list not found.");
var wtCtx = new WorktreeContext(worktree.Path, worktree.BranchName, worktree.BaseCommit);
await _worktreeManager.CommitIfChangedAsync(wtCtx, task, list, Context.ConnectionAborted);
}
else if (task.HandlerBaseCommit is { Length: > 0 })
{
// Worktree-less "list handler" host task (Mission Control's "Let Claude handle it"):
// the handler commits its own changes straight to the list's working dir, so there
// is nothing for us to commit here — just stamp the review range's head commit.
if (_git is null)
throw new InvalidOperationException("Git service is not configured.");
var list = await new ListRepository(ctx).GetByIdAsync(task.ListId, Context.ConnectionAborted)
?? throw new InvalidOperationException("Task list not found.");
if (string.IsNullOrEmpty(list.WorkingDir) || !Directory.Exists(list.WorkingDir))
throw new InvalidOperationException("The list's working directory no longer exists.");
var headCommit = await _git.RevParseHeadAsync(list.WorkingDir, Context.ConnectionAborted);
await taskRepo.SetHandlerHeadCommitAsync(taskId, headCommit, Context.ConnectionAborted);
}
else
{
throw new InvalidOperationException("This task has no active worktree to submit.");
}
var result = await _state.SubmitInteractiveForReviewAsync(taskId, DateTime.UtcNow, Context.ConnectionAborted);
if (!result.Ok)
throw new InvalidOperationException(result.Reason ?? "Could not submit for review.");
if (_interactiveReviewSubmission is null)
throw new InvalidOperationException("Interactive review submission service is not configured.");
await _interactiveReviewSubmission.SubmitAsync(taskId, Context.ConnectionAborted);
});
public async Task<DiscardPlanningOutcome> DiscardPlanningSessionAsync(string taskId, bool dequeueQueuedChildren = false)