feat(list-handler): hand off to a fresh session after Phase 2

The merge-helper ("Let Claude handle it") system prompt now calls a new
handoff_list_handler MCP tool once every surviving task is enhanced,
instead of continuing into Phases 3-5 in the same session -- avoiding
paying for Phases 0-2's dedupe/rewrite context on every polling round of
the run/review/merge phases.

The tool broadcasts HandoffRequested; Mission Control opens a second
ConPTY tile for the SAME handler task id (no new task, HandlerBaseCommit
untouched) running a fresh handoff brief that starts at Phase 3. The
original tile stays open. Adds PromptKind.MergeHelperHandoff,
InteractiveLaunchSpecService.BuildForMergeHelperHandoffAsync, and the
GetMergeHelperHandoffLaunchSpec hub method.
This commit is contained in:
mika kuns
2026-08-05 20:27:39 +02:00
parent bdee731376
commit 4ef01274f9
18 changed files with 503 additions and 1 deletions
+44
View File
@@ -0,0 +1,44 @@
using System.ComponentModel;
using ClaudeDo.Data.Repositories;
using ClaudeDo.Worker.Hub;
using ModelContextProtocol.Server;
namespace ClaudeDo.Worker.External;
public sealed record HandoffListHandlerResult(bool Requested, string TaskId, int SurvivingCount);
[McpServerToolType]
public sealed class HandoffMcpTools
{
private readonly TaskRepository _tasks;
private readonly HubBroadcaster _broadcaster;
public HandoffMcpTools(TaskRepository tasks, HubBroadcaster broadcaster)
{
_tasks = tasks;
_broadcaster = broadcaster;
}
[McpServerTool, Description(
"End of Phase 2 for the list handler (\"Let Claude handle it\"): hand this run off to a fresh " +
"ConPTY session that carries out Phases 3-5, without dragging along this session's dedupe/rewrite " +
"context. taskId is this session's own handler task id; survivingTaskIds are the tasks that made " +
"it past dedupe, in the order to run them. Reuses the SAME handler task -- no new task is created, " +
"and HandlerBaseCommit is untouched. The current tile stays open; end your own turn after calling this.")]
public async Task<HandoffListHandlerResult> HandoffListHandler(
string taskId, IReadOnlyList<string> survivingTaskIds, CancellationToken cancellationToken)
{
if (survivingTaskIds.Count == 0)
throw new InvalidOperationException("survivingTaskIds must contain at least one task id.");
_ = await _tasks.GetByIdAsync(taskId, cancellationToken)
?? throw new InvalidOperationException($"Task {taskId} not found.");
foreach (var id in survivingTaskIds)
_ = await _tasks.GetByIdAsync(id, cancellationToken)
?? throw new InvalidOperationException($"Task {id} not found.");
await _broadcaster.HandoffRequested(taskId, survivingTaskIds);
return new HandoffListHandlerResult(true, taskId, survivingTaskIds.Count);
}
}