Files
ClaudeDo/docs/superpowers/specs/2026-04-23-planning-sessions-design.md
mika kuns 8891d48af2 docs(spec): add planning sessions design
Interactive "Open planning Session" context menu: launches a scoped
MCP-backed Claude CLI session in Windows Terminal, letting the user
brainstorm and Claude break a rough task into concrete child-tasks
under a flat parent-child hierarchy. Includes schema, MCP tool surface,
terminal launch, UI changes, lifecycle, testing, and a three-plan
phasing (A foundation, then B worker + C UI in parallel).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 17:21:24 +02:00

469 lines
23 KiB
Markdown

# Planning Sessions — Design
**Status:** Approved for implementation
**Date:** 2026-04-23
**Scope:** Feature — "Open planning Session" context menu on tasks that spawns an interactive Windows Terminal with Claude (Sonnet 4.6, medium thinking) and a scoped MCP server, letting the user brainstorm and have Claude break a rough task into concrete executable child-tasks.
---
## 1. Goal
Allow a user to take a vague task (a title plus some TODO-style notes) and convert it — via interactive dialogue with Claude in a terminal — into a structured set of concrete, executable child-tasks that the worker queue can pick up and run.
The interaction is driven by Claude calling MCP tools against a scoped server running inside the existing `ClaudeDo.Worker` process. The parent task becomes a "Planning" container that holds its children as a flat (single-level) hierarchy.
---
## 2. Status Flow
**Parent (new statuses `Planning`, `Planned`):**
```
Manual ──[Open planning Session]──▶ Planning ──[finalize]──▶ Planned
│ │
│ (all children reach terminal state)
│ ▼
│ Done
│ or
│ Failed (if any child Failed)
[Discard] ──▶ Manual
```
**Child (new status `Draft`):**
```
Draft ──[finalize]──▶ Manual | Queued (if "agent" tag) ──▶ Running ──▶ Done | Failed
```
**Rules:**
- Parent with status `Planning` or `Planned` is **never** picked up by the queue.
- Children with status `Draft` are **never** picked up by the queue.
- Hierarchy is strictly **one level deep**: a child task cannot itself become a planning parent (enforced app-side: Plan menu item hidden/disabled if `ParentTaskId IS NOT NULL`).
- One planning session per parent task at a time (`StartPlanningSessionAsync` errors if parent is already `Planning`; use Resume instead).
- Parent auto-status on child completion (evaluated after any child reaches `Done` or `Failed`):
- At least one child `Failed` and no children still in non-terminal states → Parent `Failed`.
- All children `Done` → Parent `Done`.
- Any child still `Manual`/`Queued`/`Running`/`Draft` → Parent stays `Planned`.
- Worktree state (`Merged`/`Discarded`/`Kept`) is orthogonal; only `Task.Status` determines completion.
---
## 3. Data Model
### 3.1 Schema changes to `Tasks` table
| Column | Type | Nullable | Purpose |
|---|---|---|---|
| `ParentTaskId` | `string` (FK → `Tasks.Id`, `DeleteBehavior.Restrict`) | yes | When set, row is a child of a planning parent. NULL = top-level task. |
| `PlanningSessionId` | `string` | yes | Claude CLI session ID captured after first run; used with `--resume`. Only set on planning parents. |
| `PlanningSessionToken` | `string` | yes | Random 32-byte Base64 token generated per session; acts as bearer for MCP calls. NULL when no active session. |
| `PlanningFinalizedAt` | `DateTime` | yes | Timestamp when `finalize` was called. NULL until finalized. |
Index: `(ParentTaskId)` for fast children lookup.
### 3.2 Status enum additions
`ClaudeDo.Data.Models.TaskStatus` gains:
- `Planning` — parent, session active or paused, drafts may exist.
- `Planned` — parent, finalized, children are real tasks (may still be running).
- `Draft` — child, created during session, not yet finalized.
Existing values unchanged: `Manual | Queued | Running | Done | Failed`. Persisted via `ValueConverter` to string (existing convention — confirmed via `TaskEntity.cs`).
### 3.3 Navigation properties
On `TaskEntity`:
```csharp
public string? ParentTaskId { get; set; }
public TaskEntity? Parent { get; set; }
public ICollection<TaskEntity> Children { get; set; } = new List<TaskEntity>();
public string? PlanningSessionId { get; set; }
public string? PlanningSessionToken { get; set; }
public DateTime? PlanningFinalizedAt { get; set; }
```
In `TaskEntityConfiguration`:
```csharp
.HasOne(t => t.Parent)
.WithMany(t => t.Children)
.HasForeignKey(t => t.ParentTaskId)
.OnDelete(DeleteBehavior.Restrict);
```
**Rationale for `Restrict`:** cascade delete would orphan worktrees of in-flight child tasks. UI must handle the `DbUpdateException` and prompt the user to discard children first.
### 3.4 Repository additions
`ITaskRepository` gains:
- `Task<IReadOnlyList<TaskEntity>> GetChildrenAsync(string parentId, CancellationToken ct)`
- `Task<TaskEntity> CreateChildAsync(string parentId, string title, string? description, IReadOnlyList<string>? tagNames, string? commitType, CancellationToken ct)` — creates with `Status = Draft`, `ParentTaskId = parentId`.
- `Task<int> FinalizePlanningAsync(string parentId, bool queueAgentTasks, CancellationToken ct)` — transactional: all Drafts → `Manual` (or `Queued` if tagged "agent" and `queueAgentTasks=true`), parent → `Planned`, set `PlanningFinalizedAt`, clear `PlanningSessionToken`. Returns count of finalized children.
- `Task<bool> DiscardPlanningAsync(string parentId, CancellationToken ct)` — deletes all Drafts, parent → `Manual`, clears `PlanningSessionId/Token/FinalizedAt`.
- `Task<TaskEntity?> SetPlanningStartedAsync(string taskId, string sessionToken, CancellationToken ct)` — sets parent `Status = Planning`, stores token; returns null if parent not in `Manual` state.
- `Task UpdatePlanningSessionIdAsync(string parentId, string sessionId, CancellationToken ct)` — captures Claude CLI session ID after launch.
- `Task<TaskEntity?> FindByPlanningTokenAsync(string token, CancellationToken ct)` — used by MCP auth handler.
`GetNextQueuedAgentTaskAsync` — verify the existing query filters on `Status = Queued`; no additional filter needed since Planning/Planned/Draft are different statuses. Add explicit regression test.
### 3.5 Auto-status hook
After every `MarkDoneAsync`/`MarkFailedAsync` on a task with `ParentTaskId != null`, check parent children. If all in terminal state:
- Any `Failed` → parent `Failed` with `FinishedAt = now()`.
- All `Done` (or worktrees `Discarded`) → parent `Done` with `FinishedAt = now()`.
Implemented as a private helper `TryCompleteParentAsync(string parentId, CancellationToken ct)` called at the end of the two Mark methods.
### 3.6 Migration
`dotnet ef migrations add AddPlanningSupport` — adds four columns and the `(ParentTaskId)` index. No data migration needed (new columns all nullable).
---
## 4. MCP Server Surface
### 4.1 Transport
**HTTP (streamable) inside the existing Worker Kestrel host.** Mount on `/mcp` alongside the existing SignalR hub at `127.0.0.1:47821`. No separate process, no stdio proxy.
Library: `ModelContextProtocol` (official C# MCP SDK).
### 4.2 Authentication
Per-session bearer token:
1. `StartPlanningSessionAsync` generates a 32-byte random token, persists to `Tasks.PlanningSessionToken`.
2. Token is written into the session's `mcp.json` as `Authorization: Bearer <token>`.
3. Every MCP request passes through an auth filter that looks up the token via `FindByPlanningTokenAsync`. If found, the parent task ID is stored in the request context. If not, 401.
4. Token is invalidated (set NULL) on `finalize` or `discard`.
### 4.3 Tools
All tools are scoped to the parent task resolved from the request's token. `parent_id` is never an argument.
| Tool | Params | Returns | Effect |
|---|---|---|---|
| `create_child_task` | `title: string`, `description?: string`, `tags?: string[]`, `commit_type?: string` | `{ task_id, status: "Draft" }` | Creates a Draft child under this session's parent. |
| `list_child_tasks` | — | `[{ task_id, title, description, status, tags }]` | Lists children of this parent (in session context, always Drafts). |
| `update_child_task` | `task_id: string`, optional: `title`, `description`, `tags`, `commit_type` | `{ task }` | Errors if target is not a Draft or not a child of this parent. |
| `delete_child_task` | `task_id: string` | `{ ok: true }` | Errors if target is not a Draft or not a child of this parent. |
| `update_planning_task` | `title?: string`, `description?: string` | `{ task }` | Only title/description on the parent itself. |
| `finalize` | `queue_agent_tasks?: bool = true` | `{ finalized_count: int }` | Calls `FinalizePlanningAsync`. Token invalidated. |
### 4.4 Real-time UI
After each successful tool call, the MCP handler fires a `TaskUpdated` event on the Worker's SignalR hub. The UI subscribes as it already does; drafts appear/update live in the tasks list while the user chats with Claude in the terminal.
### 4.5 Errors
- 401 for missing/invalid token.
- MCP error `-32602` "task not found or not a child of this planning session" for cross-parent access attempts.
- MCP error `-32602` "cannot modify finalized task" for `update/delete` on non-Draft.
- Token validation short-circuits before tool dispatch.
---
## 5. Terminal Launch & Claude CLI Invocation
### 5.1 Launcher service
New interface `IPlanningTerminalLauncher` in the UI or App layer:
```csharp
Task LaunchAsync(PlanningSessionStart info, CancellationToken ct);
Task LaunchResumeAsync(PlanningSessionResume info, CancellationToken ct);
```
`PlanningSessionStart` contains: `WorkingDir`, `McpConfigPath`, `InitialPromptPath`, `SystemPromptPath`.
`PlanningSessionResume` contains: `WorkingDir`, `McpConfigPath`, `ClaudeSessionId`.
### 5.2 Per-session files
Path: `~/.todo-app/planning-sessions/<parentTaskId>/`
- `mcp.json` — MCP config referencing the HTTP endpoint with bearer token.
- `system-prompt.md` — planning-mode system prompt (append, not replace).
- `initial-prompt.txt` — first user-visible message (title + description + short instructions).
Cleanup:
- `Discard` → remove directory.
- `Finalize` → keep directory (for audit; prune on app start if older than N days, optional).
### 5.3 `mcp.json` format
```json
{
"mcpServers": {
"claudedo": {
"type": "http",
"url": "http://127.0.0.1:47821/mcp",
"headers": { "Authorization": "Bearer <PlanningSessionToken>" }
}
}
}
```
### 5.4 Claude CLI invocation (new session)
```
wt.exe -d "<list.WorkingDir>" cmd /k ^
claude ^
--model claude-sonnet-4-6 ^
--append-system-prompt "<contents of system-prompt.md>" ^
--mcp-config "<mcp.json absolute path>" ^
--allowedTools "mcp__claudedo__*,Read,Grep,Glob,WebFetch,WebSearch,Skill" ^
"<contents of initial-prompt.txt>"
```
### 5.5 Claude CLI invocation (resume)
```
wt.exe -d "<list.WorkingDir>" cmd /k ^
claude --resume <PlanningSessionId> --mcp-config "<mcp.json>"
```
Resume inherits model, system prompt, and allowed tools from the original session.
### 5.6 System prompt (draft, refined in Plan B)
> You are in a ClaudeDo planning session for a task. Your job is to brainstorm with the user, then break their rough intent into concrete, independently-executable child-tasks. Each child-task should be something a single automated agent can pick up and complete autonomously. Use the `mcp__claudedo__*` tools to create/update/delete drafts in real time. You may read the repository for context (Read/Grep/Glob) but must not modify any files. When the user is satisfied, call `finalize`. Skills you may find useful: `superpowers:writing-plans`, `superpowers:writing-clearly-and-concisely`.
### 5.7 Initial prompt (template)
```
<Parent task title>
<Parent task description, if any>
---
We're planning this task together. Brainstorm with me, then create concrete child-tasks via the MCP tools. I'll call `finalize` when we're done.
```
### 5.8 Unknowns to resolve during Plan B implementation
These are left **open** in this spec; they'll be pinned down during implementation via `mcp__plugin_context7_context7__query-docs` for the Claude Code CLI:
1. Exact flag for thinking budget (`--thinking-budget medium`? model suffix `claude-sonnet-4-6-thinking`? something else?).
2. Exact casing of tool names in `--allowedTools` (`Read`/`read`, `WebFetch`/`web_fetch`, `Skill`).
3. Whether `--append-system-prompt` accepts a file reference (`@path`) or requires inline string.
4. Whether Claude CLI supports a `--session-id` flag for pre-assigning the session ID, or whether we must read it back from `~/.claude/projects/<hash>/sessions/` after the process starts.
If (4) resolves to "read back", strategy:
- Poll `~/.claude/projects/<hash>/sessions/` directory modtimes shortly after launch; newest session file after launch timestamp is ours.
- Cache the result on the parent task via `UpdatePlanningSessionIdAsync`.
- If session ID can't be captured, Resume falls back to `claude --continue` (last session in that project).
### 5.9 Pre-flight checks
On `LaunchAsync`:
- `wt.exe` resolvable in PATH → else throw `PlanningLaunchException("Windows Terminal not found")`, UI shows install hint.
- `claude` resolvable in PATH → else `PlanningLaunchException("Claude CLI not installed")`.
- `list.WorkingDir` exists → else `PlanningLaunchException("Working directory not found: <path>")`.
---
## 6. UI Changes
### 6.1 Context menu (`TaskRowView.axaml`)
New entries, conditional on status:
- `Manual` + `ParentTaskId IS NULL`**"Open planning Session"**
- `Planning`**"Resume planning Session"** and **"Discard planning session"**
- `Planned` / `Done` / `Failed` (parent) → no planning-related entries (7c: no re-planning).
- Children (`ParentTaskId IS NOT NULL`) → never show planning entries.
### 6.2 Hierarchy rendering (`TasksIslandView.axaml`)
Approach: **flat stream with indentation**, not a `TreeView`.
- `TasksIslandViewModel` builds `OpenItems`/`CompletedItems`/etc. as flat `ObservableCollection<TaskRowViewModel>` with parents followed by their children if expanded.
- `TaskRowViewModel` gets `IsChild: bool` and `IsPlanningParent: bool` and `IsExpanded: bool`.
- `TaskRowView` indents 24px when `IsChild`, shows a thin left border in `TextFaintBrush`.
- Parents with `IsPlanningParent` render a chevron (▸/▾) that toggles `IsExpanded`; collapsed parents hide their children from the flat stream.
- Expanded-state map kept in the VM (`Dictionary<string, bool>`, default `true`).
### 6.3 Draft and planning styling (`TaskRowView`)
- `Status = Draft` → row italic, 70% opacity, small left-aligned badge "DRAFT".
- Parent `Status = Planning` → badge "PLANNING" (accent: warning-amber).
- Parent `Status = Planned` → badge "PLANNED" (accent: neutral-blue).
### 6.4 Unfinished-session dialog
Trigger: on app start **and** on any context-menu click against a `Planning` parent.
Modal (built with existing `TaskCompletionSource<T>` dialog pattern):
```
Unfinished planning session
"<Parent title>"
<N> draft tasks waiting to be finalized.
[Resume] [Finalize now] [Discard]
```
- Resume → `ResumePlanningSessionAsync`, opens terminal with `--resume`.
- Finalize now → `FinalizePlanningSessionAsync` (server-side, no terminal). Useful when the user is confident drafts are good.
- Discard → `DiscardPlanningSessionAsync`.
### 6.5 TasksIslandViewModel commands
- `[RelayCommand] OpenPlanningSessionAsync(TaskRowViewModel? row)`
- `[RelayCommand] ResumePlanningSessionAsync(TaskRowViewModel? row)`
- `[RelayCommand] DiscardPlanningSessionAsync(TaskRowViewModel? row)`
- `[RelayCommand] FinalizePlanningSessionAsync(TaskRowViewModel? row)`
- `[RelayCommand] ToggleExpand(TaskRowViewModel parentRow)`
### 6.6 WorkerClient additions (`ClaudeDo.Ui/Services/WorkerClient.cs`)
- `Task<PlanningSessionLaunchInfo> StartPlanningSessionAsync(string taskId, CancellationToken ct)` — returns `{ WorkingDir, McpConfigPath, InitialPromptPath, SystemPromptPath }`.
- `Task<PlanningSessionResumeInfo> ResumePlanningSessionAsync(string taskId, CancellationToken ct)` — returns `{ WorkingDir, McpConfigPath, ClaudeSessionId }`.
- `Task<int> FinalizePlanningSessionAsync(string taskId, CancellationToken ct)` — returns finalized count.
- `Task DiscardPlanningSessionAsync(string taskId, CancellationToken ct)`.
- `Task<int> GetPendingDraftCountAsync(string taskId, CancellationToken ct)` — for the unfinished-session dialog.
Existing `TaskUpdated` event covers live draft updates; no new event needed.
### 6.7 Delete handling
When the user tries to delete a parent with children:
- Repository throws `DbUpdateException` (FK Restrict).
- UI catches, shows: "This task has N child tasks. Discard drafts and delete? / Delete all including children? / Cancel."
- "Delete all including children" → UI iterates children and deletes them first, then the parent.
- "Discard drafts" option only appears if parent status is `Planning` (drafts exist to discard).
---
## 7. Lifecycle & Error Handling
### 7.1 Worker queue isolation
`GetNextQueuedAgentTaskAsync` filters on `Status = Queued` — Planning/Planned/Draft are excluded by status. Add explicit regression test to lock this in.
### 7.2 Parent auto-completion (repeat of 2, for implementation reference)
After `MarkDoneAsync`/`MarkFailedAsync`:
```csharp
if (task.ParentTaskId is not null)
await TryCompleteParentAsync(task.ParentTaskId, ct);
```
where `TryCompleteParentAsync` loads children, checks terminal status, sets parent accordingly.
### 7.3 Session-start errors
Table in §5.9 Pre-flight checks. UI receives typed exceptions, shows appropriate dialog.
### 7.4 Session-runtime errors
- Terminal crashes → drafts + token persist. Resume via dialog (§6.4).
- Worker restart → drafts + token persist. Resume rebuilds HTTP connection.
- MCP call fails transiently → Claude CLI retries or the model reports the error to the user in terminal; drafts remain in whatever state the last successful call left them.
- No session timeout — brainstorming may be long.
### 7.5 Concurrency
- Different parents → independent sessions, one token per parent.
- Same parent launched twice → `StartPlanningSessionAsync` throws; UI says "Already planning; use Resume".
- Cleanup on app exit: nothing — planning state is fully persisted in DB and files.
---
## 8. Testing
### 8.1 Automated (in `ClaudeDo.Worker.Tests`)
**Schema & repository:**
- Migration applies cleanly on fresh DB.
- `GetChildrenAsync` returns only direct children, sorted.
- `CreateChildAsync` sets Status=Draft, ParentTaskId correctly.
- `FinalizePlanningAsync` transactionally transitions drafts to Manual/Queued, sets parent to Planned, sets timestamp, clears token. On simulated DB error, rolls back fully.
- `DiscardPlanningAsync` removes drafts, resets parent.
- `GetNextQueuedAgentTaskAsync` ignores Drafts, Planning parents, Planned parents.
- `Restrict` cascade: delete parent with children throws `DbUpdateException`.
**Auto-status hook (§7.2):**
- All children Done → parent Done.
- Mix: some Done, at least one Failed, rest in terminal state → parent Failed.
- Mix with one still Running → parent stays Planned.
- Parent stays Planned while any Draft exists (defensive — finalize should have cleared them).
**MCP handlers (against SQLite + in-process HTTP):**
- Valid token → tool executes.
- Missing/invalid token → 401.
- `create_child_task` → creates Draft, emits TaskUpdated event.
- `update_child_task` on non-Draft → MCP error.
- `delete_child_task` on non-Draft → MCP error.
- `finalize` called twice: first succeeds, second errors because token is invalidated.
- Cross-parent access: tool with `task_id` belonging to another parent's session → MCP error.
**SignalR endpoints (integration with Worker host):**
- Start → token generated, session directory + files created, `mcp.json` contains token.
- Start on already-`Planning` parent → error.
- Resume → no new token, reads `PlanningSessionId` from DB.
- Discard → drafts gone, directory removed, token NULL, parent back to Manual.
### 8.2 Manual (added to `docs/open.md` checklist)
- Windows Terminal spawn with real `wt.exe`.
- Real Claude CLI end-to-end session (requires `ANTHROPIC_API_KEY`).
- Avalonia hierarchy rendering (chevron, indentation, draft styling, badges).
- Session-ID capture from `~/.claude/projects/...` (timing-sensitive, platform-specific).
---
## 9. Phasing
Work is delivered in **three sequential-then-parallel** plans. Plan A must merge before B and C can merge.
### 9.1 Plan A — Foundation
Schema migration, enum additions, repository methods, auto-status hook, delete-Restrict, regression test for queue filter. No UI-visible changes (other than delete-with-children now failing with a generic error until Plan C handles it).
Scope files (approximate):
- `src/ClaudeDo.Data/Models/TaskEntity.cs`
- `src/ClaudeDo.Data/Configuration/TaskEntityConfiguration.cs`
- `src/ClaudeDo.Data/Migrations/<new>_AddPlanningSupport.cs`
- `src/ClaudeDo.Data/Repositories/TaskRepository.cs` (+ `ITaskRepository`)
- `src/ClaudeDo.Worker/...` auto-status hook call-site updates.
- `tests/ClaudeDo.Worker.Tests/...` new test classes.
### 9.2 Plan B — Worker MCP + SignalR + Launcher (starts after A merges)
MCP service with HTTP transport, token auth, six tools. New SignalR hub endpoints for Start/Resume/Discard/Finalize/GetPendingDraftCount. Session directory management. `IPlanningTerminalLauncher` implementation for `wt.exe`. Resolves the four unknowns from §5.8.
Scope files (approximate):
- `src/ClaudeDo.Worker/Planning/PlanningMcpService.cs` (new)
- `src/ClaudeDo.Worker/Planning/PlanningSessionManager.cs` (new)
- `src/ClaudeDo.Worker/Hub/WorkerHub.cs` (extend)
- `src/ClaudeDo.Worker/Program.cs` (DI + endpoint mapping)
- `src/ClaudeDo.App/` or `src/ClaudeDo.Ui/Services/``IPlanningTerminalLauncher` + `WindowsTerminalPlanningLauncher`.
- `tests/ClaudeDo.Worker.Tests/Planning/...`
### 9.3 Plan C — UI (parallel to B after A merges)
Context menu entries, hierarchy rendering, draft styling, unfinished-session dialog, WorkerClient extensions, delete-with-children handling. During parallel development, mocks the WorkerClient against Plan B's interface contract.
Scope files (approximate):
- `src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs`
- `src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs`
- `src/ClaudeDo.Ui/Views/Islands/TasksIslandView.axaml`
- `src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml`
- `src/ClaudeDo.Ui/Services/WorkerClient.cs`
- `src/ClaudeDo.Ui/Design/IslandStyles.axaml` (draft/badge styling)
- `src/ClaudeDo.Ui/Views/Dialogs/UnfinishedPlanningDialog.axaml` (new)
### 9.4 Integration points between B and C
Interface contract locked before parallel work begins:
- SignalR method names, parameters, return DTOs (listed in §6.6).
- `TaskUpdated` event payload unchanged; carries the task's new parent-id and status so the UI can re-bucket.
- Session directory path shape: `~/.todo-app/planning-sessions/<parentTaskId>/`.
- `mcp.json` and session-file formats are internal to Plan B; UI never reads them.
---
## 10. Out of scope (for now)
- Nested planning (children of children). Explicitly one level.
- Cross-list planning (parent in list A, children in list B).
- Multi-user collaboration on the same planning session.
- Session timeouts / auto-discard.
- Planning-session history / audit UI. Directory is kept on finalize but not surfaced.
- Re-planning a finalized parent (7c: no).