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:
@@ -530,9 +530,9 @@ public static class PromptFiles
|
|||||||
|
|
||||||
## Handoff / end
|
## Handoff / end
|
||||||
|
|
||||||
- If you started any reruns: call handoff_list_handler(taskId, <the rerun task ids>, nextPhase: "wait_final"), then stop your turn immediately — do not merge anything yet that you just restarted.
|
- If you started any reruns: call handoff_list_handler(taskId, <the rerun task ids>, nextPhase: "wait_final"), then stop your turn immediately — do not merge anything yet that you just restarted. Do NOT submit this run for review yet — the chain isn't finished.
|
||||||
- If your own phase was "merge_final": never start a rerun, no matter how tempting — just merge what you can, report the rest, and print the summary below.
|
- If your own phase was "merge_final": never start a rerun, no matter how tempting — just merge what you can, report the rest, print the summary below, then call submit_task_for_review(taskId) on this session's own handler task id so its diff becomes reviewable.
|
||||||
- Otherwise (no reruns started, phase was "merge"): print the summary below and stop; there is nothing left to hand off.
|
- Otherwise (no reruns started, phase was "merge"): print the summary below, then call submit_task_for_review(taskId) on this session's own handler task id — there is nothing left to hand off.
|
||||||
|
|
||||||
## Phase 5 — Summary
|
## Phase 5 — Summary
|
||||||
Print one line per task from the brief:
|
Print one line per task from the brief:
|
||||||
|
|||||||
+16
-6
@@ -61,7 +61,8 @@ public sealed record TaskDto(
|
|||||||
// True only while Status is Queued and the picker will not claim this task yet -- either a
|
// True only while Status is Queued and the picker will not claim this task yet -- either a
|
||||||
// planning-chain predecessor or DependsOnTaskId hasn't reached Done. See BlockedReason.
|
// planning-chain predecessor or DependsOnTaskId hasn't reached Done. See BlockedReason.
|
||||||
bool Blocked = false,
|
bool Blocked = false,
|
||||||
string? BlockedReason = null);
|
string? BlockedReason = null,
|
||||||
|
bool IsManual = false);
|
||||||
|
|
||||||
// Lean counterpart to TaskDto for writing/status-changing tools: echoes back what changed
|
// Lean counterpart to TaskDto for writing/status-changing tools: echoes back what changed
|
||||||
// without re-sending Description/Result, which the caller just sent or already has.
|
// without re-sending Description/Result, which the caller just sent or already has.
|
||||||
@@ -83,7 +84,8 @@ public sealed record TaskRefDto(
|
|||||||
string? DependsOnTaskId = null,
|
string? DependsOnTaskId = null,
|
||||||
bool Blocked = false,
|
bool Blocked = false,
|
||||||
string? BlockedReason = null,
|
string? BlockedReason = null,
|
||||||
DirtyBaseWarning? BaseDirty = null);
|
DirtyBaseWarning? BaseDirty = null,
|
||||||
|
bool IsManual = false);
|
||||||
|
|
||||||
// tasks is populated when includeDescription=false (the default): lean references, no
|
// tasks is populated when includeDescription=false (the default): lean references, no
|
||||||
// Description/Result. tasksFull is populated when includeDescription=true: full tasks incl.
|
// Description/Result. tasksFull is populated when includeDescription=true: full tasks incl.
|
||||||
@@ -495,8 +497,8 @@ public sealed class ExternalMcpService
|
|||||||
}
|
}
|
||||||
|
|
||||||
[McpServerTool, Description(
|
[McpServerTool, Description(
|
||||||
"Update an existing task's title, description, commit type, and/or dependsOn link. Pass null to leave a " +
|
"Update an existing task's title, description, commit type, dependsOn link, and/or manual flag. Pass " +
|
||||||
"field unchanged." + McpToolDocs.NotWhileRunning + McpToolDocs.LeanTaskRef)]
|
"null to leave a field unchanged." + McpToolDocs.NotWhileRunning + McpToolDocs.LeanTaskRef)]
|
||||||
public async Task<TaskRefDto> UpdateTask(
|
public async Task<TaskRefDto> UpdateTask(
|
||||||
string taskId,
|
string taskId,
|
||||||
string? title = null,
|
string? title = null,
|
||||||
@@ -506,6 +508,10 @@ public sealed class ExternalMcpService
|
|||||||
"string to clear an existing link; null leaves it unchanged. Rejected if it doesn't exist, " +
|
"string to clear an existing link; null leaves it unchanged. Rejected if it doesn't exist, " +
|
||||||
"is this task's own id, or would create a dependency cycle.")]
|
"is this task's own id, or would create a dependency cycle.")]
|
||||||
string? dependsOnTaskId = null,
|
string? dependsOnTaskId = null,
|
||||||
|
[Description("true: mark this task as a manual reminder only the user can complete — the queue picker, " +
|
||||||
|
"daily prep, and the list handler all skip it (a ConPTY session may still touch it). false: " +
|
||||||
|
"clear the flag. null (default): leave unchanged.")]
|
||||||
|
bool? isManual = null,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
taskId = await TaskIdResolver.ResolveAsync(_tasks, taskId, cancellationToken);
|
taskId = await TaskIdResolver.ResolveAsync(_tasks, taskId, cancellationToken);
|
||||||
@@ -517,6 +523,7 @@ public sealed class ExternalMcpService
|
|||||||
if (title is not null) task.Title = title;
|
if (title is not null) task.Title = title;
|
||||||
if (description is not null) task.Description = description;
|
if (description is not null) task.Description = description;
|
||||||
if (commitType is not null) task.CommitType = commitType;
|
if (commitType is not null) task.CommitType = commitType;
|
||||||
|
if (isManual is not null) task.IsManual = isManual.Value;
|
||||||
await _tasks.UpdateAsync(task, cancellationToken);
|
await _tasks.UpdateAsync(task, cancellationToken);
|
||||||
|
|
||||||
if (dependsOnTaskId is not null)
|
if (dependsOnTaskId is not null)
|
||||||
@@ -1743,7 +1750,8 @@ public sealed class ExternalMcpService
|
|||||||
t.Status == TaskStatus.Failed ? t.FailureMaxTurns : null,
|
t.Status == TaskStatus.Failed ? t.FailureMaxTurns : null,
|
||||||
t.DependsOnTaskId,
|
t.DependsOnTaskId,
|
||||||
blocked,
|
blocked,
|
||||||
blockedReason);
|
blockedReason,
|
||||||
|
t.IsManual);
|
||||||
|
|
||||||
private static TaskRefDto ToRefDto(TaskEntity t, bool blocked = false, string? blockedReason = null) => new(
|
private static TaskRefDto ToRefDto(TaskEntity t, bool blocked = false, string? blockedReason = null) => new(
|
||||||
t.Id,
|
t.Id,
|
||||||
@@ -1759,7 +1767,9 @@ public sealed class ExternalMcpService
|
|||||||
t.Status == TaskStatus.Failed ? t.FailureMaxTurns : null,
|
t.Status == TaskStatus.Failed ? t.FailureMaxTurns : null,
|
||||||
t.DependsOnTaskId,
|
t.DependsOnTaskId,
|
||||||
blocked,
|
blocked,
|
||||||
blockedReason);
|
blockedReason,
|
||||||
|
BaseDirty: null,
|
||||||
|
IsManual: t.IsManual);
|
||||||
|
|
||||||
// "unknown" covers a Failed task that predates this field (never got a classified reason
|
// "unknown" covers a Failed task that predates this field (never got a classified reason
|
||||||
// stamped) — a defined value rather than null so callers don't have to special-case it.
|
// stamped) — a defined value rather than null so callers don't have to special-case it.
|
||||||
|
|||||||
+31
-1
@@ -13,11 +13,17 @@ public sealed class LifecycleMcpTools
|
|||||||
{
|
{
|
||||||
private readonly TaskRepository _tasks;
|
private readonly TaskRepository _tasks;
|
||||||
private readonly TaskResetService _reset;
|
private readonly TaskResetService _reset;
|
||||||
|
private readonly ExternalMcpService _svc;
|
||||||
|
private readonly InteractiveReviewSubmissionService _reviewSubmission;
|
||||||
|
|
||||||
public LifecycleMcpTools(TaskRepository tasks, TaskResetService reset)
|
public LifecycleMcpTools(
|
||||||
|
TaskRepository tasks, TaskResetService reset, ExternalMcpService svc,
|
||||||
|
InteractiveReviewSubmissionService reviewSubmission)
|
||||||
{
|
{
|
||||||
_tasks = tasks;
|
_tasks = tasks;
|
||||||
_reset = reset;
|
_reset = reset;
|
||||||
|
_svc = svc;
|
||||||
|
_reviewSubmission = reviewSubmission;
|
||||||
}
|
}
|
||||||
|
|
||||||
[McpServerTool, Description(
|
[McpServerTool, Description(
|
||||||
@@ -37,4 +43,28 @@ public sealed class LifecycleMcpTools
|
|||||||
await _reset.ResetAsync(taskId, cancellationToken);
|
await _reset.ResetAsync(taskId, cancellationToken);
|
||||||
return new ResetFailedTaskResult(true, taskId, task.Number);
|
return new ResetFailedTaskResult(true, taskId, task.Number);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[McpServerTool, Description(
|
||||||
|
"Submit a task into the review pipeline directly, without a headless agent run — the way a list " +
|
||||||
|
"handler ('Let Claude handle it') submits its OWN handler task once its run is done, since " +
|
||||||
|
"update_task_status doesn't reach WaitingForReview. Only Idle or Failed tasks are accepted; a " +
|
||||||
|
"Running/Queued/WaitingForReview/WaitingForChildren task throws. For a worktree-less handler task this " +
|
||||||
|
"stamps the review range's HandlerHeadCommit from the list's working dir HEAD (nothing to commit — the " +
|
||||||
|
"handler already committed straight to the list's working dir); for a task with an active worktree it " +
|
||||||
|
"commits any uncommitted changes there first. Either way the task then transitions to WaitingForReview " +
|
||||||
|
"and its diff becomes reviewable via review_task/get_task_diff." + McpToolDocs.LeanTaskRef + McpToolDocs.TaskNumberHint)]
|
||||||
|
public async Task<TaskRefDto> SubmitTaskForReview(string taskId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
taskId = await TaskIdResolver.ResolveAsync(_tasks, taskId, cancellationToken);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _reviewSubmission.SubmitAsync(taskId, cancellationToken);
|
||||||
|
}
|
||||||
|
catch (KeyNotFoundException)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Task {taskId} not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return await _svc.GetTaskRefAsync(taskId, cancellationToken);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -227,6 +227,7 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
|||||||
private readonly UsageSnapshotBuilder? _usageSnapshotBuilder;
|
private readonly UsageSnapshotBuilder? _usageSnapshotBuilder;
|
||||||
private readonly ITranscriptUsageReader? _usageReader;
|
private readonly ITranscriptUsageReader? _usageReader;
|
||||||
private readonly UsageMonitorService? _usageMonitor;
|
private readonly UsageMonitorService? _usageMonitor;
|
||||||
|
private readonly InteractiveReviewSubmissionService? _interactiveReviewSubmission;
|
||||||
|
|
||||||
public WorkerHub(
|
public WorkerHub(
|
||||||
QueueService queue,
|
QueueService queue,
|
||||||
@@ -259,7 +260,8 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
|||||||
Data.Git.GitService? git = null,
|
Data.Git.GitService? git = null,
|
||||||
UsageSnapshotBuilder? usageSnapshotBuilder = null,
|
UsageSnapshotBuilder? usageSnapshotBuilder = null,
|
||||||
ITranscriptUsageReader? usageReader = null,
|
ITranscriptUsageReader? usageReader = null,
|
||||||
UsageMonitorService? usageMonitor = null)
|
UsageMonitorService? usageMonitor = null,
|
||||||
|
InteractiveReviewSubmissionService? interactiveReviewSubmission = null)
|
||||||
{
|
{
|
||||||
_queue = queue;
|
_queue = queue;
|
||||||
_waker = waker;
|
_waker = waker;
|
||||||
@@ -292,6 +294,7 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
|||||||
_usageSnapshotBuilder = usageSnapshotBuilder;
|
_usageSnapshotBuilder = usageSnapshotBuilder;
|
||||||
_usageReader = usageReader;
|
_usageReader = usageReader;
|
||||||
_usageMonitor = usageMonitor;
|
_usageMonitor = usageMonitor;
|
||||||
|
_interactiveReviewSubmission = interactiveReviewSubmission;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Persistence boundary for the session_skills JSON-array columns (task/list/global).
|
// 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.
|
// task status on its own.
|
||||||
public Task SubmitTaskForReview(string taskId) => HubGuard(async () =>
|
public Task SubmitTaskForReview(string taskId) => HubGuard(async () =>
|
||||||
{
|
{
|
||||||
await using var ctx = await _dbFactory.CreateDbContextAsync();
|
if (_interactiveReviewSubmission is null)
|
||||||
var taskRepo = new TaskRepository(ctx);
|
throw new InvalidOperationException("Interactive review submission service is not configured.");
|
||||||
var task = await taskRepo.GetByIdAsync(taskId, Context.ConnectionAborted)
|
await _interactiveReviewSubmission.SubmitAsync(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.");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
public async Task<DiscardPlanningOutcome> DiscardPlanningSessionAsync(string taskId, bool dequeueQueuedChildren = false)
|
public async Task<DiscardPlanningOutcome> DiscardPlanningSessionAsync(string taskId, bool dequeueQueuedChildren = false)
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
using ClaudeDo.Data;
|
||||||
|
using ClaudeDo.Data.Git;
|
||||||
|
using ClaudeDo.Data.Models;
|
||||||
|
using ClaudeDo.Data.Repositories;
|
||||||
|
using ClaudeDo.Worker.Runner;
|
||||||
|
using ClaudeDo.Worker.State;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Worker.Lifecycle;
|
||||||
|
|
||||||
|
// Submits an interactively-worked task -- one whose worktree/working-dir commits were made by a
|
||||||
|
// human-in-the-loop ConPTY session rather than an autonomous run -- into the review pipeline.
|
||||||
|
// Shared by WorkerHub.SubmitTaskForReview (a UI ConPTY session) and the external
|
||||||
|
// submit_task_for_review MCP tool (a list handler submitting its OWN handler task): both must
|
||||||
|
// stamp/commit identically before TaskStateService.SubmitInteractiveForReviewAsync flips the status.
|
||||||
|
public sealed class InteractiveReviewSubmissionService
|
||||||
|
{
|
||||||
|
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
|
||||||
|
private readonly GitService _git;
|
||||||
|
private readonly ITaskStateService _state;
|
||||||
|
private readonly WorktreeManager? _worktreeManager;
|
||||||
|
|
||||||
|
public InteractiveReviewSubmissionService(
|
||||||
|
IDbContextFactory<ClaudeDoDbContext> dbFactory,
|
||||||
|
GitService git,
|
||||||
|
ITaskStateService state,
|
||||||
|
WorktreeManager? worktreeManager = null)
|
||||||
|
{
|
||||||
|
_dbFactory = dbFactory;
|
||||||
|
_git = git;
|
||||||
|
_state = state;
|
||||||
|
_worktreeManager = worktreeManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SubmitAsync(string taskId, CancellationToken ct)
|
||||||
|
{
|
||||||
|
await using var ctx = await _dbFactory.CreateDbContextAsync(ct);
|
||||||
|
var taskRepo = new TaskRepository(ctx);
|
||||||
|
var task = await taskRepo.GetByIdAsync(taskId, ct)
|
||||||
|
?? 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.");
|
||||||
|
// Check the status gate up front, 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, ct);
|
||||||
|
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, ct)
|
||||||
|
?? throw new InvalidOperationException("Task list not found.");
|
||||||
|
|
||||||
|
var wtCtx = new WorktreeContext(worktree.Path, worktree.BranchName, worktree.BaseCommit);
|
||||||
|
await _worktreeManager.CommitIfChangedAsync(wtCtx, task, list, ct);
|
||||||
|
}
|
||||||
|
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.
|
||||||
|
var list = await new ListRepository(ctx).GetByIdAsync(task.ListId, ct)
|
||||||
|
?? 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, ct);
|
||||||
|
await taskRepo.SetHandlerHeadCommitAsync(taskId, headCommit, ct);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("This task has no active worktree to submit.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await _state.SubmitInteractiveForReviewAsync(taskId, DateTime.UtcNow, ct);
|
||||||
|
if (!result.Ok)
|
||||||
|
throw new InvalidOperationException(result.Reason ?? "Could not submit for review.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -93,6 +93,7 @@ builder.Services.AddSingleton<WorktreeMaintenanceService>();
|
|||||||
builder.Services.AddSingleton<TaskResetService>();
|
builder.Services.AddSingleton<TaskResetService>();
|
||||||
builder.Services.AddSingleton<IVerifyCommandRunner, VerifyCommandRunner>();
|
builder.Services.AddSingleton<IVerifyCommandRunner, VerifyCommandRunner>();
|
||||||
builder.Services.AddSingleton<TaskMergeService>();
|
builder.Services.AddSingleton<TaskMergeService>();
|
||||||
|
builder.Services.AddSingleton<InteractiveReviewSubmissionService>();
|
||||||
builder.Services.AddSingleton<PlanningAggregator>();
|
builder.Services.AddSingleton<PlanningAggregator>();
|
||||||
builder.Services.AddSingleton<PlanningMergeOrchestrator>();
|
builder.Services.AddSingleton<PlanningMergeOrchestrator>();
|
||||||
builder.Services.AddSingleton<PlanningChainCoordinator>();
|
builder.Services.AddSingleton<PlanningChainCoordinator>();
|
||||||
@@ -311,6 +312,7 @@ if (cfg.ExternalMcpPort > 0)
|
|||||||
externalBuilder.Services.AddSingleton(app.Services.GetRequiredService<WorktreeMaintenanceService>());
|
externalBuilder.Services.AddSingleton(app.Services.GetRequiredService<WorktreeMaintenanceService>());
|
||||||
externalBuilder.Services.AddSingleton(app.Services.GetRequiredService<TaskMergeService>());
|
externalBuilder.Services.AddSingleton(app.Services.GetRequiredService<TaskMergeService>());
|
||||||
externalBuilder.Services.AddSingleton(app.Services.GetRequiredService<PlanningMergeOrchestrator>());
|
externalBuilder.Services.AddSingleton(app.Services.GetRequiredService<PlanningMergeOrchestrator>());
|
||||||
|
externalBuilder.Services.AddSingleton(app.Services.GetRequiredService<InteractiveReviewSubmissionService>());
|
||||||
externalBuilder.Services.AddScoped<ExternalMcpService>();
|
externalBuilder.Services.AddScoped<ExternalMcpService>();
|
||||||
externalBuilder.Services.AddScoped<BatchMcpTools>();
|
externalBuilder.Services.AddScoped<BatchMcpTools>();
|
||||||
externalBuilder.Services.AddScoped<ListMcpTools>();
|
externalBuilder.Services.AddScoped<ListMcpTools>();
|
||||||
|
|||||||
@@ -198,7 +198,7 @@ public sealed class ExternalMcpServiceTests : IDisposable
|
|||||||
var queue = CreateQueue();
|
var queue = CreateQueue();
|
||||||
var sut = BuildSut(queue);
|
var sut = BuildSut(queue);
|
||||||
|
|
||||||
var dto = await sut.UpdateTask(task.Id, "new title", null, null, null, CancellationToken.None);
|
var dto = await sut.UpdateTask(task.Id, "new title", null, null, null, null, CancellationToken.None);
|
||||||
|
|
||||||
Assert.Equal("new title", dto.Title);
|
Assert.Equal("new title", dto.Title);
|
||||||
var loaded = await _tasks.GetByIdAsync(task.Id);
|
var loaded = await _tasks.GetByIdAsync(task.Id);
|
||||||
@@ -219,13 +219,57 @@ public sealed class ExternalMcpServiceTests : IDisposable
|
|||||||
await _tasks.UpdateAsync(task, CancellationToken.None);
|
await _tasks.UpdateAsync(task, CancellationToken.None);
|
||||||
var sut = BuildSut(CreateQueue());
|
var sut = BuildSut(CreateQueue());
|
||||||
|
|
||||||
var dto = await sut.UpdateTask(task.Id, "new title", null, null, null, CancellationToken.None);
|
var dto = await sut.UpdateTask(task.Id, "new title", null, null, null, null, CancellationToken.None);
|
||||||
|
|
||||||
Assert.Equal(task.Id, dto.Id);
|
Assert.Equal(task.Id, dto.Id);
|
||||||
Assert.Equal(listId, dto.ListId);
|
Assert.Equal(listId, dto.ListId);
|
||||||
Assert.Equal("new title", dto.Title);
|
Assert.Equal("new title", dto.Title);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateTask_SetsIsManualTrue()
|
||||||
|
{
|
||||||
|
var listId = await SeedListAsync();
|
||||||
|
var task = await SeedTaskAsync(listId);
|
||||||
|
var sut = BuildSut(CreateQueue());
|
||||||
|
|
||||||
|
var dto = await sut.UpdateTask(task.Id, isManual: true, cancellationToken: CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.True(dto.IsManual);
|
||||||
|
var loaded = await _tasks.GetByIdAsync(task.Id);
|
||||||
|
Assert.True(loaded!.IsManual);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateTask_SetsIsManualFalse_ClearsExistingFlag()
|
||||||
|
{
|
||||||
|
var listId = await SeedListAsync();
|
||||||
|
var task = await SeedTaskAsync(listId);
|
||||||
|
task.IsManual = true;
|
||||||
|
await _tasks.UpdateAsync(task, CancellationToken.None);
|
||||||
|
var sut = BuildSut(CreateQueue());
|
||||||
|
|
||||||
|
var dto = await sut.UpdateTask(task.Id, isManual: false, cancellationToken: CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.False(dto.IsManual);
|
||||||
|
var loaded = await _tasks.GetByIdAsync(task.Id);
|
||||||
|
Assert.False(loaded!.IsManual);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateTask_IsManualNull_LeavesUnchanged()
|
||||||
|
{
|
||||||
|
var listId = await SeedListAsync();
|
||||||
|
var task = await SeedTaskAsync(listId);
|
||||||
|
task.IsManual = true;
|
||||||
|
await _tasks.UpdateAsync(task, CancellationToken.None);
|
||||||
|
var sut = BuildSut(CreateQueue());
|
||||||
|
|
||||||
|
var dto = await sut.UpdateTask(task.Id, title: "new title", cancellationToken: CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.True(dto.IsManual);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetTask_ReturnsTaskNumber()
|
public async Task GetTask_ReturnsTaskNumber()
|
||||||
{
|
{
|
||||||
@@ -443,7 +487,7 @@ public sealed class ExternalMcpServiceTests : IDisposable
|
|||||||
var sut = BuildSut(queue);
|
var sut = BuildSut(queue);
|
||||||
|
|
||||||
await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
||||||
sut.UpdateTask(task.Id, "x", null, null, null, CancellationToken.None));
|
sut.UpdateTask(task.Id, "x", null, null, null, null, CancellationToken.None));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -453,7 +497,7 @@ public sealed class ExternalMcpServiceTests : IDisposable
|
|||||||
var sut = BuildSut(queue);
|
var sut = BuildSut(queue);
|
||||||
|
|
||||||
await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
||||||
sut.UpdateTask("does-not-exist", "x", null, null, null, CancellationToken.None));
|
sut.UpdateTask("does-not-exist", "x", null, null, null, null, CancellationToken.None));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
+124
-6
@@ -2,13 +2,19 @@ using ClaudeDo.Data;
|
|||||||
using ClaudeDo.Data.Models;
|
using ClaudeDo.Data.Models;
|
||||||
using ClaudeDo.Data.Repositories;
|
using ClaudeDo.Data.Repositories;
|
||||||
using ClaudeDo.Worker.External;
|
using ClaudeDo.Worker.External;
|
||||||
|
using ClaudeDo.Worker.Git;
|
||||||
using ClaudeDo.Worker.Hub;
|
using ClaudeDo.Worker.Hub;
|
||||||
using ClaudeDo.Worker.Lifecycle;
|
using ClaudeDo.Worker.Lifecycle;
|
||||||
|
using ClaudeDo.Worker.Planning;
|
||||||
|
using ClaudeDo.Worker.Queue;
|
||||||
using ClaudeDo.Worker.Runner;
|
using ClaudeDo.Worker.Runner;
|
||||||
using ClaudeDo.Worker.State;
|
using ClaudeDo.Worker.State;
|
||||||
using ClaudeDo.Worker.Tests.Infrastructure;
|
using ClaudeDo.Worker.Tests.Infrastructure;
|
||||||
|
using ClaudeDo.Worker.Usage;
|
||||||
|
using ClaudeDo.Worker.Worktrees;
|
||||||
using ClaudeDo.Data.Git;
|
using ClaudeDo.Data.Git;
|
||||||
using ClaudeDo.Worker.Config;
|
using ClaudeDo.Worker.Config;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
|
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
|
||||||
|
|
||||||
@@ -20,6 +26,9 @@ public sealed class LifecycleMcpToolsTests : IDisposable
|
|||||||
private readonly ClaudeDoDbContext _ctx;
|
private readonly ClaudeDoDbContext _ctx;
|
||||||
private readonly TaskRepository _tasks;
|
private readonly TaskRepository _tasks;
|
||||||
private readonly ListRepository _lists;
|
private readonly ListRepository _lists;
|
||||||
|
private readonly List<GitRepoFixture> _repos = new();
|
||||||
|
|
||||||
|
private static bool GitAvailable => GitRepoFixture.IsGitAvailable();
|
||||||
|
|
||||||
public LifecycleMcpToolsTests()
|
public LifecycleMcpToolsTests()
|
||||||
{
|
{
|
||||||
@@ -28,7 +37,12 @@ public sealed class LifecycleMcpToolsTests : IDisposable
|
|||||||
_lists = new ListRepository(_ctx);
|
_lists = new ListRepository(_ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose() { _ctx.Dispose(); _db.Dispose(); }
|
public void Dispose()
|
||||||
|
{
|
||||||
|
foreach (var r in _repos) r.Dispose();
|
||||||
|
_ctx.Dispose();
|
||||||
|
_db.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
private LifecycleMcpTools BuildSut()
|
private LifecycleMcpTools BuildSut()
|
||||||
{
|
{
|
||||||
@@ -37,27 +51,65 @@ public sealed class LifecycleMcpToolsTests : IDisposable
|
|||||||
SandboxRoot = Path.Combine(Path.GetTempPath(), $"cd_{Guid.NewGuid():N}"),
|
SandboxRoot = Path.Combine(Path.GetTempPath(), $"cd_{Guid.NewGuid():N}"),
|
||||||
LogRoot = Path.Combine(Path.GetTempPath(), $"cdl_{Guid.NewGuid():N}"),
|
LogRoot = Path.Combine(Path.GetTempPath(), $"cdl_{Guid.NewGuid():N}"),
|
||||||
};
|
};
|
||||||
|
var git = new GitService();
|
||||||
var dbFactory = _db.CreateFactory();
|
var dbFactory = _db.CreateFactory();
|
||||||
var broadcaster = new HubBroadcaster(new CapturingHubContext());
|
var broadcaster = new HubBroadcaster(new CapturingHubContext());
|
||||||
var wtManager = new WorktreeManager(new GitService(), dbFactory, cfg, NullLogger<WorktreeManager>.Instance);
|
var wtManager = new WorktreeManager(git, dbFactory, cfg, NullLogger<WorktreeManager>.Instance);
|
||||||
var state = TaskStateServiceBuilder.Build(dbFactory).State;
|
var state = TaskStateServiceBuilder.Build(dbFactory).State;
|
||||||
var reset = new TaskResetService(dbFactory, wtManager, broadcaster, state, NullLogger<TaskResetService>.Instance);
|
var reset = new TaskResetService(dbFactory, wtManager, broadcaster, state, NullLogger<TaskResetService>.Instance);
|
||||||
return new LifecycleMcpTools(_tasks, reset);
|
var maintenance = new WorktreeMaintenanceService(dbFactory, git, NullLogger<WorktreeMaintenanceService>.Instance);
|
||||||
|
var merge = new TaskMergeService(dbFactory, git, broadcaster, state, new VerifyCommandRunner(), NullLogger<TaskMergeService>.Instance);
|
||||||
|
var aggregator = new PlanningAggregator(dbFactory, git, NullLogger<PlanningAggregator>.Instance);
|
||||||
|
var planningMerge = new PlanningMergeOrchestrator(
|
||||||
|
dbFactory, merge, aggregator, broadcaster, git, state, NullLogger<PlanningMergeOrchestrator>.Instance);
|
||||||
|
var svc = new ExternalMcpService(
|
||||||
|
_tasks, _lists, CreateQueue(dbFactory, wtManager, state, broadcaster), broadcaster,
|
||||||
|
state, git, dbFactory, maintenance, merge, planningMerge,
|
||||||
|
new BaseDirtyChecker(git, NullLogger<BaseDirtyChecker>.Instance));
|
||||||
|
var reviewSubmission = new InteractiveReviewSubmissionService(dbFactory, git, state, wtManager);
|
||||||
|
return new LifecycleMcpTools(_tasks, reset, svc, reviewSubmission);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<TaskEntity> SeedTaskAsync(TaskStatus status)
|
private QueueService CreateQueue(
|
||||||
|
IDbContextFactory<ClaudeDoDbContext> dbFactory, WorktreeManager wtManager, ITaskStateService state, HubBroadcaster broadcaster)
|
||||||
|
{
|
||||||
|
var cfg = new WorkerConfig
|
||||||
|
{
|
||||||
|
SandboxRoot = Path.Combine(Path.GetTempPath(), $"cdq_{Guid.NewGuid():N}"),
|
||||||
|
LogRoot = Path.Combine(Path.GetTempPath(), $"cdql_{Guid.NewGuid():N}"),
|
||||||
|
QueueBackstopIntervalMs = 50,
|
||||||
|
};
|
||||||
|
var runner = new TaskRunner(new FakeClaudeProcess(), dbFactory, broadcaster, wtManager, new ClaudeArgsBuilder(), cfg,
|
||||||
|
NullLogger<TaskRunner>.Instance, state, new TaskRunTokenRegistry(), new AttachmentStore(), new FakeSessionSkillSeeder(), new FakeTranscriptUsageReader());
|
||||||
|
var runCancels = new RunCancellationRegistry(NullLogger<RunCancellationRegistry>.Instance);
|
||||||
|
var overrideSlot = new OverrideSlotService(dbFactory, runner, NullLogger<OverrideSlotService>.Instance, runCancels);
|
||||||
|
return new QueueService(dbFactory, runner, cfg, NullLogger<QueueService>.Instance,
|
||||||
|
new QueueWaker(), new QueuePicker(dbFactory), overrideSlot, state, runCancels,
|
||||||
|
new FakeUsageGate(), new UsageState(), broadcaster);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<TaskEntity> SeedTaskAsync(
|
||||||
|
TaskStatus status, string? workingDir = null, string? handlerBaseCommit = null)
|
||||||
{
|
{
|
||||||
var listId = Guid.NewGuid().ToString();
|
var listId = Guid.NewGuid().ToString();
|
||||||
await _lists.AddAsync(new ListEntity { Id = listId, Name = "L", CreatedAt = DateTime.UtcNow });
|
await _lists.AddAsync(new ListEntity { Id = listId, Name = "L", WorkingDir = workingDir, CreatedAt = DateTime.UtcNow });
|
||||||
var task = new TaskEntity
|
var task = new TaskEntity
|
||||||
{
|
{
|
||||||
Id = Guid.NewGuid().ToString(), ListId = listId, Title = "t",
|
Id = Guid.NewGuid().ToString(), ListId = listId, Title = "t",
|
||||||
Status = status, CreatedAt = DateTime.UtcNow, CommitType = "chore",
|
Status = status, HandlerBaseCommit = handlerBaseCommit,
|
||||||
|
CreatedAt = DateTime.UtcNow, CommitType = "chore",
|
||||||
};
|
};
|
||||||
await _tasks.AddAsync(task);
|
await _tasks.AddAsync(task);
|
||||||
return task;
|
return task;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private GitRepoFixture CreateRepo()
|
||||||
|
{
|
||||||
|
var f = new GitRepoFixture();
|
||||||
|
_repos.Add(f);
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ResetFailedTask_OnFailed_ResetsToIdle()
|
public async Task ResetFailedTask_OnFailed_ResetsToIdle()
|
||||||
{
|
{
|
||||||
@@ -89,4 +141,70 @@ public sealed class LifecycleMcpToolsTests : IDisposable
|
|||||||
await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
||||||
sut.ResetFailedTask("missing", CancellationToken.None));
|
sut.ResetFailedTask("missing", CancellationToken.None));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── SubmitTaskForReview ──
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SubmitTaskForReview_WorktreeLessHandlerTask_StampsHeadCommit_TransitionsToWaitingForReview()
|
||||||
|
{
|
||||||
|
if (!GitAvailable) { Assert.True(true, "git not available -- skipping"); return; }
|
||||||
|
|
||||||
|
var repo = CreateRepo();
|
||||||
|
var task = await SeedTaskAsync(TaskStatus.Idle, workingDir: repo.RepoDir, handlerBaseCommit: repo.BaseCommit);
|
||||||
|
File.WriteAllText(Path.Combine(repo.RepoDir, "handled.txt"), "content");
|
||||||
|
GitRepoFixture.RunGit(repo.RepoDir, "add", "-A");
|
||||||
|
GitRepoFixture.RunGit(repo.RepoDir, "commit", "-m", "handled task merge");
|
||||||
|
var expectedHead = GitRepoFixture.RunGit(repo.RepoDir, "rev-parse", "HEAD").Trim();
|
||||||
|
var sut = BuildSut();
|
||||||
|
|
||||||
|
var dto = await sut.SubmitTaskForReview(task.Id, CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.Equal("WaitingForReview", dto.Status);
|
||||||
|
var reloaded = await _tasks.GetByIdAsync(task.Id);
|
||||||
|
Assert.Equal(TaskStatus.WaitingForReview, reloaded!.Status);
|
||||||
|
Assert.Equal(expectedHead, reloaded.HandlerHeadCommit);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SubmitTaskForReview_NoWorktreeAndNoHandlerBaseCommit_Throws()
|
||||||
|
{
|
||||||
|
var task = await SeedTaskAsync(TaskStatus.Idle, workingDir: Path.GetTempPath());
|
||||||
|
var sut = BuildSut();
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
||||||
|
sut.SubmitTaskForReview(task.Id, CancellationToken.None));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SubmitTaskForReview_RunningTask_Throws()
|
||||||
|
{
|
||||||
|
var task = await SeedTaskAsync(TaskStatus.Running, workingDir: Path.GetTempPath(), handlerBaseCommit: "abc123");
|
||||||
|
var sut = BuildSut();
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
||||||
|
sut.SubmitTaskForReview(task.Id, CancellationToken.None));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SubmitTaskForReview_AlreadyDone_Throws_HandlerHeadCommitUntouched()
|
||||||
|
{
|
||||||
|
var task = await SeedTaskAsync(TaskStatus.Done, workingDir: Path.GetTempPath(), handlerBaseCommit: "abc123");
|
||||||
|
var sut = BuildSut();
|
||||||
|
|
||||||
|
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
||||||
|
sut.SubmitTaskForReview(task.Id, CancellationToken.None));
|
||||||
|
Assert.Contains("Idle or Failed", ex.Message);
|
||||||
|
|
||||||
|
var reloaded = await _tasks.GetByIdAsync(task.Id);
|
||||||
|
Assert.Null(reloaded!.HandlerHeadCommit);
|
||||||
|
Assert.Equal(TaskStatus.Done, reloaded.Status);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SubmitTaskForReview_NotFound_Throws()
|
||||||
|
{
|
||||||
|
var sut = BuildSut();
|
||||||
|
await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
||||||
|
sut.SubmitTaskForReview("missing", CancellationToken.None));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using ClaudeDo.Data.Models;
|
|||||||
using ClaudeDo.Data.Repositories;
|
using ClaudeDo.Data.Repositories;
|
||||||
using ClaudeDo.Worker.Config;
|
using ClaudeDo.Worker.Config;
|
||||||
using ClaudeDo.Worker.Hub;
|
using ClaudeDo.Worker.Hub;
|
||||||
|
using ClaudeDo.Worker.Lifecycle;
|
||||||
using ClaudeDo.Worker.Runner;
|
using ClaudeDo.Worker.Runner;
|
||||||
using ClaudeDo.Worker.Skills;
|
using ClaudeDo.Worker.Skills;
|
||||||
using ClaudeDo.Worker.Tests.Infrastructure;
|
using ClaudeDo.Worker.Tests.Infrastructure;
|
||||||
@@ -65,13 +66,15 @@ public sealed class MergeHelperTaskHubTests : IDisposable
|
|||||||
factory, new FakeSessionSkillSeeder(), new FakeSessionSkillRegistry(), wtManager, git,
|
factory, new FakeSessionSkillSeeder(), new FakeSessionSkillRegistry(), wtManager, git,
|
||||||
new WorkerConfig { ClaudeBin = "claude" });
|
new WorkerConfig { ClaudeBin = "claude" });
|
||||||
var built = TaskStateServiceBuilder.Build(factory);
|
var built = TaskStateServiceBuilder.Build(factory);
|
||||||
|
var reviewSubmission = new InteractiveReviewSubmissionService(factory, git, built.State, wtManager);
|
||||||
|
|
||||||
var hub = new WorkerHub(
|
var hub = new WorkerHub(
|
||||||
null!, null!, null!, null!, null!, factory, null!, null!, null!,
|
null!, null!, null!, null!, null!, factory, null!, null!, null!,
|
||||||
null!, null!, null!, null!, null!, null!, null!, built.State, null!, null!,
|
null!, null!, null!, null!, null!, null!, null!, built.State, null!, null!,
|
||||||
null!, new ClaudeDo.Worker.Online.OnlineInboxConfig(), new ClaudeDo.Worker.Online.OnlineTokenStore(),
|
null!, new ClaudeDo.Worker.Online.OnlineInboxConfig(), new ClaudeDo.Worker.Online.OnlineTokenStore(),
|
||||||
new ClaudeDo.Worker.Runner.PendingQuestionRegistry(), null!,
|
new ClaudeDo.Worker.Runner.PendingQuestionRegistry(), null!,
|
||||||
logBuffer: null, interactiveLaunchSpec: interactiveLaunchSpec, worktreeManager: wtManager, git: git);
|
logBuffer: null, interactiveLaunchSpec: interactiveLaunchSpec, worktreeManager: wtManager, git: git,
|
||||||
|
interactiveReviewSubmission: reviewSubmission);
|
||||||
hub.Clients = new FakeHubCallerClients(_proxy);
|
hub.Clients = new FakeHubCallerClients(_proxy);
|
||||||
hub.Context = new FakeHubCallerContext();
|
hub.Context = new FakeHubCallerContext();
|
||||||
return hub;
|
return hub;
|
||||||
|
|||||||
Reference in New Issue
Block a user