diff --git a/docs/superpowers/specs/2026-04-23-planning-sessions-design.md b/docs/superpowers/specs/2026-04-23-planning-sessions-design.md new file mode 100644 index 0000000..73e7ce8 --- /dev/null +++ b/docs/superpowers/specs/2026-04-23-planning-sessions-design.md @@ -0,0 +1,468 @@ +# 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 Children { get; set; } = new List(); +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> GetChildrenAsync(string parentId, CancellationToken ct)` +- `Task CreateChildAsync(string parentId, string title, string? description, IReadOnlyList? tagNames, string? commitType, CancellationToken ct)` — creates with `Status = Draft`, `ParentTaskId = parentId`. +- `Task 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 DiscardPlanningAsync(string parentId, CancellationToken ct)` — deletes all Drafts, parent → `Manual`, clears `PlanningSessionId/Token/FinalizedAt`. +- `Task 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 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 `. +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//` +- `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 " } + } + } +} +``` + +### 5.4 Claude CLI invocation (new session) + +``` +wt.exe -d "" cmd /k ^ + claude ^ + --model claude-sonnet-4-6 ^ + --append-system-prompt "" ^ + --mcp-config "" ^ + --allowedTools "mcp__claudedo__*,Read,Grep,Glob,WebFetch,WebSearch,Skill" ^ + "" +``` + +### 5.5 Claude CLI invocation (resume) + +``` +wt.exe -d "" cmd /k ^ + claude --resume --mcp-config "" +``` +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) + +``` + + + + +--- +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//sessions/` after the process starts. + +If (4) resolves to "read back", strategy: +- Poll `~/.claude/projects//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: ")`. + +--- + +## 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` 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`, 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` dialog pattern): + +``` +Unfinished planning session +"" + 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 StartPlanningSessionAsync(string taskId, CancellationToken ct)` — returns `{ WorkingDir, McpConfigPath, InitialPromptPath, SystemPromptPath }`. +- `Task ResumePlanningSessionAsync(string taskId, CancellationToken ct)` — returns `{ WorkingDir, McpConfigPath, ClaudeSessionId }`. +- `Task FinalizePlanningSessionAsync(string taskId, CancellationToken ct)` — returns finalized count. +- `Task DiscardPlanningSessionAsync(string taskId, CancellationToken ct)`. +- `Task 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/_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//`. +- `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).