Merge branch 'claudedo/9f963a215bb34ca4a28050d4f39178d3'

This commit is contained in:
mika kuns
2026-08-10 15:19:57 +02:00
23 changed files with 481 additions and 32 deletions
+23 -5
View File
@@ -7,6 +7,7 @@ using ClaudeDo.Data;
using ClaudeDo.Data.Git;
using ClaudeDo.Data.Models;
using ClaudeDo.Data.Repositories;
using ClaudeDo.Worker.Git;
using ClaudeDo.Worker.Hub;
using ClaudeDo.Worker.Lifecycle;
using ClaudeDo.Worker.Planning;
@@ -26,7 +27,7 @@ public sealed record CancelTaskResult(bool Cancelled, string Id);
// review range (worktree ahead, or HandlerBaseCommit..HandlerHeadCommit for a worktree-less
// child) contributed nothing, so a reviewer sees them before approving instead of after.
public sealed record ReviewTaskResult(TaskRefDto Task, string? MergeStatus, IReadOnlyList<string> MergeConflicts, string? MergeMessage, string? RepoPath = null, IReadOnlyList<TaskRefDto>? EmptyChildren = null);
public sealed record RunTaskNowResult(bool Started, string TaskId);
public sealed record RunTaskNowResult(bool Started, string TaskId, DirtyBaseWarning? BaseDirty = null);
public sealed record TaskDto(
string Id,
@@ -62,6 +63,9 @@ public sealed record TaskDto(
// 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.
// BaseDirty is populated only where the task just transitioned to Queued (or ran immediately
// via run_task_now) and its list's working directory has uncommitted changes -- see
// ClaudeDo.Worker.Git.BaseDirtyChecker. Every other caller leaves it null.
public sealed record TaskRefDto(
string Id,
string ListId,
@@ -75,7 +79,8 @@ public sealed record TaskRefDto(
int? FailureMaxTurns = null,
string? DependsOnTaskId = null,
bool Blocked = false,
string? BlockedReason = null);
string? BlockedReason = null,
DirtyBaseWarning? BaseDirty = null);
// tasks is populated when includeDescription=false (the default): lean references, no
// Description/Result. tasksFull is populated when includeDescription=true: full tasks incl.
@@ -186,6 +191,7 @@ public sealed class ExternalMcpService
private readonly WorktreeMaintenanceService _maintenance;
private readonly TaskMergeService _merge;
private readonly PlanningMergeOrchestrator _planningMerge;
private readonly IBaseDirtyChecker _baseDirtyChecker;
public ExternalMcpService(
TaskRepository tasks,
@@ -197,7 +203,8 @@ public sealed class ExternalMcpService
IDbContextFactory<ClaudeDoDbContext> dbFactory,
WorktreeMaintenanceService maintenance,
TaskMergeService merge,
PlanningMergeOrchestrator planningMerge)
PlanningMergeOrchestrator planningMerge,
IBaseDirtyChecker baseDirtyChecker)
{
_tasks = tasks;
_lists = lists;
@@ -209,6 +216,7 @@ public sealed class ExternalMcpService
_maintenance = maintenance;
_merge = merge;
_planningMerge = planningMerge;
_baseDirtyChecker = baseDirtyChecker;
}
[McpServerTool, Description(
@@ -566,6 +574,7 @@ public sealed class ExternalMcpService
var task = await _tasks.GetByIdAsync(taskId, cancellationToken)
?? throw new InvalidOperationException($"Task {taskId} not found.");
DirtyBaseWarning? baseDirty = null;
switch (target)
{
case TaskStatus.Idle:
@@ -577,6 +586,7 @@ public sealed class ExternalMcpService
var enqueueResult = await _state.EnqueueAsync(taskId, cancellationToken);
if (!enqueueResult.Ok)
throw new InvalidOperationException(enqueueResult.Reason ?? "Cannot enqueue task.");
baseDirty = enqueueResult.BaseDirty;
break;
case TaskStatus.Cancelled:
@@ -605,7 +615,7 @@ public sealed class ExternalMcpService
}
var reload = (await _tasks.GetByIdAsync(taskId, cancellationToken))!;
return ToRefDto(reload);
return ToRefDto(reload) with { BaseDirty = baseDirty };
}
[McpServerTool, Description(
@@ -782,7 +792,15 @@ public sealed class ExternalMcpService
throw new InvalidOperationException($"Task {taskId} not found.");
}
await _broadcaster.TaskUpdated(taskId);
return new RunTaskNowResult(true, taskId);
// Heads-up only, same as update_task_status's Queued path: a worktree forks from the
// commit tip, not the working tree, so uncommitted changes in the list's repo right now
// won't be included.
var task = await _tasks.GetByIdAsync(taskId, cancellationToken);
var list = task is not null ? await _lists.GetByIdAsync(task.ListId, cancellationToken) : null;
var baseDirty = await _baseDirtyChecker.CheckAsync(list?.WorkingDir, cancellationToken);
return new RunTaskNowResult(true, taskId, baseDirty);
}
[McpServerTool, Description(