diff --git a/src/ClaudeDo.Data/PromptFiles.cs b/src/ClaudeDo.Data/PromptFiles.cs index b3ef6190..0c67850a 100644 --- a/src/ClaudeDo.Data/PromptFiles.cs +++ b/src/ClaudeDo.Data/PromptFiles.cs @@ -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, , 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. diff --git a/src/ClaudeDo.Worker/Runner/InteractiveLaunchSpecService.cs b/src/ClaudeDo.Worker/Runner/InteractiveLaunchSpecService.cs index 9b7134b8..e0f65201 100644 --- a/src/ClaudeDo.Worker/Runner/InteractiveLaunchSpecService.cs +++ b/src/ClaudeDo.Worker/Runner/InteractiveLaunchSpecService.cs @@ -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(); + 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(); + 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(); + 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