UseShellExecute=false only appends .exe when searching PATH, so an
npm-installed claude.cmd was never found even though it works from a shell.
Adds a shared ExecutableResolver in ClaudeDo.Data (PATH/PATHEXT aware, with
known npm/claude install-dir fallbacks) and wires it into ClaudeCliPreflight
and ClaudeProcess; shims are launched via cmd.exe /c.
StreamAnalyzer now reads subtype/terminal_reason/errors from the CLI's result
event, and TaskRunner builds a specific ErrorMarkdown from them: max_turns names
the turn budget and points at set_task_config + requeue, api_error passes
through the provider's own message (which carries the reset time), and any
other terminal_reason is appended to the previous generic text instead of
staying invisible. Falls through to the old "exited with code N and no result"
text when there's no terminal_reason at all (a real crash).
Spawned claude processes were only torn down on graceful cancellation
(Process.Kill(entireProcessTree)). A hard worker death — Task Manager
End Task, crash, OS restart, installer update — ran no cleanup, orphaning
the claude->node->conhost tree, which lingered and piled up across
restarts.
Assign every spawned claude process to a Windows Job Object with
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE. The worker holds the only handle, so
when it terminates for any reason the OS tears down the whole job. No-op
on non-Windows; best-effort with a one-time warning if the Win32 calls
fail.
A running task can call mcp__claudedo_run__AskUser(question) to block (up to 3
min) on a human answer. PendingQuestionRegistry holds the pending question +
TaskCompletionSource; the tool broadcasts TaskQuestionAsked, awaits the answer
(WorkerHub.AnswerTaskQuestion resolves it), and returns it as the tool result —
or a 'proceed on your judgment' fallback on timeout. The run stays Running
throughout (no status/schema change). ClaudeProcess raises MCP_TOOL_TIMEOUT so
the 60s HTTP-MCP cap doesn't kill the wait; the run MCP is now wired for every
task, not just standalone ones. System prompt updated to reconcile 'unattended'.
1. ArgumentList (fix injection): ClaudeArgsBuilder.Build() now returns
IReadOnlyList<string>; ClaudeProcess populates ProcessStartInfo.ArgumentList
instead of Arguments, so values like system prompts are never shell-split.
DailyPrepPrompt, RefinePrompt, and WeekReportService migrated similarly.
All IClaudeProcess fakes updated.
2. ContinueAsync exception guard: wrap RunOnceAsync in try/catch matching
the RunAsync pattern so an unexpected exception never leaves the task
stuck in Running status.
3. Planning chain cascade: OnChildFinishedAsync now calls CancelAsync on
the immediate blocked successor when a child fails or is cancelled,
triggering a recursive cascade that clears the entire remaining chain
instead of leaving it wedged.
4. FailAsync guard: restrict valid source states to Running and Queued;
WaitingForReview -> Failed is now rejected, preventing an invalid
transition that could corrupt the review workflow.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- claude process: run stdout/stderr reads without ct; rely on
kill-on-cancel closing the pipes to unblock them — previously
ReadLineAsync(ct) could hang, stalling task slots and shutdown
- task runner: terminal db writes (task_runs, MarkDone, MarkFailed,
SetLogPath) now use CancellationToken.None; RunOnceAsync catches
OCE and finalizes the run row so ContinueAsync can resume
- task repository: GetNextQueuedAgentTaskAsync is now a single
UPDATE ... RETURNING statement — closes TOCTOU window where two
loop iterations could dispatch the same queued task
- queue service: dispose CancellationTokenSource in slot-completion
ContinueWith to stop leaking wait handles
- git service: register ct.Kill(processTree), drain reads without ct,
always reap via WaitForExitAsync(None) — no more git zombies on
cancelled worktree ops
- worktree manager: branch name uses full task id (dashes stripped)
instead of 8-char prefix, eliminating collision risk
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Runner stack (non-worktree path): IClaudeProcess + ClaudeProcess spawning the
CLI with --output-format stream-json, prompt via stdin, parses the final
type:"result" line into RunResult. LogWriter appends ndjson to
~/.todo-app/logs/<taskId>.ndjson. TaskRunner orchestrates DB transitions
(MarkRunning -> MarkDone/Failed) and pushes TaskStarted/Message/Finished/
Updated via HubBroadcaster. Worktree-backed lists short-circuit with a
"Slice E" failure message until git support lands.
QueueService (BackgroundService) holds two in-memory slots (_queueSlot +
_overrideSlot) guarded by a lock. Uses PeriodicTimer + SemaphoreSlim wake
signal so WakeQueue() triggers an instant pickup. RunNow throws
InvalidOperationException when override busy; CancelTask cancels the linked
CTS which kills the child process tree.
WorkerHub extended with GetActive, RunNow (translated to HubException
variants), CancelTask, WakeQueue. HubBroadcaster exposes typed push methods.
Tests: 26 pass (12 new). QueueServiceTests cover override-busy,
schedule-filter, FIFO sequentiality, cancellation, plus a FakeClaudeProcess
that blocks on a TCS for deterministic slot-state assertions.
MessageParserTests cover result extraction + malformed/non-result lines.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>