8.9 KiB
ClaudeDo.Worker
ASP.NET Core hosted service that executes tasks via Claude CLI in isolated environments.
Folder Layout
Worker/
State/ — TaskStateService + TransitionResult (sole owner of Status/PlanningPhase/BlockedBy writes)
Queue/ — IQueueWaker, IQueuePicker, QueueService (BackgroundService), OverrideSlotService
Lifecycle/ — StaleTaskRecovery, TaskResetService, TaskMergeService
Worktrees/ — WorktreeMaintenanceService
Agents/ — AgentFileService, DefaultAgentSeeder
Runner/ — TaskRunner + Claude CLI integration
Planning/ — PlanningSessionManager, PlanningChainCoordinator, PlanningMcpService
External/ — ExternalMcpService
Hub/ — WorkerHub, HubBroadcaster
Report/ — ClaudeHistoryReader, WeekReportPromptBuilder, WeekReportService; interfaces in Report/Interfaces/
Interfaces (e.g. IQueueWaker, IPrimeClock, ITaskStateService) live in an Interfaces/ subfolder within their area; the namespace stays the area namespace.
Architecture
- Program.cs — loads config, inits schema, registers DI, configures SignalR on
/hub, binds to127.0.0.1:47821 - TaskStateService — only component that writes
Status,PlanningPhase,BlockedByTaskId. All transitions return aTransitionResult(no exceptions on invalid moves). Wakes the queue and broadcastsTaskUpdatedautomatically; advances the planning chain on child terminal transitions. - IQueueWaker / IQueuePicker / QueueService — waker is a singleton
SemaphoreSlim; picker performs the atomicQueued → Runningclaim filtered byBlockedByTaskId IS NULLand schedule; QueueService is a thinBackgroundServicethat loops on the waker and dispatches viaTaskRunner. - OverrideSlotService — owns
RunNow/ContinueTask; goes throughTaskStateService.StartRunningAsync(caller-driven, serialized by slot lock). - StaleTaskRecovery — startup-only service; calls
TaskStateService.RecoverStaleRunningAsyncto flip orphanedRunningrows toFailed. - External/* — always-on MCP tools for general Claude sessions, scoped to starting and observing sessions (no worktree/merge, multi-turn, planning, or app-settings writes). Auth via optional
X-ClaudeDo-Keyheader. Registered explicitly inProgram.cs's external app via.WithTools<T>(). Organized by concern:ExternalMcpService— task CRUD + execution:ListTaskLists,ListTasks,GetTask,AddTask,UpdateTask,UpdateTaskStatus(Idle/Queued),ReviewTask(approve/reject_rerun/reject_park/cancelfor a WaitingForReview task),RunTaskNow,CancelTask,DeleteTaskListMcpTools—CreateList,UpdateList,DeleteListConfigMcpTools—GetListConfig,SetListConfig,SetTaskConfigRunHistoryMcpTools—ListRuns,GetRun,GetTaskLog(latest run's log, tail-capped at 256 KB)AgentMcpTools—ListAgentsLifecycleMcpTools—ResetFailedTaskAppSettingsMcpTools—GetAppSettings(read-only)
Status Model
TaskEntity carries three orthogonal fields. Lifecycle, planning hierarchy, and chain blocking are no longer conflated.
| Field | Values | Meaning |
|---|---|---|
Status |
Idle, Queued, Running, WaitingForReview, Done, Failed, Cancelled |
Lifecycle only. |
PlanningPhase |
None, Active, Finalized |
Parent-only marker. Active ≈ legacy Planning; Finalized ≈ legacy Planned. |
BlockedByTaskId |
nullable FK | Replaces legacy Waiting. A queued row with BlockedByTaskId != NULL is skipped by the picker. |
ReviewFeedback |
nullable string | Reviewer's rejection comment. Set by RejectToQueueAsync; consumed and cleared by QueueService on the next re-run (resumes the Claude session with it as the next-turn prompt). |
Allowed transitions (enforced by TaskStateService):
Idle → Queued | Running (RunNow)
Queued → Running | Cancelled | Idle
Running → WaitingForReview (standalone success) | Done (planning child success) | Failed | Cancelled
WaitingForReview → Done (approve) | Queued (reject-rerun, +feedback) | Idle (reject-park) | Cancelled
Done → Idle (re-run)
Failed → Idle | Queued
Cancelled → Idle | Queued
Only standalone tasks (ParentTaskId == null) route to WaitingForReview on success. Planning children go straight to Done so the sequential chain (which advances on terminal states) is unaffected. TaskRunner.HandleSuccess makes this choice; review transitions live in TaskStateService (SubmitForReviewAsync, ApproveReviewAsync, RejectToQueueAsync, RejectToIdleAsync, ClearReviewFeedbackAsync).
Planning Flow
PlanningSessionManager.FinalizeAsync is the single path:
_state.FinalizePlanningAsync(parent)flips parentPlanningPhasetoFinalized.PlanningChainCoordinator.SetupChainAsyncattaches theagenttag to every child, enqueues child[0], andBlockOns child[i] → child[i-1].- The first child is woken automatically; successors unblock as their predecessor reaches a terminal state via
OnChildFinishedAsync.
TaskRepository.FinalizePlanningAsync no longer exists. The Mark*Async repository helpers are internal — only TaskStateService calls them.
Task Execution Pipeline
TaskRunner orchestrates:
- Load task + list metadata from DB; resolve config from
list_config+ task-level overrides (model, system_prompt, agent_path) - Create worktree (if
WorkingDirset) or sandbox directory - Mark task "running", broadcast
TaskStarted - Build CLI args via
ClaudeArgsBuilder; invokeClaudeProcesswith task prompt - Stream NDJSON output through
StreamAnalyzer; lines forwarded to log file and SignalR (TaskMessage) - On success: auto-commit changes (worktree only), store run record, mark "done"
- On failure: retry once if session ID available (
--resume), then mark "failed"
Key Components
- ClaudeProcess — spawns
claude -p --output-format stream-json --verbose --permission-mode auto(or whatever permission mode the app settings specify). Writes prompt to stdin, reads NDJSON from stdout. Supports CancellationToken (kills process tree). - ClaudeArgsBuilder — dynamically constructs CLI args; supports
--model,--append-system-prompt,--agents,--json-schema,--resume - StreamAnalyzer — parses rich NDJSON output; extracts session_id, token counts, turn counts, result text, structured output. Replaces MessageParser.
- TaskResetService — discards a failed task's worktree and resets the task row to Idle; preserves run history.
- WorktreeManager — creates worktrees at
claudedo/{taskId[:8]}branches, commits changes with semantic messages, updates DB with head commit and diff stats - CommitMessageBuilder — formats
{commitType}(slug): title\n\ndescription\n\nClaudeDo-Task: taskId - AgentFileService — manages
~/.todo-app/agents/*.mdagent definition files; exposes list/refresh via SignalR - LogWriter — async StreamWriter wrapper, auto-creates parent dirs
Execution History
Each CLI invocation is recorded in the task_runs table via TaskRunRepository:
- Fields:
session_id, input/output/cache token counts, turn count,resulttext, structured output JSON - Enables auto-retry on failure (resume last session) and multi-turn follow-up via
ContinueAsync
Multi-Turn / Continue
TaskRunner.ContinueAsync sends a follow-up prompt to an existing Claude session using --resume <session_id> with the stored session ID from the last run.
SignalR Hub
WorkerHub methods: Ping, GetActive, RunNow, CancelTask, WakeQueue, ContinueTask, ResetTask, ApproveReview, RejectReviewToQueue, RejectReviewToIdle, CancelReview, GetAgents, RefreshAgents, GetAppSettings, UpdateAppSettings, CleanupFinishedWorktrees, ResetAllWorktrees, MergeTask, GetMergeTargets, UpdateList, UpdateListConfig, GetListConfig, UpdateTaskAgentSettings, GetWeekReport, GenerateWeekReport, GetDailyNotes, AddDailyNote, UpdateDailyNote, DeleteDailyNote
HubBroadcaster events: TaskStarted, TaskFinished, TaskMessage, WorktreeUpdated, TaskUpdated, RunCreated, ListUpdated
Config
Loaded from ~/.todo-app/worker.config.json:
db_path,sandbox_root,log_rootworktree_root_strategy("sibling" | "central"),central_worktree_rootqueue_backstop_interval_ms(default 30000)signalr_port(default 47821)claude_bin(path to claude CLI)
Per-list config (list_config in DB) provides defaults for model, system_prompt, agent_path; tasks can override each individually.
Notes
- The worker runs standalone — start it separately from the UI
- Only listens on loopback (127.0.0.1)
- ClaudeProcess uses
--permission-mode autoby default; legacy "bypassPermissions" settings are mapped toautoat dispatch time.acceptEdits,plan, anddefaultpass through unchanged. - Worktree branches follow
claudedo/{id}naming convention