14 KiB
Merge Helper ("Let Claude handle it") — Design
Status: Proposed — awaiting approval
Date: 2026-07-24
Scope: Feature — a per-list and global button that opens an interactive ConPTY Claude session pre-loaded with a set of user-selected tasks. The session (the "Merge Helper") drives each selected task to completion and merge autonomously via mcp__claudedo__* tools, asks the user interactively (in the ConPTY terminal) only when uncertain, resolves merge conflicts itself, and ends with a written summary of everything that changed.
1. Goal
Collapse the repetitive per-task review→merge clicking into a single "Let Claude handle it" action. The user picks the tasks; an embedded Claude session babysits them — running the ones that still need running, reviewing diffs, merging the clean ones, resolving conflicts, and reporting back — while remaining fully interactive so the user can answer questions mid-run.
This reuses the existing ConPTY infrastructure (UI-process embedded terminal) and the globally-registered claudedo MCP server. The only genuinely new worker capability is MCP-driven conflict resolution (§5), which today exists only in the UI hub.
2. Decisions (locked with user, 2026-07-24)
| Question | Decision |
|---|---|
| Which tasks does the helper handle? | Free choice, any status. User hand-picks; helper acts per-status. |
| How are tasks selected for a run? | Checkbox dialog before launch (candidates listed, user ticks). |
| Merge authority / review-gate | Auto-merge; asks interactively on uncertainty. The app's per-task diff-gate is intentionally bypassed for helper-driven merges. |
| Conflict handling | Build MCP conflict tools so the helper resolves conflicts in the working tree itself, asking only when unsure (Option B). |
3. UX Flow
- Entry points
- Per-list: context-menu item "Let Claude handle it" on each user-list row in
ListsIslandView.axaml(alongside Settings / Worktrees / Open in Explorer). - Global: one entry (footer of the lists island) that spans all lists/repos.
- Per-list: context-menu item "Let Claude handle it" on each user-list row in
- Click opens the Merge Helper selection dialog (§4): a checkbox list of candidate tasks, grouped by list/repo, pre-filtered to tasks worth acting on but freely overridable.
- User ticks tasks → "Let Claude handle it" confirm button.
- UI asks the worker for a
MergeHelperLaunchSpec, opens a ConPTY tile in Mission Control running the realclaudeTUI with the merge-helper prompt. - The session works through the tasks, printing progress and asking questions inline; the user answers directly in the terminal.
- On completion Claude prints a summary (merged / skipped / conflicted / follow-ups). The tile stays open for review.
4. Selection Dialog
New modal MergeHelperSelectionDialog (View + VM), built with the existing TaskCompletionSource<T> dialog pattern used by other modals.
Contents:
- Title: "Let Claude handle it" + subtitle naming the scope ("List: " or "All lists").
- A scrollable checkbox list of candidate tasks. Per row: checkbox, title, status badge, list/repo name (in global mode).
- Grouping: by list/repo in global mode; flat in per-list mode.
- Default selection: all actionable tasks pre-ticked — actionable =
WaitingForReview,Idle,Queued,Failed(resettable).Running/WaitingForChildrenshown but unticked (helper will poll them). TerminalDone/Cancelledexcluded from the list entirely. - Footer: "Let Claude handle it" (disabled when nothing ticked) + Cancel. A "select all / none" affordance.
Candidate source: list_tasks via the existing worker client (per list, or across all lists for global). No new query needed; the VM filters client-side by status.
Output: an ordered IReadOnlyList<string> of selected task IDs (+ their list/repo mapping), passed to the launch request.
5. New Worker Capability — MCP Conflict Resolution
Today (verified): merge_task / review_task approve call TaskMergeService.MergeAsync(..., leaveConflictsInTree:false) — on conflict they run git merge --abort (clean rollback, no markers) and return mergeStatus="conflict"; the task stays WaitingForReview. Continue/abort/write-resolution exist only on the SignalR hub (Rider merge editor). An MCP agent therefore cannot resolve conflicts. This section adds that.
5.1 Approach
Reuse the exact engine methods the UI already uses — TaskMergeService.MergeAsync(leaveConflictsInTree:true), ContinueMergeAsync, AbortMergeAsync — and expose them over MCP. The helper resolves conflict markers on disk (it has filesystem access to the repo checkouts via --add-dir, see §6.3) and drives the merge state exclusively through MCP tools so the engine stays authoritative.
5.2 MCP surface changes (ExternalMcpService.cs)
-
review_task/merge_task— new optional paramleaveConflictsInTree: bool = false. Whentrueand the merge conflicts: leave markers in the checkout instead of aborting, and return{ mergeStatus: "conflict_in_tree", conflicts: string[], repoPath: string }whererepoPathis the checkout holding the markers. Task staysWaitingForReview, merge is in progress. Clean-merge behaviour is unchanged, so the helper can always passtrue. -
New tool
continue_merge(taskId)→TaskMergeService.ContinueMergeAsync. Stages the resolved files and commits the merge; on success the task goes toDoneand the worktree is marked merged, returning{ merged: true, mergeCommit }. If markers remain, returns{ merged: false, conflicts: string[] }. -
New tool
abort_merge(taskId)→TaskMergeService.AbortMergeAsync. Aborts the in-progress merge; task staysWaitingForReview. Returns{ aborted: true }.
Implementation notes (verify against TaskMergeService.cs during the plan):
- Confirm the exact signatures of
ContinueMergeAsync/AbortMergeAsyncand how in-progress-merge state is keyed. The hub tracks a single active conflict merge; the MCP variants must locate the merge fromtaskId(target branch + repo from the task/list), not shared hub state. - Emit the existing
TaskUpdatedevent after continue/abort so the UI list re-buckets live. - Guard against a repo already mid-merge (
Blocked) — surface it to the agent rather than clobbering.
5.3 v1 boundary — parent/children unit merges
review_task approve on a task with children drives PlanningMergeOrchestrator (multi-step unit merge with its own continue/abort). Autonomous MCP conflict resolution for that orchestrated path is out of scope for v1: if a unit merge conflicts, the helper reports it and hands off to the UI (as today). Clean unit merges still complete autonomously. Childless single-task merges — the common review/merge case — get full autonomous conflict resolution. This limit is called out to the user, not hidden.
6. Launch — Worker + Wiring
6.1 Prompt templates
Add PromptKind.MergeHelper (system) and PromptKind.MergeHelperInitial (brief) to ClaudeDo.Data/PromptFiles.cs, with built-in defaults and {{token}} rendering, mirroring Planning / PlanningInitial.
- System prompt (
merge-helper-system.md): defines the role and the per-status algorithm (§7), the merge/conflict rules, the "ask on uncertainty" posture, and the required final summary format. - Initial brief (
merge-helper-initial.md): rendered with the selected tasks — a table of{id, title, status, list, repo}plus the scope label. Written to a session-brief file on disk; the positional prompt is a single-line kickoff pointing at that file via--add-dir(planning pattern — a multi-line positional prompt truncates at the first newline).
6.2 Session files
Path: ~/.todo-app/merge-helper-sessions/<sessionId>/ (a fresh GUID per run — these sessions are ephemeral and never resumed):
brief.md— rendered task list + scope + instructions.- No per-session MCP config: the session uses the globally-registered
claudedoMCP server (same as task/ad-hoc sessions), so no token is needed.
Cleanup: prune session dirs older than N days on app start (best-effort; same posture as planning dirs).
6.3 Launch spec
New InteractiveLaunchSpecService.BuildForMergeHelper(selectedTaskIds, scope, ct) returning a LaunchSpec:
- Cwd: per-list → the list's repo working dir; global → the first selected task's repo (any valid repo; the agent works cross-repo via MCP).
--add-dir: the session-brief dir plus every distinct repo checkout among the selected tasks (so the agent can read/resolve conflict markers in each repo). Computed from each task's list working dir.- Args:
--permission-mode default,--allowedTools mcp__claudedo__*,Read,Grep,Glob,Edit,Bash,WebFetch,WebSearch,Skill,--append-system-prompt-file <merge-helper-system.md>,--add-dir ..., then the single-line kickoff prompt.Editis required for conflict resolution;Bashis allowed for read-only git inspection (git status/diff) — the system prompt mandates that all merge state changes go through MCP tools, never rawgit merge/commit, to keep the engine authoritative and honour the user's rejection of the "raw git" option.
- Env:
MCP_TOOL_TIMEOUT=200000(as task/ad-hoc sessions set).
6.4 Hub + client + Mission Control
- Hub:
WorkerHub.GetMergeHelperLaunchSpec(string[] taskIds, string? listId)→_launchSpecService.BuildForMergeHelper(...). Sibling toGetAdHocLaunchSpec/GetPlanningStartLaunchSpec. - Client:
IWorkerClient.GetMergeHelperLaunchSpecAsync(...)+WorkerClientimpl. - Mission Control:
MissionControlViewModel.OpenMergeHelperConPtySessionAsync(spec)— wraps the spec in aTerminalLaunchDescriptor, creates aConPtyPaneViewModel(ad-hoc style, never deduped — each run is its own tile), adds it toConPtySessions. - Event plumbing:
ListsIslandViewModelraisesLetClaudeHandleRequested(scope);IslandsShellViewModelforwards to Mission Control, which opens the selection dialog, then (on confirm) fetches the spec and opens the tile.
7. Helper Behaviour (encoded in the system prompt)
For each selected task, act by status:
- Idle / Queued:
run_task_now; pollget_taskuntil terminal orWaitingForReview. - Failed:
reset_failed_taskthen run, or ask the user — failures often need a human call; default to asking briefly. - Running / WaitingForChildren: poll
get_taskuntil it surfaces for review. - WaitingForReview:
get_task_diff(stat first, then full if needed), sanity-check the change against the task's intent, thenreview_task approvewithleaveConflictsInTree:true.- Clean → merged, task Done.
- Conflict (
conflict_in_tree) → open the conflicted files underrepoPath(Read/Edit), resolve the markers guided by both sides' intent, thencontinue_merge. If the resolution is non-obvious or risky, ask the user in the terminal before continuing.abort_mergeif the user declines or it's unsafe.
- Parent with children: clean unit merge proceeds; on conflict, report and tell the user to resolve in the UI (§5.3).
Cross-cutting rules (in the prompt):
- Ask the user interactively for anything ambiguous, risky, or destructive — that is the point of the ConPTY session.
- Never use raw
git merge/commit/reset; drive all merge state through the MCP tools. - Keep a running tally; at the end print a summary: per task — final status, merge commit (if any), conflicts resolved, anything skipped, and suggested follow-ups.
8. Testing
Automated (ClaudeDo.Worker.Tests, real SQLite + real git):
review_task/merge_taskwithleaveConflictsInTree:true: clean merge → Done; conflicting merge →conflict_in_tree, markers present in the checkout, task staysWaitingForReview.continue_merge: after markers resolved on disk → commits, task Done, worktree merged; with markers still present → returns remaining conflicts.abort_merge: in-progress merge aborted, markers gone, task staysWaitingForReview.continue_merge/abort_mergeon a task with no in-progress merge → clean MCP error, no clobber.TaskUpdatedfired after continue/abort.BuildForMergeHelper: computes distinct repo--add-dirset, correct cwd per scope, brief file rendered with all selected tasks, allowed-tools string correct.
No real-Claude tests (per project convention) — the end-to-end ConPTY run is a manual smoke item.
Manual (add to docs/open.md):
- ConPTY tile launches with the brief; MCP tools reachable; a clean multi-task run merges all and prints a summary.
- A seeded conflict is resolved autonomously via
continue_merge. - Interactive question round-trip (helper asks, user answers in terminal).
- Global (multi-repo) run with
--add-dirfor each repo. - Selection dialog: grouping, default ticks, select-all/none, per-list vs global scope.
9. Phasing
Delivered as one plan with three phases (see the plan doc). Phase A is independently useful and merges first.
- Phase A — Worker MCP conflict tools (§5):
leaveConflictsInTreeparam +continue_merge+abort_merge+ tests. No UI. - Phase B — Worker launch (§6.1–6.4 worker side): prompt templates,
BuildForMergeHelper, hub endpoint, session-file/brief generation, client method. Contract for C locked here. - Phase C — UI: selection dialog (View+VM), per-list + global entries, event plumbing,
OpenMergeHelperConPtySessionAsync.
10. Out of scope (v1)
- Autonomous conflict resolution for parent/children unit merges (§5.3) — report + UI hand-off only.
- Resuming a merge-helper session (
--resume); sessions are ephemeral. - A non-interactive/headless merge-helper (this is deliberately a ConPTY interactive session).
- Cross-list batching semantics beyond "act on each selected task independently."
- Any change to the existing per-task Approve/diff-gate flow.