Files
ClaudeDo/docs/explore-notes/conpty-sessions.md
T
mika kuns bee06bc307 docs(claude-do): Prompt-Audit: bekommen interaktive und autonome Sessions den
## Der Zweifel

Ein autonomer Agent und eine interaktive Session sollen sich unterschiedlich verhalten. Unklar war, ob beide denselben System-Prompt mitbekommen.

## Vorbefund (2026-08-05, beim Verfeinern erhoben — **selbst nachpruefen, nicht uebernehmen**)

Sie teilen ihn **nicht**. Pro Startweg:

| Startweg | Einstieg | System-Prompt |
|---|---|---|
| Autonomer Run | `ClaudeArgsBuilder.cs:69-73`

ClaudeDo-Task: b259a1c9a73846ce833f0b555fb1de79
2026-08-05 20:33:04 +02:00

9.6 KiB

ConPTY interactive sessions & launch specs

Explore-note — verify before trusting. Distilled map of a subsystem, not authoritative. Last verified against commit bdee731 (2026-08-05). Drift check: git log --oneline bdee731..HEAD -- src/ClaudeDo.Worker/Planning src/ClaudeDo.Worker/Hub src/ClaudeDo.Worker/Runner/ClaudeArgsBuilder.cs src/ClaudeDo.Ui/ViewModels/MissionControlViewModel.cs src/ClaudeDo.Ui/Views/InteractiveTerminalView.axaml Stable structure only (no line numbers). See docs/explore-notes/README.md.

Covers InteractiveLaunchSpecService and the four kinds of embedded ConPTY session the UI process hosts (real claude TUI in a Mission Control tile).

Autonomous queue tasks are not covered here — they stay on the stream-json path via TaskRunner. See worker-task-pipeline.md.

The four session kinds

Kind Hub spec method Notes
Task session GetInteractiveLaunchSpec Effort from the task/list model preset
Ad-hoc GetAdHocLaunchSpec Effort from the global default
Planning (planning start/resume) Effort from PlanningAlias; uses --permission-mode default, not plan
List handler GetMergeHelperLaunchSpec Effort from list config; --permission-mode auto (unattended)

⚠️ Gotcha: never pass task free-text as a CLI argument

No ConPTY path ever passes task free-text (title / description / brief) as a CLI argument. Every one of them writes it to a file first and hands claude a single-line kickoff pointing at that file, exposed via --add-dir.

Two independent reasons:

  1. The ConPTY host flattens Args into one command line to spawn the process, and claude re-splits that line on whitespace. Any token starting with - in real task text (e.g. ->, --abort) is then misread as an unknown option.
  2. A raw multi-line positional prompt truncates at its first newline regardless.

A fresh task session's brief lives at ~/.todo-app/task-sessions/<taskId>/brief.md (InteractiveLaunchSpecService.BuildFreshTaskArgsAsync). A task with neither title nor description skips the file and the positional arg entirely.

Argument ordering

Every spec passes --effort <level> from the relevant model's preset. It leads the args — except for a fresh task session with a brief, where --add-dir <sessionDir> must come first so --effort (a single-value flag) can sit directly before the positional kickoff.

--model is deliberately NOT forced on an interactive session — the user can still switch models in the TUI.

List handler ("Let Claude handle it")

BuildForMergeHelperAsync uses --permission-mode auto so it runs unattended. The --allowedTools allowlist is the security boundary: mcp__claudedo__*,Read,Grep,Glob,Edit,Bash,WebFetch,WebSearch,Skill.

MCP_TOOL_TIMEOUT is 200 s here — TaskWaitMcpTools clamps its own timeout to 170 s to stay comfortably under it (see external-mcp.md).

The host task and its commit range

The handler run owns a real ClaudeDo task, created by CreateMergeHelperTask (hub) → InteractiveLaunchSpecService.CreateMergeHelperTaskAsync, called by the UI before it opens the tile:

  • One new task per run in that list, Idle + IsManual=true (never queued).
  • Title/description localized via missionControl.mergeHelperTaskTitle / mergeHelperTaskDescriptionHeader.
  • TaskEntity.HandlerBaseCommit stamped to the list repo's current HEAD.

The host task never gets a worktree of its own — the handler commits straight into the list's working dir and merges the tasks it handles itself. Consequences:

  • SubmitTaskForReview branches on whether the task has a WorktreeEntity: with one, it commits the worktree; without one, it stamps HandlerHeadCommit to the list repo's current HEAD. Both paths then flip the task Idle/FailedWaitingForReview.
  • GetTaskDiff and the UI's DetailsIslandViewModel / MergeSectionViewModel fall back to the HandlerBaseCommit..HandlerHeadCommit range whenever Worktree is null.

UI flow

MergeHelperSelectionModalViewModel — checkbox picker over one list's non-terminal, non-manual tasks, pre-ticking the actionable ones. List-scoped only (Configure(listId, listName), no global scope). Opened from the list row's context menu, which is hidden when the list has no working dir.

On confirm: ListsIslandViewModel raises LetClaudeHandleRequested → shell → MissionControlViewModel.OpenMergeHelperConPtySessionAsync, which calls CreateMergeHelperTaskAsync and then opens a task-based tile (deduped by TaskId like OpenConPtySessionAsync, not CreateAdHoc) running the five-phase handler prompt.

Tile lifecycle

ConPtyPaneViewModel resolves its own launch spec — the ctor takes a descriptor factory, the host wires handlers and then calls Start(). So the tile appears immediately with its spinner while the worker is still preparing the worktree. A failed launch keeps the tile with an inline error banner instead of the tile never appearing.

Ellipse.spinner (IslandStyles) is the shared indeterminate spinner — used for a starting pane (InteractiveTerminalViewModel.IsStarting) and in place of the refine button while TaskRowViewModel.IsRefining.

Focus / key handling

InteractiveTerminalView lives in MissionControlWindow, so the FocusClearing Escape handler (scoped to MainWindow via AddClassHandler<MainWindow>) never runs there — Escape always reaches the PTY. See the note in src/ClaudeDo.Ui/CLAUDE.md.

TaskRowViewModel.HasInteractiveSession shows an accent "Interactive" chip instead of "Parked"; tapping it jumps to that Mission Control pane. TasksIslandViewModel.SyncInteractiveSessions mirrors Mission Control's open panes onto the rows.

System-prompt matrix (autonomous vs. interactive)

Autonomous and interactive sessions do not share a system prompt. Per start path:

Start path Entry point System prompt
Autonomous run/continue/retry TaskRunner.ResolveConfigAsyncClaudeArgsBuilder.Build --append-system-prompt <text>, recomputed and re-sent on every invocation including a --resume continue (PromptKind.System + improvement/list/task overrides)
Interactive task session, fresh InteractiveLaunchSpecService.BuildForTaskAsyncBuildFreshTaskArgsAsync none — no --append-system-prompt(-file) at all
Interactive task session, resume InteractiveLaunchSpecService.BuildForTaskAsyncWindowsTerminalLauncher.BuildResumeArgs none — only --resume <id> (+ --effort)
Ad-hoc directory session InteractiveLaunchSpecService.BuildForDirectoryAsync none
Planning session start InteractiveLaunchSpecService.BuildPlanningStartWindowsTerminalLauncher.BuildPlanningStartArgs --append-system-prompt-file <path> (PromptKind.Planning)
Planning session resume InteractiveLaunchSpecService.BuildPlanningResumeWindowsTerminalLauncher.BuildPlanningResumeArgs none — only --permission-mode default --allowedTools <planning allowlist> --resume <id>
List handler ("Let Claude handle it") InteractiveLaunchSpecService.BuildForMergeHelperAsync --append-system-prompt-file <path> (PromptKind.MergeHelper), always fresh — this path never resumes

So every interactive resume (task session and planning) drops the system prompt entirely — it's not that they inherit the autonomous one, it's that no claude process on any resume path ever passes --append-system-prompt(-file).

Does --resume bring back a prior --append-system-prompt? No.

Checked by reading real session transcripts (~/.claude/projects/<cwd>/<sessionId>.jsonl) for several autonomous ClaudeDo task runs, including ones with multiple invocations (initial run + ContinueAsync/retry on the same session id, confirmed via that project's task_runs history). Grepped for the PromptKind.System default text ("You are completing one well-defined task autonomously...") and for any "type":"system" entry or message.role == "system" anywhere in those files: the prompt text only ever showed up as ordinary tool-result content (e.g. a task that happened to read PromptFiles.cs's own source), never as a persisted system/config entry. No session transcript — autonomous or interactive — carries a system-role message or a per-session record of the CLI flags it was launched with; there is no sidecar file next to the .jsonl either. The system prompt is purely a per-process request parameter the CLI builds fresh from that invocation's own flags, never replayed from a resumed session's history. This matches why TaskRunner.ContinueAsync (autonomous) explicitly re-resolves and re-passes --append-system-prompt on every continue instead of relying on --resume to carry it — if inheritance worked, that re-resolution would be redundant.

Conclusion: no leak. An interactive resume (task or planning) does not pick up the autonomous run's --append-system-prompt text — including the "commit your work" / CLAUDEDO_BLOCKED instructions from PromptKind.System. It simply runs with the claude CLI's own baseline system prompt, same as every other path in this table that passes no system-prompt flag. No code change needed here.

GetInteractiveLaunchSpec, GetAdHocLaunchSpec, GetMergeHelperLaunchSpec, CreateMergeHelperTask, SubmitTaskForReview.

Planning sessions: StartPlanningSession, ResumePlanningSession, DiscardPlanningSession, FinalizePlanningSession, QueuePlanningSubtasks, GetPendingDraftCount, GetPlanningAggregate, BuildPlanningIntegrationBranch.