diff --git a/docs/superpowers/plans/2026-06-04-bundled-prompts-overhaul.md b/docs/superpowers/plans/2026-06-04-bundled-prompts-overhaul.md new file mode 100644 index 0000000..b63c19d --- /dev/null +++ b/docs/superpowers/plans/2026-06-04-bundled-prompts-overhaul.md @@ -0,0 +1,972 @@ +# Bundled Prompts Overhaul Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Externalize every bundled prose prompt into editable files with strong defaults, collapse system+agent, and add an inline `CLAUDEDO_BLOCKED:` roadblock protocol surfaced at review. + +**Architecture:** `PromptFiles` becomes the single source of prompt defaults + a pure token renderer. Each consumer (TaskRunner, PlanningSessionManager, DailyPrepPrompt, WeekReportPromptBuilder) reads its prompt via `PromptFiles`. `StreamAnalyzer` collects roadblock markers from streamed assistant text; the runner folds them into the review result. + +**Tech Stack:** .NET 8, xUnit, EF Core (no schema change in this plan). + +Spec: `docs/superpowers/specs/2026-06-04-bundled-prompts-overhaul-design.md` + +--- + +## File structure + +- `src/ClaudeDo.Data/PromptFiles.cs` — new `PromptKind` members, new defaults, `RenderTemplate` + `ReadOrDefault` + `Render`. +- `src/ClaudeDo.Worker/Runner/StreamAnalyzer.cs` — collect `Blocks` from assistant text. +- `src/ClaudeDo.Worker/Runner/RunResult.cs` — carry `Blocks`. +- `src/ClaudeDo.Worker/Runner/ClaudeProcess.cs` — pass `Blocks`; expose no-result prefix const. +- `src/ClaudeDo.Worker/Runner/TaskRunner.cs` — drop agent file; retry via `retry.md`; fold blocks into review result. +- `src/ClaudeDo.Worker/Planning/PlanningSessionManager.cs` — read planning prompts via `PromptFiles`. +- `src/ClaudeDo.Worker/Prime/DailyPrepPrompt.cs` — read `daily-prep.md`. +- `src/ClaudeDo.Worker/Report/WeekReportPromptBuilder.cs` — read `weekly-report.md`. +- `src/ClaudeDo.Ui/ViewModels/Modals/Settings/FilesSettingsTabViewModel.cs` + its view — expose new prompt files, drop agent. +- Tests in `tests/ClaudeDo.Data.Tests` and `tests/ClaudeDo.Worker.Tests`. + +Build commands (this repo is on .NET 8 — build per project, not the .slnx): +```bash +dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release +dotnet test tests/ClaudeDo.Data.Tests/ClaudeDo.Data.Tests.csproj -c Release +dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release +``` + +--- + +## Task 1: PromptFiles — kinds, defaults, pure renderer + +**Files:** +- Modify: `src/ClaudeDo.Data/PromptFiles.cs` +- Test: `tests/ClaudeDo.Data.Tests/PromptFilesTests.cs` (create) + +- [ ] **Step 1: Write failing tests for the pure renderer** + +Create `tests/ClaudeDo.Data.Tests/PromptFilesTests.cs`: + +```csharp +using ClaudeDo.Data; + +namespace ClaudeDo.Data.Tests; + +public class PromptFilesTests +{ + [Fact] + public void RenderTemplate_replaces_known_tokens() + { + var outp = PromptFiles.RenderTemplate( + "Plan for {date}, cap {maxTasks}.", + new Dictionary { ["date"] = "2026-06-04", ["maxTasks"] = "5" }); + Assert.Equal("Plan for 2026-06-04, cap 5.", outp); + } + + [Fact] + public void RenderTemplate_leaves_unknown_braces_intact() + { + var outp = PromptFiles.RenderTemplate( + "## {Wochentag}, {dd.MM.yyyy} — {start}", + new Dictionary { ["start"] = "01.06.2026" }); + Assert.Equal("## {Wochentag}, {dd.MM.yyyy} — 01.06.2026", outp); + } + + [Fact] + public void DefaultFor_system_mentions_blocked_marker_and_scope() + { + var d = PromptFiles.DefaultFor(PromptKind.System); + Assert.Contains("CLAUDEDO_BLOCKED:", d); + Assert.Contains("unattended", d, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void DefaultFor_planning_initial_has_title_and_description_tokens() + { + var d = PromptFiles.DefaultFor(PromptKind.PlanningInitial); + Assert.Contains("{title}", d); + Assert.Contains("{description}", d); + } + + [Fact] + public void PathFor_planning_is_planning_system_file() + { + Assert.EndsWith("planning-system.md", PromptFiles.PathFor(PromptKind.Planning)); + } +} +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `dotnet test tests/ClaudeDo.Data.Tests/ClaudeDo.Data.Tests.csproj -c Release` +Expected: FAIL — `RenderTemplate`/`DefaultFor` don't exist, `PromptKind.PlanningInitial` undefined. + +- [ ] **Step 3: Rewrite PromptFiles.cs** + +Replace the entire contents of `src/ClaudeDo.Data/PromptFiles.cs` with: + +```csharp +using System.Text; + +namespace ClaudeDo.Data; + +public enum PromptKind { System, Planning, PlanningInitial, Retry, DailyPrep, WeeklyReport } + +public static class PromptFiles +{ + public static string Root => Path.Combine(Paths.AppDataRoot(), "prompts"); + + public static string PathFor(PromptKind kind) => kind switch + { + PromptKind.System => Path.Combine(Root, "system.md"), + PromptKind.Planning => Path.Combine(Root, "planning-system.md"), + PromptKind.PlanningInitial => Path.Combine(Root, "planning-initial.md"), + PromptKind.Retry => Path.Combine(Root, "retry.md"), + PromptKind.DailyPrep => Path.Combine(Root, "daily-prep.md"), + PromptKind.WeeklyReport => Path.Combine(Root, "weekly-report.md"), + _ => throw new ArgumentOutOfRangeException(nameof(kind)) + }; + + public static void EnsureExists(PromptKind kind) + { + Directory.CreateDirectory(Root); + var path = PathFor(kind); + if (File.Exists(path)) return; + File.WriteAllText(path, DefaultFor(kind)); + } + + public static string? ReadOrNull(PromptKind kind) + { + var path = PathFor(kind); + if (!File.Exists(path)) return null; + var content = File.ReadAllText(path).Trim(); + return string.IsNullOrEmpty(content) ? null : content; + } + + /// File content if present and non-empty, otherwise the bundled default. + public static string ReadOrDefault(PromptKind kind) => ReadOrNull(kind) ?? DefaultFor(kind); + + /// Render a prompt: read file-or-default, then substitute named tokens. + public static string Render(PromptKind kind, IReadOnlyDictionary values) + => RenderTemplate(ReadOrDefault(kind), values); + + /// Replace only the given {name} tokens; any other braces pass through untouched. + public static string RenderTemplate(string template, IReadOnlyDictionary values) + { + var sb = new StringBuilder(template); + foreach (var (key, val) in values) + sb.Replace("{" + key + "}", val); + return sb.ToString(); + } + + public static string DefaultFor(PromptKind kind) => kind switch + { + PromptKind.System => SystemDefault, + PromptKind.Planning => PlanningSystemDefault, + PromptKind.PlanningInitial => PlanningInitialDefault, + PromptKind.Retry => RetryDefault, + PromptKind.DailyPrep => DailyPrepDefault, + PromptKind.WeeklyReport => WeeklyReportDefault, + _ => "" + }; + + private const string SystemDefault = """ + # Working Agreement + + You are completing one well-defined task autonomously in a git repository. + + ## Scope + - Do exactly what the task asks — no unrequested refactors, renames, dependency + changes, or "while I'm here" cleanup. + - If intent is ambiguous, state the assumption you're making and proceed with the + most reasonable reading. Stop only if you genuinely cannot move forward. + - Prefer three similar lines over a premature abstraction. Don't build for + hypothetical future needs. + + ## Working in the repo + - Read a file before editing it. Match the conventions already in this codebase — + they override generic defaults. + - Prefer editing existing files to creating new ones. Don't write comments that + just restate the code. + - Validate only at real boundaries (user input, external APIs). + + ## Finishing + - Before claiming done, verify: run the build and relevant tests, confirm they + pass, and report what you ran. If you couldn't verify something, say so plainly. + - Make focused commits using the repository's existing commit-message convention. + + ## Safety + - Never force-push, hard-reset, or delete branches/files beyond the task's scope + without being asked. + - Don't introduce injection/XSS/secret-leak issues. Never commit credentials. + + ## You are running unattended + You run autonomously with no human watching. There is no one to answer mid-task + questions, so never stop to ask — make the most reasonable decision, note the + assumption, and continue. + + ## When you are blocked + If something genuinely prevents you from completing part of the task (missing + credentials, contradictory requirements, a destructive action you won't take + unasked), do NOT silently give up. Write this marker on its own line, then keep + working on whatever else you can: + + CLAUDEDO_BLOCKED: + + Emit it as many times as needed — once per distinct blocker. Use it only for true + blockers, not for routine decisions you can make yourself. + """; + + private const string PlanningSystemDefault = """ + You are the planning assistant for ClaudeDo. Your job is to break a task into + smaller, independently executable subtasks — the session ends by creating those + subtasks. + + Start every session by invoking the `superpowers:brainstorming` skill (Skill + tool) and follow it end to end: clarifying questions one at a time, then 2–3 + approaches with a recommendation, then a short design. Do not create any subtasks + until the user has approved the design. + + You can ONLY shape this task's plan — you cannot edit files or touch other tasks. + The tools available to you are: CreateChildTask, ListChildTasks, UpdateChildTask, + DeleteChildTask, UpdatePlanningTask, and Finalize. Use nothing else. + + Once the design is approved, create the child tasks with CreateChildTask, then + call Finalize. Keep each subtask concrete and self-contained with a clear + done-state, ordered so dependencies come first. + """; + + private const string PlanningInitialDefault = """ + # Task to plan: {title} + + {description} + """; + + private const string RetryDefault = """ + The task did not complete on the previous attempt — you may have run out of + turns, hit an error, or stopped before finishing. + + Review the work already done in this session and the current state of the + repository, identify what is still incomplete or broken, and finish the task. + Don't restart from scratch or repeat a failed approach. Verify the result + (build + tests) before you stop. + """; + + private const string DailyPrepDefault = """ + You are preparing my workday for {date}. + + 1. Call mcp__claudedo__get_daily_prep_candidates. + 2. Keep tasks already marked MyDay (currentMyDay) — never remove them. + 3. Fill MyDay to at most {maxTasks} open tasks TOTAL (currentMyDay counts). Never exceed it. + 4. Estimate each candidate's effort and pick a feasible mix — not only big items. + Prioritize isStarred, due (scheduledFor), and older tasks. + 5. Place related tasks next to each other using consecutive sortOrder values. + 6. Apply via mcp__claudedo__set_my_day(taskId, true, sortOrder). Never mark anything + outside the candidate list. + + If there are no candidates, do nothing. + """; + + private const string WeeklyReportDefault = """ + You are generating a concise weekly standup report for a software developer, + covering {start} to {end}. + + Rules: + - Write the ENTIRE report in German. + - Group by day. One "## {Wochentag}, {dd.MM.yyyy}" section per day that has + activity (German weekday names). Omit days with no activity. + - Within each day: 3–5 first-person, past-tense bullets ("- Habe X umgesetzt", + "- Y behoben"). Merge related small work into one bullet. + - Drop trivia: typo fixes, pure exploration, false starts, tooling/log noise. + - Blend the developer's own notes and the derived activity into ONE deduplicated + bullet list per day. The notes are authoritative — never omit or contradict them. + - Name the project/repo when it adds clarity. + - Output ONLY the dated sections. No preamble, no intro, no closing remarks. + + Two sections follow below: an activity log derived from Claude session history, + and the developer's own notes. Base the report on both; the notes are + authoritative where they conflict with the derived activity. + """; +} +``` + +- [ ] **Step 4: Run tests to verify they pass** + +Run: `dotnet test tests/ClaudeDo.Data.Tests/ClaudeDo.Data.Tests.csproj -c Release` +Expected: PASS (5 new tests). + +- [ ] **Step 5: Commit** + +```bash +git add src/ClaudeDo.Data/PromptFiles.cs tests/ClaudeDo.Data.Tests/PromptFilesTests.cs +git commit -m "feat(prompts): externalize prompt kinds with defaults and token renderer" +``` + +--- + +## Task 2: TaskRunner — drop agent file from system prompt merge + +**Files:** +- Modify: `src/ClaudeDo.Worker/Runner/TaskRunner.cs:382-386` + +- [ ] **Step 1: Remove the agent-file read and merge** + +In `ResolveConfigAsync`, replace: + +```csharp + var systemFile = PromptFiles.ReadOrNull(PromptKind.System); + var agentFile = PromptFiles.ReadOrNull(PromptKind.Agent); + + var instructions = MergeInstructions( + systemFile, global.DefaultClaudeInstructions, listConfig?.SystemPrompt, task.SystemPrompt, agentFile); +``` + +with: + +```csharp + var systemFile = PromptFiles.ReadOrNull(PromptKind.System); + + var instructions = MergeInstructions( + systemFile, global.DefaultClaudeInstructions, listConfig?.SystemPrompt, task.SystemPrompt); +``` + +- [ ] **Step 2: Build to verify it compiles** + +Run: `dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release` +Expected: PASS (no reference to `PromptKind.Agent` remains). + +- [ ] **Step 3: Commit** + +```bash +git add src/ClaudeDo.Worker/Runner/TaskRunner.cs +git commit -m "refactor(prompts): collapse agent prompt into system prompt" +``` + +--- + +## Task 3: Retry prompt from file + conditional stderr append + +**Files:** +- Modify: `src/ClaudeDo.Worker/Runner/ClaudeProcess.cs:101-103` (expose prefix const) +- Modify: `src/ClaudeDo.Worker/Runner/TaskRunner.cs` (add `BuildRetryPrompt`, use it at ~L107) +- Test: `tests/ClaudeDo.Worker.Tests/Runner/RetryPromptTests.cs` (create) + +- [ ] **Step 1: Write failing tests for the retry-prompt helper** + +Create `tests/ClaudeDo.Worker.Tests/Runner/RetryPromptTests.cs`: + +```csharp +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); + } +} +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter RetryPromptTests` +Expected: FAIL — `BuildRetryPrompt` / `NoResultPrefix` don't exist. + +- [ ] **Step 3: Expose the no-result prefix in ClaudeProcess** + +In `src/ClaudeDo.Worker/Runner/ClaudeProcess.cs`, add the const near the top of the class and use it in the error fallback. Replace: + +```csharp + var error = lastStderr.Length > 0 + ? lastStderr.ToString().Trim() + : $"Claude exited with code {exitCode} and no result."; +``` + +with: + +```csharp + var error = lastStderr.Length > 0 + ? lastStderr.ToString().Trim() + : $"{NoResultPrefix} {exitCode} and no result."; +``` + +and add inside the class (e.g. just below the fields): + +```csharp + public const string NoResultPrefix = "Claude exited with code"; +``` + +- [ ] **Step 4: Add BuildRetryPrompt to TaskRunner and use it** + +In `src/ClaudeDo.Worker/Runner/TaskRunner.cs`, add this static method (next to `MergeInstructions`): + +```csharp + 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; + } +``` + +Then replace the inline retry prompt at ~L107: + +```csharp + var retryPrompt = $"The previous attempt failed with:\n\n{result.ErrorMarkdown}\n\nTry again and fix the issues."; +``` + +with: + +```csharp + var retryPrompt = BuildRetryPrompt(result.ErrorMarkdown); +``` + +- [ ] **Step 5: Run tests to verify they pass** + +Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter RetryPromptTests` +Expected: PASS. + +- [ ] **Step 6: Commit** + +```bash +git add src/ClaudeDo.Worker/Runner/ClaudeProcess.cs src/ClaudeDo.Worker/Runner/TaskRunner.cs tests/ClaudeDo.Worker.Tests/Runner/RetryPromptTests.cs +git commit -m "feat(prompts): retry prompt from file, append only real captured errors" +``` + +--- + +## Task 4: PlanningSessionManager reads planning prompts from files + +**Files:** +- Modify: `src/ClaudeDo.Worker/Planning/PlanningSessionManager.cs` (`BuildSystemPrompt` ~L366, `BuildInitialPrompt` ~L392) + +- [ ] **Step 1: Replace BuildSystemPrompt body** + +Replace the whole method body of `BuildSystemPrompt()` with: + +```csharp + private static string BuildSystemPrompt() => PromptFiles.ReadOrDefault(PromptKind.Planning); +``` + +(Delete the inline fallback string literal that followed.) + +- [ ] **Step 2: Replace BuildInitialPrompt body** + +Replace the whole method body of `BuildInitialPrompt(TaskEntity task)` with: + +```csharp + private static string BuildInitialPrompt(TaskEntity task) => + PromptFiles.Render(PromptKind.PlanningInitial, new Dictionary + { + ["title"] = task.Title, + ["description"] = task.Description ?? "", + }); +``` + +Ensure `using ClaudeDo.Data;` is present (it is — `PromptFiles` lived there already via `ReadOrNull`). + +- [ ] **Step 3: Build to verify it compiles** + +Run: `dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release` +Expected: PASS. + +- [ ] **Step 4: Commit** + +```bash +git add src/ClaudeDo.Worker/Planning/PlanningSessionManager.cs +git commit -m "refactor(prompts): planning prompts read from editable files" +``` + +--- + +## Task 5: DailyPrepPrompt reads from file + +**Files:** +- Modify: `src/ClaudeDo.Worker/Prime/DailyPrepPrompt.cs` +- Modify: `tests/ClaudeDo.Worker.Tests/Prime/DailyPrepPromptTests.cs` + +- [ ] **Step 1: Update DailyPrepPromptTests to assert the English default render** + +Replace the `Build_prompt_contains_cap_and_date` test body with: + +```csharp + [Fact] + public void Build_prompt_contains_cap_and_date() + { + var prompt = DailyPrepPrompt.BuildPrompt(maxTasks: 5, today: new DateOnly(2026, 6, 3)); + Assert.Contains("5", prompt); + Assert.Contains("2026-06-03", prompt); + Assert.Contains("get_daily_prep_candidates", prompt); + Assert.Contains("set_my_day", prompt); + Assert.Contains("preparing my workday", prompt); + } +``` + +(The new assertion pins the English default; the file-read path is exercised by the same default when no `daily-prep.md` exists.) + +- [ ] **Step 2: Run test to verify it fails** + +Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter DailyPrepPromptTests` +Expected: FAIL — current German prompt has no "preparing my workday". + +- [ ] **Step 3: Rewrite BuildPrompt to read the file** + +In `src/ClaudeDo.Worker/Prime/DailyPrepPrompt.cs`, replace the `BuildPrompt` method with: + +```csharp + public static string BuildPrompt(int maxTasks, DateOnly today) => + ClaudeDo.Data.PromptFiles.Render( + ClaudeDo.Data.PromptKind.DailyPrep, + new Dictionary + { + ["date"] = today.ToString("yyyy-MM-dd"), + ["maxTasks"] = maxTasks.ToString(), + }); +``` + +Leave `BuildArgs`, `LogPath`, and the tool-name consts unchanged. + +- [ ] **Step 4: Run test to verify it passes** + +Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter DailyPrepPromptTests` +Expected: PASS. + +- [ ] **Step 5: Commit** + +```bash +git add src/ClaudeDo.Worker/Prime/DailyPrepPrompt.cs tests/ClaudeDo.Worker.Tests/Prime/DailyPrepPromptTests.cs +git commit -m "feat(prompts): daily-prep prompt from file, English default" +``` + +--- + +## Task 6: WeekReportPromptBuilder reads instructions from file + +**Files:** +- Modify: `src/ClaudeDo.Worker/Report/WeekReportPromptBuilder.cs` +- Check: `tests/ClaudeDo.Worker.Tests/Report/WeekReportPromptBuilderTests.cs` + +- [ ] **Step 1: Replace the inline Instructions with a file read** + +In `WeekReportPromptBuilder.Build`, replace: + +```csharp + var sb = new StringBuilder(); + sb.AppendLine(string.Format(CultureInfo.InvariantCulture, Instructions, + start.ToString("dd.MM.yyyy", CultureInfo.InvariantCulture), + end.ToString("dd.MM.yyyy", CultureInfo.InvariantCulture))); + sb.AppendLine(); +``` + +with: + +```csharp + var sb = new StringBuilder(); + sb.AppendLine(ClaudeDo.Data.PromptFiles.Render( + ClaudeDo.Data.PromptKind.WeeklyReport, + new Dictionary + { + ["start"] = start.ToString("dd.MM.yyyy", CultureInfo.InvariantCulture), + ["end"] = end.ToString("dd.MM.yyyy", CultureInfo.InvariantCulture), + })); + sb.AppendLine(); +``` + +Then delete the now-unused `private const string Instructions = ...` block. (The `{Wochentag}`/`{dd.MM.yyyy}` literals inside the default survive because `RenderTemplate` only replaces `{start}`/`{end}`.) + +- [ ] **Step 2: Verify the existing builder test still passes** + +Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter WeekReportPromptBuilderTests` +Expected: PASS. If a test asserted exact old wording, update it to assert the date appears and that activity/notes sections render (the new default keeps German output rules). + +- [ ] **Step 3: Commit** + +```bash +git add src/ClaudeDo.Worker/Report/WeekReportPromptBuilder.cs tests/ClaudeDo.Worker.Tests/Report/WeekReportPromptBuilderTests.cs +git commit -m "feat(prompts): weekly-report instructions from file, point at data sections" +``` + +--- + +## Task 7: StreamAnalyzer collects roadblock markers + +**Files:** +- Modify: `src/ClaudeDo.Worker/Runner/StreamAnalyzer.cs` +- Test: `tests/ClaudeDo.Worker.Tests/Runner/StreamAnalyzerTests.cs` + +- [ ] **Step 1: Write failing tests** + +Append to `StreamAnalyzerTests`: + +```csharp + [Fact] + public void Collects_Blocked_Markers_From_Assistant_Text() + { + var analyzer = new StreamAnalyzer(); + analyzer.ProcessLine("""{"type":"assistant","message":{"content":[{"type":"text","text":"working\nCLAUDEDO_BLOCKED: missing API key\nmoving on"}]}}"""); + analyzer.ProcessLine("""{"type":"assistant","message":{"content":[{"type":"text","text":"CLAUDEDO_BLOCKED: cannot reach db"}]}}"""); + analyzer.ProcessLine("""{"type":"result","result":"done","session_id":"s1"}"""); + var result = analyzer.GetResult(); + Assert.Equal(2, result.Blocks.Count); + Assert.Equal("missing API key", result.Blocks[0]); + Assert.Equal("cannot reach db", result.Blocks[1]); + } + + [Fact] + public void Strips_Blocked_Markers_From_Result_Text() + { + var analyzer = new StreamAnalyzer(); + analyzer.ProcessLine("""{"type":"result","result":"All set.\nCLAUDEDO_BLOCKED: no creds\nDone.","session_id":"s1"}"""); + var result = analyzer.GetResult(); + Assert.DoesNotContain("CLAUDEDO_BLOCKED", result.ResultMarkdown); + Assert.Single(result.Blocks); + Assert.Equal("no creds", result.Blocks[0]); + } + + [Fact] + public void No_Markers_Means_Empty_Blocks() + { + var analyzer = new StreamAnalyzer(); + analyzer.ProcessLine("""{"type":"result","result":"done","session_id":"s1"}"""); + Assert.Empty(analyzer.GetResult().Blocks); + } +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter StreamAnalyzerTests` +Expected: FAIL — `Blocks` doesn't exist. + +- [ ] **Step 3: Implement marker collection in StreamAnalyzer** + +In `src/ClaudeDo.Worker/Runner/StreamAnalyzer.cs`: + +Add to `StreamResult`: + +```csharp + public IReadOnlyList Blocks { get; set; } = Array.Empty(); +``` + +Add a field and a constant to `StreamAnalyzer`: + +```csharp + private readonly List _blocks = new(); + private const string BlockedPrefix = "CLAUDEDO_BLOCKED:"; +``` + +In the `case "result":` branch, after `_resultMarkdown` is assigned, scan and strip: + +```csharp + if (root.TryGetProperty("result", out var resultProp)) + _resultMarkdown = StripAndCollect(resultProp.GetString()); +``` + +In the `case "assistant":` branch, collect from text content (keep `_turnCount++`): + +```csharp + case "assistant": + _turnCount++; + CollectFromAssistant(root); + break; +``` + +Add these helpers to the class: + +```csharp + private void CollectFromAssistant(JsonElement root) + { + if (!root.TryGetProperty("message", out var msg)) return; + if (!msg.TryGetProperty("content", out var content) || content.ValueKind != JsonValueKind.Array) return; + foreach (var block in content.EnumerateArray()) + if (block.TryGetProperty("type", out var t) && t.GetString() == "text" + && block.TryGetProperty("text", out var txt)) + ScanForBlocks(txt.GetString()); + } + + private void ScanForBlocks(string? text) + { + if (string.IsNullOrEmpty(text)) return; + foreach (var line in text.Split('\n')) + { + var trimmed = line.Trim(); + if (trimmed.StartsWith(BlockedPrefix, StringComparison.Ordinal)) + _blocks.Add(trimmed[BlockedPrefix.Length..].Trim()); + } + } + + private string? StripAndCollect(string? text) + { + if (string.IsNullOrEmpty(text)) return text; + ScanForBlocks(text); + var kept = text.Split('\n') + .Where(l => !l.Trim().StartsWith(BlockedPrefix, StringComparison.Ordinal)); + return string.Join('\n', kept).Trim(); + } +``` + +Add `Blocks = _blocks` to the `GetResult()` initializer: + +```csharp + public StreamResult GetResult() => new() + { + ResultMarkdown = FallbackResult(), + StructuredOutputJson = _structuredOutputJson, + SessionId = _sessionId, + TurnCount = _turnCount, + TokensIn = _tokensIn, + TokensOut = _tokensOut, + ApiRetryCount = _apiRetryCount, + Blocks = _blocks, + }; +``` + +- [ ] **Step 4: Run tests to verify they pass** + +Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter StreamAnalyzerTests` +Expected: PASS (all old + 3 new). + +- [ ] **Step 5: Commit** + +```bash +git add src/ClaudeDo.Worker/Runner/StreamAnalyzer.cs tests/ClaudeDo.Worker.Tests/Runner/StreamAnalyzerTests.cs +git commit -m "feat(roadblock): collect and strip CLAUDEDO_BLOCKED markers in StreamAnalyzer" +``` + +--- + +## Task 8: RunResult + ClaudeProcess carry Blocks + +**Files:** +- Modify: `src/ClaudeDo.Worker/Runner/RunResult.cs` +- Modify: `src/ClaudeDo.Worker/Runner/ClaudeProcess.cs:89-113` + +- [ ] **Step 1: Add Blocks to RunResult** + +In `src/ClaudeDo.Worker/Runner/RunResult.cs`, add inside the class: + +```csharp + public IReadOnlyList Blocks { get; init; } = Array.Empty(); +``` + +- [ ] **Step 2: Populate Blocks in both RunResult returns** + +In `ClaudeProcess.RunAsync`, add `Blocks = streamResult.Blocks,` to **both** the success `RunResult { ... }` (after `TokensOut`) and the error `RunResult { ... }` initializer. + +- [ ] **Step 3: Build to verify it compiles** + +Run: `dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release` +Expected: PASS. + +- [ ] **Step 4: Commit** + +```bash +git add src/ClaudeDo.Worker/Runner/RunResult.cs src/ClaudeDo.Worker/Runner/ClaudeProcess.cs +git commit -m "feat(roadblock): carry blocks through RunResult" +``` + +--- + +## Task 9: Fold roadblocks into the review result + +**Files:** +- Modify: `src/ClaudeDo.Worker/Runner/TaskRunner.cs` (`HandleSuccess` ~L319-352; add `ComposeReviewResult`) +- Test: `tests/ClaudeDo.Worker.Tests/Runner/ReviewResultTests.cs` (create) + +- [ ] **Step 1: Write failing tests for the compose helper** + +Create `tests/ClaudeDo.Worker.Tests/Runner/ReviewResultTests.cs`: + +```csharp +using ClaudeDo.Worker.Runner; + +namespace ClaudeDo.Worker.Tests.Runner; + +public class ReviewResultTests +{ + [Fact] + public void No_blocks_returns_result_unchanged() + { + Assert.Equal("done", TaskRunner.ComposeReviewResult("done", Array.Empty())); + } + + [Fact] + public void Blocks_are_appended_as_a_section() + { + var outp = TaskRunner.ComposeReviewResult("done", new[] { "no creds", "db down" }); + Assert.Contains("⚠ Roadblocks", outp); + Assert.Contains("- no creds", outp); + Assert.Contains("- db down", outp); + Assert.Contains("done", outp); + } + + [Fact] + public void Null_result_with_blocks_still_lists_them() + { + var outp = TaskRunner.ComposeReviewResult(null, new[] { "x" }); + Assert.Contains("⚠ Roadblocks", outp); + Assert.Contains("- x", outp); + } +} +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter ReviewResultTests` +Expected: FAIL — `ComposeReviewResult` doesn't exist. + +- [ ] **Step 3: Add ComposeReviewResult and use it in HandleSuccess** + +In `TaskRunner`, add: + +```csharp + public static string? ComposeReviewResult(string? result, IReadOnlyList blocks) + { + if (blocks.Count == 0) return result; + var section = "⚠ Roadblocks reported during the run:\n" + + string.Join('\n', blocks.Select(b => $"- {b}")); + return string.IsNullOrWhiteSpace(result) ? section : $"{result}\n\n{section}"; + } +``` + +In `HandleSuccess`, compute the composed result once and pass it to both terminal writes: + +```csharp + var finishedAt = DateTime.UtcNow; + var reviewResult = ComposeReviewResult(result.ResultMarkdown, result.Blocks); + if (task.ParentTaskId is null && task.PlanningPhase == PlanningPhase.None) + { + await _state.SubmitForReviewAsync(task.Id, finishedAt, reviewResult, CancellationToken.None); + await _broadcaster.WorkerLog($"Finished \"{task.Title}\" (waiting for review)", WorkerLogLevel.Success, DateTime.UtcNow); + await _broadcaster.TaskFinished(slot, task.Id, "waiting_for_review", finishedAt); + } + else + { + await _state.CompleteAsync(task.Id, finishedAt, reviewResult, CancellationToken.None); + await _broadcaster.WorkerLog($"Finished \"{task.Title}\" (done)", WorkerLogLevel.Success, DateTime.UtcNow); + await _broadcaster.TaskFinished(slot, task.Id, "done", finishedAt); + } +``` + +(Make sure `using System.Linq;` is available — it is, via implicit usings.) + +- [ ] **Step 4: Run tests to verify they pass** + +Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter ReviewResultTests` +Expected: PASS. + +- [ ] **Step 5: Commit** + +```bash +git add src/ClaudeDo.Worker/Runner/TaskRunner.cs tests/ClaudeDo.Worker.Tests/Runner/ReviewResultTests.cs +git commit -m "feat(roadblock): surface reported roadblocks in the review result" +``` + +--- + +## Task 10: Files-settings UI exposes the new prompt files + +**Files:** +- Modify: `src/ClaudeDo.Ui/ViewModels/Modals/Settings/FilesSettingsTabViewModel.cs` +- Modify: the Files settings view (find with: `Grep "SystemPromptPath" src/ClaudeDo.Ui` → the `.axaml` binding to `OpenPromptCommand`) + +- [ ] **Step 1: Replace the prompt-path properties** + +In `FilesSettingsTabViewModel`, replace the three path properties with the new set (drop Agent, add the rest): + +```csharp + public string SystemPromptPath { get; } = PromptFiles.PathFor(PromptKind.System); + public string PlanningPromptPath { get; } = PromptFiles.PathFor(PromptKind.Planning); + public string PlanningInitialPromptPath { get; } = PromptFiles.PathFor(PromptKind.PlanningInitial); + public string RetryPromptPath { get; } = PromptFiles.PathFor(PromptKind.Retry); + public string DailyPrepPromptPath { get; } = PromptFiles.PathFor(PromptKind.DailyPrep); + public string WeeklyReportPromptPath { get; } = PromptFiles.PathFor(PromptKind.WeeklyReport); +``` + +(`OpenPromptCommand` already parses the `PromptKind` name from its parameter, so no command change is needed.) + +- [ ] **Step 2: Update the view** + +Open the Files settings `.axaml`. For the existing System/Planning/Agent rows: keep System, keep Planning, **remove the Agent row**, and add four rows mirroring the System row's markup — each binding its label/path to the new property and passing the matching `PromptKind` name as the `OpenPromptCommand` parameter: + +- `Planning` (system) → "Planning system prompt", `PlanningPromptPath`, parameter `Planning` +- `PlanningInitial` → "Planning kickoff prompt", `PlanningInitialPromptPath`, parameter `PlanningInitial` +- `Retry` → "Retry prompt", `RetryPromptPath`, parameter `Retry` +- `DailyPrep` → "Daily-prep prompt", `DailyPrepPromptPath`, parameter `DailyPrep` +- `WeeklyReport` → "Weekly-report prompt", `WeeklyReportPromptPath`, parameter `WeeklyReport` + +Use the exact same control template as the existing System row (same button + `CommandParameter` shape); only the bound property, label text, and parameter string differ. + +- [ ] **Step 3: Build the UI project** + +Run: `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release` +Expected: PASS. + +- [ ] **Step 4: Visual check (manual — flag for user)** + +Start the app, open Settings → Files tab. Confirm six "Open" prompt buttons appear (System, Planning system, Planning kickoff, Retry, Daily-prep, Weekly-report), no Agent row, and each opens/seeds the right file under `~/.todo-app/prompts/`. **This step cannot be verified by the agent — ask the user to confirm visually.** + +- [ ] **Step 5: Commit** + +```bash +git add src/ClaudeDo.Ui/ViewModels/Modals/Settings/FilesSettingsTabViewModel.cs src/ClaudeDo.Ui/Views/**/*Files*.axaml +git commit -m "feat(ui): expose all editable prompt files, drop agent prompt" +``` + +--- + +## Task 11: Full build + test sweep + +- [ ] **Step 1: Build worker + app** + +Run: +```bash +dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release +dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release +``` +Expected: PASS. + +- [ ] **Step 2: Run all affected test projects** + +Run: +```bash +dotnet test tests/ClaudeDo.Data.Tests/ClaudeDo.Data.Tests.csproj -c Release +dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release +``` +Expected: PASS. + +- [ ] **Step 3: Update docs** + +Update `docs/prompts-inventory.md` to note the externalized files and that `agent.md`/`planning.md` are retired in favor of `system.md`/`planning-system.md`. Note `CLAUDEDO_BLOCKED:` in the inventory. + +```bash +git add docs/prompts-inventory.md +git commit -m "docs: refresh prompt inventory for externalized prompts + roadblock marker" +``` + +--- + +## Self-review notes + +- **Spec coverage:** system.md collapse (T2), planning prompts (T4), retry (T3), daily-prep English (T5), weekly-report + data pointer (T6), templating/`Render` (T1), roadblock detect/strip/route (T7–T9), file layout + migration via `EnsureExists`/new `PathFor` (T1), UI surface (T10). The "Out-of-scope improvements" system.md section is intentionally **deferred to the child-tasks plan** (it depends on the `SuggestImprovement` tool). +- **Migration:** old `planning.md`/`agent.md` go inert automatically — `TaskRunner` no longer reads agent (T2), planning now reads `planning-system.md` (T1 PathFor). No code deletes the old files; harmless. +- **Determinism:** content tests target `DefaultFor`/`RenderTemplate` (pure, no disk). Consumers fall back to the same default when no user file exists.