diff --git a/docs/superpowers/plans/2026-06-25-interactive-ask-user.md b/docs/superpowers/plans/2026-06-25-interactive-ask-user.md new file mode 100644 index 0000000..3a49d6f --- /dev/null +++ b/docs/superpowers/plans/2026-06-25-interactive-ask-user.md @@ -0,0 +1,56 @@ +# Plan — Interactive "Answer Claude's Questions" + +Spec: `docs/superpowers/specs/2026-06-25-interactive-ask-user-design.md` + +Implement on the shared main tree. Commit explicit paths per task (never `git add -A`). +Build with `-c Release` (running Worker locks Debug). No real-Claude tests. + +## Task 1 — PendingQuestionRegistry (worker, new file) +- `src/ClaudeDo.Worker/Runner/PendingQuestionRegistry.cs`: singleton; `record PendingQuestion(TaskId, QuestionId, Question)`. + - `(string QuestionId, Task Answer) Register(taskId, question)` — overwrites any stale entry, `RunContinuationsAsynchronously`. + - `bool TryAnswer(taskId, questionId, answer)`; `PendingQuestion? Get(taskId)`; `void Remove(taskId, questionId)`. +- Test: `tests/ClaudeDo.Worker.Tests/Runner/PendingQuestionRegistryTests.cs` — register→answer resolves the task; wrong questionId no-ops; Get reflects state; second Register overwrites. + +## Task 2 — AskUser MCP tool (worker) +- `TaskRunMcpService.cs`: inject `PendingQuestionRegistry`; add + `[McpServerTool] async Task AskUser(string question, CancellationToken ct)`: + - caller id from `_ctx.Current.CallerTaskId`; register; broadcast `TaskQuestionAsked`. + - await answer via `Task.WaitAsync` with a 3-min linked-CTS; on timeout return the fallback string; on request-cancel rethrow. + - `finally`: `Remove` + broadcast `TaskQuestionResolved`. + - `[Description]`: when to use (only when a wrong guess is costly/irreversible; otherwise proceed). +- Test: `tests/ClaudeDo.Worker.Tests/Runner/AskUserToolTests.cs` — answer path returns the answer; timeout path returns fallback (inject a short timeout or a seam) with a fake broadcaster + stub context accessor. + +## Task 3 — Wire MCP for all runs + timeout env (worker) +- `TaskRunner.RunAsync`: move MCP-identity setup out of the `standalone` gate so every run gets `claudedo_run`; `AllowedTools` = `mcp__claudedo_run__AskUser` always, append `,mcp__claudedo_run__SuggestImprovement` when standalone. Keep token cleanup in `finally`. +- `ClaudeProcess.cs`: `psi.Environment["MCP_TOOL_TIMEOUT"] = "200000";`. +- System prompt file (PromptKind.System default): add one guidance line about `AskUser`. + +## Task 4 — Hub + Broadcaster (worker) +- `HubBroadcaster.cs`: `TaskQuestionAsked(taskId, questionId, question)`, `TaskQuestionResolved(taskId, questionId)`. +- `WorkerHub.cs`: inject registry; `bool AnswerTaskQuestion(taskId, questionId, answer)`; `PendingQuestionDto? GetPendingQuestion(taskId)`; `record PendingQuestionDto(...)`. +- `Program.cs`: register `PendingQuestionRegistry` as singleton. + +## Task 5 — UI client (IWorkerClient/WorkerClient + fakes) +- `IWorkerClient`: `Task AnswerTaskQuestionAsync(taskId, questionId, answer)`, `Task GetPendingQuestionAsync(taskId)`, events `Action? TaskQuestionAskedEvent`, `Action? TaskQuestionResolvedEvent`; UI DTO record. +- `WorkerClient`: implement invokes + `On<...>` handlers raising the events. +- Update hand-rolled `IWorkerClient` fakes in Ui.Tests (and Worker.Tests if present). + +## Task 6 — TaskMonitorViewModel (hot file) +- Subscribe both events (filter by `_subscribedTaskId`); dispose handlers. +- Props: `PendingQuestionId`, `PendingQuestion`, `HasPendingQuestion`, `AnswerDraft`, `IsWaitingForInput`. +- `SubmitAnswerCommand` (CanExecute: non-empty draft + HasPendingQuestion) → `AnswerTaskQuestionAsync`; clear draft. +- Clear pending on `TaskFinished` for this task and in `Reset()`. +- Test: `TaskMonitorViewModelTests` — asked event surfaces question; submit invokes client + clears; resolved/finished clears. + +## Task 7 — Hydrate on attach (MissionControlViewModel) +- In `HydrateAsync`, after `ApplyState`, call `GetPendingQuestionAsync(taskId)`; if present, set the monitor's pending question (re-attach case). + +## Task 8 — View banner (hot file, additive) +- `MonitorPaneView.axaml`: a `Border DockPanel.Dock="Top"` above `SessionTerminalView`, `IsVisible="{Binding HasPendingQuestion}"`, showing the question text, a `TextBox` bound to `AnswerDraft` (Enter submits), and a Send `Button` → `SubmitAnswerCommand`. Mirror the roadblock-banner styling. + +## Task 9 — Localization +- `en.json` + `de.json`: `missionControl.question.title`, `.placeholder`, `.send`. Keep parity (Localization.Tests). + +## Task 10 — Build + test + verify +- `dotnet build` App + Worker `-c Release`; run Worker.Tests, Ui.Tests, Localization.Tests. +- Self-review diffs. Flag the two manual verification gaps to Mika. Do not push. diff --git a/docs/superpowers/specs/2026-06-25-interactive-ask-user-design.md b/docs/superpowers/specs/2026-06-25-interactive-ask-user-design.md new file mode 100644 index 0000000..3c5a54d --- /dev/null +++ b/docs/superpowers/specs/2026-06-25-interactive-ask-user-design.md @@ -0,0 +1,102 @@ +# Interactive "Answer Claude's Questions" — Design + +**Date:** 2026-06-25 +**Status:** Approved (brainstormed with Mika) + +## Goal + +Let the user answer a question Claude raises *mid-run* from inside Mission Control, +without leaving the autonomous-execution model. Not a chat panel, not a terminal, not +proactive steering — only: *Claude surfaces a question → the user types an answer → the +run continues with that answer in context.* + +User decisions (brainstorm): +- Scope: "I mostly want to answer his questions if he surfaces any." +- Trigger: **any running task** may ask, with a **3-minute** answer window. + +## Why not the alternatives + +- **Embedded terminal / PTY** — would destroy the NDJSON contract the whole worker + pipeline depends on (StreamAnalyzer, token accounting, auto-commit, status flow) and + needs a terminal-emulator control Avalonia doesn't have. Rejected. +- **Streaming-stdin (`--input-format stream-json`)** — right tool for a free-form chat, + overkill here. Rejected for v1. +- **`--resume` per-turn** — already exists; not live (cold process per turn). + +## Mechanism + +The in-task MCP already blocks the `claude -p` process while a tool call is in flight. +That blocking *is* the pause. Add one in-task MCP tool, `AskUser(question)`: + +1. The tool resolves the caller task id, registers a pending question + a + `TaskCompletionSource` in a singleton `PendingQuestionRegistry`, and + broadcasts `TaskQuestionAsked(taskId, questionId, question)`. +2. Mission Control surfaces the question with an input box. +3. The user answers → `WorkerHub.AnswerTaskQuestion` resolves the TCS → the tool + returns the answer as its result → Claude continues. +4. No answer within **3 minutes** → the tool returns *"No response received within 3 + minutes — proceed using your best judgment."* and the run carries on autonomously. + +### Key facts that make this work + +- **No persisted status change.** The task is still genuinely `Running` (process alive, + blocked mid-tool-call). "Waiting for input" is **ephemeral**: in-memory registry + + live SignalR events + a UI overlay. No `TaskStatus` enum value, no `TaskStateService` + transition, **no EF migration**. If the worker dies mid-wait, `StaleTaskRecovery` + flips the orphaned `Running` row to `Failed` like any interrupted run. +- **`MCP_TOOL_TIMEOUT` must be raised.** Claude Code caps HTTP MCP tool calls at **60 s** + by default. The `claudedo_run` MCP is HTTP, so `ClaudeProcess` must set + `MCP_TOOL_TIMEOUT=200000` (≈3 min + margin) on the spawned process or the 3-min window + is silently truncated to 60 s. +- **MCP wired for all runs.** Today `TaskRunner` only mints the run MCP for standalone + top-level tasks (for `SuggestImprovement`). To satisfy "any running task," move the + MCP-identity setup out of that gate so every `RunAsync` gets `claudedo_run`. + `AllowedTools` always includes `mcp__claudedo_run__AskUser`; `SuggestImprovement` stays + gated to improvement-eligible (standalone) runs. + +## Surface changes + +**Worker (mostly new files):** +- `Runner/PendingQuestionRegistry.cs` (new, singleton) — `Register`, `TryAnswer`, `Get`, + `Remove`; one pending question per task. +- `Runner/TaskRunMcpService.cs` (edit) — add `AskUser` `[McpServerTool]`; inject the + registry. +- `Runner/TaskRunner.cs` (edit) — wire MCP identity for all runs; add `AskUser` to + allowed tools. +- `Runner/ClaudeProcess.cs` (edit) — set `MCP_TOOL_TIMEOUT` env. +- `Hub/HubBroadcaster.cs` (edit) — `TaskQuestionAsked`, `TaskQuestionResolved`. +- `Hub/WorkerHub.cs` (edit) — `AnswerTaskQuestion`, `GetPendingQuestion` + DTO. +- `Program.cs` (edit) — register `PendingQuestionRegistry` singleton. +- System prompt (edit) — one line telling Claude the tool exists and to use it only when + a wrong guess would be costly/irreversible (otherwise proceed). + +**UI:** +- `Services/IWorkerClient.cs` + `WorkerClient.cs` (edit) — `AnswerTaskQuestionAsync`, + `GetPendingQuestionAsync`, `TaskQuestionAskedEvent`, `TaskQuestionResolvedEvent`. +- `ViewModels/Islands/TaskMonitorViewModel.cs` (edit, **hot file**) — pending-question + state, `AnswerDraft`, `SubmitAnswerCommand`, clear on finish/resolve. +- `ViewModels/MissionControlViewModel.cs` (edit) — hydrate pending question on attach. +- `Views/MissionControl/MonitorPaneView.axaml` (edit, **hot file**) — additive + question/answer banner above the terminal. +- `Localization/locales/en.json` + `de.json` — `missionControl.question.*` keys. + +**Tests:** `PendingQuestionRegistry` (answer/timeout/unknown/overwrite), `AskUser` tool +(answer + timeout fallback, fake broadcaster — no real Claude), `TaskMonitorViewModel` +(surface/submit/clear). Update IWorkerClient fakes in both test projects. + +## Concurrency note + +Two files (`TaskMonitorViewModel.cs`, `MonitorPaneView.axaml`) are also being touched by +a concurrent Mission Control drag-and-drop session on the shared main tree. Keep edits +additive, commit explicit paths only (never `git add -A`). + +## Verification gaps (manual) + +1. **Real-Claude smoke test** — confirm a blocking `AskUser` call survives ≥3 min with + `MCP_TOOL_TIMEOUT=200000` and that the model actually calls the tool when uncertain. +2. **Visual** — the question banner + input box in the pane (Mika does the visual pass). + +## Non-goals + +Free-form chat panel; proactive steering; tool-permission prompts (stays `auto`); +`ContinueAsync`/resumed runs gaining `AskUser` (deferred follow-up).