diff --git a/docs/mailbox-proposal.md b/docs/mailbox-proposal.md new file mode 100644 index 0000000..8c62c02 --- /dev/null +++ b/docs/mailbox-proposal.md @@ -0,0 +1,98 @@ +# Task Mailbox — Push Messages Into Running Sessions + +**Status:** proposal +**Context:** the user runs parallel Claude sessions (e.g. backend + frontend) and wants to push messages into a session while it's busy inside a subagent. A shared folder works for one-offs; this turns it into a first-class ClaudeDo feature so every future parallel-session project gets it for free. + +## Problem + +Claude CLI processes one turn at a time. While a subagent (or any long tool) runs, no new user input can be injected. The harness offers no mid-execution interrupt. The workable window is *between* tool calls — so we need a cheap "inbox check" the agent can poll at natural checkpoints, plus a UI affordance and a cross-session sender. + +## Design + +### 1. Data + +New table `task_messages`: + +| col | type | notes | +|---|---|---| +| `id` | INTEGER PK | | +| `task_id` | TEXT FK → tasks.id | recipient | +| `sender` | TEXT | `'user'` \| `'task:'` (for cross-session) | +| `body` | TEXT | markdown | +| `created_at` | TEXT | ISO | +| `delivered_at` | TEXT NULL | set when inbox pulls it | + +EF Core migration + repository. Async, CancellationToken, matches existing conventions. + +### 2. Worker MCP tools (extend existing `mcp__claudedo__*` server) + +- **`check_inbox(task_id)`** → returns undelivered messages for this task and marks them delivered. Idempotent. Empty array if nothing pending. +- **`send_to_task(task_id, body)`** → inserts a row. Callable from *any* session — this is how the frontend session tells the backend session something. +- **`inbox_status(task_id)`** → `{ pending: int }` for a cheap "is there anything?" poll. + +All three run in-proc in the Worker, go through the existing repository layer. + +### 3. SignalR additions on `WorkerHub` + +Server methods (UI → Worker): +- `SendTaskMessage(taskId, body)` — UI calls this; worker inserts the row and fires `TaskMessageQueued`. + +Client events (Worker → UI): +- `TaskMessageQueued(taskId, pendingCount)` — so the UI can show an unread badge. +- `TaskMessageDelivered(taskId, pendingCount)` — when the agent pulls it, badge clears. + +### 4. UI + +On every `Running` task row + detail pane: +- "Send to session" textarea + Enter to submit → `SendTaskMessage`. +- Unread badge showing `pendingCount`. +- Read-only message timeline (who sent what, when delivered). + +### 5. Agent-side poll discipline + +Two complementary mechanisms so it's robust whether or not the agent remembers: + +**a) CLAUDE.md instruction** (seeded by worker into each worktree's `CLAUDE.md`): +> After every subagent completes and before starting the next step, call `mcp__claudedo__check_inbox`. Treat returned messages as user input with priority over the current plan. + +**b) PostToolUse hook on `Agent`** (written into the worktree's `.claude/settings.json` by the Worker when it creates the tree): +- Runs `mcp__claudedo__inbox_status` via a tiny CLI shim the worker ships. +- If `pending > 0`, the hook emits a system reminder: "Inbox has N pending messages — call `mcp__claudedo__check_inbox` now." +- Keeps the burden off the agent's memory. Belt + suspenders. + +### 6. Cross-session pattern + +Backend session and frontend session are just two tasks with known IDs. Either can call `send_to_task(other_id, body)` via the MCP server. No shared folder needed — the DB is already the shared channel. + +To make this ergonomic: +- A "linked tasks" concept: tag two tasks as peers at creation time. The Worker exposes `send_to_peer(body)` as sugar around `send_to_task` so neither session needs to hardcode the other's UUID. + +## Limits (honest) + +- Messages arrive *between* tool calls, not mid-tool. A 20-minute subagent still blocks 20 minutes. Splitting work into shorter subagents is still the right discipline. +- If the agent ignores the CLAUDE.md instruction, the hook catches it next tool call — but we can't force immediate consumption. +- `-p` (print) mode with stdin prompt is one-shot and can't be extended. This design targets *interactive* sessions (Planning Sessions already use this mode). For queued `-p` runs, the mailbox is effectively a post-run instruction carrier. + +## Why this is the repeatable "Grundgerüst" + +Once this lands in ClaudeDo, the workflow becomes: +1. Create two linked tasks (`backend`, `frontend`) with `working_dir` set. +2. Start each — each gets its own worktree, its own Planning Session terminal, its own inbox with `check_inbox` + `send_to_peer`. +3. Push messages from the UI or from the other session. No per-project scaffolding, no custom hooks, no shared folder. + +Every future parallel-session project inherits the mailbox. + +## Build order (suggested) + +1. Migration + repo + model. Tests first. +2. MCP tools (`check_inbox`, `send_to_task`, `inbox_status`) + unit tests. +3. SignalR method + events + UI textarea/badge. +4. Worker writes CLAUDE.md addendum + `.claude/settings.json` hook into each new worktree. +5. Linked-tasks sugar (`send_to_peer`). +6. Manual verification: queue a long subagent, send a message, confirm it's picked up at the next tool boundary. + +## Open questions + +- Should messages be deleted or soft-kept after delivery? Leaning soft-kept for the timeline UI. +- Priority / interrupt semantics — do we want a "high priority" flag that the agent should surface immediately vs. batch? +- Should `send_to_peer` also work when the peer is `Queued` (i.e. not yet running)? Probably yes — deliver on start. diff --git a/docs/prompts-inventory.md b/docs/prompts-inventory.md new file mode 100644 index 0000000..a465fe7 --- /dev/null +++ b/docs/prompts-inventory.md @@ -0,0 +1,198 @@ +# ClaudeDo — Prompt & CLI Inventory + +Snapshot of every string ClaudeDo sends to Claude CLI, plus the CLI-flag surface that shapes each run. Intended as a working doc for tomorrow's prompt-tuning pass. + +Date: 2026-04-24 + +--- + +## 1. Task-execution prompts (agent-tagged tasks → Claude CLI) + +Used for every "agent" task that the queue picks up or that `RunNow` dispatches. +Orchestration lives in `src/ClaudeDo.Worker/Runner/TaskRunner.cs` and `ClaudeArgsBuilder.cs`. + +### 1.1 User prompt (stdin) — `TaskRunner.RunAsync` ~L101–L110 + +Plain text, no template around it: + +``` +{task.Title} + +{task.Description?.Trim()} ← only if non-empty + +## Sub-Tasks ← only if subtasks exist +- [ ] {subtask.Title} ← "[x]" if completed +... +``` + +Notes +- Title is included verbatim — no leading `#` heading. +- No role tags, no XML, no delimiters between title and description — just blank lines. +- Sub-Tasks section uses markdown checkboxes. This is the only structural scaffolding. +- No context about the project, working dir, or git state is added here. + +### 1.2 Retry prompt (on failure, when a session ID exists) — `TaskRunner` ~L126 + +``` +The previous attempt failed with: + +{result.ErrorMarkdown} + +Try again and fix the issues. +``` + +Fired once per task via `--resume `; if the retry also fails, the task is marked Failed. + +### 1.3 Follow-up prompt (multi-turn `ContinueAsync`) — `TaskRunner.ContinueAsync` L159 + +The UI/hub supplies `followUpPrompt` as-is; no wrapping. The session is resumed via `--resume`. So the effective "prompt template" is whatever the user types in the Continue textbox. + +### 1.4 System prompt — merged in `TaskRunner` ~L413–L418 + +Built by `TaskRunner.MergeInstructions(global, list, task)` which concatenates three optional strings with `\n\n`: + +1. `AppSettings.DefaultClaudeInstructions` (global, set in Settings modal, default `""`) +2. `list_config.SystemPrompt` (per-list override) +3. `task.SystemPrompt` (per-task override) + +The merged string is passed as `--append-system-prompt ` to the CLI. Empty/whitespace → flag is omitted entirely. + +**Currently the global `DefaultClaudeInstructions` ships as empty string** (see `AppSettingsEntity.cs` L9). Anything in the system prompt today is whatever the user typed into Settings / List-Settings / Task-Settings. + +### 1.5 CLI args — `ClaudeArgsBuilder.Build` (`ClaudeArgsBuilder.cs`) + +Always on: +- `-p` +- `--output-format stream-json` +- `--verbose` +- `--permission-mode {auto|acceptEdits|plan|default}` (legacy `bypassPermissions` → `auto`) + +Conditional: +- `--model {sonnet|opus|haiku|...}` — from `task.Model ?? list.Model ?? AppSettings.DefaultModel` (default `sonnet`) +- `--max-turns {n}` — `AppSettings.DefaultMaxTurns` (default `100`) +- `--append-system-prompt "{merged instructions}"` — see 1.4 +- `--agents '[{"file":"{path}"}]'` — from task or list override, points at an agent `.md` +- `--resume {session_id}` — for retries and `ContinueAsync` + +Unused but pre-declared: +- `ResultSchema` — a `{summary, files_changed, commit_type}` JSON schema is serialized but **never attached** to args in `Build`. Dead code today; relevant if we turn on `--output-schema`. + +--- + +## 2. Planning-agent prompts (`/plan` / Planning session) + +Used by the Planning feature, which spawns a Claude session inside a git worktree with MCP tools so the agent can create Subtasks under the parent. +Source: `src/ClaudeDo.Worker/Planning/PlanningSessionManager.cs`. + +### 2.1 System prompt — `BuildSystemPrompt()` L290–L308 + +``` +You are a planning assistant for ClaudeDo. +Your role is to help break down a task into smaller, actionable subtasks. +Your final goal WILL ALWAYS be the creation of Subtasks + +ALWAYS invoke the `superpowers:brainstorming` skill via the Skill tool at the +start of every planning session, and follow its process end-to-end. It guides +you through clarifying questions, approach exploration, and design approval +BEFORE any subtasks are created. Do not create child tasks until the user has +approved a design. + +NEVER Change files yourself. + +ALWAYS Use the available MCP tools (mcp__claudedo__*) to create child tasks once the +design is approved. When you are done planning, finalize the session. + +Be concise and focused. Each subtask should be independently executable. +``` + +Written to `{session-dir}/system-prompt.md` at session start and fed via `--append-system-prompt`. + +Notes / known oddities +- Trailing space on "NEVER Change files yourself. " and on the blank line above the ALWAYS/MCP block. +- Mixes voice ("Your role is", "ALWAYS invoke") — could be tightened. +- Implicitly relies on the `superpowers:brainstorming` skill being installed in the worktree's Claude config. +- Does not name the MCP tools explicitly (the `mcp__claudedo__*` wildcard assumes the agent discovers them via tool listing). + +### 2.2 Initial prompt — `BuildInitialPrompt(task)` L310–L323 + +``` +# Task: {task.Title} + +{task.Description} ← only if non-empty + +--- + +Please analyze this task and break it down into concrete subtasks. +``` + +Written to `{session-dir}/initial-prompt.txt`; the Windows Terminal launcher pipes it to the Claude CLI on start. + +### 2.3 Planning session CLI flags + +`PlanningSessionManager` itself does not build CLI args — the `WindowsTerminalPlanningLauncher` does. Relevant facts: +- Permission mode: **plan** (per recent commit `8e9f09a` "run planning agent in plan permission mode and enforce brainstorming skill"). +- Runs with an `.mcp.json` that points at our local MCP server (`http://127.0.0.1:{port}/mcp`) with a per-session bearer token. +- `.claude/settings.local.json` sets `"enableAllProjectMcpServers": true` so the MCP tools auto-activate. + +--- + +## 3. Commit-message template (not a prompt, but agent-visible) + +Built by `CommitMessageBuilder.Build` (`CommitMessageBuilder.cs`). Format: + +``` +{commitType}({listSlug}): {title ≤60 chars} + +{description ≤400 chars} ← only if set + +ClaudeDo-Task: {taskId} +``` + +- `commitType` comes from `task.CommitType` (default `chore`, list default configurable). +- Slug = lowercased list name with non-alphanumerics stripped, runs collapsed to `-`. +- The agent sees the resulting commit in `git log` during retries and follow-ups, so phrasing here bleeds into model behavior on multi-turn work. + +--- + +## 4. Where each prompt is edited (UI surface) + +| Prompt slot | Edited in | Stored as | +|-------------------------------------|--------------------------------------------|--------------------------------------------| +| Global `DefaultClaudeInstructions` | Settings modal (`SettingsModalViewModel`) | `app_settings.DefaultClaudeInstructions` | +| Per-list system prompt | List-Settings modal | `list_config.SystemPrompt` | +| Per-task system prompt | Details island / task agent settings | `tasks.system_prompt` | +| Per-task agent file | Details island | `tasks.agent_path` (absolute `.md` path) | +| Default model / max turns / perms | Settings modal | `app_settings.*` | +| Planning system prompt | **Hard-coded** in `PlanningSessionManager` | not editable from UI | +| Planning initial prompt template | **Hard-coded** in `PlanningSessionManager` | not editable from UI | +| Retry prompt | **Hard-coded** in `TaskRunner` | not editable | +| Task prompt structure (title/desc) | **Hard-coded** in `TaskRunner` | not editable | + +--- + +## 5. Things worth reviewing tomorrow + +1. **Task-execution prompt has no frame at all.** Just title + description. Consider whether a thin wrapper (goal / constraints / done-criteria) improves agent focus without bloating small tasks. +2. **Global DefaultClaudeInstructions is empty out of the box.** This is the cleanest place to put project-wide guardrails (commit format, branch etiquette, verify-before-done, no force push). Right now nothing is there. +3. **Planning system prompt**: + - Typo-level: trailing spaces, inconsistent capitalization ("ALWAYS"/"NEVER"/"Always"). + - "Your final goal WILL ALWAYS be the creation of Subtasks" conflicts slightly with "Do not create child tasks until the user has approved a design" — rewordable. + - Does not state how many subtasks is reasonable, nor how granular. + - Does not describe the MCP tool surface; the agent has to discover `mcp__claudedo__*` tools. +4. **Retry prompt is minimal.** `"Try again and fix the issues."` — could be firmer about not repeating the same failure mode. +5. **Sub-Tasks block** is dumped as plain checkboxes with no instruction ("please complete all open items", "do them in order", etc.). If the user relies on subtasks for ordering, that intent isn't conveyed. +6. **ResultSchema is defined but unused.** Decide: drop it, or wire it up (`--output-schema`) and start asking for structured summaries. +7. **Commit-message template** never tells the agent what `commit_type` to pick when it has flexibility — the value is hard-coded per task. Consider exposing as a prompt hint or inferring from diffs. + +--- + +## 6. File pointers + +- `src/ClaudeDo.Worker/Runner/TaskRunner.cs` — user/retry/follow-up prompts, MergeInstructions +- `src/ClaudeDo.Worker/Runner/ClaudeArgsBuilder.cs` — CLI args + ResultSchema +- `src/ClaudeDo.Worker/Runner/CommitMessageBuilder.cs` — commit template +- `src/ClaudeDo.Worker/Planning/PlanningSessionManager.cs` — planning system + initial prompts +- `src/ClaudeDo.Worker/Planning/WindowsTerminalPlanningLauncher.cs` — planning CLI invocation +- `src/ClaudeDo.Data/Models/AppSettingsEntity.cs` — global defaults +- `src/ClaudeDo.Ui/ViewModels/Modals/SettingsModalViewModel.cs` — UI for global defaults +- `src/ClaudeDo.Ui/ViewModels/Modals/ListSettingsModalViewModel.cs` — UI for per-list overrides diff --git a/docs/superpowers/plans/2026-04-24-planning-ux-and-sequential-subtasks.md b/docs/superpowers/plans/2026-04-24-planning-ux-and-sequential-subtasks.md new file mode 100644 index 0000000..d7c3e7f --- /dev/null +++ b/docs/superpowers/plans/2026-04-24-planning-ux-and-sequential-subtasks.md @@ -0,0 +1,799 @@ +# Planning UX Polish + Sequential Subtask Queue — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add sequential execution of planning subtasks (new `Waiting` status, context-menu trigger, worker-side chain advancement) plus three small UX changes (auto-collapse done planning parents in the task list, collapsible Description in the Details pane, narrower island GridSplitters). + +**Architecture:** Foundation first — add the new `Waiting` enum value and its surface in the UI (chip, virtual-queued filter, row plumbing). Then ship the three UI polish items independently. Finally build the worker-side chain coordinator behind TDD and wire up the SignalR method + context-menu entry. + +**Tech Stack:** .NET 8, Avalonia 12, CommunityToolkit.Mvvm, EF Core (Sqlite), SignalR, xUnit. + +**Spec:** `docs/superpowers/specs/2026-04-24-planning-ux-and-sequential-subtasks-design.md` + +--- + +## Task 1: Add `Waiting` status to the enum + +**Files:** +- Modify: `src/ClaudeDo.Data/Models/TaskEntity.cs` (TaskStatus enum) +- Modify: `src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs` (chip class switch) +- Modify: `src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs` (virtual-queued match predicate) + +- [ ] **Step 1: Add `Waiting` to the enum** + +Append `Waiting` as the last value (keeps existing numeric slots stable for any int-serialized rows). + +`src/ClaudeDo.Data/Models/TaskEntity.cs`: + +```csharp +public enum TaskStatus +{ + Manual, + Queued, + Running, + Done, + Failed, + Planning, + Planned, + Draft, + Waiting, +} +``` + +- [ ] **Step 2: Extend `StatusChipClass` switch in TaskRowViewModel** + +`src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs` — update the switch: + +```csharp +public string StatusChipClass => Status switch +{ + TaskStatus.Running => "running", + TaskStatus.Failed => "error", + TaskStatus.Done => "review", + TaskStatus.Queued => "queued", + TaskStatus.Waiting => "waiting", + _ => "idle", +}; +``` + +- [ ] **Step 3: Add `IsWaiting` and include it in virtual-queued matching** + +In the same `TaskRowViewModel.cs`, add alongside `IsQueued`: + +```csharp +public bool IsWaiting => Status == TaskStatus.Waiting; +``` + +In `src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs`, find the `TaskMatchesList` static method and update the `virtual:queued` branch so tasks in `Waiting` also match. Locate the existing match for `ListKind.Virtual when list.Id == "virtual:queued"` and change it to match `t.Status == TaskStatus.Queued || t.Status == TaskStatus.Waiting`. If the existing line reads `t.Status == TaskStatus.Queued` exactly, replace it with `t.Status == TaskStatus.Queued || t.Status == TaskStatus.Waiting`. + +- [ ] **Step 4: Build** + +Run: + +```bash +dotnet build src/ClaudeDo.Data/ClaudeDo.Data.csproj +dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj +``` + +Expected: both build with 0 errors. Existing warnings OK. + +- [ ] **Step 5: Commit** + +```bash +git add src/ClaudeDo.Data/Models/TaskEntity.cs \ + src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs \ + src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs +git commit -m "feat(data): add Waiting task status and include it in virtual:queued" +``` + +--- + +## Task 2: Narrower island GridSplitters + +**Files:** +- Modify: `src/ClaudeDo.Ui/Views/MainWindow.axaml` (lines 158 and 170) + +- [ ] **Step 1: Halve the splitter width** + +Both `GridSplitter` elements currently use `Width="5"`. Change both to `Width="3"`. Leave all other attributes untouched. + +- [ ] **Step 2: Build** + +```bash +dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj +``` + +Expected: 0 errors. + +- [ ] **Step 3: Commit** + +```bash +git add src/ClaudeDo.Ui/Views/MainWindow.axaml +git commit -m "style(ui): narrow island GridSplitters from 5 to 3" +``` + +--- + +## Task 3: Collapsible Description section in Details pane + +**Files:** +- Modify: `src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs` +- Modify: `src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml` + +- [ ] **Step 1: Add observable flag + toggle command** + +In `DetailsIslandViewModel.cs`, add beside the existing editable fields: + +```csharp +[ObservableProperty] private bool _isDescriptionExpanded = true; + +[RelayCommand] +private void ToggleDescriptionExpanded() => IsDescriptionExpanded = !IsDescriptionExpanded; +``` + +- [ ] **Step 2: Reset flag when a new task is loaded** + +Find the method that handles a new `Task` being bound (the existing `OnTaskChanged` / `Bind` path — it's the spot that already sets `EditableTitle`, `EditableDescription`, etc.). At the start of the load path where fields get reset, add: + +```csharp +IsDescriptionExpanded = true; +``` + +(If the reset is scattered, put it next to the `EditableDescription = ""` assignment.) + +- [ ] **Step 3: Wrap the description TextBox in a collapsible section** + +In `DetailsIslandView.axaml`, locate the description TextBox. Wrap it so it looks like: + +```xml + + + + + + +``` + +If the existing `Icon.ChevronDown` / `Icon.ChevronRight` static resources don't exist, inspect `App.axaml` (or wherever `StaticResource Icon.*` icons live) and pick the closest existing chevron pair. If only one direction exists, use a simple `▾` / `▸` TextBlock substitute: + +```xml + + +``` + +- [ ] **Step 4: Build** + +```bash +dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj +``` + +Expected: 0 errors. + +- [ ] **Step 5: Manual verify** + +Launch the app (`dotnet run --project src/ClaudeDo.App`), open a task with a description, click the chevron. Verify the body collapses/expands; verify opening a different task restores the expanded default. + +- [ ] **Step 6: Commit** + +```bash +git add src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs \ + src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml +git commit -m "feat(ui): collapsible description section in details pane" +``` + +--- + +## Task 4: Auto-collapse done planning parents in task list + +**Files:** +- Modify: `src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs` +- Modify: `src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs` +- Modify: `src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml` + +- [ ] **Step 1: Add expansion state + "all children done" flag to `TaskRowViewModel`** + +In `TaskRowViewModel.cs`, add below the existing observable properties: + +```csharp +[ObservableProperty] private bool _areChildrenExpanded = true; +[ObservableProperty] private bool _allChildrenDone; + +partial void OnAllChildrenDoneChanged(bool value) +{ + // Default children to collapsed once the planning parent is fully done. + if (value) AreChildrenExpanded = false; +} + +[RelayCommand] +private void ToggleChildrenExpanded() => AreChildrenExpanded = !AreChildrenExpanded; +``` + +- [ ] **Step 2: Compute `AllChildrenDone` during Regroup in `TasksIslandViewModel`** + +In `TasksIslandViewModel.cs`, locate the `Regroup()` method (the one that clears and repopulates `OverdueItems`/`OpenItems`/`CompletedItems`). Before it distributes rows, build a lookup of children by parent id: + +```csharp +var childrenByParent = Items + .Where(r => r.IsChild && r.ParentTaskId is not null) + .GroupBy(r => r.ParentTaskId!) + .ToDictionary(g => g.Key, g => g.ToList()); + +foreach (var parent in Items.Where(r => r.IsPlanningParent && !r.IsChild)) +{ + if (childrenByParent.TryGetValue(parent.Id, out var kids) && kids.Count > 0) + parent.AllChildrenDone = kids.All(c => c.Status == TaskStatus.Done); + else + parent.AllChildrenDone = false; +} +``` + +Then inside the existing distribution loop, skip child rows whose parent row has `AreChildrenExpanded == false`: + +```csharp +foreach (var row in Items) +{ + if (row.IsChild && row.ParentTaskId is not null) + { + var parentRow = Items.FirstOrDefault(p => p.Id == row.ParentTaskId); + if (parentRow is not null && !parentRow.AreChildrenExpanded) continue; + } + // ... existing distribution into Overdue/Open/Completed ... +} +``` + +If `Regroup()` currently uses LINQ expressions instead of a loop, split them out into explicit foreach so the skip is clear. Keep the overdue/completed logic intact — children of a collapsed parent are excluded from every bucket. + +- [ ] **Step 3: Re-run Regroup when a row's expansion flag toggles** + +In `TasksIslandViewModel.cs`, in the constructor (after `Items` is created), subscribe to changes so toggling one row triggers a regroup: + +```csharp +Items.CollectionChanged += (_, e) => +{ + if (e.NewItems is not null) + foreach (TaskRowViewModel r in e.NewItems) + r.PropertyChanged += OnItemPropertyChanged; + if (e.OldItems is not null) + foreach (TaskRowViewModel r in e.OldItems) + r.PropertyChanged -= OnItemPropertyChanged; +}; +``` + +Add the handler: + +```csharp +private void OnItemPropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) +{ + if (e.PropertyName == nameof(TaskRowViewModel.AreChildrenExpanded)) + Regroup(); +} +``` + +- [ ] **Step 4: Add chevron toggle button to the planning-parent row** + +In `src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml`, inside the main task card where the title/eyebrow row lives (co-located with `PlanningBadge`), add a chevron button visible only when `IsPlanningParent && HasPlanningChildren`: + +```xml + +``` + +Place it immediately before the title TextBlock in the parent-row layout. Leave child rows untouched. + +- [ ] **Step 5: Build** + +```bash +dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj +``` + +Expected: 0 errors. + +- [ ] **Step 6: Manual verify** + +Create a planning parent with ≥2 children. Mark both children `Done` (manually via DB if needed, or via a full planning run). Reload the list — the children should be hidden by default. Click the chevron on the parent — children appear. Click again — collapse. + +- [ ] **Step 7: Commit** + +```bash +git add src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs \ + src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs \ + src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml +git commit -m "feat(ui): auto-collapse done planning parents in task list" +``` + +--- + +## Task 5: PlanningChainCoordinator — worker-side chain advancement (TDD) + +**Files:** +- Create: `src/ClaudeDo.Worker/Planning/PlanningChainCoordinator.cs` +- Create: `tests/ClaudeDo.Worker.Tests/Planning/PlanningChainCoordinatorTests.cs` + +- [ ] **Step 1: Write the first failing test — queueing sets first child Queued, rest Waiting** + +Create `tests/ClaudeDo.Worker.Tests/Planning/PlanningChainCoordinatorTests.cs`: + +```csharp +using System.Threading.Tasks; +using ClaudeDo.Data; +using ClaudeDo.Data.Models; +using ClaudeDo.Data.Repositories; +using ClaudeDo.Worker.Planning; +using Microsoft.EntityFrameworkCore; +using Xunit; +using TaskStatus = ClaudeDo.Data.Models.TaskStatus; + +namespace ClaudeDo.Worker.Tests.Planning; + +public class PlanningChainCoordinatorTests +{ + private static DbContextOptions InMemoryOptions() => + new DbContextOptionsBuilder() + .UseSqlite("DataSource=:memory:;Cache=Shared") + .Options; + + private static async Task<(ClaudeDoDbContext ctx, TaskRepository repo)> NewDbAsync() + { + var ctx = new ClaudeDoDbContext(InMemoryOptions()); + await ctx.Database.OpenConnectionAsync(); + await ctx.Database.EnsureCreatedAsync(); + return (ctx, new TaskRepository(ctx)); + } + + private static async Task SeedPlanningFamily(TaskRepository repo, string parentId, int childCount) + { + await repo.AddAsync(new TaskEntity + { + Id = parentId, ListId = "L1", Title = "Parent", + CreatedAt = System.DateTime.UtcNow, Status = TaskStatus.Planned, + }); + for (int i = 0; i < childCount; i++) + { + await repo.AddAsync(new TaskEntity + { + Id = $"{parentId}-c{i}", ListId = "L1", Title = $"Child {i}", + CreatedAt = System.DateTime.UtcNow, Status = TaskStatus.Manual, + ParentTaskId = parentId, SortOrder = i, + }); + } + } + + [Fact] + public async Task QueueSubtasksSequentially_SetsFirstQueued_RestWaiting() + { + var (ctx, repo) = await NewDbAsync(); + await using var _ = ctx; + await SeedPlanningFamily(repo, "P", 3); + + var coord = new PlanningChainCoordinator(repo); + await coord.QueueSubtasksSequentiallyAsync("P", default); + + var kids = await ctx.Tasks.Where(t => t.ParentTaskId == "P").OrderBy(t => t.SortOrder).ToListAsync(); + Assert.Equal(TaskStatus.Queued, kids[0].Status); + Assert.Equal(TaskStatus.Waiting, kids[1].Status); + Assert.Equal(TaskStatus.Waiting, kids[2].Status); + } +} +``` + +- [ ] **Step 2: Run the test — expect failure (class doesn't exist)** + +```bash +dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj --filter FullyQualifiedName~PlanningChainCoordinatorTests +``` + +Expected: compile error "PlanningChainCoordinator not found". + +- [ ] **Step 3: Create the coordinator with the minimum to pass** + +`src/ClaudeDo.Worker/Planning/PlanningChainCoordinator.cs`: + +```csharp +using ClaudeDo.Data.Models; +using ClaudeDo.Data.Repositories; + +namespace ClaudeDo.Worker.Planning; + +public sealed class PlanningChainCoordinator +{ + private readonly TaskRepository _tasks; + + public PlanningChainCoordinator(TaskRepository tasks) => _tasks = tasks; + + public async Task QueueSubtasksSequentiallyAsync(string parentTaskId, CancellationToken ct) + { + var parent = await _tasks.GetByIdAsync(parentTaskId, ct) + ?? throw new InvalidOperationException($"Task {parentTaskId} not found."); + + var children = (await _tasks.GetChildrenAsync(parentTaskId, ct)) + .OrderBy(t => t.SortOrder) + .ToList(); + if (children.Count == 0) + throw new InvalidOperationException("Parent has no subtasks."); + + var bad = children.FirstOrDefault(c => c.Status is not (TaskStatus.Manual or TaskStatus.Planned)); + if (bad is not null) + throw new InvalidOperationException($"Child {bad.Id} is in status {bad.Status}; expected Manual or Planned."); + + for (int i = 0; i < children.Count; i++) + { + children[i].Status = i == 0 ? TaskStatus.Queued : TaskStatus.Waiting; + await _tasks.UpdateAsync(children[i], ct); + } + } +} +``` + +If `TaskRepository.GetChildrenAsync` does not yet exist, add it: + +```csharp +// in src/ClaudeDo.Data/Repositories/TaskRepository.cs +public Task> GetChildrenAsync(string parentTaskId, CancellationToken ct = default) => + _ctx.Tasks.Where(t => t.ParentTaskId == parentTaskId).ToListAsync(ct); +``` + +(If the repo uses `AsNoTracking()` elsewhere for reads, match that pattern. For this method we want tracked entities so `UpdateAsync` works without extra attach.) + +- [ ] **Step 4: Run the test — expect pass** + +```bash +dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj --filter FullyQualifiedName~PlanningChainCoordinatorTests +``` + +Expected: 1 passed. + +- [ ] **Step 5: Add failing test — on child Done, next Waiting sibling flips to Queued** + +Append to `PlanningChainCoordinatorTests.cs`: + +```csharp +[Fact] +public async Task OnChildDone_FlipsNextWaitingToQueued() +{ + var (ctx, repo) = await NewDbAsync(); + await using var _ = ctx; + await SeedPlanningFamily(repo, "P", 3); + + var coord = new PlanningChainCoordinator(repo); + await coord.QueueSubtasksSequentiallyAsync("P", default); + + // Simulate first child finishing Done. + var first = await ctx.Tasks.FirstAsync(t => t.Id == "P-c0"); + first.Status = TaskStatus.Done; + await ctx.SaveChangesAsync(); + + var advanced = await coord.OnChildFinishedAsync("P-c0", TaskStatus.Done, default); + + Assert.Equal("P-c1", advanced); + var kids = await ctx.Tasks.Where(t => t.ParentTaskId == "P").OrderBy(t => t.SortOrder).ToListAsync(); + Assert.Equal(TaskStatus.Done, kids[0].Status); + Assert.Equal(TaskStatus.Queued, kids[1].Status); + Assert.Equal(TaskStatus.Waiting, kids[2].Status); +} +``` + +- [ ] **Step 6: Run — expect failure** + +Expected: compile error "OnChildFinishedAsync does not exist". + +- [ ] **Step 7: Implement `OnChildFinishedAsync`** + +In `PlanningChainCoordinator.cs`: + +```csharp +/// +/// Call after a child task transitions to a terminal status. +/// Returns the id of the newly-queued sibling (if any), else null. +/// +public async Task OnChildFinishedAsync(string childTaskId, TaskStatus finalStatus, CancellationToken ct) +{ + if (finalStatus != TaskStatus.Done) return null; + + var child = await _tasks.GetByIdAsync(childTaskId, ct); + if (child?.ParentTaskId is null) return null; + + var siblings = (await _tasks.GetChildrenAsync(child.ParentTaskId, ct)) + .OrderBy(t => t.SortOrder) + .ToList(); + + var next = siblings + .Where(s => s.SortOrder > child.SortOrder && s.Status == TaskStatus.Waiting) + .FirstOrDefault(); + if (next is null) return null; + + next.Status = TaskStatus.Queued; + await _tasks.UpdateAsync(next, ct); + return next.Id; +} +``` + +- [ ] **Step 8: Run — expect pass** + +```bash +dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj --filter FullyQualifiedName~PlanningChainCoordinatorTests +``` + +Expected: 2 passed. + +- [ ] **Step 9: Add failing test — on Failed, chain stops** + +```csharp +[Fact] +public async Task OnChildFailed_DoesNotAdvanceChain() +{ + var (ctx, repo) = await NewDbAsync(); + await using var _ = ctx; + await SeedPlanningFamily(repo, "P", 3); + + var coord = new PlanningChainCoordinator(repo); + await coord.QueueSubtasksSequentiallyAsync("P", default); + + var first = await ctx.Tasks.FirstAsync(t => t.Id == "P-c0"); + first.Status = TaskStatus.Failed; + await ctx.SaveChangesAsync(); + + var advanced = await coord.OnChildFinishedAsync("P-c0", TaskStatus.Failed, default); + + Assert.Null(advanced); + var kids = await ctx.Tasks.Where(t => t.ParentTaskId == "P").OrderBy(t => t.SortOrder).ToListAsync(); + Assert.Equal(TaskStatus.Failed, kids[0].Status); + Assert.Equal(TaskStatus.Waiting, kids[1].Status); + Assert.Equal(TaskStatus.Waiting, kids[2].Status); +} +``` + +- [ ] **Step 10: Run — expect pass (existing guard handles it)** + +```bash +dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj --filter FullyQualifiedName~PlanningChainCoordinatorTests +``` + +Expected: 3 passed. + +- [ ] **Step 11: Commit** + +```bash +git add src/ClaudeDo.Worker/Planning/PlanningChainCoordinator.cs \ + src/ClaudeDo.Data/Repositories/TaskRepository.cs \ + tests/ClaudeDo.Worker.Tests/Planning/PlanningChainCoordinatorTests.cs +git commit -m "feat(worker): add PlanningChainCoordinator with sequential subtask advancement" +``` + +--- + +## Task 6: Hook chain advancement into TaskRunner finish path + +**Files:** +- Modify: `src/ClaudeDo.Worker/Runner/TaskRunner.cs` +- Modify: `src/ClaudeDo.Worker/Program.cs` (DI registration) + +- [ ] **Step 1: Register `PlanningChainCoordinator` in DI** + +Locate `src/ClaudeDo.Worker/Program.cs` where other services are registered (look for `services.AddSingleton` or similar). Add: + +```csharp +services.AddScoped(); +``` + +Use `AddScoped` if `TaskRepository` is scoped (check how it's registered — match its lifetime). If `TaskRepository` is constructed ad-hoc inside the worker, add a constructor overload on `PlanningChainCoordinator` that takes `IDbContextFactory` and builds its own `TaskRepository` per call, then register as Singleton. Mirror the pattern used by `PlanningSessionManager`. + +- [ ] **Step 2: Inject coordinator into `TaskRunner`** + +In `src/ClaudeDo.Worker/Runner/TaskRunner.cs`, add `PlanningChainCoordinator` to the constructor parameter list and store it in a readonly field (match the style used for `_broadcaster`). + +If `TaskRunner` is not a good fit for direct injection (e.g., it's used in contexts without DI), instead inject `IServiceProvider` / `IDbContextFactory` and new-up a coordinator inside the finish handler. Pick whichever matches existing `TaskRunner` patterns. + +- [ ] **Step 3: Call coordinator after Done/Failed emission** + +Immediately after each `await _broadcaster.TaskFinished(slot, task.Id, "done", finishedAt);` on line ~338 and the two failed emissions on lines ~355 and ~372, add: + +```csharp +if (task.ParentTaskId is not null) +{ + var advancedId = await _chainCoordinator.OnChildFinishedAsync( + task.Id, + /* Done or Failed based on path */, + CancellationToken.None); + if (advancedId is not null) + await _broadcaster.TaskUpdated(advancedId); +} +``` + +Use `TaskStatus.Done` in the done-path call site and `TaskStatus.Failed` in the failed-path call sites. For the failed paths that use `justFailed` rather than `task`, read `justFailed?.ParentTaskId` and `justFailed?.Id` to stay consistent with the surrounding code. + +After this call the existing queue-pickup loop will see the newly-Queued sibling and dispatch it on its next tick. + +- [ ] **Step 4: Build** + +```bash +dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj +``` + +Expected: 0 errors. + +- [ ] **Step 5: Run full test suite** + +```bash +dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj +``` + +Expected: all pre-existing tests + 3 new ones pass. + +- [ ] **Step 6: Commit** + +```bash +git add src/ClaudeDo.Worker/Runner/TaskRunner.cs \ + src/ClaudeDo.Worker/Program.cs +git commit -m "feat(worker): advance planning subtask chain on child finish" +``` + +--- + +## Task 7: Hub method + client + context menu entry + +**Files:** +- Modify: `src/ClaudeDo.Worker/Hub/WorkerHub.cs` +- Modify: `src/ClaudeDo.Ui/Services/IWorkerClient.cs` +- Modify: `src/ClaudeDo.Ui/Services/WorkerClient.cs` +- Modify: `src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs` +- Modify: `src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml` +- Modify: `src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml.cs` + +- [ ] **Step 1: Add hub method** + +In `src/ClaudeDo.Worker/Hub/WorkerHub.cs`, add (match the style of other planning methods): + +```csharp +public async Task QueuePlanningSubtasks(string parentTaskId) +{ + await using var ctx = await _dbFactory.CreateDbContextAsync(); + var repo = new TaskRepository(ctx); + var coord = new PlanningChainCoordinator(repo); + await coord.QueueSubtasksSequentiallyAsync(parentTaskId, CancellationToken.None); + + // Broadcast updates for the parent and all its children so the UI refreshes. + var children = await ctx.Tasks + .Where(t => t.ParentTaskId == parentTaskId) + .Select(t => t.Id) + .ToListAsync(); + await _broadcaster.TaskUpdated(parentTaskId); + foreach (var id in children) + await _broadcaster.TaskUpdated(id); + + // Make sure the queue picks up the now-Queued first child immediately. + _queueSignal.Wake(); +} +``` + +If the existing hub constructs `PlanningSessionManager` via DI directly, inject `PlanningChainCoordinator` the same way and call `_chainCoordinator.QueueSubtasksSequentiallyAsync(...)` instead of newing one up. If the hub exposes a queue-wakeup via a different name than `_queueSignal`, use that (search the file for `WakeQueue` or `.Wake()`). + +- [ ] **Step 2: Add method to `IWorkerClient`** + +In `src/ClaudeDo.Ui/Services/IWorkerClient.cs`, add next to the other planning methods: + +```csharp +Task QueuePlanningSubtasksAsync(string parentTaskId, CancellationToken ct = default); +``` + +- [ ] **Step 3: Implement in `WorkerClient`** + +In `src/ClaudeDo.Ui/Services/WorkerClient.cs`, add (match the pattern of `StartPlanningSessionAsync` etc.): + +```csharp +public Task QueuePlanningSubtasksAsync(string parentTaskId, CancellationToken ct = default) => + _connection.InvokeAsync("QueuePlanningSubtasks", parentTaskId, ct); +``` + +- [ ] **Step 4: Add `CanQueueSubtasksSequentially` + `HasPlanningChildren` observable to `TaskRowViewModel`** + +Confirm `HasPlanningChildren` exists (it's referenced in the spec). If not, add it as `[ObservableProperty] bool _hasPlanningChildren;` and ensure `TasksIslandViewModel.Regroup()` already sets it (there should be a parent-side "has children" pass similar to the `AllChildrenDone` one added in Task 4 — if not, set it there). + +Then add: + +```csharp +public bool CanQueueSubtasksSequentially => + IsPlanningParent && HasPlanningChildren && !IsChild; +``` + +Add `OnPropertyChanged(nameof(CanQueueSubtasksSequentially))` inside `OnStatusChanged` and `OnHasPlanningChildrenChanged` so the flag refreshes when status or children change. + +- [ ] **Step 5: Add context-menu entry** + +In `src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml`, inside the existing ``, directly after the "Discard planning session" item: + +```xml + + +``` + +- [ ] **Step 6: Add click handler in code-behind** + +In `src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml.cs`, add (match the other `On*Click` handlers — they pull the `TaskRowViewModel` from `DataContext` and call the shell / worker): + +```csharp +private async void OnQueueSubtasksSequentiallyClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e) +{ + if (DataContext is not TaskRowViewModel row) return; + var worker = App.Services.GetRequiredService(); + try + { + await worker.QueuePlanningSubtasksAsync(row.Id); + } + catch (Exception ex) + { + // Match the toast/log pattern used by OnSendToQueueClick et al. + System.Diagnostics.Debug.WriteLine($"QueuePlanningSubtasks failed: {ex}"); + } +} +``` + +Use the same `App.Services` / `IWorkerClient` lookup pattern as `OnSendToQueueClick` — do not introduce a new DI pattern. If the existing handlers use a shell/mediator indirection, use that instead. + +- [ ] **Step 7: Build** + +```bash +dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj +dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj +``` + +Expected: 0 errors. + +- [ ] **Step 8: Manual verify end-to-end** + +1. Launch app: `dotnet run --project src/ClaudeDo.App`. +2. Open a planning task with ≥2 subtasks (all in `Manual`/`Planned`). +3. Right-click parent → **Queue subtasks sequentially**. +4. Confirm in the task list: first child shows `Queued` chip, others show `Waiting` chip. +5. Let the first run to completion (or, for a quick smoke test, edit the DB to mark it `Done` and emit `TaskUpdated` via a restart). +6. Confirm the next child's status flips `Waiting → Queued` without user interaction. +7. Force-fail a child (cancel it mid-run) — confirm remaining `Waiting` children stay `Waiting`. + +- [ ] **Step 9: Commit** + +```bash +git add src/ClaudeDo.Worker/Hub/WorkerHub.cs \ + src/ClaudeDo.Ui/Services/IWorkerClient.cs \ + src/ClaudeDo.Ui/Services/WorkerClient.cs \ + src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs \ + src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml \ + src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml.cs +git commit -m "feat(ui+worker): context menu to queue planning subtasks sequentially" +``` + +--- + +## Self-review checklist (for the plan author before handing off) + +- All four spec items mapped: auto-collapse (Task 4), collapsible description (Task 3), narrower splitters (Task 2), sequential subtask queue (Tasks 1, 5, 6, 7). +- `Waiting` enum touches: enum, chip class, virtual:queued filter — covered in Task 1. +- TDD applied where it pays off (the coordinator); UI tasks rely on manual verification (correct for this codebase). +- No placeholders. Every code step shows the code to paste. +- Type names consistent: `PlanningChainCoordinator`, `QueueSubtasksSequentiallyAsync`, `OnChildFinishedAsync`, `QueuePlanningSubtasksAsync`, `AreChildrenExpanded`, `AllChildrenDone`, `IsDescriptionExpanded` — used the same across tasks. +- Commits are small and conventional. diff --git a/docs/superpowers/specs/2026-04-24-planning-ux-and-sequential-subtasks-design.md b/docs/superpowers/specs/2026-04-24-planning-ux-and-sequential-subtasks-design.md new file mode 100644 index 0000000..9f89386 --- /dev/null +++ b/docs/superpowers/specs/2026-04-24-planning-ux-and-sequential-subtasks-design.md @@ -0,0 +1,95 @@ +# Planning UX Polish + Sequential Subtask Queue + +**Status:** design +**Date:** 2026-04-24 +**Scope:** three small UX changes + one feature — sequential execution of planning subtasks triggered from the context menu. + +## Goals + +1. Collapse the children of a finished planning-parent row in the task list by default. +2. Allow the user to collapse the Description section in the Details pane. +3. Halve the width of the GridSplitters between islands. +4. Let the user queue all subtasks of a planning parent so they run one after another, with a new `Waiting` status for pending siblings. + +## 1. Auto-collapse done planning parents + +**Rule for "done":** a planning parent is "done" when every one of its children has `Status == Done`. + +**Changes:** +- `TaskRowViewModel`: add UI-only `[ObservableProperty] bool _areChildrenExpanded`. Default computed from status — `false` when the row is a done planning parent, else `true`. Not persisted. +- Add `[RelayCommand] void ToggleChildrenExpanded()`. +- `TasksIslandView.axaml` (or `TaskRowView.axaml`): chevron button on the planning-parent row, visible only when `IsPlanningParent && HasPlanningChildren`. Bound to the toggle command. +- `TasksIslandViewModel.Regroup()`: before adding child rows to `OpenItems`/`CompletedItems`, check each child's parent row in `Items`. If the parent's `AreChildrenExpanded == false`, skip the child. +- When a planning parent flips from "not done" → "done" in `OnWorkerTaskUpdated`, call `Regroup()` so the collapse takes effect. + +No DB changes. + +## 2. Collapsible description in Details pane + +**Changes:** +- `DetailsIslandViewModel`: `[ObservableProperty] bool _isDescriptionExpanded = true` + `[RelayCommand] void ToggleDescriptionExpanded()`. +- `DetailsIslandView.axaml`: wrap the existing description `TextBox` in a `StackPanel`; add a thin header row with the label "Description" and a chevron button. Body's `IsVisible` binds to the flag. +- State is per ViewModel instance — reset to `true` whenever a different task is loaded. + +No persistence. + +## 3. Narrower GridSplitters + +`MainWindow.axaml` lines 158 and 170: `Width="5"` → `Width="3"` on both `GridSplitter` elements. + +That's the whole change. + +## 4. Sequential subtask queue + +### Data + +- `ClaudeDo.Data/Models/TaskStatus.cs`: add a new enum value `Waiting` (lowercase serialized form `waiting`, matching existing convention). +- Verify status is stored as string (it should be based on existing patterns). If stored as int, ensure new value gets a stable numeric slot at the end of the enum to avoid breaking existing rows. **No EF migration** beyond what the enum emits automatically. + +### Worker + +- New SignalR hub method: `QueuePlanningSubtasksAsync(string parentTaskId) : Task`. + - Loads all children of the parent, ordered by `SortOrder`. + - Validates: parent must be a planning parent, children must currently all be in `Manual` or `Planned` (reject if any child is already Queued/Running/Done/Failed, surface a friendly error). + - First child → `Queued`. All other children → `Waiting`. Save. + - Emit `TaskUpdated` for each affected task. +- Chain progression — hook into the existing finish/complete path that already fires `TaskFinished`: + - On a child task finishing with status `Done` **and** its parent has waiting siblings: find the next sibling by `(ParentTaskId == parent.Id && Status == Waiting)` ordered by `SortOrder`, flip to `Queued`, emit `TaskUpdated`, and let the existing queue pickup loop pick it up. + - On `Failed`: do nothing. Remaining `Waiting` siblings stay waiting. (A toast for failed tasks will be added in a later spec.) + +This logic lives in a new `PlanningChainCoordinator` service (or similar) in `ClaudeDo.Worker/Planning/`, registered as a singleton and wired into whatever already emits task-finished events. + +### UI + +- `TaskRowView` — add context menu entry **"Queue subtasks sequentially"**: + - `IsVisible` bound to `IsPlanningParent && HasPlanningChildren`. + - `IsEnabled` when all children are in `Manual` / `Planned` state (new property on `TaskRowViewModel`: `CanQueueSubtasksSequentially`). + - Calls `WorkerClient.QueuePlanningSubtasksAsync(Id)`. +- `TaskRowViewModel`: + - Add `IsWaiting => Status == TaskStatus.Waiting` and extend `StatusChipClass` switch to return a new class `"waiting"`. + - Add `CanQueueSubtasksSequentially` (computed; requires access to children). +- `StatusColorConverter` — add a muted color for `Waiting` (proposed: the existing `TextMuteBrush` or a faint cyan). +- Task list — planning parent continues to appear in virtual:queued because it has a `Queued` child (existing logic). **Extend** the virtual:queued match predicate in `TasksIslandViewModel.TaskMatchesList` so a task matches when `Status == Queued || Status == Waiting`. This ensures all sibling subtasks (the queued one + the waiting ones) render under the parent in that list. + +### Client + +- `IWorkerClient` / `WorkerClient`: add `QueuePlanningSubtasksAsync(string parentTaskId)` that calls the hub method. + +## Out of scope + +- Toast notifications on subtask failure (separate follow-up spec). +- Retrying a stopped chain from a failed task (user does it manually via existing actions). +- Persisting the collapse state of planning parents or the Description across sessions. +- Drag-to-reorder of waiting subtasks (execution order = `SortOrder` at the moment the chain starts). + +## Validation plan + +Manual: +- Plan a task with 3 subtasks. Context-menu → Queue subtasks sequentially. Confirm first = Queued, others = Waiting. Watch the first run to Done, confirm the second flips Queued → Running automatically. +- Force-fail subtask 2 (cancel or make it fail). Confirm subtask 3 stays Waiting; no further dispatch. +- Once all three are Done, confirm the planning parent auto-collapses in the list. +- Toggle the Description chevron in the Details pane on an arbitrary task. +- Eyeball the narrower GridSplitter — still resizable, still hittable. + +Automated (minimal — only where cheap): +- Worker-level unit test for `PlanningChainCoordinator`: happy-path chain advance on Done; no advance on Failed; correct ordering by `SortOrder`.