36 KiB
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— newPromptKindmembers, new defaults,RenderTemplate+ReadOrDefault+Render.src/ClaudeDo.Worker/Runner/StreamAnalyzer.cs— collectBlocksfrom assistant text.src/ClaudeDo.Worker/Runner/RunResult.cs— carryBlocks.src/ClaudeDo.Worker/Runner/ClaudeProcess.cs— passBlocks; expose no-result prefix const.src/ClaudeDo.Worker/Runner/TaskRunner.cs— drop agent file; retry viaretry.md; fold blocks into review result.src/ClaudeDo.Worker/Planning/PlanningSessionManager.cs— read planning prompts viaPromptFiles.src/ClaudeDo.Worker/Prime/DailyPrepPrompt.cs— readdaily-prep.md.src/ClaudeDo.Worker/Report/WeekReportPromptBuilder.cs— readweekly-report.md.src/ClaudeDo.Ui/ViewModels/Modals/Settings/FilesSettingsTabViewModel.cs+ its view — expose new prompt files, drop agent.- Tests in
tests/ClaudeDo.Data.Testsandtests/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 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
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(addBuildRetryPrompt, 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; addComposeReviewResult) -
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.axamlbinding toOpenPromptCommand) -
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, parameterPlanningPlanningInitial→ "Planning kickoff prompt",PlanningInitialPromptPath, parameterPlanningInitialRetry→ "Retry prompt",RetryPromptPath, parameterRetryDailyPrep→ "Daily-prep prompt",DailyPrepPromptPath, parameterDailyPrepWeeklyReport→ "Weekly-report prompt",WeeklyReportPromptPath, parameterWeeklyReport
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 (T7–T9), file layout + migration viaEnsureExists/newPathFor(T1), UI surface (T10). The "Out-of-scope improvements" system.md section is intentionally deferred to the child-tasks plan (it depends on theSuggestImprovementtool). - Migration: old
planning.md/agent.mdgo inert automatically —TaskRunnerno longer reads agent (T2), planning now readsplanning-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.