feat(claude-do): Merge-Session soll die Queue selbst beobachten statt fertige

## WO — verifiziert am 2026-09-02, nicht neu suchen

- Merge-Phasen-Prompt: `src/ClaudeDo.Data/PromptFiles.cs`, Konstante `MergeHelperMergeDefault` (ab Z. 495). Der neue Warte-Abschnitt gehört zwischen "## Verify once — after the last merge" (Z. 526) und "## Handoff / end" (Z. 542) — also nach dem Verify, vor der Handoff-Entscheidung.
- Brief-Template: `MergeHelperInitialDefault` (Z. 555) und `Mer

ClaudeDo-Task: 1788adc4-c671-4942-ab78-1ef1812da494
This commit is contained in:
Mika Kuns
2026-09-02 13:49:58 +02:00
parent 7e847638cf
commit 6c1ea1ae25
2 changed files with 51 additions and 6 deletions
+24
View File
@@ -548,6 +548,30 @@ public static class PromptFiles
Allow at most ONE rerun per task in this session.
## Keep watching the queue
Do not assume the tasks you were handed are the only ones that matter. A prior Wait session
can already have finished before every one of its tasks actually reached a terminal status,
or new tasks can still be running when you start — merging only what happened to be ready the
moment you looked in and then stopping is how work silently falls through the cracks.
Before you go to Handoff / end, call get_queue_state and check whether any task id from your
brief still shows up in activeSlots or waitingTaskIds. If none do, your list is clear — go to
Handoff / end now.
If some do, call wait_for_task_change with exactly those still-busy task ids,
timeoutSeconds=900, treatWaitingForChildrenAsBusy=true. When it reports a change, go back to
Phase 4 and merge/review whatever it now says is WaitingForReview, then repeat this check.
This is NOT a `sleep`-and-poll loop — never write your own retry/sleep loop around get_task or
get_queue_state. wait_for_task_change already blocks server-side and reports progress so it
survives your own client's idle-silence abort; that is the only waiting mechanism allowed here.
Hard cap: stop after at most THREE consecutive wait_for_task_change calls made this way (three
900s timeouts ≈ 45 minutes total), even if tasks are still busy. When you stop — because the
queue cleared or because you hit the cap — report whatever is still Queued/Running in the
summary below and proceed to Handoff / end exactly as if this section didn't exist.
## Handoff / end
- If you started any reruns: call handoff_list_handler(taskId, <the rerun task ids>, nextPhase: "wait_final"), then stop your turn immediately — do not merge anything yet that you just restarted. Do NOT submit this run for review yet — the chain isn't finished.
@@ -284,12 +284,12 @@ public sealed class InteractiveLaunchSpecService
throw new InvalidOperationException($"list '{list.Name}' has no existing working directory");
repoDir = TrimTrailingSeparator(repoDir);
var briefLines = new List<string>();
var briefEntries = new List<(TaskEntity Task, string Line)>();
foreach (var id in taskIds)
{
var task = await taskRepo.GetByIdAsync(id, ct)
?? throw new KeyNotFoundException($"Task not found: {id}");
briefLines.Add(RenderBriefEntry(task));
briefEntries.Add((task, RenderBriefEntry(task)));
}
var sessionDir = Path.Combine(Paths.AppDataRoot(), "merge-helper-sessions", Guid.NewGuid().ToString());
@@ -305,7 +305,7 @@ public sealed class InteractiveLaunchSpecService
["scope"] = $"List: {list.Name}",
["repo"] = repoDir,
["handlerTaskId"] = handlerTaskId,
["tasks"] = string.Join("\n", briefLines),
["tasks"] = RenderGroupedBrief(briefEntries),
}), ct);
var resolvedClaude = WindowsTerminalLauncher.Resolve(_claudePath)
@@ -379,12 +379,12 @@ public sealed class InteractiveLaunchSpecService
throw new InvalidOperationException($"list '{list.Name}' has no existing working directory");
repoDir = TrimTrailingSeparator(repoDir);
var briefLines = new List<string>();
var briefEntries = new List<(TaskEntity Task, string Line)>();
foreach (var id in survivingTaskIds)
{
var task = await taskRepo.GetByIdAsync(id, ct)
?? throw new KeyNotFoundException($"Task not found: {id}");
briefLines.Add($"- [{task.Status}] {task.Title} (id: {task.Id})");
briefEntries.Add((task, $"- [{task.Status}] {task.Title} (id: {task.Id})"));
}
var sessionDir = Path.Combine(Paths.AppDataRoot(), "merge-helper-sessions", Guid.NewGuid().ToString());
@@ -408,7 +408,7 @@ public sealed class InteractiveLaunchSpecService
["scope"] = $"List: {list.Name}",
["repo"] = repoDir,
["handlerTaskId"] = taskId,
["tasks"] = string.Join("\n", briefLines),
["tasks"] = RenderGroupedBrief(briefEntries),
["finalNote"] = finalNote,
}), ct);
@@ -444,6 +444,27 @@ public sealed class InteractiveLaunchSpecService
// at all. ListRepository normalizes on write now, but rows written before that still carry one.
private static string TrimTrailingSeparator(string dir) => Paths.TrimTrailingSeparator(dir)!;
// A task the picker would still touch (or is mid-run) vs. one that already landed
// somewhere terminal-ish. Split the brief on this so a handler session picking up after a
// Wait phase can see at a glance which of its tasks are actually ready to review and which
// are still moving underneath it -- the flat list this replaces made every task look
// equally "done" regardless of status.
private static readonly TaskStatus[] StillRunningStatuses =
{ TaskStatus.Queued, TaskStatus.Running, TaskStatus.WaitingForChildren };
private static string RenderGroupedBrief(IReadOnlyList<(TaskEntity Task, string Line)> entries)
{
var finished = entries.Where(e => !StillRunningStatuses.Contains(e.Task.Status)).Select(e => e.Line).ToList();
var running = entries.Where(e => StillRunningStatuses.Contains(e.Task.Status)).Select(e => e.Line).ToList();
var sections = new List<string>();
if (finished.Count > 0)
sections.Add("### Done / waiting for review\n" + string.Join("\n", finished));
if (running.Count > 0)
sections.Add("### Still running\n" + string.Join("\n", running));
return string.Join("\n\n", sections);
}
// Renders one task as a brief list item. A description can itself be arbitrary Markdown
// (headings, lists, fenced code) — those must not merge into the brief's own task list, so
// the description is placed in a fenced code block indented to the list item's continuation