feat(worker): add continue_merge and abort_merge MCP tools
This commit is contained in:
+100
@@ -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), " +
|
||||
|
||||
@@ -108,6 +108,10 @@ public sealed class PlanningMergeOrchestrator
|
||||
await DrainAsync(parentTaskId, ct);
|
||||
}
|
||||
|
||||
/// <summary>True when a unit merge for this parent is paused on a conflict (in-memory state).</summary>
|
||||
public bool HasActiveMerge(string parentTaskId) =>
|
||||
_states.TryGetValue(parentTaskId, out var s) && s.CurrentSubtaskId is not null;
|
||||
|
||||
public async Task ContinueAsync(string planningTaskId, CancellationToken ct)
|
||||
{
|
||||
if (!_states.TryGetValue(planningTaskId, out var state) || state.CurrentSubtaskId is null)
|
||||
|
||||
Reference in New Issue
Block a user