feat(worker): record merge commit SHA and add revert_merge tool

Persists the merge commit SHA on WorktreeEntity for every successful
single-task and unit merge, and adds a TaskMergeService.RevertMergeAsync
+ revert_merge MCP tool that undoes a merged task's merge via
`git revert -m 1` (never reset/rewrite, since the target checkout is
shared). Rejects cleanly when there's no recorded SHA, the repo is
mid-merge/mid-revert, or the target has foreign uncommitted changes;
a conflicting revert aborts immediately. Also exposes the new
mergeCommit field via get_task_worktree.
This commit is contained in:
mika kuns
2026-08-05 11:46:51 +02:00
parent 6c5acd09b9
commit 10e561f336
14 changed files with 1452 additions and 14 deletions
+37 -3
View File
@@ -39,7 +39,7 @@ public sealed record TaskDto(
public sealed record WorktreeInfoDto(
string Path, string Branch, string HeadCommit, string BaseCommit,
int Ahead, int Behind, bool IsDirty);
int Ahead, int Behind, bool IsDirty, string? MergeCommit = null);
public sealed record TaskDiffDto(
string Content, IReadOnlyList<string> Files, bool Truncated, int TotalBytes);
@@ -59,6 +59,9 @@ public sealed record WorktreeListItemDto(
public sealed record CleanupWorktreeResult(
bool Removed, string WorktreePath, bool BranchDeleted);
public sealed record RevertMergeResultDto(
bool Reverted, string? RevertCommit, IReadOnlyList<string> Conflicts, string? Message);
public sealed record DailyPrepCandidateDto(
string Id, string ListId, string ListName, string Title, string? Description,
bool IsStarred, DateTime? ScheduledFor, DateTime CreatedAt);
@@ -486,7 +489,10 @@ public sealed class ExternalMcpService
"Get git worktree details for a task: path, branch, headCommit (current HEAD SHA), " +
"baseCommit (SHA where the branch was created), ahead (commits on branch since base), " +
"behind (commits on main not yet on this branch; 0 if 'main' ref is unreachable), " +
"isDirty (has uncommitted changes in the worktree directory). " +
"isDirty (has uncommitted changes in the worktree directory), " +
"mergeCommit (SHA of the merge commit this worktree's branch produced on the target branch, " +
"if it has been merged and that succeeded after this field was introduced; null otherwise — " +
"required by revert_merge). " +
"Throws if the task or its worktree does not exist.")]
public async Task<WorktreeInfoDto> GetTaskWorktree(string taskId, CancellationToken cancellationToken)
{
@@ -500,7 +506,7 @@ public sealed class ExternalMcpService
var ahead = await GitRevListCountAsync(wt.Path, $"{wt.BaseCommit}..HEAD", cancellationToken);
var behind = await GitRevListCountAsync(wt.Path, "HEAD..main", cancellationToken);
return new WorktreeInfoDto(wt.Path, wt.BranchName, headCommit!, wt.BaseCommit, ahead, behind, isDirty);
return new WorktreeInfoDto(wt.Path, wt.BranchName, headCommit!, wt.BaseCommit, ahead, behind, isDirty, wt.MergeCommit);
}
[McpServerTool, Description(
@@ -729,6 +735,34 @@ public sealed class ExternalMcpService
return ToDto((await _tasks.GetByIdAsync(taskId, cancellationToken))!);
}
[McpServerTool, Description(
"Revert a previously merged task's merge commit on targetBranch (default: main), via `git revert -m 1` — " +
"a new commit, never a reset/rewrite (the target working directory is shared with other sessions). " +
"Requires the task to be Done with a Merged worktree that has a recorded merge commit; tasks merged " +
"before this feature existed have no recorded commit and are refused rather than guessed via git log. " +
"On success: reverted=true, revertCommit is the new commit's SHA, and the task returns to " +
"WaitingForReview so it can be reconsidered. " +
"On a conflicting revert: reverted=false, the revert is aborted immediately (no half-resolved state " +
"left in the tree) and conflicts lists the files that would have conflicted. " +
"Throws if there is no recorded merge commit, the repo is mid-merge/mid-revert, or the target working " +
"tree has uncommitted changes from another session.")]
public async Task<RevertMergeResultDto> RevertMerge(
string taskId, string targetBranch = "main", CancellationToken cancellationToken = default)
{
var result = await _merge.RevertMergeAsync(taskId, targetBranch, cancellationToken);
if (result.Status == TaskMergeService.StatusReverted)
{
await _broadcaster.TaskUpdated(taskId);
return new RevertMergeResultDto(true, result.RevertCommit, Array.Empty<string>(), null);
}
if (result.Status == TaskMergeService.StatusConflictAborted)
return new RevertMergeResultDto(false, null, result.ConflictFiles, result.ErrorMessage);
throw new InvalidOperationException(result.ErrorMessage ?? $"Revert blocked: {result.Status}");
}
[McpServerTool, Description(
"List all ClaudeDo-tracked worktrees. " +
"Each entry: taskId, path, branch, headCommit (empty if path missing on disk), " +