diff --git a/src/ClaudeDo.Data/PromptFiles.cs b/src/ClaudeDo.Data/PromptFiles.cs index d1a98cb..4d8d113 100644 --- a/src/ClaudeDo.Data/PromptFiles.cs +++ b/src/ClaudeDo.Data/PromptFiles.cs @@ -1,6 +1,8 @@ +using System.Text; + namespace ClaudeDo.Data; -public enum PromptKind { System, Planning, Agent } +public enum PromptKind { System, Planning, PlanningInitial, Retry, DailyPrep, WeeklyReport } public static class PromptFiles { @@ -9,8 +11,11 @@ public static class PromptFiles public static string PathFor(PromptKind kind) => kind switch { PromptKind.System => Path.Combine(Root, "system.md"), - PromptKind.Planning => Path.Combine(Root, "planning.md"), - PromptKind.Agent => Path.Combine(Root, "agent.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)) }; @@ -30,29 +35,148 @@ public static class PromptFiles return string.IsNullOrEmpty(content) ? null : content; } - private static string DefaultFor(PromptKind kind) => kind switch + /// File content if present and non-empty, otherwise the bundled default. + public static string ReadOrDefault(PromptKind kind) => ReadOrNull(kind) ?? DefaultFor(kind); + + /// Render a prompt: read file-or-default, then substitute named tokens. + public static string Render(PromptKind kind, IReadOnlyDictionary values) + => RenderTemplate(ReadOrDefault(kind), values); + + /// Replace only the given {name} tokens; any other braces pass through untouched. + public static string RenderTemplate(string template, IReadOnlyDictionary values) { - PromptKind.System => - "# System Prompt\n\n" + - "Baseline instructions appended to every task run.\n" + - "Edit this file to inject project-wide rules (style, conventions, hard constraints).\n", - PromptKind.Planning => - "You are a planning assistant for ClaudeDo.\n" + - "Your role is to help break down a task into smaller, actionable subtasks.\n" + - "Your final goal WILL ALWAYS be the creation of Subtasks.\n\n" + - "ALWAYS invoke the `superpowers:brainstorming` skill via the Skill tool at the\n" + - "start of every planning session, and follow its process end-to-end. It guides\n" + - "you through clarifying questions, approach exploration, and design approval\n" + - "BEFORE any subtasks are created. Do not create child tasks until the user has\n" + - "approved a design.\n\n" + - "NEVER change files yourself.\n\n" + - "ALWAYS use the available MCP tools (mcp__claudedo__*) to create child tasks once\n" + - "the design is approved. When you are done planning, finalize the session.\n\n" + - "Be concise and focused. Each subtask should be independently executable.\n", - PromptKind.Agent => - "# Agent Prompt\n\n" + - "Appended to the system prompt for tasks tagged \"agent\" (auto-queued runs).\n" + - "Use this for autonomous-execution rules that don't apply to manual runs.\n", + var sb = new StringBuilder(template); + foreach (var (key, val) in values) + sb.Replace("{" + key + "}", val); + return sb.ToString(); + } + + public static string DefaultFor(PromptKind kind) => kind switch + { + PromptKind.System => SystemDefault, + PromptKind.Planning => PlanningSystemDefault, + PromptKind.PlanningInitial => PlanningInitialDefault, + PromptKind.Retry => RetryDefault, + PromptKind.DailyPrep => DailyPrepDefault, + PromptKind.WeeklyReport => WeeklyReportDefault, _ => "" }; + + private const string SystemDefault = """ + # Working Agreement + + You are completing one well-defined task autonomously in a git repository. + + ## Scope + - Do exactly what the task asks — no unrequested refactors, renames, dependency + changes, or "while I'm here" cleanup. + - If intent is ambiguous, state the assumption you're making and proceed with the + most reasonable reading. Stop only if you genuinely cannot move forward. + - Prefer three similar lines over a premature abstraction. Don't build for + hypothetical future needs. + + ## Working in the repo + - Read a file before editing it. Match the conventions already in this codebase — + they override generic defaults. + - Prefer editing existing files to creating new ones. Don't write comments that + just restate the code. + - Validate only at real boundaries (user input, external APIs). + + ## Finishing + - Before claiming done, verify: run the build and relevant tests, confirm they + pass, and report what you ran. If you couldn't verify something, say so plainly. + - Make focused commits using the repository's existing commit-message convention. + + ## Safety + - Never force-push, hard-reset, or delete branches/files beyond the task's scope + without being asked. + - Don't introduce injection/XSS/secret-leak issues. Never commit credentials. + + ## You are running unattended + You run autonomously with no human watching. There is no one to answer mid-task + questions, so never stop to ask — make the most reasonable decision, note the + assumption, and continue. + + ## When you are blocked + If something genuinely prevents you from completing part of the task (missing + credentials, contradictory requirements, a destructive action you won't take + unasked), do NOT silently give up. Write this marker on its own line, then keep + working on whatever else you can: + + CLAUDEDO_BLOCKED: + + Emit it as many times as needed — once per distinct blocker. Use it only for true + blockers, not for routine decisions you can make yourself. + """; + + private const string PlanningSystemDefault = """ + You are the planning assistant for ClaudeDo. Your job is to break a task into + smaller, independently executable subtasks — the session ends by creating those + subtasks. + + Start every session by invoking the `superpowers:brainstorming` skill (Skill + tool) and follow it end to end: clarifying questions one at a time, then 2–3 + approaches with a recommendation, then a short design. Do not create any subtasks + until the user has approved the design. + + You can ONLY shape this task's plan — you cannot edit files or touch other tasks. + The tools available to you are: CreateChildTask, ListChildTasks, UpdateChildTask, + DeleteChildTask, UpdatePlanningTask, and Finalize. Use nothing else. + + Once the design is approved, create the child tasks with CreateChildTask, then + call Finalize. Keep each subtask concrete and self-contained with a clear + done-state, ordered so dependencies come first. + """; + + private const string PlanningInitialDefault = """ + # Task to plan: {title} + + {description} + """; + + private const string RetryDefault = """ + The task did not complete on the previous attempt — you may have run out of + turns, hit an error, or stopped before finishing. + + Review the work already done in this session and the current state of the + repository, identify what is still incomplete or broken, and finish the task. + Don't restart from scratch or repeat a failed approach. Verify the result + (build + tests) before you stop. + """; + + private const string DailyPrepDefault = """ + You are preparing my workday for {date}. + + 1. Call mcp__claudedo__get_daily_prep_candidates. + 2. Keep tasks already marked MyDay (currentMyDay) — never remove them. + 3. Fill MyDay to at most {maxTasks} open tasks TOTAL (currentMyDay counts). Never exceed it. + 4. Estimate each candidate's effort and pick a feasible mix — not only big items. + Prioritize isStarred, due (scheduledFor), and older tasks. + 5. Place related tasks next to each other using consecutive sortOrder values. + 6. Apply via mcp__claudedo__set_my_day(taskId, true, sortOrder). Never mark anything + outside the candidate list. + + If there are no candidates, do nothing. + """; + + private const string WeeklyReportDefault = """ + You are generating a concise weekly standup report for a software developer, + covering {start} to {end}. + + Rules: + - Write the ENTIRE report in German. + - Group by day. One "## {Wochentag}, {dd.MM.yyyy}" section per day that has + activity (German weekday names). Omit days with no activity. + - Within each day: 3–5 first-person, past-tense bullets ("- Habe X umgesetzt", + "- Y behoben"). Merge related small work into one bullet. + - Drop trivia: typo fixes, pure exploration, false starts, tooling/log noise. + - Blend the developer's own notes and the derived activity into ONE deduplicated + bullet list per day. The notes are authoritative — never omit or contradict them. + - Name the project/repo when it adds clarity. + - Output ONLY the dated sections. No preamble, no intro, no closing remarks. + + Two sections follow below: an activity log derived from Claude session history, + and the developer's own notes. Base the report on both; the notes are + authoritative where they conflict with the derived activity. + """; } diff --git a/tests/ClaudeDo.Data.Tests/PromptFilesTests.cs b/tests/ClaudeDo.Data.Tests/PromptFilesTests.cs new file mode 100644 index 0000000..6f00b50 --- /dev/null +++ b/tests/ClaudeDo.Data.Tests/PromptFilesTests.cs @@ -0,0 +1,46 @@ +using ClaudeDo.Data; + +namespace ClaudeDo.Data.Tests; + +public class PromptFilesTests +{ + [Fact] + public void RenderTemplate_replaces_known_tokens() + { + var outp = PromptFiles.RenderTemplate( + "Plan for {date}, cap {maxTasks}.", + new Dictionary { ["date"] = "2026-06-04", ["maxTasks"] = "5" }); + Assert.Equal("Plan for 2026-06-04, cap 5.", outp); + } + + [Fact] + public void RenderTemplate_leaves_unknown_braces_intact() + { + var outp = PromptFiles.RenderTemplate( + "## {Wochentag}, {dd.MM.yyyy} — {start}", + new Dictionary { ["start"] = "01.06.2026" }); + Assert.Equal("## {Wochentag}, {dd.MM.yyyy} — 01.06.2026", outp); + } + + [Fact] + public void DefaultFor_system_mentions_blocked_marker_and_scope() + { + var d = PromptFiles.DefaultFor(PromptKind.System); + Assert.Contains("CLAUDEDO_BLOCKED:", d); + Assert.Contains("unattended", d, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void DefaultFor_planning_initial_has_title_and_description_tokens() + { + var d = PromptFiles.DefaultFor(PromptKind.PlanningInitial); + Assert.Contains("{title}", d); + Assert.Contains("{description}", d); + } + + [Fact] + public void PathFor_planning_is_planning_system_file() + { + Assert.EndsWith("planning-system.md", PromptFiles.PathFor(PromptKind.Planning)); + } +}