feat(runner): point autonomous runs at the findings index

This commit is contained in:
mika kuns
2026-08-10 10:32:46 +02:00
parent 519f64c02e
commit 87397ecfa9
2 changed files with 51 additions and 4 deletions
+21 -4
View File
@@ -123,7 +123,7 @@ public sealed class TaskRunner
var wtCtx = prep.WtCtx; var wtCtx = prep.WtCtx;
var runDir = prep.RunDir!; 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 // 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) // mid-run question via AskUser. Improvement-eligible (standalone top-level)
@@ -237,7 +237,7 @@ public sealed class TaskRunner
worktree = await wtRepo.GetByTaskIdAsync(taskId, ct); 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. // Determine run directory from existing worktree or sandbox.
string runDir; string runDir;
@@ -556,7 +556,7 @@ public sealed class TaskRunner
} }
private async Task<ClaudeRunConfig> ResolveConfigAsync( private async Task<ClaudeRunConfig> ResolveConfigAsync(
TaskEntity task, ListConfigEntity? listConfig, string? resumeSessionId, CancellationToken ct) TaskEntity task, ListEntity list, ListConfigEntity? listConfig, string? resumeSessionId, CancellationToken ct)
{ {
AppSettingsEntity global; AppSettingsEntity global;
using (var ctx = _dbFactory.CreateDbContext()) 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 isImprovementChild = task.ParentTaskId is not null && task.CreatedBy == task.ParentTaskId;
var improvementPrompt = isImprovementChild ? PromptFiles.ReadOrDefault(PromptKind.ImprovementChild) : null; var improvementPrompt = isImprovementChild ? PromptFiles.ReadOrDefault(PromptKind.ImprovementChild) : null;
var findingsPointer = BuildFindingsPointer(list.WorkingDir);
var instructions = MergeInstructions( 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 requestedSkills = UnionSkillNames(task.SessionSkills, listConfig?.SessionSkills, global.SessionSkills);
var skillNames = await FilterToInstalledSkillsAsync(task.Id, requestedSkills, ct); 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) internal static int? ResolveMaxTurns(int? taskTurns, int? listTurns, int globalDefault, int ceiling)
=> Math.Min(taskTurns ?? listTurns ?? globalDefault, ceiling); => Math.Min(taskTurns ?? listTurns ?? globalDefault, ceiling);
/// <summary>
/// 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.
/// </summary>
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) public static string MergeInstructions(params string?[] parts)
{ {
var trimmed = parts var trimmed = parts
@@ -244,4 +244,34 @@ public sealed class MergeInstructionsTests
var s = ClaudeDo.Worker.Runner.TaskRunner.MergeInstructions(" ", "L", "\t\n"); var s = ClaudeDo.Worker.Runner.TaskRunner.MergeInstructions(" ", "L", "\t\n");
Assert.Equal("L", s); 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); }
}
} }