# 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). The helper must handle **all** cases including parent/children unit merges; where the MCP path doesn't reach, **manual resolution by hand (Edit + git) is an accepted fallback** (user-confirmed 2026-07-24). | --- ## 3. UX Flow 1. **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. 2. 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. 3. User ticks tasks → **"Let Claude handle it"** confirm button. 4. UI asks the worker for a `MergeHelperLaunchSpec`, opens a **ConPTY tile in Mission Control** running the real `claude` TUI with the merge-helper prompt. 5. The session works through the tasks, printing progress and asking questions inline; the user answers directly in the terminal. 6. 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` 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` / `WaitingForChildren` shown but unticked (helper will poll them). Terminal `Done`/`Cancelled` excluded 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` 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`) 1. **`review_task` / `merge_task` — new optional param `leaveConflictsInTree: bool = false`.** When `true` and the merge conflicts: leave markers in the checkout instead of aborting, and return `{ mergeStatus: "conflict_in_tree", conflicts: string[], repoPath: string }` where `repoPath` is the checkout holding the markers. Task stays `WaitingForReview`, merge is in progress. Clean-merge behaviour is unchanged, so the helper can always pass `true`. 2. **New tool `continue_merge(taskId)`** → `TaskMergeService.ContinueMergeAsync`. Stages the resolved files and commits the merge; on success the task goes to `Done` and the worktree is marked merged, returning `{ merged: true, mergeCommit }`. If markers remain, returns `{ merged: false, conflicts: string[] }`. 3. **New tool `abort_merge(taskId)`** → `TaskMergeService.AbortMergeAsync`. Aborts the in-progress merge; task stays `WaitingForReview`. Returns `{ aborted: true }`. **Implementation notes (verify against `TaskMergeService.cs` during the plan):** - Confirm the exact signatures of `ContinueMergeAsync` / `AbortMergeAsync` and how in-progress-merge state is keyed. The hub tracks a single active conflict merge; the MCP variants must locate the merge from `taskId` (target branch + repo from the task/list), not shared hub state. - Emit the existing `TaskUpdated` event 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 Parent/children unit merges `review_task approve` on a task **with children** drives `PlanningMergeOrchestrator` (a multi-step unit merge with its own continue/abort on the hub). The helper must handle these too. Two paths, tried in order: 1. **MCP (preferred):** `continue_merge` / `abort_merge` detect *which* kind of in-progress merge the task has (single-task `TaskMergeService` vs orchestrated `PlanningMergeOrchestrator`) and route to the matching engine continue/abort. This keeps the orchestrated path engine-mediated over MCP too. Implement if the orchestrator's continue/abort can be located from the task without shared hub UI state (verify in A1). 2. **Manual fallback (accepted):** where the MCP path genuinely can't reach an in-progress merge, the helper resolves the conflict markers on disk (Read/Edit) and completes the merge by hand (`git add ` + `git commit`, or `git merge --continue`). The user has explicitly accepted hand-merging as a fallback. The system prompt still mandates: **prefer the MCP tools whenever they apply**; only drop to raw git for cases the MCP tools don't cover, and honour the shared-checkout rule (`git commit -- `, never a bare commit that sweeps peers' index). --- ## 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//` (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 `claudedo` MCP 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 `, `--add-dir ...`, then the single-line kickoff prompt. - `Edit` is required for conflict resolution; `Bash` is allowed for **read-only** git inspection (`git status`/`diff`) — the system prompt mandates that all merge *state changes* go through MCP tools, never raw `git 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 to `GetAdHocLaunchSpec` / `GetPlanningStartLaunchSpec`. - **Client:** `IWorkerClient.GetMergeHelperLaunchSpecAsync(...)` + `WorkerClient` impl. - **Mission Control:** `MissionControlViewModel.OpenMergeHelperConPtySessionAsync(spec)` — wraps the spec in a `TerminalLaunchDescriptor`, creates a `ConPtyPaneViewModel` (ad-hoc style, **never deduped** — each run is its own tile), adds it to `ConPtySessions`. - **Event plumbing:** `ListsIslandViewModel` raises `LetClaudeHandleRequested(scope)`; `IslandsShellViewModel` forwards 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`; poll `get_task` until terminal or `WaitingForReview`. - **Failed:** `reset_failed_task` then run, *or* ask the user — failures often need a human call; default to asking briefly. - **Running / WaitingForChildren:** poll `get_task` until it surfaces for review. - **WaitingForReview:** `get_task_diff` (stat first, then full if needed), sanity-check the change against the task's intent, then `review_task approve` with `leaveConflictsInTree:true`. - **Clean →** merged, task Done. - **Conflict (`conflict_in_tree`) →** open the conflicted files under `repoPath` (Read/Edit), resolve the markers guided by both sides' intent, then `continue_merge`. If the resolution is non-obvious or risky, **ask the user in the terminal** before continuing. `abort_merge` if the user declines or it's unsafe. - **Parent with children:** clean unit merge proceeds; on conflict, resolve via the MCP tools if they reach the orchestrated merge, else hand-merge the markers and complete it (§5.3) — asking the user first when the resolution is non-obvious. 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_task` with `leaveConflictsInTree:true`: clean merge → Done; conflicting merge → `conflict_in_tree`, markers present in the checkout, task stays `WaitingForReview`. - `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 stays `WaitingForReview`. - `continue_merge`/`abort_merge` on a task with no in-progress merge → clean MCP error, no clobber. - `TaskUpdated` fired after continue/abort. - `BuildForMergeHelper`: computes distinct repo `--add-dir` set, 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-dir` for 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): `leaveConflictsInTree` param + `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) - 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.