docs(daily-prep): add design specs and implementation plans

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
mika kuns
2026-06-04 08:42:41 +02:00
parent c45f892591
commit 9470c5b10b
4 changed files with 1586 additions and 0 deletions

View File

@@ -0,0 +1,182 @@
# 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.

View File

@@ -0,0 +1,151 @@
# Daily Prep — Live Output View + Clear Day — Design
Date: 2026-06-03
## Overview
Two follow-ups to the daily-prep ("Prime Claude") feature:
1. **Live output view.** While Claude prepares the day, there is no feedback. Add a
live, human-readable view of the prep run's output, shown as a new content mode in
the existing right-hand **Details island** (mirroring how Daily Notes works — a mode
swap, not a separate window/column).
2. **Clear Day button.** A MyDay-header button that clears the MyDay selection
immediately.
## Goals
- See the prep run's progress live, rendered with the same friendly terminal renderer
used for task runs (assistant text + tool calls like `set_my_day …`, not raw NDJSON).
- Both manual (button) and scheduled prep runs stream into the log.
- The manual button opens the prep view; a scheduled run fills the log silently and is
opened via a dedicated "Vorbereitungs-Log" button (the existing `PrimeStatus` footer
remains the hint that a run happened).
- A "Tag leeren" button clears all MyDay tasks (any status) with no confirmation.
## Non-Goals
- No new island/column and no popup/overlay — reuse the Details island as a mode swap.
- No persistence of prep output across app restarts (in-memory log only).
- No undo for Clear Day (re-runnable via "Tag vorbereiten").
## Key Decisions
| Topic | Decision |
| --- | --- |
| Rendering | Reuse the existing `SessionTerminalView` / `StreamLineFormatter` renderer. |
| Location | New `IsPrepMode` content panel inside the Details island (like `IsNotesMode`). |
| Lifecycle | Manual click opens the view (UI-local); `PrepStarted/PrepLine/PrepFinished` events fill the log regardless of current mode; scheduled runs do not auto-open. |
| Open after schedule | Dedicated "Vorbereitungs-Log" header button + existing `PrimeStatus` footer hint. |
| Clear Day scope | All MyDay tasks regardless of status. |
| Clear Day confirm | None — clear directly. |
## Architecture
### Feature A — Live prep output
**Worker**
- Extend `IPrimeBroadcaster` (`src/ClaudeDo.Worker/Prime/Interfaces/IPrimeBroadcaster.cs`)
with `PrepStartedAsync()`, `PrepLineAsync(string line)`, `PrepFinishedAsync(bool success)`.
- Implement in `HubBroadcaster` (`src/ClaudeDo.Worker/Hub/HubBroadcaster.cs`) sending
SignalR events `PrepStarted`, `PrepLine` (string), `PrepFinished` (bool).
- `PrimeRunner` (`src/ClaudeDo.Worker/Prime/PrimeRunner.cs`): inject `IPrimeBroadcaster`.
In `FireAsync`, after the single-flight gate is entered and a run will actually happen:
call `PrepStartedAsync()` before `RunAsync`; replace the discard lambda with
`async line => await _broadcaster.PrepLineAsync(line)`; call
`PrepFinishedAsync(result.IsSuccess)` after. The "already running" early-return path
emits nothing (no run occurs). Both scheduled and manual runs go through `FireAsync`,
so both stream.
**UI**
- `WorkerClient` (`src/ClaudeDo.Ui/Services/WorkerClient.cs`): register
`_hub.On<…>("PrepStarted"/"PrepLine"/"PrepFinished", …)` each via
`Dispatcher.UIThread.Post`, raising `PrepStartedEvent` / `PrepLineEvent(string)` /
`PrepFinishedEvent(bool)`. Declare these on `IWorkerClient`.
- `DetailsIslandViewModel`: add `IsPrepMode` (bool), `IsPrepRunning` (bool), a dedicated
`PrepLog` (`ObservableCollection<LogLineViewModel>`), and `ShowPrep()` (calls
`Bind(null)`, sets `IsNotesMode=false`, `IsPrepMode=true`). Subscribe to the three prep
events in the ctor (always active, independent of mode):
- `PrepStarted` → clear `PrepLog`, `IsPrepRunning=true`.
- `PrepLine` → format the line with the same `StreamLineFormatter` path used by the
stdout branch of `OnTaskMessage`, append a `LogLineViewModel` to `PrepLog`.
- `PrepFinished``IsPrepRunning=false` (optionally append a status line).
Mode exclusivity: the normal task-details panel becomes visible on
`!IsNotesMode && !IsPrepMode`; `ShowNotes()` also sets `IsPrepMode=false`; `Bind(task)`
resets both flags.
- `DetailsIslandView.axaml`: add a third `<Panel IsVisible="{Binding IsPrepMode}">` in the
body grid alongside the existing details/notes panels, rendering `PrepLog` in the
terminal style (reuse the `LogLineViewModel` item template used by `SessionTerminalView`).
**Wiring**
- `TasksIslandViewModel`: add a `PrepRequested` event (mirror `NotesRequested`).
`PrepareDayCommand` raises `PrepRequested` in addition to calling
`RunDailyPrepNowAsync()`. Add `ShowPrepLogCommand` that raises `PrepRequested`. Add the
"Vorbereitungs-Log" button to the MyDay header (`IsVisible="{Binding IsMyDayList}"`).
- `IslandsShellViewModel`: wire `Tasks.PrepRequested += () => Details.ShowPrep()`.
### Feature B — Clear Day
**Worker**
- `WorkerHub.ClearMyDay()` (`src/ClaudeDo.Worker/Hub/WorkerHub.cs`): query ids where
`IsMyDay == true`; `ExecuteUpdateAsync` setting `is_my_day = false`; broadcast
`TaskUpdated(id)` for each affected id (the UI reloads the current list on `TaskUpdated`).
**UI**
- `IWorkerClient.ClearMyDayAsync()` + `WorkerClient` impl invoking `"ClearMyDay"`.
- `TasksIslandViewModel.ClearDayCommand` calls `_worker.ClearMyDayAsync()` (no confirm).
Add the "Tag leeren" button to the MyDay header next to "Tag vorbereiten".
## Data Flow (live view)
1. Trigger (schedule or button) → `PrimeRunner.FireAsync`.
2. `PrepStartedAsync()` → SignalR `PrepStarted``WorkerClient.PrepStartedEvent`
`DetailsIslandViewModel` clears `PrepLog`, sets `IsPrepRunning`.
3. Each Claude stdout line → `PrepLineAsync(line)``PrepLine` → formatted, appended to
`PrepLog` (visible if the user is in prep mode; filled silently otherwise).
4. Run ends → `PrepFinishedAsync(success)``PrepFinished``IsPrepRunning=false`.
5. Manual button click also raised `PrepRequested``Details.ShowPrep()` (view open).
After a scheduled run, the user clicks "Vorbereitungs-Log" to open it.
## Error Handling
- Prep run fails/times out → `PrepFinished(false)`; the existing `PrimeFired` footer
status still reports failure.
- "Already running" → no prep events emitted (no run happened); existing behavior intact.
- `ClearMyDay` with zero MyDay tasks → no-op, no broadcasts.
## Testing
- Worker: `PrimeRunner` streams `PrepStarted` → N×`PrepLine``PrepFinished` (fake
`IClaudeProcess` invokes `onStdoutLine` with sample lines; fake `IPrimeBroadcaster`
records calls). `WorkerHub.ClearMyDay` clears all IsMyDay rows and broadcasts per id
(real SQLite, mirror existing hub tests).
- UI: `DetailsIslandViewModel` appends to `PrepLog` on `PrepLineEvent` and `ShowPrep()`
sets the mode flags (mutual exclusivity with notes); `TasksIslandViewModel.ClearDayCommand`
calls `ClearMyDayAsync` (stub worker client).
## Files (high level)
**Modify**
- `src/ClaudeDo.Worker/Prime/Interfaces/IPrimeBroadcaster.cs`
- `src/ClaudeDo.Worker/Hub/HubBroadcaster.cs`
- `src/ClaudeDo.Worker/Prime/PrimeRunner.cs`
- `src/ClaudeDo.Worker/Hub/WorkerHub.cs` (ClearMyDay)
- `src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs`
- `src/ClaudeDo.Ui/Services/WorkerClient.cs`
- `src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs`
- `src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml`
- `src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs`
- `src/ClaudeDo.Ui/Views/Islands/TasksIslandView.axaml`
- `src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs`
- `src/ClaudeDo.Localization/locales/en.json`, `de.json` (button labels)
**Test**
- `tests/ClaudeDo.Worker.Tests/Prime/PrimeRunnerTests.cs`
- `tests/ClaudeDo.Worker.Tests/Hub/…` (ClearMyDay)
- `tests/ClaudeDo.Ui.Tests/…` (DetailsIslandViewModel prep events; TasksIslandViewModel ClearDay) + `StubWorkerClient`
## Known fragility
Changing `IWorkerClient` / `WorkerClient` / VM constructors breaks hand-rolled fakes
(`StubWorkerClient`, `FakeWorkerClient`) in both test projects — update all of them.