feat(mcp): add get_task_config, continue_task; fix status enum, branchDeleted, merge-from-review

- ConfigMcpTools: add get_task_config read-back (was write-only)
- ExternalMcpService: add WaitingForChildren to ListTasks filter and GetTaskStatusValues
- ExternalMcpService: add continue_task tool wrapping QueueService.ContinueTask
- ExternalMcpService: add allowWaitingForReview param to merge_task (default false)
- ExternalMcpService: fix CleanupTaskWorktree branchDeleted — now uses real branch-delete outcome
- WorktreeMaintenanceService: TryRemoveAsync returns (Removed, BranchDeleted) tuple; ForceRemoveResult gains BranchDeleted field
- Tests: 9 new cases covering all five changes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
mika kuns
2026-06-09 09:57:47 +02:00
parent 763732a9b3
commit 9f19a714f7
4 changed files with 201 additions and 28 deletions

View File

@@ -9,7 +9,7 @@ public sealed class WorktreeMaintenanceService
{
public sealed record CleanupResult(int Removed, IReadOnlyList<string> RemovedTaskIds);
public sealed record ResetResult(int Removed, int TasksAffected, bool Blocked, int RunningTasks, IReadOnlyList<string> RemovedTaskIds);
public sealed record ForceRemoveResult(bool Removed, string? Reason);
public sealed record ForceRemoveResult(bool Removed, string? Reason, bool BranchDeleted);
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
private readonly GitService _git;
@@ -43,7 +43,8 @@ public sealed class WorktreeMaintenanceService
var removedTaskIds = new List<string>();
foreach (var row in rows)
{
if (await TryRemoveAsync(row, force: false, ct))
var (rowRemoved, _) = await TryRemoveAsync(row, force: false, ct);
if (rowRemoved)
{
removed++;
removedTaskIds.Add(row.TaskId);
@@ -71,7 +72,8 @@ public sealed class WorktreeMaintenanceService
var removedTaskIds = new List<string>();
foreach (var row in rows)
{
if (await TryRemoveAsync(row, force: true, ct))
var (rowRemoved, _) = await TryRemoveAsync(row, force: true, ct);
if (rowRemoved)
{
removed++;
removedTaskIds.Add(row.TaskId);
@@ -118,16 +120,16 @@ public sealed class WorktreeMaintenanceService
.FirstOrDefaultAsync(ct);
if (row is null)
return new ForceRemoveResult(false, "worktree not found");
return new ForceRemoveResult(false, "worktree not found", false);
if (row.Status == ClaudeDo.Data.Models.TaskStatus.Running)
return new ForceRemoveResult(false, "task is currently running");
return new ForceRemoveResult(false, "task is currently running", false);
var ok = await TryRemoveAsync(row.Row, force: true, ct);
return new ForceRemoveResult(ok, ok ? null : "remove failed");
var (ok, branchDeleted) = await TryRemoveAsync(row.Row, force: true, ct);
return new ForceRemoveResult(ok, ok ? null : "remove failed", branchDeleted);
}
private async Task<bool> TryRemoveAsync(WorktreeRow row, bool force, CancellationToken ct)
private async Task<(bool Removed, bool BranchDeleted)> TryRemoveAsync(WorktreeRow row, bool force, CancellationToken ct)
{
var repoDirExists = !string.IsNullOrWhiteSpace(row.WorkingDir) && Directory.Exists(row.WorkingDir);
bool dirRemoved;
@@ -163,8 +165,13 @@ public sealed class WorktreeMaintenanceService
}
}
// Drop the DB row only when the on-disk worktree is gone; otherwise we'd silently
// strand a directory while reporting success.
if (!dirRemoved) return (false, false);
// Branch cleanup: otherwise rerunning the task hits "branch already exists".
// Prune first so git no longer thinks the branch is checked out by a phantom worktree.
bool branchDeleted = false;
if (repoDirExists)
{
try { await _git.WorktreePruneAsync(row.WorkingDir!, ct); }
@@ -175,6 +182,7 @@ public sealed class WorktreeMaintenanceService
try
{
await _git.BranchDeleteAsync(row.WorkingDir!, row.BranchName, force: true, ct);
branchDeleted = true;
}
catch (Exception ex)
{
@@ -184,13 +192,9 @@ public sealed class WorktreeMaintenanceService
}
}
// Drop the DB row only when the on-disk worktree is gone; otherwise we'd silently
// strand a directory while reporting success.
if (!dirRemoved) return false;
using var context = _dbFactory.CreateDbContext();
await context.Worktrees.Where(w => w.TaskId == row.TaskId).ExecuteDeleteAsync(ct);
return true;
return (true, branchDeleted);
}
private sealed record WorktreeRow(string TaskId, string Path, string BranchName, string? WorkingDir);