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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user