feat(worker): add continue_merge and abort_merge MCP tools

This commit is contained in:
mika kuns
2026-07-24 14:21:26 +02:00
parent f4f7c81059
commit 7517f2a9b3
3 changed files with 254 additions and 0 deletions
+100
View File
@@ -47,6 +47,10 @@ public sealed record MergeTaskResultDto(
bool Merged, string? MergeCommit, IReadOnlyList<string> Conflicts,
bool ConflictsInTree = false, string? RepoPath = null);
public sealed record MergeContinuationResultDto(
bool Merged, string TaskStatus, IReadOnlyList<string> Conflicts,
string? RepoPath, string? Message);
public sealed record WorktreeListItemDto(
string? TaskId, string Path, string Branch,
string HeadCommit, bool IsDirty, bool MergedIntoMain);
@@ -570,6 +574,102 @@ public sealed class ExternalMcpService
throw new InvalidOperationException(result.ErrorMessage ?? $"Merge blocked: {result.Status}");
}
[McpServerTool, Description(
"Finish an in-progress conflicted merge after the conflict markers in the working tree (repoPath from " +
"merge_task/review_task) have been resolved. Handles both a single task's merge and a parent/children unit " +
"merge — pass the PARENT task id to continue a unit merge. On success merged=true and the task reaches its " +
"post-merge status (Done when approving). If conflict markers are still present, merged=false and conflicts " +
"lists the affected files — resolve them and call continue_merge again. " +
"Throws if there is no in-progress merge for the task; use abort_merge to cancel a paused merge instead.")]
public async Task<MergeContinuationResultDto> ContinueMerge(string taskId, CancellationToken cancellationToken)
{
var task = await _tasks.GetByIdAsync(taskId, cancellationToken)
?? throw new InvalidOperationException($"Task {taskId} not found.");
var list = await _lists.GetByIdAsync(task.ListId, cancellationToken);
var workingDir = list?.WorkingDir;
bool merged;
IReadOnlyList<string> conflicts = Array.Empty<string>();
string? repoPath = null;
string? message = null;
if (_planningMerge.HasActiveMerge(taskId))
{
await _planningMerge.ContinueAsync(taskId, cancellationToken);
var parent = (await _tasks.GetByIdAsync(taskId, cancellationToken))!;
if (parent.Status == TaskStatus.Done)
{
merged = true;
}
else
{
var remaining = !string.IsNullOrWhiteSpace(workingDir)
? await _git.ListConflictedFilesAsync(workingDir, cancellationToken)
: new List<string>();
merged = false;
if (remaining.Count > 0)
{
conflicts = remaining;
repoPath = workingDir;
message = "conflicts remain — resolve and call continue_merge again";
}
else
{
message = "unit merge did not complete — the orchestrator aborted or was blocked; " +
"check the parent task and approve again to restart the merge";
}
}
}
else
{
var r = await _merge.ContinueMergeAsync(taskId, cancellationToken);
if (r.Status == TaskMergeService.StatusMerged)
{
merged = true;
}
else if (r.Status == TaskMergeService.StatusConflict)
{
merged = false;
conflicts = r.ConflictFiles;
repoPath = workingDir;
message = r.ErrorMessage;
}
else
{
throw new InvalidOperationException(r.ErrorMessage ?? "continue failed");
}
}
var reloaded = (await _tasks.GetByIdAsync(taskId, cancellationToken))!;
await _broadcaster.TaskUpdated(taskId);
return new MergeContinuationResultDto(merged, reloaded.Status.ToString(), conflicts, repoPath, message);
}
[McpServerTool, Description(
"Abort an in-progress conflicted merge, discarding the conflict markers and restoring a clean working tree. " +
"Handles both a single task's merge and a parent/children unit merge — pass the PARENT task id to abort a " +
"unit merge. The task keeps its pre-merge status (e.g. WaitingForReview). " +
"Throws if there is no in-progress merge for the task.")]
public async Task<TaskDto> AbortMerge(string taskId, CancellationToken cancellationToken)
{
_ = await _tasks.GetByIdAsync(taskId, cancellationToken)
?? throw new InvalidOperationException($"Task {taskId} not found.");
if (_planningMerge.HasActiveMerge(taskId))
{
await _planningMerge.AbortAsync(taskId, cancellationToken);
}
else
{
var r = await _merge.AbortMergeAsync(taskId, cancellationToken);
if (r.Status == TaskMergeService.StatusBlocked)
throw new InvalidOperationException(r.ErrorMessage ?? "abort failed");
}
await _broadcaster.TaskUpdated(taskId);
return ToDto((await _tasks.GetByIdAsync(taskId, cancellationToken))!);
}
[McpServerTool, Description(
"List all ClaudeDo-tracked worktrees. " +
"Each entry: taskId, path, branch, headCommit (empty if path missing on disk), " +