feat(worker): wire list-handler phase parameter through handoff chain

Threads a nextPhase parameter (wait/merge/wait_final/merge_final, validated
by the new MergeHelperPhase) from handoff_list_handler through
HubBroadcaster/WorkerHub into InteractiveLaunchSpecService, which now picks
the next session's system prompt (MergeHelperWait/MergeHelperMerge) and
model (HandlerWaitAlias/HandlerMergeAlias) from it instead of hardcoding the
old two-phase Execute prompt -- this also fixes a build break left by the
prior prompt-split task, which removed PromptKind.MergeHelperExecute without
updating its only caller.

Also sets --model/--effort/--permission-mode explicitly for every list-handler
session (Triage included) via PermissionModeResolver instead of inheriting the
CLI's ambient model and hardcoding "auto", and adds Task to the merge-helper
allowlist so the Merge phase can delegate diff reviews to subagents.
This commit is contained in:
mika kuns
2026-08-11 09:27:16 +02:00
parent 1657a70962
commit f2609d186a
11 changed files with 240 additions and 63 deletions
+16 -9
View File
@@ -1,11 +1,12 @@
using System.ComponentModel;
using ClaudeDo.Data.Repositories;
using ClaudeDo.Worker.Hub;
using ClaudeDo.Worker.Runner;
using ModelContextProtocol.Server;
namespace ClaudeDo.Worker.External;
public sealed record HandoffListHandlerResult(bool Requested, string TaskId, int SurvivingCount);
public sealed record HandoffListHandlerResult(bool Requested, string TaskId, int SurvivingCount, string NextPhase);
[McpServerToolType]
public sealed class HandoffMcpTools
@@ -20,16 +21,22 @@ public sealed class HandoffMcpTools
}
[McpServerTool, Description(
"Call at the end of Phase 2 of the list handler (\"Let Claude handle it\") to hand this run off " +
"to a fresh ConPTY session that carries out Phases 3-5, without dragging along this session's " +
"dedupe/rewrite context. Reuses the SAME handler task -- no new task is created, and " +
"HandlerBaseCommit is untouched. The current tile stays open; you must end your own turn " +
"immediately after calling this.")]
"Call at the end of a list-handler (\"Let Claude handle it\") phase to hand this run off to a " +
"fresh ConPTY session that carries out the next phase, without dragging along this session's " +
"own context. Reuses the SAME handler task -- no new task is created, and HandlerBaseCommit is " +
"untouched. nextPhase picks the next session's role: \"wait\" (queue + wait; the normal handoff " +
"after triage) or \"merge\" (review + merge; the normal handoff after wait) -- \"wait_final\"/" +
"\"merge_final\" are only for a merge session that started reruns and must eventually stop the " +
"chain instead of restarting again. An unrecognized value is rejected. The current tile stays " +
"open; you must end your own turn immediately after calling this.")]
public async Task<HandoffListHandlerResult> HandoffListHandler(
[Description("This session's own handler task id.")] string taskId,
[Description("The tasks that made it past dedupe, in the order to run them.")] IReadOnlyList<string> survivingTaskIds,
CancellationToken cancellationToken)
[Description("Next session's phase: wait | merge | wait_final | merge_final.")] string nextPhase = "wait",
CancellationToken cancellationToken = default)
{
MergeHelperPhase.Validate(nextPhase);
if (survivingTaskIds.Count == 0)
throw new InvalidOperationException("survivingTaskIds must contain at least one task id.");
@@ -40,7 +47,7 @@ public sealed class HandoffMcpTools
_ = await _tasks.GetByIdAsync(id, cancellationToken)
?? throw new InvalidOperationException($"Task {id} not found.");
await _broadcaster.HandoffRequested(taskId, survivingTaskIds);
return new HandoffListHandlerResult(true, taskId, survivingTaskIds.Count);
await _broadcaster.HandoffRequested(taskId, survivingTaskIds, nextPhase);
return new HandoffListHandlerResult(true, taskId, survivingTaskIds.Count, nextPhase);
}
}