8.9 KiB
Daily Prep ("Prime Claude") — Design
Date: 2026-06-03
Overview
Turn the existing Prime Time warm-up into a daily preparation ("Tagesvorbereitung").
At a scheduled time (or on demand), Claude reads the open tasks, estimates effort,
and selects a focused subset into the MyDay list — capped so it never moves
everything in. Claude does the reasoning itself (agentic), via the already-registered
ClaudeDo MCP. This replaces the current "ping" behavior entirely.
A later phase will feed external tickets (Jira, possibly a second system) into the same candidate pool; that is out of scope for this spec.
Goals
- Scheduled and manual ("Tag vorbereiten" button) daily prep.
- Claude picks a subset of open tasks into MyDay, ordered so related tasks sit together.
- Effort-aware selection, hard-capped at
Xopen MyDay tasks. - Keep existing MyDay tasks across re-runs; only top up to
X. - Candidates limited to tasks in repos that are not excluded from the weekly report.
Non-Goals
- External ticket integration (Jira etc.) — future phase.
- Group labels/headers in the MyDay view — grouping is ordering-only via
SortOrder. - A user-editable prep prompt — the prompt is fixed, parameterized.
Key Decisions
| Topic | Decision |
|---|---|
| Who reasons | Agentic — Claude decides via MCP tools. |
| MyDay model | TaskEntity.IsMyDay flag (smart list smart:my-day). |
| Grouping | Ordering only via existing SortOrder (no new field, no migration for grouping). |
| Selection | Effort estimate, hard cap X tasks/day. |
| Candidates | Status == Idle, BlockedByTaskId == null, list WorkingDir not under ReportExcludedPaths. |
| Re-run | Keep existing MyDay tasks; top up to X. |
| Trigger | Existing Prime schedule and a manual button. |
| Ping | Removed — daily prep replaces it. |
| Prompt | Fixed, with injected parameters (X, today's date). |
| Tool access | Reuse the globally registered claudedo MCP — no separate --mcp-config. |
Architecture
1. MCP tools (extend ExternalMcpService, port 47822)
The worker already exposes ExternalMcpService as the claudedo MCP server. Add two tools;
they automatically surface as mcp__claudedo__get_daily_prep_candidates and
mcp__claudedo__set_my_day.
-
get_daily_prep_candidates()→ JSON containing:candidates[]: open, non-blocked tasks in non-excluded repos, each withid, title, description, listName, isStarred, scheduledFor, age(age derived fromCreatedAt).currentMyDay[]: currently-IsMyDayopen tasks (so Claude sees remaining capacity).- Filter:
Status == IdleANDBlockedByTaskId == nullAND the task's listWorkingDirdoes not start with any prefix inAppSettings.ReportExcludedPaths(default["C:\\Private"]; case-insensitive prefix match, same semantics as the weekly report).
-
set_my_day(taskId, isMyDay, sortOrder?)→- Sets
IsMyDayand (optionally)SortOrderon the task viaTaskRepository. - Broadcasts
TaskUpdatedviaHubBroadcasterso the UI updates live. - Cap-guard: when
isMyDay == true, count current open (Idle) tasks withIsMyDay == true. Ifcount >= X, reject with an error message ("MyDay limit {X} reached").isMyDay == falseis always allowed.X = AppSettings.DailyPrepMaxTasks. This guarantees the "never move everything in" invariant server-side, independent of Claude's behavior.
- Sets
2. DailyPrepRunner (replaces ping logic)
Rename IPrimeRunner/PrimeRunner → IDailyPrepRunner/DailyPrepRunner (the "ping"
concept is gone). It:
- Loads
AppSettings(X = DailyPrepMaxTasks). - Builds the fixed prompt with injected parameters (
X, today's date). - Invokes
claude -p --output-format stream-json --verbosewith:--permission-modeset so the headless run won't block on permission prompts,--allowedTools mcp__claudedo__get_daily_prep_candidates mcp__claudedo__set_my_day,--max-turns 30(constant), timeout 5 min (constant; larger than the old 60s ping).
- No
--mcp-config— relies on the globally registeredclaudedoMCP (the worker runs as the user via the per-user logon Scheduled Task, so the headless run inherits the user-scope registration and its auth). - Returns an outcome (e.g. number of tasks added) for broadcasting.
3. Scheduler
PrimeScheduler is unchanged in structure — it now calls IDailyPrepRunner instead of the
ping runner. NextDueCalculator and the schedule model are untouched.
4. Manual trigger
- Worker hub method
RunDailyPrepNow()invokes the sameDailyPrepRunner. - UI button "Tag vorbereiten" in the MyDay list header.
- Single-flight guard: if a prep run is already in progress, the trigger reports "already running" and does not start a parallel run (applies to both schedule and button).
5. Parameter config
- New field
DailyPrepMaxTasks(int, default5) onAppSettingsEntity. - Plumbing: EF config + migration,
AppSettingsRepository,WorkerHubAppSettings DTO, UI DTO mirror +WorkerClient, and a numeric editor in the Prime Claude settings tab. ReportExcludedPathsis reused as-is (already onAppSettings).
Data Flow
- Trigger (schedule due or button) →
DailyPrepRunner.RunAsync. - Runner loads
AppSettings(X), builds prompt, launches Claude. - Claude →
get_daily_prep_candidates→ DB query returns filtered candidates + current MyDay. - Claude estimates effort, tops up to X total, calls
set_my_day(id, true, sortOrder)for each chosen task (consecutivesortOrderfor related tasks). ExternalMcpServicewritesIsMyDay/SortOrder, broadcastsTaskUpdated→ MyDay list updates live.- Runner updates
LastRunAt, broadcasts "prep done" (count added).
Fixed Prompt (parameterized)
Content (parameters in {}):
Du bereitest meinen Arbeitstag für {today} vor.
- Rufe
get_daily_prep_candidatesauf.- Behalte bereits als MyDay markierte offene Tasks.
- Fülle bis maximal {X} offene Tasks gesamt in MyDay auf — niemals mehr.
- Schätze pro Task grob den Aufwand; wähle eine machbare Mischung (nicht nur Großbrocken). Priorisiere
isStarred, fällige (scheduledFor) und ältere Tasks.- Lege thematisch verwandte Tasks durch aufeinanderfolgende
sortOrder-Werte nebeneinander.- Setze die Auswahl via
set_my_day(id, true, sortOrder). Markiere nichts außerhalb der Kandidatenliste.
Injected parameters: {today} (date) and {X} (= DailyPrepMaxTasks).
Error Handling
- No candidates → Claude marks nothing; runner reports "0 added".
- Claude run fails / times out → log + failure broadcast (existing scheduler event channel);
LastRunAtis set on attempt, as today, to avoid tight retry loops. set_my_dayon an invalid/ineligible id → tool returns an error string; Claude adapts.- Cap exceeded → tool returns an error; Claude stops adding.
- Concurrent trigger → single-flight guard reports "already running".
Testing
Real SQLite + real git (project convention).
get_daily_prep_candidates: onlyIdle; blocked excluded; tasks in excluded repos (ReportExcludedPaths) excluded; current MyDay tasks included.set_my_day: sets flag +SortOrder; broadcastsTaskUpdated; cap-guard rejects at limit; unset always allowed.DailyPrepRunner: prompt contains{X}+ date; args contain--allowedTools+ permission-mode +--max-turns; success/failure outcomes via anIClaudeProcessfake.- Rename
IPrimeRunner→IDailyPrepRunnerrequires syncingPrimeSchedulertests/fakes.
Files to Create / Modify (high level)
Data
Models/AppSettingsEntity.cs— addDailyPrepMaxTasks.Configuration/AppSettingsEntityConfiguration.cs— map new column.Migrations/— new migration fordaily_prep_max_tasks.Repositories/AppSettingsRepository.cs— persist new field.
Worker
External/ExternalMcpService.cs— addget_daily_prep_candidates,set_my_day(+ cap-guard).Prime/PrimeRunner.cs→DailyPrepRunner.cs;Prime/Interfaces/IPrimeRunner.cs→IDailyPrepRunner.cs; prompt builder + arg builder.Prime/PrimeScheduler.cs— depend onIDailyPrepRunner.Hub/WorkerHub.cs— AppSettings DTO field;RunDailyPrepNow().Program.cs— DI registration update.
UI
Services/WorkerClient.cs+ AppSettings DTO mirror — new field;RunDailyPrepNowcall.- Prime Claude settings tab VM/view — numeric editor for
DailyPrepMaxTasks. - MyDay list header — "Tag vorbereiten" button + command (Lists/IslandsShell VM).
Tests
ClaudeDo.Worker.Tests— MCP tools, runner, scheduler fakes.ClaudeDo.Data.Tests— AppSettings persistence (if covered there).ClaudeDo.Ui.Tests— settings VM / button wiring as applicable.
Future Phase (out of scope)
External ticket sources (Jira, possibly a second system) feed into the candidate pool used by
get_daily_prep_candidates, behind a task-source abstraction. Designed separately.