# 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).