183 lines
8.9 KiB
Markdown
183 lines
8.9 KiB
Markdown
# 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 `X` open 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 with
|
|
`id, title, description, listName, isStarred, scheduledFor, age` (age derived from `CreatedAt`).
|
|
- `currentMyDay[]`: currently-`IsMyDay` open tasks (so Claude sees remaining capacity).
|
|
- Filter: `Status == Idle` AND `BlockedByTaskId == null` AND the task's list `WorkingDir`
|
|
does not start with any prefix in `AppSettings.ReportExcludedPaths`
|
|
(default `["C:\\Private"]`; case-insensitive prefix match, same semantics as the weekly report).
|
|
|
|
- **`set_my_day(taskId, isMyDay, sortOrder?)`** →
|
|
- Sets `IsMyDay` and (optionally) `SortOrder` on the task via `TaskRepository`.
|
|
- Broadcasts `TaskUpdated` via `HubBroadcaster` so the UI updates live.
|
|
- **Cap-guard:** when `isMyDay == true`, count current open (`Idle`) tasks with
|
|
`IsMyDay == true`. If `count >= X`, reject with an error message
|
|
("MyDay limit {X} reached"). `isMyDay == false` is always allowed.
|
|
`X = AppSettings.DailyPrepMaxTasks`. This guarantees the "never move everything in"
|
|
invariant server-side, independent of Claude's behavior.
|
|
|
|
### 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 --verbose` with:
|
|
- `--permission-mode` set 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 registered `claudedo` MCP (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 same `DailyPrepRunner`.
|
|
- 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, default `5`) on `AppSettingsEntity`.
|
|
- Plumbing: EF config + migration, `AppSettingsRepository`, `WorkerHub` AppSettings DTO,
|
|
UI DTO mirror + `WorkerClient`, and a numeric editor in the Prime Claude settings tab.
|
|
- `ReportExcludedPaths` is reused as-is (already on `AppSettings`).
|
|
|
|
## Data Flow
|
|
|
|
1. Trigger (schedule due **or** button) → `DailyPrepRunner.RunAsync`.
|
|
2. Runner loads `AppSettings` (`X`), builds prompt, launches Claude.
|
|
3. Claude → `get_daily_prep_candidates` → DB query returns filtered candidates + current MyDay.
|
|
4. Claude estimates effort, tops up to **X total**, calls `set_my_day(id, true, sortOrder)`
|
|
for each chosen task (consecutive `sortOrder` for related tasks).
|
|
5. `ExternalMcpService` writes `IsMyDay`/`SortOrder`, broadcasts `TaskUpdated` → MyDay list
|
|
updates live.
|
|
6. Runner updates `LastRunAt`, broadcasts "prep done" (count added).
|
|
|
|
## Fixed Prompt (parameterized)
|
|
|
|
Content (parameters in `{}`):
|
|
|
|
> Du bereitest meinen Arbeitstag für **{today}** vor.
|
|
> 1. Rufe `get_daily_prep_candidates` auf.
|
|
> 2. Behalte bereits als MyDay markierte offene Tasks.
|
|
> 3. Fülle bis **maximal {X} offene Tasks gesamt** in MyDay auf — niemals mehr.
|
|
> 4. Schätze pro Task grob den Aufwand; wähle eine machbare Mischung (nicht nur Großbrocken).
|
|
> Priorisiere `isStarred`, fällige (`scheduledFor`) und ältere Tasks.
|
|
> 5. Lege thematisch verwandte Tasks durch aufeinanderfolgende `sortOrder`-Werte nebeneinander.
|
|
> 6. 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);
|
|
`LastRunAt` is set on attempt, as today, to avoid tight retry loops.
|
|
- `set_my_day` on 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`: only `Idle`; blocked excluded; tasks in excluded repos
|
|
(`ReportExcludedPaths`) excluded; current MyDay tasks included.
|
|
- `set_my_day`: sets flag + `SortOrder`; broadcasts `TaskUpdated`; cap-guard rejects at limit;
|
|
unset always allowed.
|
|
- `DailyPrepRunner`: prompt contains `{X}` + date; args contain `--allowedTools` +
|
|
permission-mode + `--max-turns`; success/failure outcomes via an `IClaudeProcess` fake.
|
|
- Rename `IPrimeRunner` → `IDailyPrepRunner` requires syncing `PrimeScheduler` tests/fakes.
|
|
|
|
## Files to Create / Modify (high level)
|
|
|
|
**Data**
|
|
- `Models/AppSettingsEntity.cs` — add `DailyPrepMaxTasks`.
|
|
- `Configuration/AppSettingsEntityConfiguration.cs` — map new column.
|
|
- `Migrations/` — new migration for `daily_prep_max_tasks`.
|
|
- `Repositories/AppSettingsRepository.cs` — persist new field.
|
|
|
|
**Worker**
|
|
- `External/ExternalMcpService.cs` — add `get_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 on `IDailyPrepRunner`.
|
|
- `Hub/WorkerHub.cs` — AppSettings DTO field; `RunDailyPrepNow()`.
|
|
- `Program.cs` — DI registration update.
|
|
|
|
**UI**
|
|
- `Services/WorkerClient.cs` + AppSettings DTO mirror — new field; `RunDailyPrepNow` call.
|
|
- 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.
|