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
@@ -87,6 +87,15 @@ public class PromptFilesTests
Assert.DoesNotContain("whenever you are unsure", d);
}
[Fact]
public void DefaultFor_merge_helper_phase0_treats_brief_as_primary_source()
{
var d = PromptFiles.DefaultFor(PromptKind.MergeHelper);
Assert.Contains("primary source", d, StringComparison.OrdinalIgnoreCase);
Assert.Contains("batch_get_tasks", d);
Assert.Contains("Do not act on any single task before you have read", d);
}
[Fact]
public void DefaultFor_merge_helper_initial_has_repo_token()
{
@@ -84,13 +84,13 @@ public sealed class InteractiveLaunchSpecServiceTests : IDisposable
return listId;
}
private async Task SeedTaskAsync(string taskId, string listId, TaskStatus status, string? sessionSkillsJson = null, string title = "T")
private async Task SeedTaskAsync(string taskId, string listId, TaskStatus status, string? sessionSkillsJson = null, string title = "T", string? description = null)
{
using var ctx = _db.CreateContext();
await new TaskRepository(ctx).AddAsync(new TaskEntity
{
Id = taskId, ListId = listId, Title = title, Status = status,
CreatedAt = DateTime.UtcNow, SessionSkills = sessionSkillsJson,
CreatedAt = DateTime.UtcNow, SessionSkills = sessionSkillsJson, Description = description,
});
}
@@ -478,6 +478,82 @@ public sealed class InteractiveLaunchSpecServiceTests : IDisposable
Assert.Contains(t2, brief);
}
[Fact]
public async Task BuildForMergeHelperAsync_BriefIncludesTaskDescription()
{
var repo = Path.Combine(_tempDir, "repoDesc");
Directory.CreateDirectory(repo);
var listId = await SeedListAsync(workingDir: repo, name: "Alpha");
var t1 = Guid.NewGuid().ToString();
await SeedTaskAsync(t1, listId, TaskStatus.Idle, title: "First task",
description: "Do the thing carefully and report back.");
var svc = BuildService();
var spec = await svc.BuildForMergeHelperAsync(new[] { t1 }, listId, CancellationToken.None);
var sessionDir = TrackSessionDir(spec);
var brief = File.ReadAllText(Path.Combine(sessionDir, "brief.md"));
Assert.Contains("Do the thing carefully and report back.", brief);
}
[Fact]
public async Task BuildForMergeHelperAsync_TaskWithoutDescription_RendersCleanly()
{
var repo = Path.Combine(_tempDir, "repoNoDesc");
Directory.CreateDirectory(repo);
var listId = await SeedListAsync(workingDir: repo, name: "Alpha");
var t1 = Guid.NewGuid().ToString();
await SeedTaskAsync(t1, listId, TaskStatus.Idle, title: "No description task", description: null);
var svc = BuildService();
var spec = await svc.BuildForMergeHelperAsync(new[] { t1 }, listId, CancellationToken.None);
var sessionDir = TrackSessionDir(spec);
var brief = File.ReadAllText(Path.Combine(sessionDir, "brief.md"));
var line = brief.Split('\n').Single(l => l.Contains("No description task"));
Assert.Equal($"- [Idle] No description task (id: {t1})", line.TrimEnd('\r'));
}
[Fact]
public async Task BuildForMergeHelperAsync_BriefDescriptionWithCodeFenceAndHeadingsKeepsTaskListRecognizable()
{
var repo = Path.Combine(_tempDir, "repoMd");
Directory.CreateDirectory(repo);
var listId = await SeedListAsync(workingDir: repo, name: "Alpha");
var t1 = Guid.NewGuid().ToString();
var t2 = Guid.NewGuid().ToString();
var trickyDescription = """
# Heading inside description
- a nested list item
- another one
```csharp
var x = "fenced code block";
```
""";
await SeedTaskAsync(t1, listId, TaskStatus.Idle, title: "First task", description: trickyDescription);
await SeedTaskAsync(t2, listId, TaskStatus.WaitingForReview, title: "Second task", description: "plain description");
var svc = BuildService();
var spec = await svc.BuildForMergeHelperAsync(new[] { t1, t2 }, listId, CancellationToken.None);
var sessionDir = TrackSessionDir(spec);
var brief = File.ReadAllText(Path.Combine(sessionDir, "brief.md"));
var lines = brief.Replace("\r\n", "\n").Split('\n');
// The two task bullets must still be recognizable top-level list items, unbroken
// by the embedded heading/list/code-fence in the first task's description.
Assert.Contains(lines, l => l == $"- [Idle] First task (id: {t1})");
Assert.Contains(lines, l => l == $"- [WaitingForReview] Second task (id: {t2})");
Assert.Contains("Heading inside description", brief);
Assert.Contains("fenced code block", brief);
Assert.Contains("var x = \"fenced code block\";", brief);
}
// ── CreateMergeHelperTaskAsync ──
[Fact]