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
@@ -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