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
@@ -26,6 +26,7 @@ public sealed class MergeHelperTaskHubTests : IDisposable
private readonly TaskRepository _tasks;
private readonly ListRepository _lists;
private readonly List<GitRepoFixture> _repos = new();
private readonly List<string> _mergeHelperSessionDirs = new();
private readonly RecordingClientProxy _proxy = new();
private static bool GitAvailable => GitRepoFixture.IsGitAvailable();
@@ -42,6 +43,8 @@ public sealed class MergeHelperTaskHubTests : IDisposable
foreach (var r in _repos) r.Dispose();
_ctx.Dispose();
_db.Dispose();
foreach (var d in _mergeHelperSessionDirs)
try { Directory.Delete(d, true); } catch { /* best effort */ }
}
private sealed class FakeSessionSkillRegistry : ISessionSkillRegistry
@@ -136,6 +139,37 @@ public sealed class MergeHelperTaskHubTests : IDisposable
() => hub.CreateMergeHelperTask(new[] { "t1" }, "no-such-list", "title", "header"));
}
// ── GetMergeHelperHandoffLaunchSpec ──
[Fact]
public async Task GetMergeHelperHandoffLaunchSpec_ReusesHandlerTaskId_NoNewTaskCreated()
{
var listId = await SeedListAsync(Path.GetTempPath(), name: "Alpha");
var handlerTask = await SeedTaskAsync(listId, TaskStatus.Idle, title: "List handler: Alpha");
var survivor = await SeedTaskAsync(listId, TaskStatus.WaitingForReview, title: "Survivor task");
var hub = CreateHub();
var spec = await hub.GetMergeHelperHandoffLaunchSpec(handlerTask.Id, new[] { survivor.Id });
var args = spec.Args.ToList();
var sessionDir = args[args.IndexOf("--add-dir") + 1];
_mergeHelperSessionDirs.Add(sessionDir);
Assert.Contains("--allowedTools", args);
Assert.Contains("mcp__claudedo__*,Read,Grep,Glob,Edit,Bash,WebFetch,WebSearch,Skill", args);
var kickoff = args[^1];
Assert.Contains(Path.Combine(sessionDir, "handoff.md"), kickoff);
}
[Fact]
public async Task GetMergeHelperHandoffLaunchSpec_UnknownHandlerTask_Throws()
{
var hub = CreateHub();
await Assert.ThrowsAsync<HubException>(
() => hub.GetMergeHelperHandoffLaunchSpec("no-such-task", new[] { "x" }));
}
// ── SubmitTaskForReview (worktree-less branch) ──
[Fact]