From 87397ecfa983878b5b4727d178c5ac9c7bba2303 Mon Sep 17 00:00:00 2001 From: mika kuns Date: Mon, 10 Aug 2026 10:32:46 +0200 Subject: [PATCH] feat(runner): point autonomous runs at the findings index --- src/ClaudeDo.Worker/Runner/TaskRunner.cs | 25 +++++++++++++--- .../Runner/ClaudeArgsBuilderTests.cs | 30 +++++++++++++++++++ 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/src/ClaudeDo.Worker/Runner/TaskRunner.cs b/src/ClaudeDo.Worker/Runner/TaskRunner.cs index 1f5b1442..2d7e15b0 100644 --- a/src/ClaudeDo.Worker/Runner/TaskRunner.cs +++ b/src/ClaudeDo.Worker/Runner/TaskRunner.cs @@ -123,7 +123,7 @@ public sealed class TaskRunner var wtCtx = prep.WtCtx; var runDir = prep.RunDir!; - var resolvedConfig = await ResolveConfigAsync(task, listConfig, null, ct); + var resolvedConfig = await ResolveConfigAsync(task, list, listConfig, null, ct); // Every run gets a per-run MCP identity so the agent can ask the user a // mid-run question via AskUser. Improvement-eligible (standalone top-level) @@ -237,7 +237,7 @@ public sealed class TaskRunner worktree = await wtRepo.GetByTaskIdAsync(taskId, ct); } - var resolvedConfig = await ResolveConfigAsync(task, listConfig, lastRun.SessionId, ct); + var resolvedConfig = await ResolveConfigAsync(task, list, listConfig, lastRun.SessionId, ct); // Determine run directory from existing worktree or sandbox. string runDir; @@ -556,7 +556,7 @@ public sealed class TaskRunner } private async Task ResolveConfigAsync( - TaskEntity task, ListConfigEntity? listConfig, string? resumeSessionId, CancellationToken ct) + TaskEntity task, ListEntity list, ListConfigEntity? listConfig, string? resumeSessionId, CancellationToken ct) { AppSettingsEntity global; using (var ctx = _dbFactory.CreateDbContext()) @@ -572,8 +572,11 @@ public sealed class TaskRunner var isImprovementChild = task.ParentTaskId is not null && task.CreatedBy == task.ParentTaskId; var improvementPrompt = isImprovementChild ? PromptFiles.ReadOrDefault(PromptKind.ImprovementChild) : null; + var findingsPointer = BuildFindingsPointer(list.WorkingDir); + var instructions = MergeInstructions( - systemFile, improvementPrompt, global.DefaultClaudeInstructions, listConfig?.SystemPrompt, task.SystemPrompt); + systemFile, improvementPrompt, global.DefaultClaudeInstructions, findingsPointer, + listConfig?.SystemPrompt, task.SystemPrompt); var requestedSkills = UnionSkillNames(task.SessionSkills, listConfig?.SessionSkills, global.SessionSkills); var skillNames = await FilterToInstalledSkillsAsync(task.Id, requestedSkills, ct); @@ -661,6 +664,20 @@ public sealed class TaskRunner internal static int? ResolveMaxTurns(int? taskTurns, int? listTurns, int globalDefault, int ceiling) => Math.Min(taskTurns ?? listTurns ?? globalDefault, ceiling); + /// + /// Points the run at the project's findings index. Kept to one sentence on purpose: it enters the + /// prefix of every message in the run, so its cost is multiplied by the turn count. + /// + internal static string? BuildFindingsPointer(string? workingDir) + { + if (string.IsNullOrWhiteSpace(workingDir)) return null; + var index = Path.Combine(workingDir, ".claudedo", "INDEX.md"); + if (!File.Exists(index)) return null; + return $"Known traps for this project are indexed at {index}. Read that index before exploring " + + "the code, and open only the findings that touch your task. When you hit a lasting, " + + "non-obvious trap yourself, record it with the save_finding tool."; + } + public static string MergeInstructions(params string?[] parts) { var trimmed = parts diff --git a/tests/ClaudeDo.Worker.Tests/Runner/ClaudeArgsBuilderTests.cs b/tests/ClaudeDo.Worker.Tests/Runner/ClaudeArgsBuilderTests.cs index 2dd827c0..7158a203 100644 --- a/tests/ClaudeDo.Worker.Tests/Runner/ClaudeArgsBuilderTests.cs +++ b/tests/ClaudeDo.Worker.Tests/Runner/ClaudeArgsBuilderTests.cs @@ -244,4 +244,34 @@ public sealed class MergeInstructionsTests var s = ClaudeDo.Worker.Runner.TaskRunner.MergeInstructions(" ", "L", "\t\n"); Assert.Equal("L", s); } + + [Fact] + public void BuildFindingsPointer_ReturnsNullWhenNoIndexExists() + { + var dir = Path.Combine(Path.GetTempPath(), "cdo-ptr-" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(dir); + try + { + Assert.Null(ClaudeDo.Worker.Runner.TaskRunner.BuildFindingsPointer(dir)); + Assert.Null(ClaudeDo.Worker.Runner.TaskRunner.BuildFindingsPointer(null)); + Assert.Null(ClaudeDo.Worker.Runner.TaskRunner.BuildFindingsPointer(" ")); + } + finally { Directory.Delete(dir, recursive: true); } + } + + [Fact] + public void BuildFindingsPointer_MentionsTheIndexPathWhenItExists() + { + var dir = Path.Combine(Path.GetTempPath(), "cdo-ptr-" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(Path.Combine(dir, ".claudedo")); + File.WriteAllText(Path.Combine(dir, ".claudedo", "INDEX.md"), "# Findings\n"); + try + { + var pointer = ClaudeDo.Worker.Runner.TaskRunner.BuildFindingsPointer(dir); + Assert.NotNull(pointer); + Assert.Contains("INDEX.md", pointer); + Assert.Contains("save_finding", pointer); + } + finally { Directory.Delete(dir, recursive: true); } + } }