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

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 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:

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 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:

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, 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:

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