diff --git a/src/ClaudeDo.Data/PromptFiles.cs b/src/ClaudeDo.Data/PromptFiles.cs index 4d8d113..e3a554e 100644 --- a/src/ClaudeDo.Data/PromptFiles.cs +++ b/src/ClaudeDo.Data/PromptFiles.cs @@ -2,7 +2,7 @@ using System.Text; namespace ClaudeDo.Data; -public enum PromptKind { System, Planning, PlanningInitial, Retry, DailyPrep, WeeklyReport } +public enum PromptKind { System, Planning, PlanningInitial, Retry, DailyPrep, WeeklyReport, Agent } public static class PromptFiles { @@ -16,6 +16,7 @@ public static class PromptFiles PromptKind.Retry => Path.Combine(Root, "retry.md"), PromptKind.DailyPrep => Path.Combine(Root, "daily-prep.md"), PromptKind.WeeklyReport => Path.Combine(Root, "weekly-report.md"), + PromptKind.Agent => Path.Combine(Root, "agent.md"), _ => throw new ArgumentOutOfRangeException(nameof(kind)) }; @@ -59,6 +60,7 @@ public static class PromptFiles PromptKind.Retry => RetryDefault, PromptKind.DailyPrep => DailyPrepDefault, PromptKind.WeeklyReport => WeeklyReportDefault, + PromptKind.Agent => "", _ => "" }; diff --git a/src/ClaudeDo.Worker/Runner/ClaudeProcess.cs b/src/ClaudeDo.Worker/Runner/ClaudeProcess.cs index 39a59cf..fee8f65 100644 --- a/src/ClaudeDo.Worker/Runner/ClaudeProcess.cs +++ b/src/ClaudeDo.Worker/Runner/ClaudeProcess.cs @@ -6,6 +6,8 @@ namespace ClaudeDo.Worker.Runner; public sealed class ClaudeProcess : IClaudeProcess { + public const string NoResultPrefix = "Claude exited with code"; + private readonly WorkerConfig _cfg; private readonly ILogger _logger; @@ -100,7 +102,7 @@ public sealed class ClaudeProcess : IClaudeProcess var error = lastStderr.Length > 0 ? lastStderr.ToString().Trim() - : $"Claude exited with code {exitCode} and no result."; + : $"{NoResultPrefix} {exitCode} and no result."; return new RunResult { diff --git a/src/ClaudeDo.Worker/Runner/TaskRunner.cs b/src/ClaudeDo.Worker/Runner/TaskRunner.cs index 97b4b6d..ce6caab 100644 --- a/src/ClaudeDo.Worker/Runner/TaskRunner.cs +++ b/src/ClaudeDo.Worker/Runner/TaskRunner.cs @@ -104,7 +104,7 @@ public sealed class TaskRunner { _logger.LogInformation("Auto-retrying task {TaskId} with session {SessionId}", task.Id, result.SessionId); var retryConfig = resolvedConfig with { ResumeSessionId = result.SessionId }; - var retryPrompt = $"The previous attempt failed with:\n\n{result.ErrorMarkdown}\n\nTry again and fix the issues."; + var retryPrompt = BuildRetryPrompt(result.ErrorMarkdown); var retryResult = await RunOnceAsync(task.Id, task.Title, slot, runDir, retryConfig, 2, true, retryPrompt, ct); @@ -403,4 +403,14 @@ public sealed class TaskRunner .Select(p => p!.Trim()); return string.Join("\n\n", trimmed); } + + public static string BuildRetryPrompt(string? capturedError) + { + var basePrompt = PromptFiles.ReadOrDefault(PromptKind.Retry); + var isReal = !string.IsNullOrWhiteSpace(capturedError) + && !capturedError!.StartsWith(ClaudeProcess.NoResultPrefix, StringComparison.Ordinal); + return isReal + ? $"{basePrompt}\n\nCaptured error from the failed run:\n\n{capturedError!.Trim()}" + : basePrompt; + } } diff --git a/tests/ClaudeDo.Worker.Tests/Runner/RetryPromptTests.cs b/tests/ClaudeDo.Worker.Tests/Runner/RetryPromptTests.cs new file mode 100644 index 0000000..bd55de4 --- /dev/null +++ b/tests/ClaudeDo.Worker.Tests/Runner/RetryPromptTests.cs @@ -0,0 +1,29 @@ +using ClaudeDo.Worker.Runner; + +namespace ClaudeDo.Worker.Tests.Runner; + +public class RetryPromptTests +{ + [Fact] + public void Generic_no_result_error_is_not_appended() + { + var prompt = TaskRunner.BuildRetryPrompt($"{ClaudeProcess.NoResultPrefix} 1 and no result."); + Assert.DoesNotContain("Captured error", prompt); + Assert.Contains("did not complete", prompt); + } + + [Fact] + public void Real_error_is_appended() + { + var prompt = TaskRunner.BuildRetryPrompt("error CS1002: ; expected"); + Assert.Contains("Captured error", prompt); + Assert.Contains("CS1002", prompt); + } + + [Fact] + public void Null_error_yields_bare_prompt() + { + var prompt = TaskRunner.BuildRetryPrompt(null); + Assert.DoesNotContain("Captured error", prompt); + } +}