feat(worker): report progress for continue_merge and unit-merge verify gate

continue_merge and the parent/children unit-merge drain (PlanningMergeOrchestrator)
re-run the post-merge verify gate but never forwarded their IProgress token into it,
so a slow verify command on either path went silent past Claude Code's 300s MCP
idle-abort even though D1-D3 already fixed this for merge_task/review_task's
childless path. list_worktrees also gets elapsed-time progress: many tracked
worktrees means many concurrent git subprocess spawns.

Worker CLAUDE.md's existing progress rule now points at ProgressReporter as the
one implementation instead of a fresh polling loop.
This commit is contained in:
Mika Kuns
2026-08-17 09:56:59 +02:00
parent 1341c5f4f3
commit 23aab26daf
5 changed files with 189 additions and 27 deletions
@@ -6,6 +6,7 @@ using ClaudeDo.Worker.Hub;
using ClaudeDo.Worker.Lifecycle;
using ClaudeDo.Worker.State;
using Microsoft.EntityFrameworkCore;
using ModelContextProtocol;
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
namespace ClaudeDo.Worker.Planning;
@@ -57,7 +58,8 @@ public sealed class PlanningMergeOrchestrator : IActiveMergeState
}
public async Task StartAsync(
string parentTaskId, string targetBranch, CancellationToken ct, bool externallyDriven = false)
string parentTaskId, string targetBranch, CancellationToken ct, bool externallyDriven = false,
IProgress<ProgressNotificationValue>? progress = null)
{
string workingDir;
List<TaskEntity> children;
@@ -121,7 +123,7 @@ public sealed class PlanningMergeOrchestrator : IActiveMergeState
throw new InvalidOperationException($"Merge already in progress for {parentTaskId}.");
await _broadcaster.PlanningMergeStarted(parentTaskId, targetBranch);
await DrainAsync(parentTaskId, ct);
await DrainAsync(parentTaskId, ct, progress);
}
/// <summary>True when a unit merge for this parent is paused on a conflict (in-memory state).</summary>
@@ -145,14 +147,15 @@ public sealed class PlanningMergeOrchestrator : IActiveMergeState
return result;
}
public async Task ContinueAsync(string planningTaskId, CancellationToken ct)
public async Task ContinueAsync(
string planningTaskId, CancellationToken ct, IProgress<ProgressNotificationValue>? progress = null)
{
if (!_states.TryGetValue(planningTaskId, out var state) || state.CurrentSubtaskId is null)
throw new InvalidOperationException(
"no in-progress merge to continue; if the worker was restarted during a conflict, use AbortPlanningMerge to reset the repository");
var current = state.CurrentSubtaskId;
var result = await _merge.ContinueMergeAsync(current, ct);
var result = await _merge.ContinueMergeAsync(current, ct, progress);
if (result.Status == TaskMergeService.StatusConflict)
{
@@ -173,7 +176,7 @@ public sealed class PlanningMergeOrchestrator : IActiveMergeState
await _broadcaster.PlanningSubtaskMerged(planningTaskId, current);
state.CurrentSubtaskId = null;
await DrainAsync(planningTaskId, ct);
await DrainAsync(planningTaskId, ct, progress);
}
public async Task AbortAsync(string planningTaskId, CancellationToken ct)
@@ -213,7 +216,8 @@ public sealed class PlanningMergeOrchestrator : IActiveMergeState
// Parent remains WaitingForReview — Approve will restart the unit merge from scratch.
}
private async Task DrainAsync(string planningTaskId, CancellationToken ct)
private async Task DrainAsync(
string planningTaskId, CancellationToken ct, IProgress<ProgressNotificationValue>? progress = null)
{
if (!_states.TryGetValue(planningTaskId, out var state)) return;
@@ -229,7 +233,8 @@ public sealed class PlanningMergeOrchestrator : IActiveMergeState
removeWorktree: true,
commitMessage: "", // blank -> TaskMergeService builds the conventional default
leaveConflictsInTree: true,
ct);
ct,
progress);
if (result.Status == TaskMergeService.StatusConflict)
{