183 lines
15 KiB
Markdown
183 lines
15 KiB
Markdown
# 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<T>` dialog pattern used by other modals.
|
||
|
||
**Contents:**
|
||
- Title: *"Let Claude handle it"* + subtitle naming the scope ("List: <name>" 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<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`)
|
||
|
||
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 <paths>` + `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 -- <paths>`, 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/<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 `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 <merge-helper-system.md>`, `--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.
|