diff --git a/src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs b/src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs
index 3e0f8ebf..9b78e4bc 100644
--- a/src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs
+++ b/src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs
@@ -85,6 +85,9 @@ public interface IWorkerClient : INotifyPropertyChanged
/// Launch spec for an ad-hoc interactive session in an arbitrary directory --
/// no task, no worktree.
Task GetAdHocLaunchSpecAsync(string directory, CancellationToken ct = default);
+ /// Launch spec for an embedded ConPTY "merge helper" session that drives the given
+ /// tasks to a merged/Done state. listId scopes the session to that list; null = all lists.
+ Task GetMergeHelperLaunchSpecAsync(IReadOnlyList taskIds, string? listId, CancellationToken ct = default);
/// Starts a planning session and returns the launch spec for an embedded ConPTY
/// planning terminal (replaces StartPlanningSessionAsync's external wt window).
Task GetPlanningStartLaunchSpecAsync(string taskId, CancellationToken ct = default);
diff --git a/src/ClaudeDo.Ui/Services/WorkerClient.cs b/src/ClaudeDo.Ui/Services/WorkerClient.cs
index 5ce7adf6..16a1c971 100644
--- a/src/ClaudeDo.Ui/Services/WorkerClient.cs
+++ b/src/ClaudeDo.Ui/Services/WorkerClient.cs
@@ -522,6 +522,9 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
public async Task GetAdHocLaunchSpecAsync(string directory, CancellationToken ct = default)
=> await _hub.InvokeAsync("GetAdHocLaunchSpec", directory, ct);
+ public async Task GetMergeHelperLaunchSpecAsync(IReadOnlyList taskIds, string? listId, CancellationToken ct = default)
+ => await _hub.InvokeAsync("GetMergeHelperLaunchSpec", taskIds, listId, ct);
+
public async Task GetPlanningStartLaunchSpecAsync(string taskId, CancellationToken ct = default)
=> await _hub.InvokeAsync("GetPlanningStartLaunchSpec", taskId, ct);
diff --git a/src/ClaudeDo.Worker/Hub/WorkerHub.cs b/src/ClaudeDo.Worker/Hub/WorkerHub.cs
index 7b2731a5..93cfc43c 100644
--- a/src/ClaudeDo.Worker/Hub/WorkerHub.cs
+++ b/src/ClaudeDo.Worker/Hub/WorkerHub.cs
@@ -676,6 +676,16 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
return _interactiveLaunchSpec.BuildForDirectoryAsync(directory, Context.ConnectionAborted);
});
+ // Builds the launch spec for an embedded ConPTY "merge helper" session that drives the given
+ // tasks to a merged/Done state via the mcp__claudedo__* tools. listId scopes the brief label
+ // and cwd to that list; null means all lists.
+ public Task GetMergeHelperLaunchSpec(string[] taskIds, string? listId) => HubGuard(() =>
+ {
+ if (_interactiveLaunchSpec is null)
+ throw new InvalidOperationException("Interactive launch spec service is not configured.");
+ return _interactiveLaunchSpec.BuildForMergeHelperAsync(taskIds, listId, Context.ConnectionAborted);
+ });
+
// Starts a planning session (worktree + prompt files + token, task -> Planning) and returns
// the launch spec for an embedded ConPTY planning terminal -- the ConPTY replacement for
// StartPlanningSessionAsync's external wt window. On any spec-build failure the just-started
diff --git a/tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs b/tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs
index e13cafe8..7faea86c 100644
--- a/tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs
+++ b/tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs
@@ -100,6 +100,8 @@ public abstract class StubWorkerClient : IWorkerClient
=> Task.FromResult(new LaunchSpec(".", "claude", Array.Empty(), new Dictionary()));
public virtual Task GetAdHocLaunchSpecAsync(string directory, CancellationToken ct = default)
=> Task.FromResult(new LaunchSpec(directory, "claude", Array.Empty(), new Dictionary()));
+ public virtual Task GetMergeHelperLaunchSpecAsync(IReadOnlyList taskIds, string? listId, CancellationToken ct = default)
+ => Task.FromResult(new LaunchSpec(".", "claude", Array.Empty(), new Dictionary()));
public virtual Task GetPlanningStartLaunchSpecAsync(string taskId, CancellationToken ct = default)
=> Task.FromResult(new LaunchSpec(".", "claude", Array.Empty(), new Dictionary()));
public virtual Task GetPlanningResumeLaunchSpecAsync(string taskId, CancellationToken ct = default)
diff --git a/tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs b/tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs
index 60cdd73f..1bc6598c 100644
--- a/tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs
+++ b/tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs
@@ -75,6 +75,8 @@ sealed class FakeWorkerClient : IWorkerClient
public Task SubmitTaskForReviewAsync(string taskId, CancellationToken ct = default) => Task.CompletedTask;
public Task GetInteractiveLaunchSpecAsync(string taskId, CancellationToken ct = default)
=> Task.FromResult(new LaunchSpec(".", "claude", Array.Empty(), new Dictionary()));
+ public Task GetMergeHelperLaunchSpecAsync(IReadOnlyList taskIds, string? listId, CancellationToken ct = default)
+ => Task.FromResult(new LaunchSpec(".", "claude", Array.Empty(), new Dictionary()));
public Task GetAdHocLaunchSpecAsync(string directory, CancellationToken ct = default)
=> Task.FromResult(new LaunchSpec(directory, "claude", Array.Empty(), new Dictionary()));
public int PlanningStartSpecCalls { get; private set; }