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:
@@ -1,3 +1,4 @@
|
||||
using System.Diagnostics;
|
||||
using ClaudeDo.Data;
|
||||
using ClaudeDo.Data.Git;
|
||||
using ClaudeDo.Data.Models;
|
||||
@@ -204,9 +205,10 @@ public sealed class InteractiveLaunchSpecService : IInteractiveLaunchSpecService
|
||||
|
||||
// Tools the merge helper may use without prompting: the claudedo MCP surface (run, poll,
|
||||
// diff, review/merge, continue/abort merge), read/search, Edit + Bash for hand-resolving
|
||||
// conflict markers the MCP tools left behind, and web/skill lookups.
|
||||
// conflict markers the MCP tools left behind, web/skill lookups, and Task so the Merge phase
|
||||
// can delegate diff reviews to sonnet subagents instead of reading every diff itself.
|
||||
private const string MergeHelperAllowedTools =
|
||||
"mcp__claudedo__*,Read,Grep,Glob,Edit,Bash,WebFetch,WebSearch,Skill";
|
||||
"mcp__claudedo__*,Read,Grep,Glob,Edit,Bash,WebFetch,WebSearch,Skill,Task";
|
||||
|
||||
public async Task<LaunchSpec> BuildForMergeHelperAsync(IReadOnlyList<string> taskIds, string listId, CancellationToken ct)
|
||||
{
|
||||
@@ -255,13 +257,16 @@ public sealed class InteractiveLaunchSpecService : IInteractiveLaunchSpecService
|
||||
// (--allowedTools, --add-dir) first, then a single-value flag, then the single-line
|
||||
// positional kickoff LAST — a multi-line positional prompt truncates at the first
|
||||
// newline, so the full multi-line brief travels via the file exposed through --add-dir.
|
||||
var listConfig = await listRepo.GetConfigAsync(listId, ct);
|
||||
// Model/effort/permission-mode are fixed to the Triage role -- the list config's model no
|
||||
// longer influences a handler session at all, it would otherwise apply the LIST's effort
|
||||
// preset to a completely different model.
|
||||
var settings = await new AppSettingsRepository(ctx).GetAsync(ct);
|
||||
|
||||
var args = new List<string>
|
||||
{
|
||||
"--effort", EffortFor(settings, listConfig?.Model),
|
||||
"--permission-mode", "auto",
|
||||
"--model", ModelRegistry.HandlerTriageAlias,
|
||||
"--effort", EffortFor(settings, ModelRegistry.HandlerTriageAlias),
|
||||
"--permission-mode", PermissionModeResolver.Resolve(ModelRegistry.HandlerTriageAlias, "auto"),
|
||||
"--allowedTools", MergeHelperAllowedTools,
|
||||
"--add-dir", sessionDir, repoDir,
|
||||
"--append-system-prompt-file", systemPromptPath,
|
||||
@@ -277,19 +282,22 @@ public sealed class InteractiveLaunchSpecService : IInteractiveLaunchSpecService
|
||||
return new LaunchSpec(repoDir, resolvedClaude, args, env);
|
||||
}
|
||||
|
||||
// Builds the LaunchSpec for the fresh session a merge-helper run hands off to once Phase 2
|
||||
// (enhance) is done -- SAME handler task id as the run that called handoff_list_handler, so
|
||||
// Builds the LaunchSpec for the fresh session a merge-helper run hands off to for the given
|
||||
// phase -- SAME handler task id as the run that called handoff_list_handler, so
|
||||
// HandlerBaseCommit/HandlerHeadCommit and the review range stay untouched; this never creates
|
||||
// a task. Uses the MergeHelperExecute system prompt (phases 3-5 only) rather than the Triage
|
||||
// one this run started with, so the handoff session carries no dedupe/enhance instructions it
|
||||
// would have to ignore. Writes a fresh handoff kickoff file in a NEW session dir -- the old
|
||||
// ConPTY tile keeps running against its own session-dir files untouched.
|
||||
// a task. nextPhase picks the system prompt (MergeHelperWait for wait/wait_final,
|
||||
// MergeHelperMerge for merge/merge_final) so the handoff session only ever carries the
|
||||
// instructions for the role it is actually about to run, never the Triage dedupe/enhance ones.
|
||||
// Writes a fresh handoff kickoff file in a NEW session dir -- the old ConPTY tile keeps running
|
||||
// against its own session-dir files untouched.
|
||||
public async Task<LaunchSpec> BuildForMergeHelperHandoffAsync(
|
||||
string taskId, IReadOnlyList<string> survivingTaskIds, CancellationToken ct)
|
||||
string taskId, IReadOnlyList<string> survivingTaskIds, string nextPhase, CancellationToken ct)
|
||||
{
|
||||
if (survivingTaskIds.Count == 0)
|
||||
throw new InvalidOperationException("No surviving tasks to hand off.");
|
||||
|
||||
var phase = ResolveHandoffPhase(nextPhase);
|
||||
|
||||
await using var ctx = await _dbFactory.CreateDbContextAsync(ct);
|
||||
var taskRepo = new TaskRepository(ctx);
|
||||
var listRepo = new ListRepository(ctx);
|
||||
@@ -316,7 +324,15 @@ public sealed class InteractiveLaunchSpecService : IInteractiveLaunchSpecService
|
||||
Directory.CreateDirectory(sessionDir);
|
||||
|
||||
var systemPromptPath = Path.Combine(sessionDir, "system-prompt.md");
|
||||
await File.WriteAllTextAsync(systemPromptPath, PromptFiles.ReadOrDefault(PromptKind.MergeHelperExecute), ct);
|
||||
await File.WriteAllTextAsync(systemPromptPath, PromptFiles.ReadOrDefault(phase.PromptKind), ct);
|
||||
|
||||
// A "_final" round differs from its non-final counterpart in exactly one rendered line --
|
||||
// the same system prompt and model drive both -- marking the final round and forbidding a
|
||||
// further rerun, so the chain is bounded structurally rather than by prompt appeal alone.
|
||||
var finalNote = phase.IsFinal
|
||||
? "\nThis is the FINAL round of this list-handler run -- do not start any further reruns; " +
|
||||
"merge what you can, report the rest, and print the summary."
|
||||
: "";
|
||||
|
||||
var briefPath = Path.Combine(sessionDir, "handoff.md");
|
||||
await File.WriteAllTextAsync(briefPath, PromptFiles.Render(PromptKind.MergeHelperHandoff,
|
||||
@@ -325,23 +341,24 @@ public sealed class InteractiveLaunchSpecService : IInteractiveLaunchSpecService
|
||||
["scope"] = $"List: {list.Name}",
|
||||
["repo"] = repoDir,
|
||||
["tasks"] = string.Join("\n", briefLines),
|
||||
["finalNote"] = finalNote,
|
||||
}), ct);
|
||||
|
||||
var resolvedClaude = WindowsTerminalLauncher.Resolve(_claudePath)
|
||||
?? throw new InvalidOperationException($"claude executable not found: {_claudePath}");
|
||||
|
||||
var listConfig = await listRepo.GetConfigAsync(handlerTask.ListId, ct);
|
||||
var settings = await new AppSettingsRepository(ctx).GetAsync(ct);
|
||||
|
||||
var args = new List<string>
|
||||
{
|
||||
"--effort", EffortFor(settings, listConfig?.Model),
|
||||
"--permission-mode", "auto",
|
||||
"--model", phase.Model,
|
||||
"--effort", EffortFor(settings, phase.Model),
|
||||
"--permission-mode", PermissionModeResolver.Resolve(phase.Model, "auto"),
|
||||
"--allowedTools", MergeHelperAllowedTools,
|
||||
"--add-dir", sessionDir, repoDir,
|
||||
"--append-system-prompt-file", systemPromptPath,
|
||||
$"Read the file {briefPath} first. It lists the surviving tasks and their status. " +
|
||||
"After reading it, begin at phase 3 as your instructions describe.",
|
||||
$"After reading it, begin at {phase.PhaseLabel} as your instructions describe.",
|
||||
};
|
||||
|
||||
var env = new Dictionary<string, string>
|
||||
@@ -443,6 +460,23 @@ public sealed class InteractiveLaunchSpecService : IInteractiveLaunchSpecService
|
||||
return handlerTask.Id;
|
||||
}
|
||||
|
||||
// Maps a handoff nextPhase to the system prompt/model the next session runs under, plus the
|
||||
// "phase N" label used in its short positional kickoff. wait/wait_final share MergeHelperWait
|
||||
// + HandlerWaitAlias; merge/merge_final share MergeHelperMerge + HandlerMergeAlias -- only
|
||||
// IsFinal differs between a phase and its "_final" counterpart.
|
||||
private static (PromptKind PromptKind, string Model, string PhaseLabel, bool IsFinal) ResolveHandoffPhase(string nextPhase)
|
||||
{
|
||||
MergeHelperPhase.Validate(nextPhase);
|
||||
return nextPhase switch
|
||||
{
|
||||
MergeHelperPhase.Wait => (PromptKind.MergeHelperWait, ModelRegistry.HandlerWaitAlias, "phase 3", false),
|
||||
MergeHelperPhase.WaitFinal => (PromptKind.MergeHelperWait, ModelRegistry.HandlerWaitAlias, "phase 3", true),
|
||||
MergeHelperPhase.Merge => (PromptKind.MergeHelperMerge, ModelRegistry.HandlerMergeAlias, "phase 4", false),
|
||||
MergeHelperPhase.MergeFinal => (PromptKind.MergeHelperMerge, ModelRegistry.HandlerMergeAlias, "phase 4", true),
|
||||
_ => throw new UnreachableException(),
|
||||
};
|
||||
}
|
||||
|
||||
// The reasoning effort configured for a model in Settings → General. Falls back to the shipped
|
||||
// preset for that model, so a missing/malformed settings row can never block a launch.
|
||||
private static string EffortFor(AppSettingsEntity settings, string? model)
|
||||
|
||||
Reference in New Issue
Block a user