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>
23 KiB
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
PlanningorPlannedis never picked up by the queue. - Children with status
Draftare 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 (
StartPlanningSessionAsyncerrors if parent is alreadyPlanning; use Resume instead). - Parent auto-status on child completion (evaluated after any child reaches
DoneorFailed):- At least one child
Failedand no children still in non-terminal states → ParentFailed. - All children
Done→ ParentDone. - Any child still
Manual/Queued/Running/Draft→ Parent staysPlanned. - Worktree state (
Merged/Discarded/Kept) is orthogonal; onlyTask.Statusdetermines completion.
- At least one child
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:
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:
.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 withStatus = Draft,ParentTaskId = parentId.Task<int> FinalizePlanningAsync(string parentId, bool queueAgentTasks, CancellationToken ct)— transactional: all Drafts →Manual(orQueuedif tagged "agent" andqueueAgentTasks=true), parent →Planned, setPlanningFinalizedAt, clearPlanningSessionToken. Returns count of finalized children.Task<bool> DiscardPlanningAsync(string parentId, CancellationToken ct)— deletes all Drafts, parent →Manual, clearsPlanningSessionId/Token/FinalizedAt.Task<TaskEntity?> SetPlanningStartedAsync(string taskId, string sessionToken, CancellationToken ct)— sets parentStatus = Planning, stores token; returns null if parent not inManualstate.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→ parentFailedwithFinishedAt = now(). - All
Done(or worktreesDiscarded) → parentDonewithFinishedAt = 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:
StartPlanningSessionAsyncgenerates a 32-byte random token, persists toTasks.PlanningSessionToken.- Token is written into the session's
mcp.jsonasAuthorization: Bearer <token>. - 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. - Token is invalidated (set NULL) on
finalizeordiscard.
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" forupdate/deleteon 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:
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
{
"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, callfinalize. 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:
- Exact flag for thinking budget (
--thinking-budget medium? model suffixclaude-sonnet-4-6-thinking? something else?). - Exact casing of tool names in
--allowedTools(Read/read,WebFetch/web_fetch,Skill). - Whether
--append-system-promptaccepts a file reference (@path) or requires inline string. - Whether Claude CLI supports a
--session-idflag 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.exeresolvable in PATH → else throwPlanningLaunchException("Windows Terminal not found"), UI shows install hint.clauderesolvable in PATH → elsePlanningLaunchException("Claude CLI not installed").list.WorkingDirexists → elsePlanningLaunchException("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.
TasksIslandViewModelbuildsOpenItems/CompletedItems/etc. as flatObservableCollection<TaskRowViewModel>with parents followed by their children if expanded.TaskRowViewModelgetsIsChild: boolandIsPlanningParent: boolandIsExpanded: bool.TaskRowViewindents 24px whenIsChild, shows a thin left border inTextFaintBrush.- Parents with
IsPlanningParentrender a chevron (▸/▾) that togglesIsExpanded; collapsed parents hide their children from the flat stream. - Expanded-state map kept in the VM (
Dictionary<string, bool>, defaulttrue).
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:
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 →
StartPlanningSessionAsyncthrows; 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.
GetChildrenAsyncreturns only direct children, sorted.CreateChildAsyncsets Status=Draft, ParentTaskId correctly.FinalizePlanningAsynctransactionally transitions drafts to Manual/Queued, sets parent to Planned, sets timestamp, clears token. On simulated DB error, rolls back fully.DiscardPlanningAsyncremoves drafts, resets parent.GetNextQueuedAgentTaskAsyncignores Drafts, Planning parents, Planned parents.Restrictcascade: delete parent with children throwsDbUpdateException.
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_taskon non-Draft → MCP error.delete_child_taskon non-Draft → MCP error.finalizecalled twice: first succeeds, second errors because token is invalidated.- Cross-parent access: tool with
task_idbelonging to another parent's session → MCP error.
SignalR endpoints (integration with Worker host):
- Start → token generated, session directory + files created,
mcp.jsoncontains token. - Start on already-
Planningparent → error. - Resume → no new token, reads
PlanningSessionIdfrom 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.cssrc/ClaudeDo.Data/Configuration/TaskEntityConfiguration.cssrc/ClaudeDo.Data/Migrations/<new>_AddPlanningSupport.cssrc/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/orsrc/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.cssrc/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cssrc/ClaudeDo.Ui/Views/Islands/TasksIslandView.axamlsrc/ClaudeDo.Ui/Views/Islands/TaskRowView.axamlsrc/ClaudeDo.Ui/Services/WorkerClient.cssrc/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).
TaskUpdatedevent 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.jsonand 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).