# 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.
