Files
ClaudeDo/src/ClaudeDo.Worker/CLAUDE.md
mika kuns fb055ce740 docs: document daily-prep across area CLAUDE.md files; add Installer CLAUDE.md
Worker/Ui/Data CLAUDE.md updated for the daily-prep feature (Prime/ area,
new MCP tools, hub methods, broadcaster events, prep mode, DailyPrepMaxTasks);
new ClaudeDo.Installer/CLAUDE.md maps the WPF installer (modes, pipelines,
steps, MCP registration, Startup-shortcut autostart).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-04 10:54:13 +02:00

11 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/
  Prime/        — daily-prep ("Prime Claude"): PrimeScheduler (BackgroundService), PrimeRunner (runs the daily prep), DailyPrepPrompt (fixed prompt + CLI args + LogPath() helper), NextDueCalculator, PrimeScheduleSignal; interfaces in Prime/Interfaces/ (IPrimeRunner, IPrimeClock, IPrimeScheduleSignal, IPrimeBroadcaster)

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 to 127.0.0.1:47821
  • TaskStateService — only component that writes Status, PlanningPhase, BlockedByTaskId. All transitions return a TransitionResult (no exceptions on invalid moves). Wakes the queue and broadcasts TaskUpdated automatically; advances the planning chain on child terminal transitions.
  • IQueueWaker / IQueuePicker / QueueService — waker is a singleton SemaphoreSlim; picker performs the atomic Queued → Running claim filtered by BlockedByTaskId IS NULL and schedule; QueueService is a thin BackgroundService that loops on the waker and dispatches via TaskRunner.
  • OverrideSlotService — owns RunNow / ContinueTask; goes through TaskStateService.StartRunningAsync (caller-driven, serialized by slot lock).
  • StaleTaskRecovery — startup-only service; calls TaskStateService.RecoverStaleRunningAsync to flip orphaned Running rows to Failed.
  • 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-Key header. Registered explicitly in Program.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 / cancel for a WaitingForReview task), RunTaskNow, CancelTask, DeleteTask
    • ListMcpToolsCreateList, UpdateList, DeleteList
    • ConfigMcpToolsGetListConfig, SetListConfig, SetTaskConfig
    • RunHistoryMcpToolsListRuns, GetRun, GetTaskLog (latest run's log, tail-capped at 256 KB)
    • AgentMcpToolsListAgents
    • LifecycleMcpToolsResetFailedTask
    • AppSettingsMcpToolsGetAppSettings (read-only)
    • ExternalMcpService also exposes two daily-prep tools:
      • GetDailyPrepCandidates — returns Idle, non-blocked tasks in a git repo NOT excluded by AppSettings.ReportExcludedPaths and not already IsMyDay, plus the current Idle MyDay tasks and maxTasks (= DailyPrepMaxTasks). Repo-exclusion logic lives in the DailyPrepFilter helper (same file).
      • SetMyDay — sets a task's IsMyDay (+ optional SortOrder); server-side cap-guard rejects turning on MyDay beyond DailyPrepMaxTasks open (Idle) MyDay tasks.

Daily Prep (Prime Claude)

  • PrimeScheduler (hosted BackgroundService) computes the next due time from the prime_schedules table and at that time calls IPrimeRunner.FireAsync. A manual run arrives via WorkerHub.RunDailyPrepNow. A SemaphoreSlim single-flight gate in PrimeRunner prevents overlapping runs (returns "already running"); both scheduled and manual runs go through it.
  • PrimeRunner builds a fixed prompt via DailyPrepPrompt.BuildPrompt, parameterized by AppSettings.DailyPrepMaxTasks and today's date, then invokes:
    claude -p --output-format stream-json --verbose --permission-mode acceptEdits --max-turns 30
           --allowedTools mcp__claudedo__get_daily_prep_candidates mcp__claudedo__set_my_day
    
    It relies on the globally-registered claudedo MCP (installer's RegisterMcpStep) — no separate --mcp-config. This replaced the old warm-up "ping".
  • Each stdout line is streamed to the UI via IPrimeBroadcaster.PrepLineAsync AND written to DailyPrepPrompt.LogPath() = <appdata>/logs/daily-prep.log (truncated at the start of each run → last run only). PrepStarted/PrepFinished events bracket the run.
  • Agentic behaviour: Claude calls get_daily_prep_candidates, picks an effort-aware subset capped at DailyPrepMaxTasks, and marks them via set_my_day (which broadcasts TaskUpdated so the UI updates live).

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:

  1. _state.FinalizePlanningAsync(parent) flips parent PlanningPhase to Finalized.
  2. PlanningChainCoordinator.SetupChainAsync attaches the agent tag to every child, enqueues child[0], and BlockOns child[i] → child[i-1].
  3. 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:

  1. Load task + list metadata from DB; resolve config from list_config + task-level overrides (model, system_prompt, agent_path)
  2. Create worktree (if WorkingDir set) or sandbox directory
  3. Mark task "running", broadcast TaskStarted
  4. Build CLI args via ClaudeArgsBuilder; invoke ClaudeProcess with task prompt
  5. Stream NDJSON output through StreamAnalyzer; lines forwarded to log file and SignalR (TaskMessage)
  6. On success: auto-commit changes (worktree only), store run record, mark "done"
  7. 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/*.md agent 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, result text, 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, RunDailyPrepNow, ClearMyDay, GetLastPrepLog

HubBroadcaster events: TaskStarted, TaskFinished, TaskMessage, WorktreeUpdated, TaskUpdated, RunCreated, ListUpdated, PrimeFired, PrepStarted, PrepLine, PrepFinished

Config

Loaded from ~/.todo-app/worker.config.json:

  • db_path, sandbox_root, log_root
  • worktree_root_strategy ("sibling" | "central"), central_worktree_root
  • queue_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 auto by default; legacy "bypassPermissions" settings are mapped to auto at dispatch time. acceptEdits, plan, and default pass through unchanged.
  • Worktree branches follow claudedo/{id} naming convention