diff --git a/docs/superpowers/specs/2026-08-24-prime-action-kind-design.md b/docs/superpowers/specs/2026-08-24-prime-action-kind-design.md new file mode 100644 index 00000000..a504f688 --- /dev/null +++ b/docs/superpowers/specs/2026-08-24-prime-action-kind-design.md @@ -0,0 +1,141 @@ +# Prime Claude: per-schedule action kind + +**Date:** 2026-08-24 +**Status:** approved + +## Problem + +`PrimeRunner.FireAsync` is hardcoded to one action: build `DailyPrepPrompt`, run `claude -p` +with the two MyDay MCP tools, let Claude fill MyDay. A Prime schedule is therefore always a +"fill my day" run, even when all the user wants is to open the usage window, or to run some +other recurring instruction. + +## Goal + +Each Prime schedule picks one of three actions: + +| Kind | What it does | +|---|---| +| `Ping` | Fires one throwaway `claude -p` turn to open the usage window. Nothing else. **Default for new schedules.** | +| `FillMyDay` | Today's behaviour: daily-prep prompt, MyDay selection capped by `DailyPrepMaxTasks`. | +| `Custom` | Runs the schedule's own prompt with the ClaudeDo MCP tools available. | + +## Data + +New enum in `ClaudeDo.Data.Models`: + +```csharp +public enum PrimeActionKind { Ping = 0, FillMyDay = 1, Custom = 2 } +``` + +`PrimeScheduleEntity` gains `public PrimeActionKind Kind { get; set; } = PrimeActionKind.Ping;`, +mapped in `PrimeScheduleEntityConfiguration` to column `action_kind`, required, default value +`PrimeActionKind.Ping` — the same shape as the existing `days_of_week` mapping. + +Migration `AddPrimeActionKind`, based on `20260821155619_AddUsageLimitAutoContinue` (current +head — check for a newer head before scaffolding; sibling migrations off the same parent +silently drop each other's columns on a SQLite table rebuild). + +**Existing rows migrate to `Ping`** (column default 0, no data backfill). This is a deliberate +behaviour change requested by the user: today's schedules stop filling MyDay until he switches +them back in Settings. Do not "helpfully" backfill them to `FillMyDay`. + +`prompt_override` is reused rather than split, with a role that depends on `Kind`: + +| Kind | `prompt_override` means | +|---|---| +| `Ping` | ignored | +| `FillMyDay` | an *addition*, appended to the daily-prep prompt (unchanged from today) | +| `Custom` | the *entire* prompt, used verbatim | + +`PrimeScheduleRepository.UpsertAsync` must copy `Kind` onto the existing row alongside +`Days`/`TimeOfDay`/`Enabled`/`PromptOverride`. Forgetting it is silent — the update just keeps +the old kind. + +## Worker + +`PrimeScheduleDto` gains `PrimeActionKind Kind`. `PrimeScheduler.ToDto` maps it. + +`PrimeRunner.FireAsync` keeps its single-flight `SemaphoreSlim` gate (it guards *all* kinds — +two schedules must never overlap regardless of kind) and its `FireTimeout`, then branches: + +| Kind | Prompt | CLI args | Log file | `PrepStarted`/`PrepLine`/`PrepFinished` | +|---|---|---|---|---| +| `Ping` | fixed one-liner, not user-editable | `-p --output-format stream-json --verbose --permission-mode default --max-turns 1 --strict-mcp-config` | none | not raised | +| `FillMyDay` | `DailyPrepPrompt.BuildPrompt(maxTasks, today, promptOverride)` | unchanged (`DailyPrepPrompt.BuildArgs`) | `daily-prep.log`, truncated per run | raised | +| `Custom` | `promptOverride` verbatim | `-p --output-format stream-json --verbose --permission-mode acceptEdits --max-turns 30 --allowedTools mcp__claudedo` | none | not raised | + +`--strict-mcp-config` without a `--mcp-config` argument loads no MCP server at all, so a Ping +cannot reach ClaudeDo's tools even if the model tries. Combined with `--max-turns 1` a Ping is +one turn and done. + +`Custom` deliberately gets **only** the ClaudeDo MCP server — no Read/Write/Edit/Bash. An +unattended 07:00 run should be able to create and queue tasks, not touch the filesystem. + +All three kinds still: +- run in `Paths.AppDataRoot()` as cwd, +- update `last_run_at` (in `PrimeScheduler.FireAsync`, unchanged), +- broadcast `PrimeFired(scheduleId, success, message, firedAt)`, which drives the footer flash. + +`WorkerHub.RunDailyPrepNow` builds its synthetic `PrimeScheduleDto` with +`Kind: PrimeActionKind.FillMyDay`. The "Plan day" button in the MyDay panel means "fill my day" +and must keep meaning that no matter how the schedules are configured. + +`WorkerHub.UpsertPrimeSchedule` rejects `Kind == Custom` with a blank `PromptOverride`, so a +schedule can never fire `claude -p ""` at 07:00. + +## UI + +`PrimeScheduleRowViewModel` gains `[ObservableProperty] PrimeActionKind _kind` plus three +derived booleans for radio binding — `IsPing`, `IsFillMyDay`, `IsCustom`. Setting a boolean to +`true` sets `Kind`; `OnKindChanged` raises change notifications for all three. Three booleans +rather than an enum-to-bool converter keeps compiled bindings straightforward. + +`PrimeClaudeTabViewModel.AddSchedule` creates rows with `Kind: PrimeActionKind.Ping`. + +`PrimeClaudeTabViewModel.Validate` adds: a `Custom` row with a blank prompt returns an error +naming the row's time. + +`SettingsModalView.axaml`, Prime tab, inside the schedule `DataTemplate`: + +- A horizontal row of three `RadioButton`s bound to `IsPing`/`IsFillMyDay`/`IsCustom`. + **`GroupName` must be per-row unique** (bind it to the row `Id`) — a shared group name makes + every schedule in the `ItemsControl` share one selection. +- The prompt `TextBox` is hidden when `IsPing`, and its label switches between the existing + "Prompt addition" (FillMyDay) and a new "Prompt" (Custom). +- The `DailyPrepMaxTasks` `NumericUpDown` below the list is disabled when no row is + `FillMyDay`, via a computed `AnyFillMyDay` on the tab VM that recomputes on row add/remove + and on `Kind` changes. A live number that controls nothing is worse than a greyed-out one. + +New locale keys in both `en.json` and `de.json` (parity is test-enforced): +`settings.prime.actionLabel`, `actionPing`, `actionFillMyDay`, `actionCustom`, +`actionPingHint`, `customPromptLabel`, `customPromptPlaceholder`, `customPromptRequired`, +`maxTasksDisabledHint`. + +## Tests + +**Worker.Tests** +- `PrimeRunnerTests`: one test per kind against the fake CLI runner asserting the args and the + prompt handed to it; a `Ping` run writes no log file and raises no `Prep*` events; a + `FillMyDay` run still does both. +- `PrimeScheduleRepositoryTests`: `Kind` round-trips, and `UpsertAsync` on an existing row + updates it. +- Migration default: a schedule row created before the column exists reads back as `Ping`. + +**Ui.Tests** +- `PrimeClaudeTabViewModelTests`: a new row defaults to `Ping`; setting `IsCustom` flips `Kind` + and clears the other two; `Validate` rejects `Custom` with a blank prompt; `AnyFillMyDay` + tracks row changes. + +`Localization.Tests` covers the new keys' en/de parity automatically. + +## Out of scope + +- No new log file or log viewer for Ping/Custom. The existing `daily-prep.log` and its + `ShowPrepLogCommand` viewer stay exactly as they are, now fed only by `FillMyDay`. +- No per-kind model/effort override — all kinds use whatever the Prime path uses today. + +## Visual verification gap + +The radio-group layout inside the schedule card, the hidden/relabelled prompt box, and the +disabled `DailyPrepMaxTasks` field cannot be verified headlessly. The user does the visual pass.