# 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.