diff --git a/src/ClaudeDo.Worker/External/ExternalMcpService.cs b/src/ClaudeDo.Worker/External/ExternalMcpService.cs index e943ee2..8e13557 100644 --- a/src/ClaudeDo.Worker/External/ExternalMcpService.cs +++ b/src/ClaudeDo.Worker/External/ExternalMcpService.cs @@ -468,12 +468,6 @@ public sealed class ExternalMcpService if (result.Status == TaskMergeService.StatusMerged) { - // MergeAsync only flips the worktree to Merged; a merged task must also - // reach Done. If it was still awaiting review, approve it now (a Done task - // is already terminal and needs no transition). - if (task.Status == TaskStatus.WaitingForReview) - await _state.ApproveReviewAsync(taskId, cancellationToken); - string? mergeCommit = null; try { diff --git a/src/ClaudeDo.Worker/Lifecycle/TaskMergeService.cs b/src/ClaudeDo.Worker/Lifecycle/TaskMergeService.cs index 50e1fe6..7af24e3 100644 --- a/src/ClaudeDo.Worker/Lifecycle/TaskMergeService.cs +++ b/src/ClaudeDo.Worker/Lifecycle/TaskMergeService.cs @@ -85,6 +85,15 @@ public sealed class TaskMergeService await _broadcaster.WorktreeUpdated(taskId); } + private async Task ApproveIfWaitingForReviewAsync(TaskEntity task, CancellationToken ct) + { + // A merged worktree means the work is integrated, so the task must reach Done. + // MarkWorktreeMergedAsync only flips the worktree state; transition the task + // itself when it was still awaiting review (a Done task is already terminal). + if (task.Status == TaskStatus.WaitingForReview) + await _state.ApproveReviewAsync(task.Id, ct); + } + public async Task MergeAsync( string taskId, string targetBranch, @@ -168,6 +177,7 @@ public sealed class TaskMergeService } await MarkWorktreeMergedAsync(taskId, ct); + await ApproveIfWaitingForReviewAsync(task, ct); _logger.LogInformation( "Merged task {TaskId} branch {Branch} into {Target} (remove worktree: {Remove})", @@ -187,7 +197,7 @@ public sealed class TaskMergeService public async Task ContinueMergeAsync(string taskId, CancellationToken ct) { - var (_, list, wt) = await LoadMergeContextAsync(taskId, ct); + var (task, list, wt) = await LoadMergeContextAsync(taskId, ct); if (wt is null) return Blocked("task has no worktree"); if (wt.State != WorktreeState.Active) return Blocked($"worktree state is {wt.State}"); @@ -205,6 +215,7 @@ public sealed class TaskMergeService catch (Exception ex) { return Blocked($"commit failed: {ex.Message}"); } await MarkWorktreeMergedAsync(taskId, ct); + await ApproveIfWaitingForReviewAsync(task, ct); _logger.LogInformation("Continued merge of task {TaskId} branch {Branch}", taskId, wt.BranchName); return new MergeResult(StatusMerged, Array.Empty(), null); @@ -313,12 +324,8 @@ public sealed class TaskMergeService ? await _git.GetCurrentBranchAsync(list.WorkingDir, ct) : targetBranch; - var merge = await MergeAsync(taskId, target, removeWorktree: false, $"Merge {wt.BranchName}", ct); - if (merge.Status != StatusMerged) - return merge; - - var approve = await _state.ApproveReviewAsync(taskId, ct); - return approve.Ok ? merge : Blocked(approve.Reason ?? "approve failed"); + // MergeAsync transitions the task WaitingForReview -> Done on a successful merge. + return await MergeAsync(taskId, target, removeWorktree: false, $"Merge {wt.BranchName}", ct); } private static MergeResult Blocked(string reason) =>