docs: implementation plan for bundled-prompts overhaul
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
972
docs/superpowers/plans/2026-06-04-bundled-prompts-overhaul.md
Normal file
972
docs/superpowers/plans/2026-06-04-bundled-prompts-overhaul.md
Normal file
@@ -0,0 +1,972 @@
|
|||||||
|
# Bundled Prompts Overhaul Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Externalize every bundled prose prompt into editable files with strong defaults, collapse system+agent, and add an inline `CLAUDEDO_BLOCKED:` roadblock protocol surfaced at review.
|
||||||
|
|
||||||
|
**Architecture:** `PromptFiles` becomes the single source of prompt defaults + a pure token renderer. Each consumer (TaskRunner, PlanningSessionManager, DailyPrepPrompt, WeekReportPromptBuilder) reads its prompt via `PromptFiles`. `StreamAnalyzer` collects roadblock markers from streamed assistant text; the runner folds them into the review result.
|
||||||
|
|
||||||
|
**Tech Stack:** .NET 8, xUnit, EF Core (no schema change in this plan).
|
||||||
|
|
||||||
|
Spec: `docs/superpowers/specs/2026-06-04-bundled-prompts-overhaul-design.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File structure
|
||||||
|
|
||||||
|
- `src/ClaudeDo.Data/PromptFiles.cs` — new `PromptKind` members, new defaults, `RenderTemplate` + `ReadOrDefault` + `Render`.
|
||||||
|
- `src/ClaudeDo.Worker/Runner/StreamAnalyzer.cs` — collect `Blocks` from assistant text.
|
||||||
|
- `src/ClaudeDo.Worker/Runner/RunResult.cs` — carry `Blocks`.
|
||||||
|
- `src/ClaudeDo.Worker/Runner/ClaudeProcess.cs` — pass `Blocks`; expose no-result prefix const.
|
||||||
|
- `src/ClaudeDo.Worker/Runner/TaskRunner.cs` — drop agent file; retry via `retry.md`; fold blocks into review result.
|
||||||
|
- `src/ClaudeDo.Worker/Planning/PlanningSessionManager.cs` — read planning prompts via `PromptFiles`.
|
||||||
|
- `src/ClaudeDo.Worker/Prime/DailyPrepPrompt.cs` — read `daily-prep.md`.
|
||||||
|
- `src/ClaudeDo.Worker/Report/WeekReportPromptBuilder.cs` — read `weekly-report.md`.
|
||||||
|
- `src/ClaudeDo.Ui/ViewModels/Modals/Settings/FilesSettingsTabViewModel.cs` + its view — expose new prompt files, drop agent.
|
||||||
|
- Tests in `tests/ClaudeDo.Data.Tests` and `tests/ClaudeDo.Worker.Tests`.
|
||||||
|
|
||||||
|
Build commands (this repo is on .NET 8 — build per project, not the .slnx):
|
||||||
|
```bash
|
||||||
|
dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release
|
||||||
|
dotnet test tests/ClaudeDo.Data.Tests/ClaudeDo.Data.Tests.csproj -c Release
|
||||||
|
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 1: PromptFiles — kinds, defaults, pure renderer
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/ClaudeDo.Data/PromptFiles.cs`
|
||||||
|
- Test: `tests/ClaudeDo.Data.Tests/PromptFilesTests.cs` (create)
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write failing tests for the pure renderer**
|
||||||
|
|
||||||
|
Create `tests/ClaudeDo.Data.Tests/PromptFilesTests.cs`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using ClaudeDo.Data;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Data.Tests;
|
||||||
|
|
||||||
|
public class PromptFilesTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void RenderTemplate_replaces_known_tokens()
|
||||||
|
{
|
||||||
|
var outp = PromptFiles.RenderTemplate(
|
||||||
|
"Plan for {date}, cap {maxTasks}.",
|
||||||
|
new Dictionary<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:
|
||||||
|
|
||||||
|
```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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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**
|
||||||
|
|
||||||
|
```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<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**
|
||||||
|
|
||||||
|
```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<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**
|
||||||
|
|
||||||
|
```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<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**
|
||||||
|
|
||||||
|
```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<string> Blocks { get; set; } = Array.Empty<string>();
|
||||||
|
```
|
||||||
|
|
||||||
|
Add a field and a constant to `StreamAnalyzer`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private readonly List<string> _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<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**
|
||||||
|
|
||||||
|
```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<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:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
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:
|
||||||
|
|
||||||
|
```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.
|
||||||
Reference in New Issue
Block a user