docs(merge-helper): spec + implementation plan
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
# Merge Helper — Implementation Plan
|
||||
|
||||
Spec: `docs/superpowers/specs/2026-07-24-merge-helper-design.md`
|
||||
Approach: subagent-driven (one subagent per task, `sonnet`, TDD, stage files by path — never `git add -A`). Build with `-c Release` per-csproj (a running Worker locks `Debug`). Commit per task, Conventional Commits.
|
||||
|
||||
---
|
||||
|
||||
## Phase A — Worker MCP conflict tools
|
||||
|
||||
Independently useful; merges first. All in `src/ClaudeDo.Worker/External/ExternalMcpService.cs` + tests in `tests/ClaudeDo.Worker.Tests/`.
|
||||
|
||||
### A1 — Verify engine surface (spike, no commit)
|
||||
Read `TaskMergeService.MergeAsync` / `ContinueMergeAsync` / `AbortMergeAsync` and the hub conflict flow (`WorkerHub.StartConflictMerge`/`ContinueConflictMerge`/`AbortConflictMerge`). Pin down:
|
||||
- exact `ContinueMergeAsync` / `AbortMergeAsync` signatures and how in-progress-merge state is located (repo + target branch from task/list, not shared hub state);
|
||||
- how the childless approve path (`ApproveAndMergeAsync`) threads `leaveConflictsInTree`.
|
||||
Record findings in the task notes; feeds A2/A3.
|
||||
|
||||
### A2 — `leaveConflictsInTree` on review_task / merge_task
|
||||
- TDD: tests in `Worker.Tests` (real git) — clean merge → Done; conflict + flag → `conflict_in_tree`, markers present, task stays `WaitingForReview`, `repoPath` returned.
|
||||
- Add optional param `leaveConflictsInTree = false` to `MergeTask` and `ReviewTask` (approve branch). When true, call the `leaveConflictsInTree:true` engine path and map the conflict result to `{ mergeStatus/merged, conflicts, repoPath }`.
|
||||
- Keep default behaviour (abort-on-conflict) byte-identical when the flag is absent/false.
|
||||
- Commit: `feat(worker): let review_task/merge_task leave conflicts in tree via MCP`
|
||||
|
||||
### A3 — `continue_merge` + `abort_merge` MCP tools
|
||||
- TDD: continue after on-disk resolution → committed, task Done, worktree merged; continue with markers remaining → returns conflicts; abort → markers gone, task `WaitingForReview`; both on no-active-merge → clean MCP error; `TaskUpdated` fired.
|
||||
- Add `[McpServerTool] continue_merge(taskId)` → `ContinueMergeAsync`; `abort_merge(taskId)` → `AbortMergeAsync`. Locate the merge from the task's repo/target. Emit `TaskUpdated`.
|
||||
- Commit: `feat(worker): add continue_merge and abort_merge MCP tools`
|
||||
|
||||
---
|
||||
|
||||
## Phase B — Worker launch for the merge-helper session
|
||||
|
||||
### B1 — Prompt templates
|
||||
- Add `PromptKind.MergeHelper` + `PromptKind.MergeHelperInitial` to `ClaudeDo.Data/PromptFiles.cs` (file names `merge-helper-system.md` / `merge-helper-initial.md`, built-in `DefaultFor`, `Render` tokens for the initial brief).
|
||||
- System prompt encodes §7 behaviour (per-status algorithm, ask-on-uncertainty, MCP-only merge state, summary format).
|
||||
- Initial brief renders a task table `{id,title,status,list,repo}` + scope label.
|
||||
- TDD: `PromptFiles` tests — kinds resolve, defaults non-empty, `Render` substitutes brief tokens.
|
||||
- Commit: `feat(data): add merge-helper prompt templates`
|
||||
|
||||
### B2 — `BuildForMergeHelper` launch spec
|
||||
- TDD (`Worker.Tests`): distinct-repo `--add-dir` set computed from selected tasks; correct cwd per scope (per-list repo vs first repo global); brief file written to `~/.todo-app/merge-helper-sessions/<guid>/brief.md`; allowed-tools + `--permission-mode default` + `MCP_TOOL_TIMEOUT` env correct; single-line kickoff points at the brief.
|
||||
- Implement `InteractiveLaunchSpecService.BuildForMergeHelper(IReadOnlyList<string> taskIds, MergeHelperScope scope, ct)`. Reuse the planning brief-file/kickoff pattern.
|
||||
- Commit: `feat(worker): build merge-helper interactive launch spec`
|
||||
|
||||
### B3 — Hub endpoint + client method
|
||||
- `WorkerHub.GetMergeHelperLaunchSpec(string[] taskIds, string? listId)`; `IWorkerClient.GetMergeHelperLaunchSpecAsync(...)` + `WorkerClient` impl.
|
||||
- Update hand-rolled `IWorkerClient` fakes in **both** test projects (see gotcha memory).
|
||||
- Commit: `feat(worker): expose merge-helper launch spec over the hub`
|
||||
|
||||
---
|
||||
|
||||
## Phase C — UI
|
||||
|
||||
### C1 — Selection dialog (View + VM)
|
||||
- New `MergeHelperSelectionViewModel` + `MergeHelperSelectionDialog.axaml` (compiled bindings, `TaskCompletionSource<T>` pattern). Checkbox rows (title, status badge, list/repo), grouping in global mode, default ticks per §4, select-all/none, confirm disabled when empty.
|
||||
- Candidates via existing `list_tasks`/worker client; filter client-side.
|
||||
- TDD (`Ui.Tests`): default-tick logic, empty→confirm-disabled, returns ordered selected IDs + list mapping.
|
||||
- Commit: `feat(ui): add merge-helper task selection dialog`
|
||||
|
||||
### C2 — Entry points + event plumbing
|
||||
- Per-list context-menu item **"Let Claude handle it"** in `ListsIslandView.axaml` (user-list rows) + one global entry in the footer. Bind to `LetClaudeHandleCommand` on `ListsIslandViewModel` (param = `ListNavItemViewModel` or a global sentinel).
|
||||
- VM raises `LetClaudeHandleRequested(MergeHelperScope)`; `IslandsShellViewModel` forwards to Mission Control.
|
||||
- Commit: `feat(ui): add "Let Claude handle it" entry points`
|
||||
|
||||
### C3 — Mission Control wiring
|
||||
- `MissionControlViewModel.OpenMergeHelperConPtySessionAsync(scope)`: open selection dialog → on confirm, `GetMergeHelperLaunchSpecAsync` → wrap in `TerminalLaunchDescriptor` → new `ConPtyPaneViewModel` (never deduped) → add to `ConPtySessions`/`Panes`.
|
||||
- Commit: `feat(ui): open merge-helper ConPTY tile from selection`
|
||||
|
||||
---
|
||||
|
||||
## Verify (per task + at the end)
|
||||
- Read each subagent diff; build the touched csproj `-c Release`; run the relevant test project.
|
||||
- `locales/en.json` + `de.json` parity for any new UI strings (Localization.Tests enforces it).
|
||||
- Flag visual-verification gaps (dialog layout, tile) for the user — never claim UI works without a run.
|
||||
- End-to-end ConPTY smoke (real Claude) is a manual item in `docs/open.md`.
|
||||
|
||||
## Commit docs first
|
||||
`docs(merge-helper): spec + implementation plan` (this file + the spec).
|
||||
@@ -0,0 +1,180 @@
|
||||
# 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
|
||||
|
||||
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 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 `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, 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_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)
|
||||
|
||||
- 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.
|
||||
Reference in New Issue
Block a user