diff --git a/src/ClaudeDo.Data/PromptFiles.cs b/src/ClaudeDo.Data/PromptFiles.cs index 35b9a5c8..e6f22940 100644 --- a/src/ClaudeDo.Data/PromptFiles.cs +++ b/src/ClaudeDo.Data/PromptFiles.cs @@ -2,7 +2,7 @@ using System.Text; namespace ClaudeDo.Data; -public enum PromptKind { System, Planning, PlanningInitial, Retry, DailyPrep, WeeklyReport, ImprovementChild, Refine } +public enum PromptKind { System, Planning, PlanningInitial, Retry, DailyPrep, WeeklyReport, ImprovementChild, Refine, MergeHelper, MergeHelperInitial } public static class PromptFiles { @@ -18,6 +18,8 @@ public static class PromptFiles PromptKind.WeeklyReport => Path.Combine(Root, "weekly-report.md"), PromptKind.ImprovementChild => Path.Combine(Root, "improvement-child.md"), PromptKind.Refine => Path.Combine(Root, "refine.md"), + PromptKind.MergeHelper => Path.Combine(Root, "merge-helper-system.md"), + PromptKind.MergeHelperInitial => Path.Combine(Root, "merge-helper-initial.md"), _ => throw new ArgumentOutOfRangeException(nameof(kind)) }; @@ -63,6 +65,8 @@ public static class PromptFiles PromptKind.WeeklyReport => WeeklyReportDefault, PromptKind.ImprovementChild => ImprovementChildDefault, PromptKind.Refine => RefineDefault, + PromptKind.MergeHelper => MergeHelperDefault, + PromptKind.MergeHelperInitial => MergeHelperInitialDefault, _ => "" }; @@ -224,6 +228,52 @@ public static class PromptFiles task, stop. """; + private const string MergeHelperDefault = """ + You are the ClaudeDo Merge Helper, running as an interactive session with the user watching. Ask them questions whenever you are unsure — that is the point of this session. + + Your job: take the tasks listed in the brief and drive each one to a merged, Done state, then print a summary of everything that changed. You act through the mcp__claudedo__* tools. Read the brief file first (the kickoff message gives its path); it lists each task's id, title, status, list and repo. + + Handle the tasks in the order listed, one at a time. For each task, act on its current status: + - Idle or Queued: run it with run_task_now, then poll get_task until it leaves Running (it lands in WaitingForReview on success, or Failed). + - Running or WaitingForChildren: poll get_task until it surfaces for review. + - Failed: this usually needs human judgement — ask the user whether to reset_failed_task and re-run, or skip it. + - WaitingForReview: review, then merge it (below). + + Reviewing and merging a WaitingForReview task: + 1. Inspect the change with get_task_diff (stat first, then the full diff if it is non-trivial) and sanity-check it against the task's title/description. + 2. If the change looks wrong, incomplete, or risky, STOP and ask the user before merging — offer reject_rerun (with feedback) or skip. + 3. Otherwise merge it with review_task(taskId, decision="approve", leaveConflictsInTree=true). + - Clean merge → the task is Done; move on. + - Conflict (markers left in the working tree, repoPath returned) → resolve it (below). + + Resolving a conflict: + - Open each conflicted file under repoPath (Read/Edit) and resolve the <<<<<<< ======= >>>>>>> markers, guided by BOTH sides' intent. Then call continue_merge(taskId). If markers remain it tells you — fix and call again. Use abort_merge(taskId) to cancel a merge you cannot safely resolve. + - For a task WITH children (a unit merge), pass the PARENT task id to continue_merge / abort_merge. + - If a resolution is non-obvious, ambiguous, or might drop someone's work, ASK THE USER before continuing. + - Prefer the MCP tools whenever they apply. Only if the MCP tools cannot reach an in-progress merge may you finish it by hand: resolve the markers, then `git add -- ` and `git commit` — NEVER `git add -A` or a bare commit, because the checkout is shared with other sessions. + + Rules: + - Never use raw `git merge`, `git reset`, or `git checkout` to force a merge. Drive merges through the MCP tools; hand-resolution is only for markers the tools left and cannot finish. + - Ask the user for anything ambiguous, risky, or destructive. + + When every task is handled, print a SUMMARY: + - one line per task: title — final status — merge commit (if any) — conflicts resolved (if any) + - anything you skipped or left for the user, and why + - suggested follow-ups, if any. + """; + + private const string MergeHelperInitialDefault = """ + # Merge Helper brief + + Scope: {scope} + + Handle the following tasks, in order. For each, drive it to a merged/Done state per your instructions, asking me when unsure. + + {tasks} + + When every task is handled, print the summary. + """; + private const string WeeklyReportDefault = """ You are generating a concise weekly standup report for a software developer, covering {start} to {end}. diff --git a/tests/ClaudeDo.Data.Tests/PromptFilesTests.cs b/tests/ClaudeDo.Data.Tests/PromptFilesTests.cs index 6f00b506..ee62b13d 100644 --- a/tests/ClaudeDo.Data.Tests/PromptFilesTests.cs +++ b/tests/ClaudeDo.Data.Tests/PromptFilesTests.cs @@ -43,4 +43,41 @@ public class PromptFilesTests { Assert.EndsWith("planning-system.md", PromptFiles.PathFor(PromptKind.Planning)); } + + [Fact] + public void PathFor_merge_helper_kinds_map_to_their_files() + { + Assert.EndsWith("merge-helper-system.md", PromptFiles.PathFor(PromptKind.MergeHelper)); + Assert.EndsWith("merge-helper-initial.md", PromptFiles.PathFor(PromptKind.MergeHelperInitial)); + } + + [Fact] + public void DefaultFor_merge_helper_is_non_empty_and_mentions_the_merge_tools() + { + var d = PromptFiles.DefaultFor(PromptKind.MergeHelper); + Assert.False(string.IsNullOrWhiteSpace(d)); + Assert.Contains("review_task", d); + Assert.Contains("continue_merge", d); + } + + [Fact] + public void DefaultFor_merge_helper_initial_has_scope_and_tasks_tokens() + { + var d = PromptFiles.DefaultFor(PromptKind.MergeHelperInitial); + Assert.False(string.IsNullOrWhiteSpace(d)); + Assert.Contains("{scope}", d); + Assert.Contains("{tasks}", d); + } + + [Fact] + public void RenderTemplate_merge_helper_initial_substitutes_scope_and_tasks() + { + var outp = PromptFiles.RenderTemplate( + PromptFiles.DefaultFor(PromptKind.MergeHelperInitial), + new Dictionary { ["scope"] = "All lists", ["tasks"] = "- [Idle] T1" }); + Assert.Contains("Scope: All lists", outp); + Assert.Contains("- [Idle] T1", outp); + Assert.DoesNotContain("{scope}", outp); + Assert.DoesNotContain("{tasks}", outp); + } }