docs(daily-prep): add design specs and implementation plans
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
182
docs/superpowers/specs/2026-06-03-daily-prep-design.md
Normal file
182
docs/superpowers/specs/2026-06-03-daily-prep-design.md
Normal 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.
|
||||
151
docs/superpowers/specs/2026-06-03-daily-prep-live-view-design.md
Normal file
151
docs/superpowers/specs/2026-06-03-daily-prep-live-view-design.md
Normal 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.
|
||||
Reference in New Issue
Block a user