feat(worker): render task descriptions into the list-handler brief

Phase 0 forced a batch_get_tasks full-fetch across every task just to see
descriptions, which blew past the client's token limit on larger lists.
brief.md lives on disk and has no such limit, so descriptions now render
there directly (fenced with an extended backtick run, indented under the
list bullet, so embedded headings/lists/code fences can't break the task
list's own structure). Phase 0 now treats the brief as the primary source
and only falls back to batch_get_tasks for fields it doesn't carry.
This commit is contained in:
mika kuns
2026-08-05 10:48:37 +02:00
parent 334cf1e1d2
commit b38b0857dd
4 changed files with 122 additions and 4 deletions
+1 -1
View File
@@ -236,7 +236,7 @@ public static class PromptFiles
Work the five phases in order. Do not start a phase before the previous one is finished.
## Phase 0 Read everything
Call batch_get_tasks with every id from the brief and read each task's title, description, status and parent/child links. Do not act on any single task before you have read them all Phase 1 needs the whole set in view.
The brief is the primary source: it already lists every task's title, id, status and full description. Read it in full before acting. Only call batch_get_tasks if you need something the brief does not carry for a specific task, e.g. parent/child links. Do not act on any single task before you have read them all Phase 1 needs the whole set in view.
## Phase 1 Dedupe
Compare the tasks pairwise for overlap: same goal stated twice, one task fully contained in another, two tasks that would edit the same thing for the same reason.
@@ -206,7 +206,7 @@ public sealed class InteractiveLaunchSpecService : IInteractiveLaunchSpecService
{
var task = await taskRepo.GetByIdAsync(id, ct)
?? throw new KeyNotFoundException($"Task not found: {id}");
briefLines.Add($"- [{task.Status}] {task.Title} (id: {task.Id})");
briefLines.Add(RenderBriefEntry(task));
}
var sessionDir = Path.Combine(Paths.AppDataRoot(), "merge-helper-sessions", Guid.NewGuid().ToString());
@@ -253,6 +253,39 @@ public sealed class InteractiveLaunchSpecService : IInteractiveLaunchSpecService
return new LaunchSpec(repoDir, resolvedClaude, args, env);
}
// 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
// column (2 spaces, matching "- "). That keeps CommonMark parsing the fence as part of THIS
// bullet rather than breaking the list, while the code fence itself stops any inner heading
// or list syntax from being interpreted. The fence length is extended past the longest run of
// backticks already present in the description so an embedded ``` block can't prematurely
// close it.
private static string RenderBriefEntry(TaskEntity task)
{
var header = $"- [{task.Status}] {task.Title} (id: {task.Id})";
var description = task.Description?.Trim();
if (string.IsNullOrEmpty(description)) return header;
var fence = new string('`', Math.Max(3, LongestBacktickRun(description) + 1));
var lines = new List<string>(4) { header, $" {fence}" };
lines.AddRange(description.Replace("\r\n", "\n").Split('\n').Select(line => $" {line}"));
lines.Add($" {fence}");
return string.Join("\n", lines);
}
private static int LongestBacktickRun(string text)
{
var max = 0;
var current = 0;
foreach (var ch in text)
{
current = ch == '`' ? current + 1 : 0;
if (current > max) max = current;
}
return max;
}
// Creates the ClaudeDo task that hosts a list-handler run (Mission Control's "Let Claude
// handle it") and stamps the list repo's current HEAD as the review range's base commit.
// The handler never gets its own worktree -- it commits straight to the list's working