142 lines
6.8 KiB
Markdown
142 lines
6.8 KiB
Markdown
# 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.
|