Files
ClaudeDo/docs/superpowers/specs/2026-08-24-prime-action-kind-design.md
T

7.0 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 (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 RadioButtons 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.