Files
ClaudeDo/docs/superpowers/specs/2026-07-27-list-handler-design.md

9.1 KiB

"Let Claude handle it" — per-list handler with read / dedupe / enhance / run / merge

Date: 2026-07-27 Supersedes parts of: 2026-07-24-merge-helper-design.md (global scope, run-then-merge-only prompt)

1. Problem

The merge helper shipped in v2.3.0 with two entry points — a per-list context-menu item and a global footer Broom button — and a prompt that only runs and merges the selected tasks.

Two things are wrong with that:

  • The global scope is unwanted. A run spanning several lists spans several repos, which makes cwd, the --add-dir set and the merge order ambiguous for no benefit. The user works one list (= one repo) at a time.
  • The helper starts too late. It takes the task list as given: it never reads the tasks as a set, so duplicates run twice and produce conflicting worktrees, and vague tasks go into an autonomous run under-specified and come back wrong.

2. Goal

One entry point, on a user list. It opens an interactive ConPTY session that takes the selected tasks through five phases: read them all, dedupe them, sharpen them for autonomous execution, run them, then review and merge each worktree.

Non-goals: no change to the autonomous queue path, no change to the ConPTY tile plumbing, no rename of the MergeHelper* identifiers (the user-facing label stays "Let Claude handle it").

3. Scope becomes list-only

listId becomes non-nullable across the whole chain:

Layer Change
ListsIslandViewModel MergeHelperRequest(string ListId, …); LetClaudeHandleAllAsync deleted
IslandsShellViewModel:241 unchanged (already forwards req.ListId)
MissionControlViewModel:327 OpenMergeHelperConPtySessionAsync(string listId, …)
IWorkerClient:90 / WorkerClient:525 GetMergeHelperLaunchSpecAsync(taskIds, string listId, ct)
WorkerHub:682 GetMergeHelperLaunchSpec(string[] taskIds, string listId)
IInteractiveLaunchSpecService:36 BuildForMergeHelperAsync(taskIds, string listId, ct)

Deleted UI surface:

  • Broom button ListsIslandView.axaml:205-210 and LetClaudeHandleAllCommand.
  • IsGlobal on MergeHelperSelectionModalViewModel and the LIST column (MergeHelperSelectionModal.axaml:71) — with a single list the column is constant.
  • Localization keys lists.letClaudeAllTip, modals.mergeHelper.scopeAll, modals.mergeHelper.columnList (en + de, parity test enforces both).

Configure(string listId, string listName) loses its nullable overload; ScopeLabel always renders modals.mergeHelper.scopeList.

3.1 Single repo in the launch spec

BuildForMergeHelperAsync currently collects a distinct repoDirs set across the selected tasks and picks cwd per scope. With a list scope every task shares the list's WorkingDir, so this collapses to:

  • Load the list; throw InvalidOperationException if it has no existing WorkingDir.
  • cwd = that directory; --add-dir = the session dir + that one directory.
  • The per-task brief line drops the now-constant list: and repo: fields.

The context-menu item is already hidden when WorkingDir is empty, so the throw is a guard, not a normal path.

3.2 Entry point visibility

The item stays in the list row's context menu, next to "List settings", "Worktrees overview", "Open in Explorer" and "Open in Terminal". That is the established place for list-scoped actions; a second always-visible button in the row would break the pattern.

4. Worker: Cancelled becomes externally settable

ExternalMcpService.UpdateTaskStatus accepts only Idle and Queued and throws "Status '{target}' is not settable externally. Use run_task_now or cancel_task." for the rest — but neither escape hatch reaches an Idle task: cancel_task only cancels a running task, and review_task(decision="cancel") requires WaitingForReview/Running/Queued. The dedupe phase needs exactly that: retire an Idle duplicate without destroying it.

Add to the switch:

case TaskStatus.Cancelled:
    var cancelResult = await _state.CancelAsync(taskId, DateTime.UtcNow, cancellationToken);
    if (!cancelResult.Ok)
        throw new InvalidOperationException(cancelResult.Reason ?? "Cannot cancel task.");
    break;

TaskStateService.CancelAsync (State/TaskStateService.cs:244) already owns the transition and its worktree/parent side effects. The existing error message in UpdateTaskStatus already lists Cancelled as valid, so this also removes a lie. A cancelled task stays visible and can be reset to Idle — nothing is lost, unlike delete_task.

5. The five-phase prompt

PromptFiles.MergeHelperDefault is rewritten. The session stays interactive and the helper is told to ask whenever unsure — that is the point of a watched ConPTY session.

Phase 0 — Read

batch_get_tasks over every id in the brief before touching anything: title, description, status, parent/child links. The helper must hold the whole set in mind before acting on any single task.

Phase 1 — Dedupe

Compare the tasks pairwise for overlap. Emit a table of candidate pairs with the reason each pair looks like a duplicate, then ask per pair:

  • merge → fold the loser's unique content into the survivor via update_task, then update_task_status(loserId, "Cancelled");
  • keep both → note why and move on.

Nothing is cancelled without an explicit answer.

Phase 2 — Enhance

For each surviving task, sharpen title and description for autonomous execution:

  • concrete acceptance criteria,
  • the files/areas actually involved — grounded in the repo via Read/Grep/Glob, not guessed,
  • explicit out-of-scope.

Write back with update_task (title / description / commitType are the settable fields; it refuses while Running, which cannot happen this early). Rules: do not change intent, do not invent requirements. A task too vague to sharpen safely gets a question, not a guess.

Phase 3 — Run

run_task_now cannot be used for a batch: OverrideSlotService.StartInSlot (Queue/OverrideSlotService.cs:65-70) holds a single slot and throws "override slot busy" on the second concurrent call. The queue picker is the only parallel path.

So: read get_app_settings, tell the user how many parallel slots are configured (MaxParallelExecutions, default 1 — AppSettingsEntity.cs:14), then update_task_status(id, "Queued") for every surviving task, then poll get_task until each has left Queued/Running — WaitingForReview on success, Failed on error. Announcing the slot count up front stops the user wondering why "run them all" executes one at a time.

A task already Running or WaitingForChildren when the session starts is not re-queued, only polled. A task already WaitingForReview skips straight to Phase 4.

Phase 4 — Review and merge

Sequential, in list order. A task that came back Failed needs human judgement — ask whether to reset_failed_task and re-queue it, or skip it. Otherwise, unchanged from the shipped prompt: get_task_diff (stat first, full diff when non-trivial), sanity-check against the task's intent, ask before merging anything that looks wrong, then review_task(taskId, decision="approve", leaveConflictsInTree=true) and the conflict loop (continue_merge / abort_merge, parent id for unit merges, hand-resolution only where MCP cannot reach, always git commit -- <paths> and never git add -A because the checkout is shared).

New in this phase: the prompt states that because Phase 3 branches all fork from the same base, conflicts are the normal case, not an exception — resolve them rather than bailing out of the run.

Phase 5 — Summary

One line per task: title — dedupe action — enhanced? — final status — merge commit — conflicts resolved. Then anything skipped and why, then follow-ups.

Brief template

MergeHelperInitialDefault names the list and repo once in the header and drops the per-task list:/repo: fields, leaving - [{status}] {title} (id: {id}). Descriptions stay out of the brief; Phase 0 fetches them.

6. Testing

Test Change
MergeHelperSelectionModalViewModelTests drop IsGlobal, list-scoped Configure
InteractiveLaunchSpecServiceTests three null-listId cases → non-null; drop the two-repo global-scope test; add "list without WorkingDir throws"
MissionControlViewModelTests four OpenMergeHelperConPtySessionAsync(null, …) calls
StubWorkerClient, TasksIslandViewModelPlanningTests fake signature
PromptFilesTests assert the five phase markers in the default prompt
Localization.Tests parity after removing three keys
new: ExternalMcpService / worker test UpdateTaskStatus(id, "Cancelled") cancels an Idle task; unknown status still throws

No test spawns the real claude CLI — the prompt content is asserted as text, the session itself is a manual smoke step.

7. Verification left to the user

  • The list context menu shows "Let Claude handle it" only for lists with a working dir, and the Broom button is gone from the footer row.
  • The selection dialog has no LIST column and reads "List: ".
  • A real ConPTY run: dedupe questions appear, enhancements land in the task descriptions, queued tasks execute, merges complete or hand off to conflict resolution.