feat(queue): warn when the base branch has uncommitted changes
Queuing (update_task_status, batch_update_task_status) and run_task_now now surface a non-blocking baseDirty warning (separate modified/untracked counts) when the list's working dir has uncommitted changes at enqueue time, since a new worktree forks from the commit tip and silently misses them. BaseDirtyChecker caches per working dir for a few seconds so a batch queue over many tasks in one list only shells out to git once. The UI surfaces the same warning via the footer error strip on queue actions.
This commit is contained in:
+23
-5
@@ -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,
|
||||
@@ -49,6 +50,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,
|
||||
@@ -56,7 +60,8 @@ public sealed record TaskRefDto(
|
||||
string Status,
|
||||
int SortOrder,
|
||||
bool IsMyDay,
|
||||
int RoadblockCount = 0);
|
||||
int RoadblockCount = 0,
|
||||
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.
|
||||
@@ -135,6 +140,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,
|
||||
@@ -146,7 +152,8 @@ public sealed class ExternalMcpService
|
||||
IDbContextFactory<ClaudeDoDbContext> dbFactory,
|
||||
WorktreeMaintenanceService maintenance,
|
||||
TaskMergeService merge,
|
||||
PlanningMergeOrchestrator planningMerge)
|
||||
PlanningMergeOrchestrator planningMerge,
|
||||
IBaseDirtyChecker baseDirtyChecker)
|
||||
{
|
||||
_tasks = tasks;
|
||||
_lists = lists;
|
||||
@@ -158,6 +165,7 @@ public sealed class ExternalMcpService
|
||||
_maintenance = maintenance;
|
||||
_merge = merge;
|
||||
_planningMerge = planningMerge;
|
||||
_baseDirtyChecker = baseDirtyChecker;
|
||||
}
|
||||
|
||||
[McpServerTool, Description(
|
||||
@@ -436,6 +444,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:
|
||||
@@ -447,6 +456,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:
|
||||
@@ -475,7 +485,7 @@ public sealed class ExternalMcpService
|
||||
}
|
||||
|
||||
var reload = (await _tasks.GetByIdAsync(taskId, cancellationToken))!;
|
||||
return ToRefDto(reload);
|
||||
return ToRefDto(reload) with { BaseDirty = baseDirty };
|
||||
}
|
||||
|
||||
[McpServerTool, Description(
|
||||
@@ -652,7 +662,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(
|
||||
|
||||
Reference in New Issue
Block a user