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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user