6.8 KiB
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:
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(inPrimeScheduler.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
RadioButtons bound toIsPing/IsFillMyDay/IsCustom.GroupNamemust be per-row unique (bind it to the rowId) — a shared group name makes every schedule in theItemsControlshare one selection. - The prompt
TextBoxis hidden whenIsPing, and its label switches between the existing "Prompt addition" (FillMyDay) and a new "Prompt" (Custom). - The
DailyPrepMaxTasksNumericUpDownbelow the list is disabled when no row isFillMyDay, via a computedAnyFillMyDayon the tab VM that recomputes on row add/remove and onKindchanges. 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; aPingrun writes no log file and raises noPrep*events; aFillMyDayrun still does both.PrimeScheduleRepositoryTests:Kindround-trips, andUpsertAsyncon 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 toPing; settingIsCustomflipsKindand clears the other two;ValidaterejectsCustomwith a blank prompt;AnyFillMyDaytracks 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.logand itsShowPrepLogCommandviewer stay exactly as they are, now fed only byFillMyDay. - 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.