docs: add planning UX spec/plan and prompts/mailbox proposals

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
mika kuns
2026-04-25 09:37:32 +02:00
parent e192285f5d
commit 615c1da665
4 changed files with 1190 additions and 0 deletions

98
docs/mailbox-proposal.md Normal file
View File

@@ -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:<id>'` (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.

198
docs/prompts-inventory.md Normal file
View File

@@ -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` ~L101L110
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 <session_id>`; 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` ~L413L418
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 <instructions>` 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()` L290L308
```
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)` L310L323
```
# 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

View File

@@ -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
<StackPanel Spacing="4">
<Button Classes="flat"
Command="{Binding ToggleDescriptionExpandedCommand}"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left"
Padding="0">
<StackPanel Orientation="Horizontal" Spacing="6">
<PathIcon Width="10" Height="10"
Data="{StaticResource Icon.ChevronDown}"
IsVisible="{Binding IsDescriptionExpanded}"/>
<PathIcon Width="10" Height="10"
Data="{StaticResource Icon.ChevronRight}"
IsVisible="{Binding !IsDescriptionExpanded}"/>
<TextBlock Classes="eyebrow" Text="DESCRIPTION"/>
</StackPanel>
</Button>
<!-- existing description TextBox goes here unchanged, but add: -->
<TextBox ...existing attributes...
IsVisible="{Binding IsDescriptionExpanded}"/>
</StackPanel>
```
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
<TextBlock Text="▾" IsVisible="{Binding IsDescriptionExpanded}"/>
<TextBlock Text="▸" IsVisible="{Binding !IsDescriptionExpanded}"/>
```
- [ ] **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
<Button Classes="flat"
Command="{Binding ToggleChildrenExpandedCommand}"
IsVisible="{Binding HasPlanningChildren}"
Padding="0" Margin="0,0,6,0"
VerticalAlignment="Center">
<TextBlock FontSize="10"
Text="▾"
IsVisible="{Binding AreChildrenExpanded}"/>
<TextBlock FontSize="10"
Text="▸"
IsVisible="{Binding !AreChildrenExpanded}"/>
</Button>
```
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<ClaudeDoDbContext> InMemoryOptions() =>
new DbContextOptionsBuilder<ClaudeDoDbContext>()
.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<List<TaskEntity>> 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
/// <summary>
/// Call after a child task transitions to a terminal status.
/// Returns the id of the newly-queued sibling (if any), else null.
/// </summary>
public async Task<string?> 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<PlanningSessionManager>` or similar). Add:
```csharp
services.AddScoped<PlanningChainCoordinator>();
```
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<ClaudeDoDbContext>` 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<ClaudeDoDbContext>` 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 `<ContextMenu>`, directly after the "Discard planning session" item:
```xml
<Separator IsVisible="{Binding CanQueueSubtasksSequentially}"/>
<MenuItem Header="Queue subtasks sequentially"
IsVisible="{Binding CanQueueSubtasksSequentially}"
Click="OnQueueSubtasksSequentiallyClick"/>
```
- [ ] **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<IWorkerClient>();
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.

View File

@@ -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`.