Files
ClaudeDo/docs/superpowers/plans/2026-06-04-bundled-prompts-overhaul.md
2026-06-04 13:51:37 +02:00

36 KiB
Raw Permalink Blame History

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):

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:

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<string, string> { ["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<string, string> { ["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:

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;
    }

    /// <summary>File content if present and non-empty, otherwise the bundled default.</summary>
    public static string ReadOrDefault(PromptKind kind) => ReadOrNull(kind) ?? DefaultFor(kind);

    /// <summary>Render a prompt: read file-or-default, then substitute named tokens.</summary>
    public static string Render(PromptKind kind, IReadOnlyDictionary<string, string> values)
        => RenderTemplate(ReadOrDefault(kind), values);

    /// <summary>Replace only the given {name} tokens; any other braces pass through untouched.</summary>
    public static string RenderTemplate(string template, IReadOnlyDictionary<string, string> 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: <one short sentence describing what blocked you>

        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 23
        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: 35 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
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:

        var systemFile = PromptFiles.ReadOrNull(PromptKind.System);
        var agentFile = PromptFiles.ReadOrNull(PromptKind.Agent);

        var instructions = MergeInstructions(
            systemFile, global.DefaultClaudeInstructions, listConfig?.SystemPrompt, task.SystemPrompt, agentFile);

with:

        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
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:

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:

        var error = lastStderr.Length > 0
            ? lastStderr.ToString().Trim()
            : $"Claude exited with code {exitCode} and no result.";

with:

        var error = lastStderr.Length > 0
            ? lastStderr.ToString().Trim()
            : $"{NoResultPrefix} {exitCode} and no result.";

and add inside the class (e.g. just below the fields):

    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):

    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:

                    var retryPrompt = $"The previous attempt failed with:\n\n{result.ErrorMarkdown}\n\nTry again and fix the issues.";

with:

                    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
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:

    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:

    private static string BuildInitialPrompt(TaskEntity task) =>
        PromptFiles.Render(PromptKind.PlanningInitial, new Dictionary<string, string>
        {
            ["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
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:

    [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:

    public static string BuildPrompt(int maxTasks, DateOnly today) =>
        ClaudeDo.Data.PromptFiles.Render(
            ClaudeDo.Data.PromptKind.DailyPrep,
            new Dictionary<string, string>
            {
                ["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
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:

        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:

        var sb = new StringBuilder();
        sb.AppendLine(ClaudeDo.Data.PromptFiles.Render(
            ClaudeDo.Data.PromptKind.WeeklyReport,
            new Dictionary<string, string>
            {
                ["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
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:

    [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:

    public IReadOnlyList<string> Blocks { get; set; } = Array.Empty<string>();

Add a field and a constant to StreamAnalyzer:

    private readonly List<string> _blocks = new();
    private const string BlockedPrefix = "CLAUDEDO_BLOCKED:";

In the case "result": branch, after _resultMarkdown is assigned, scan and strip:

                    if (root.TryGetProperty("result", out var resultProp))
                        _resultMarkdown = StripAndCollect(resultProp.GetString());

In the case "assistant": branch, collect from text content (keep _turnCount++):

                case "assistant":
                    _turnCount++;
                    CollectFromAssistant(root);
                    break;

Add these helpers to the class:

    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:

    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
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:

    public IReadOnlyList<string> Blocks { get; init; } = Array.Empty<string>();
  • 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
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:

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<string>()));
    }

    [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:

    public static string? ComposeReviewResult(string? result, IReadOnlyList<string> 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:

        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
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):

    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
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:

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:

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.

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 (T7T9), 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.