# 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`, `actionHint`, `customPromptLabel`, `customPromptPlaceholder`, `maxTasksDisabledHint`. The tab's existing `description` is reworded — it currently claims every schedule fires a ping. The validation message stays a hardcoded English string in `PrimeClaudeTabViewModel.Validate`, matching the messages already there ("select at least one day."). ## 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.