# Prime Action Kind Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Give every Prime Claude schedule a selectable action — `Ping` (open the usage window and nothing else, the new default), `FillMyDay` (today's daily prep), or `Custom` (the schedule's own prompt with the ClaudeDo MCP tools). **Architecture:** A new `PrimeActionKind` enum on `PrimeScheduleEntity` (column `action_kind`, TEXT, default `"ping"`) flows through `PrimeScheduleDto` into `PrimeRunner.FireAsync`, which branches to one of three prompt/args/logging combinations. The existing `prompt_override` column is reused with a kind-dependent role. The Settings UI gets a per-row radio group. **Tech Stack:** .NET 8, EF Core (SQLite), SignalR, Avalonia 12 + CommunityToolkit.Mvvm, xUnit. **Spec:** `docs/superpowers/specs/2026-08-24-prime-action-kind-design.md` --- ## File Structure **Create:** - `src/ClaudeDo.Data/Models/PrimeActionKind.cs` — the enum - `src/ClaudeDo.Data/Migrations/_AddPrimeActionKind.cs` — scaffolded - `src/ClaudeDo.Worker/Prime/PrimePrompts.cs` — Ping/Custom prompt + args builders (`DailyPrepPrompt` stays as-is for FillMyDay) - `src/ClaudeDo.Worker/Prime/PrimeScheduleValidation.cs` — pure validation, callable from the hub and unit-testable without constructing `WorkerHub` - `tests/ClaudeDo.Worker.Tests/Prime/PrimeScheduleValidationTests.cs` - `tests/ClaudeDo.Worker.Tests/Repositories/PrimeScheduleRepositoryTests.cs` **Modify:** - `src/ClaudeDo.Data/Models/PrimeScheduleEntity.cs` — `Kind` property - `src/ClaudeDo.Data/Configuration/PrimeScheduleEntityConfiguration.cs` — column + converter - `src/ClaudeDo.Data/Repositories/PrimeScheduleRepository.cs` — `UpsertAsync` copies `Kind` - `src/ClaudeDo.Worker/Prime/PrimeScheduleDto.cs` — `Kind` member - `src/ClaudeDo.Worker/Prime/PrimeScheduler.cs` — `ToDto` maps `Kind` - `src/ClaudeDo.Worker/Prime/PrimeRunner.cs` — three-way branch - `src/ClaudeDo.Worker/Hub/WorkerHub.cs` — `ListPrimeSchedules`, `UpsertPrimeSchedule`, `RunDailyPrepNow` - `src/ClaudeDo.Ui/Services/PrimeScheduleDto.cs` — `Kind` member - `src/ClaudeDo.Ui/ViewModels/Modals/Settings/PrimeScheduleRowViewModel.cs` — `Kind` + radio booleans + prompt label/placeholder - `src/ClaudeDo.Ui/ViewModels/Modals/Settings/PrimeClaudeTabViewModel.cs` — default `Ping`, `AnyFillMyDay`, validation - `src/ClaudeDo.Ui/Views/Modals/SettingsModalView.axaml` — radio group, conditional prompt box, gated max-tasks field - `src/ClaudeDo.Localization/locales/en.json` + `de.json` — new `settings.prime.*` keys - `tests/ClaudeDo.Worker.Tests/Prime/PrimeRunnerTests.cs` — existing DTO construction + new per-kind tests - `tests/ClaudeDo.Ui.Tests/ViewModels/PrimeClaudeTabViewModelTests.cs` — existing DTO construction + new tests - `src/ClaudeDo.Data/CLAUDE.md`, `src/ClaudeDo.Worker/CLAUDE.md` — doc updates **Staging rule for every commit in this plan:** stage the listed files **by path**. Never `git add -A` — parallel sessions leave unrelated WIP in this tree (`git status` at plan time already showed modified `UsageMonitorModalViewModel.cs`, `TaskStateService.cs` and others that are not ours). --- ### Task 1: The `PrimeActionKind` enum and entity column **Files:** - Create: `src/ClaudeDo.Data/Models/PrimeActionKind.cs` - Modify: `src/ClaudeDo.Data/Models/PrimeScheduleEntity.cs` - Modify: `src/ClaudeDo.Data/Configuration/PrimeScheduleEntityConfiguration.cs` - Modify: `src/ClaudeDo.Data/Repositories/PrimeScheduleRepository.cs:24-38` - Test: `tests/ClaudeDo.Worker.Tests/Repositories/PrimeScheduleRepositoryTests.cs` - [ ] **Step 1: Write the failing test** Create `tests/ClaudeDo.Worker.Tests/Repositories/PrimeScheduleRepositoryTests.cs`: ```csharp using ClaudeDo.Data.Models; using ClaudeDo.Data.Repositories; using ClaudeDo.Worker.Tests.Infrastructure; namespace ClaudeDo.Worker.Tests.Repositories; public class PrimeScheduleRepositoryTests : IDisposable { private readonly DbFixture _db = new(); public void Dispose() => _db.Dispose(); private static PrimeScheduleEntity Row(PrimeActionKind kind) => new() { Id = Guid.NewGuid(), Days = PrimeDays.Weekdays, TimeOfDay = new TimeSpan(7, 0, 0), Enabled = true, Kind = kind, }; [Fact] public void Kind_defaults_to_ping_on_a_new_entity() { Assert.Equal(PrimeActionKind.Ping, new PrimeScheduleEntity().Kind); } [Fact] public async Task Kind_round_trips_through_the_database() { await using var ctx = await _db.CreateFactory().CreateDbContextAsync(); var repo = new PrimeScheduleRepository(ctx); var row = Row(PrimeActionKind.Custom); await repo.UpsertAsync(row); var reloaded = await repo.GetAsync(row.Id); Assert.NotNull(reloaded); Assert.Equal(PrimeActionKind.Custom, reloaded!.Kind); } [Fact] public async Task UpsertAsync_updates_kind_on_an_existing_row() { await using var ctx = await _db.CreateFactory().CreateDbContextAsync(); var repo = new PrimeScheduleRepository(ctx); var row = Row(PrimeActionKind.Ping); await repo.UpsertAsync(row); await repo.UpsertAsync(new PrimeScheduleEntity { Id = row.Id, Days = row.Days, TimeOfDay = row.TimeOfDay, Enabled = row.Enabled, Kind = PrimeActionKind.FillMyDay, }); var reloaded = await repo.GetAsync(row.Id); Assert.Equal(PrimeActionKind.FillMyDay, reloaded!.Kind); } } ``` - [ ] **Step 2: Run the test to verify it fails** ```bash dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter FullyQualifiedName~PrimeScheduleRepositoryTests ``` Expected: compile error — `PrimeScheduleEntity` has no `Kind`, `PrimeActionKind` does not exist. - [ ] **Step 3: Add the enum** Create `src/ClaudeDo.Data/Models/PrimeActionKind.cs`: ```csharp namespace ClaudeDo.Data.Models; /// What a Prime schedule actually does when it fires. public enum PrimeActionKind { /// One throwaway turn that only opens the usage window. Default for new schedules. Ping, /// Run the daily prep prompt and let Claude fill MyDay. FillMyDay, /// Run the schedule's own prompt with the ClaudeDo MCP tools available. Custom } ``` - [ ] **Step 4: Add the entity property** In `src/ClaudeDo.Data/Models/PrimeScheduleEntity.cs`, add after `Days`: ```csharp public PrimeActionKind Kind { get; set; } = PrimeActionKind.Ping; ``` - [ ] **Step 5: Map the column** In `src/ClaudeDo.Data/Configuration/PrimeScheduleEntityConfiguration.cs`, add the converter above `Configure` (this file currently has no converters — add the `using` for `Microsoft.EntityFrameworkCore.Storage.ValueConversion`, matching `WorktreeEntityConfiguration`): ```csharp private static string KindToString(PrimeActionKind v) => v == PrimeActionKind.Ping ? "ping" : v == PrimeActionKind.FillMyDay ? "fill_my_day" : v == PrimeActionKind.Custom ? "custom" : throw new ArgumentOutOfRangeException(nameof(v)); private static PrimeActionKind KindFromString(string v) => v == "ping" ? PrimeActionKind.Ping : v == "fill_my_day" ? PrimeActionKind.FillMyDay : v == "custom" ? PrimeActionKind.Custom : throw new ArgumentOutOfRangeException(nameof(v)); private static readonly ValueConverter KindConverter = new(v => KindToString(v), v => KindFromString(v)); ``` And inside `Configure`, after the `days_of_week` property: ```csharp builder.Property(s => s.Kind).HasColumnName("action_kind") .IsRequired().HasDefaultValue(PrimeActionKind.Ping) .HasConversion(KindConverter); ``` - [ ] **Step 6: Copy `Kind` in the repository upsert** In `src/ClaudeDo.Data/Repositories/PrimeScheduleRepository.cs`, inside the `else` branch of `UpsertAsync`, add: ```csharp existing.Kind = entity.Kind; ``` This branch lists every field explicitly — a missing one is silent, the update just keeps the old value. - [ ] **Step 7: Run the tests to verify they pass** ```bash dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter FullyQualifiedName~PrimeScheduleRepositoryTests ``` Expected: 3 passed. (`DbFixture` uses `EnsureCreated`, so the column comes from the model, not the migration — Task 2 covers the migration.) - [ ] **Step 8: Commit** ```bash git commit -m "feat(prime): add PrimeActionKind to the schedule entity" -- \ src/ClaudeDo.Data/Models/PrimeActionKind.cs \ src/ClaudeDo.Data/Models/PrimeScheduleEntity.cs \ src/ClaudeDo.Data/Configuration/PrimeScheduleEntityConfiguration.cs \ src/ClaudeDo.Data/Repositories/PrimeScheduleRepository.cs \ tests/ClaudeDo.Worker.Tests/Repositories/PrimeScheduleRepositoryTests.cs ``` --- ### Task 2: The EF migration **Files:** - Create: `src/ClaudeDo.Data/Migrations/_AddPrimeActionKind.cs` (+ `.Designer.cs`) - Modify: `src/ClaudeDo.Data/Migrations/ClaudeDoDbContextModelSnapshot.cs` - [ ] **Step 1: Confirm the current migration head** ```bash ls src/ClaudeDo.Data/Migrations/ | grep -v Designer | tail -3 ``` Expected at plan time: `20260821155619_AddUsageLimitAutoContinue.cs` is the newest. If a newer one appeared, that is fine — but **do not** scaffold if another uncommitted migration is sitting in the tree from a parallel session: two migrations off the same parent silently drop each other's columns when SQLite rebuilds the table. If you see one, stop and report it. - [ ] **Step 2: Scaffold the migration** ```bash dotnet ef migrations add AddPrimeActionKind --project src/ClaudeDo.Data --startup-project src/ClaudeDo.Worker ``` - [ ] **Step 3: Verify the generated `Up`** Open the new `_AddPrimeActionKind.cs`. It must be exactly one `AddColumn`: ```csharp migrationBuilder.AddColumn( name: "action_kind", table: "prime_schedules", type: "TEXT", nullable: false, defaultValue: "ping"); ``` Expected: existing rows get `"ping"`. **This is intentional** — the user asked for existing schedules to become Ping. Do not add an `UPDATE` that backfills them to `fill_my_day`. If the scaffold produced anything else (a table rebuild, extra columns), delete the migration files and re-scaffold — extra columns mean a sibling migration collided. - [ ] **Step 4: Build to confirm the snapshot compiles** ```bash dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release ``` Expected: Build succeeded. - [ ] **Step 5: Commit** ```bash git commit -m "feat(prime): migrate prime_schedules with action_kind defaulting to ping" -- \ src/ClaudeDo.Data/Migrations/ ``` --- ### Task 3: `PrimeRunner` branches on the kind **Files:** - Create: `src/ClaudeDo.Worker/Prime/PrimePrompts.cs` - Modify: `src/ClaudeDo.Worker/Prime/PrimeScheduleDto.cs` - Modify: `src/ClaudeDo.Worker/Prime/PrimeScheduler.cs:104-105` - Modify: `src/ClaudeDo.Worker/Prime/PrimeRunner.cs:34-95` - Test: `tests/ClaudeDo.Worker.Tests/Prime/PrimeRunnerTests.cs` - [ ] **Step 1: Extend the DTO and the mapper** `PrimeActionKind` is added as a 7th positional member with **no default** on purpose — every construction site must state its kind rather than silently inheriting one. Replace the whole of `src/ClaudeDo.Worker/Prime/PrimeScheduleDto.cs`: ```csharp using ClaudeDo.Data.Models; namespace ClaudeDo.Worker.Prime; public sealed record PrimeScheduleDto( Guid Id, int Days, TimeSpan TimeOfDay, bool Enabled, DateTimeOffset? LastRunAt, string? PromptOverride, PrimeActionKind Kind); ``` In `src/ClaudeDo.Worker/Prime/PrimeScheduler.cs`, `ToDto` becomes: ```csharp private static PrimeScheduleDto ToDto(Data.Models.PrimeScheduleEntity e) => new(e.Id, (int)e.Days, e.TimeOfDay, e.Enabled, e.LastRunAt, e.PromptOverride, e.Kind); ``` - [ ] **Step 2: Update the existing tests to compile, and add the new failing tests** In `tests/ClaudeDo.Worker.Tests/Prime/PrimeRunnerTests.cs`: Replace `DefaultSchedule()` (line 74-75) — the existing tests all assert daily-prep behaviour, so they become explicitly `FillMyDay`: ```csharp private static PrimeScheduleDto DefaultSchedule() => new(Guid.Empty, 0, TimeSpan.Zero, true, null, null, PrimeActionKind.FillMyDay); private static PrimeScheduleDto Schedule(PrimeActionKind kind, string? promptOverride = null) => new(Guid.Empty, 0, TimeSpan.Zero, true, null, promptOverride, kind); ``` Add `using ClaudeDo.Data.Models;` at the top of the file, and replace every inline `new PrimeScheduleDto(Guid.Empty, 0, TimeSpan.Zero, true, null, null)` (lines 117, 136, 145) with `DefaultSchedule()`. Extend `FakeClaudeProcess` so tests can inspect what the runner asked for — add these members to the class and record them at the top of `RunAsync`: ```csharp public IReadOnlyList? LastArguments { get; private set; } public string? LastPrompt { get; private set; } ``` ```csharp LastArguments = arguments; LastPrompt = prompt; ``` Now append the new tests: ```csharp [Fact] public async Task Ping_runs_a_single_turn_with_no_mcp_servers() { var claude = new FakeClaudeProcess(exitCode: 0, result: "ok"); var runner = NewRunner(claude, new RecordingPrimeBroadcaster()); var outcome = await runner.FireAsync(Schedule(PrimeActionKind.Ping), CancellationToken.None); Assert.True(outcome.Success); Assert.Contains("--strict-mcp-config", claude.LastArguments!); var turnsIndex = claude.LastArguments!.ToList().IndexOf("--max-turns"); Assert.Equal("1", claude.LastArguments[turnsIndex + 1]); Assert.DoesNotContain(DailyPrepPrompt.SetMyDayTool, claude.LastArguments); } [Fact] public async Task Ping_writes_no_prep_log_and_raises_no_prep_events() { var path = DailyPrepPrompt.LogPath(); Directory.CreateDirectory(Path.GetDirectoryName(path)!); await File.WriteAllTextAsync(path, "previous fill-my-day run"); var broadcaster = new RecordingPrimeBroadcaster(); var claude = new FakeClaudeProcess(emitLines: ["ping-line"], exitCode: 0, result: "ok"); var runner = NewRunner(claude, broadcaster); await runner.FireAsync(Schedule(PrimeActionKind.Ping), CancellationToken.None); Assert.Equal("previous fill-my-day run", await File.ReadAllTextAsync(path)); Assert.Equal(0, broadcaster.StartedCount); Assert.Empty(broadcaster.Lines); Assert.Empty(broadcaster.FinishedResults); } [Fact] public async Task Custom_sends_the_prompt_override_verbatim_with_claudedo_mcp_only() { var claude = new FakeClaudeProcess(exitCode: 0, result: "ok"); var runner = NewRunner(claude, new RecordingPrimeBroadcaster()); var outcome = await runner.FireAsync( Schedule(PrimeActionKind.Custom, "Queue the three oldest idle tasks."), CancellationToken.None); Assert.True(outcome.Success); Assert.Equal("Queue the three oldest idle tasks.", claude.LastPrompt); Assert.Contains("--allowedTools", claude.LastArguments!); Assert.Contains("mcp__claudedo", claude.LastArguments!); Assert.DoesNotContain("--strict-mcp-config", claude.LastArguments!); } [Fact] public async Task Custom_writes_no_prep_log_and_raises_no_prep_events() { var path = DailyPrepPrompt.LogPath(); Directory.CreateDirectory(Path.GetDirectoryName(path)!); await File.WriteAllTextAsync(path, "previous fill-my-day run"); var broadcaster = new RecordingPrimeBroadcaster(); var claude = new FakeClaudeProcess(emitLines: ["custom-line"], exitCode: 0, result: "ok"); var runner = NewRunner(claude, broadcaster); await runner.FireAsync(Schedule(PrimeActionKind.Custom, "do a thing"), CancellationToken.None); Assert.Equal("previous fill-my-day run", await File.ReadAllTextAsync(path)); Assert.Equal(0, broadcaster.StartedCount); Assert.Empty(broadcaster.FinishedResults); } [Fact] public async Task FillMyDay_still_appends_the_prompt_override() { var claude = new FakeClaudeProcess(exitCode: 0, result: "ok"); var runner = NewRunner(claude, new RecordingPrimeBroadcaster()); await runner.FireAsync( Schedule(PrimeActionKind.FillMyDay, "Prefer the LogX tasks."), CancellationToken.None); Assert.Contains("Prefer the LogX tasks.", claude.LastPrompt); Assert.Contains(DailyPrepPrompt.SetMyDayTool, claude.LastArguments!); } [Fact] public async Task Gate_is_shared_across_kinds() { var runner = NewRunner(claudeDelay: TimeSpan.FromSeconds(2)); var first = runner.FireAsync(Schedule(PrimeActionKind.Ping), CancellationToken.None); var second = await runner.FireAsync(Schedule(PrimeActionKind.FillMyDay), CancellationToken.None); Assert.False(second.Success); Assert.Contains("already running", second.Message, StringComparison.OrdinalIgnoreCase); await first; } ``` - [ ] **Step 3: Run the tests to verify they fail** ```bash dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter FullyQualifiedName~PrimeRunnerTests ``` Expected: the four new kind-specific tests fail — the runner ignores `Kind` and always does daily prep (log file overwritten, `PrepStarted` raised, daily-prep args). - [ ] **Step 4: Add the Ping/Custom prompt builders** Create `src/ClaudeDo.Worker/Prime/PrimePrompts.cs`: ```csharp namespace ClaudeDo.Worker.Prime; /// Prompts and CLI args for the non-daily-prep Prime actions. /// The daily-prep ones live in . public static class PrimePrompts { /// The whole point of a ping is to open the usage window, so it asks for the /// cheapest possible answer. Not user-editable — a schedule's prompt override is ignored /// for this kind. public const string PingPrompt = "Reply with the single word: ok"; /// No tools at all: --strict-mcp-config without a --mcp-config argument loads no /// MCP server, and one turn leaves no room to act on a tool result anyway. public static IReadOnlyList BuildPingArgs() => [ "-p", "--output-format", "stream-json", "--verbose", "--permission-mode", "default", "--max-turns", "1", "--strict-mcp-config", ]; /// ClaudeDo's MCP server only — no Read/Write/Edit/Bash. An unattended 07:00 run /// may create and queue tasks; it may not touch the filesystem. public static IReadOnlyList BuildCustomArgs(int maxTurns) => [ "-p", "--output-format", "stream-json", "--verbose", "--permission-mode", "acceptEdits", "--max-turns", maxTurns.ToString(), "--allowedTools", "mcp__claudedo", ]; } ``` - [ ] **Step 5: Branch in `PrimeRunner`** Replace the body of `FireAsync` in `src/ClaudeDo.Worker/Prime/PrimeRunner.cs` (keep the class fields, constructor, `FireTimeout` and `MaxTurns` as they are; add `using ClaudeDo.Data.Models;`): ```csharp public async Task FireAsync(PrimeScheduleDto schedule, CancellationToken ct) { // One gate for all kinds — two schedules must never run at the same time. if (!await _gate.WaitAsync(0, ct)) return new PrimeRunOutcome(false, "Prime run already in progress"); try { return schedule.Kind switch { PrimeActionKind.FillMyDay => await RunFillMyDayAsync(schedule, ct), PrimeActionKind.Custom => await RunCustomAsync(schedule, ct), _ => await RunPingAsync(ct), }; } finally { _gate.Release(); } } private async Task RunFillMyDayAsync(PrimeScheduleDto schedule, CancellationToken ct) { var success = false; try { var logPath = DailyPrepPrompt.LogPath(); try { if (File.Exists(logPath)) File.Delete(logPath); } catch { /* best effort */ } await using var logWriter = new LogWriter(logPath); await _broadcaster.PrepStartedAsync(); var cwd = Cwd(); int maxTasks; await using (var dbCtx = await _dbFactory.CreateDbContextAsync(ct)) { var settings = await new AppSettingsRepository(dbCtx).GetAsync(ct); maxTasks = settings.DailyPrepMaxTasks < 1 ? 1 : settings.DailyPrepMaxTasks; } var today = DateOnly.FromDateTime(_clock.Now.LocalDateTime); var prompt = DailyPrepPrompt.BuildPrompt(maxTasks, today, schedule.PromptOverride); using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(ct); timeoutCts.CancelAfter(FireTimeout); var result = await _claude.RunAsync( arguments: DailyPrepPrompt.BuildArgs(MaxTurns), prompt: prompt, workingDirectory: cwd, onStdoutLine: async line => { await logWriter.WriteLineAsync(line); await _broadcaster.PrepLineAsync(line); }, ct: timeoutCts.Token); success = result.IsSuccess; return success ? new PrimeRunOutcome(true, "Daily prep complete") : new PrimeRunOutcome(false, $"exit code {result.ExitCode}"); } catch (OperationCanceledException) when (!ct.IsCancellationRequested) { return new PrimeRunOutcome(false, $"timed out after {FireTimeout.TotalMinutes:0} min"); } catch (Exception ex) { _logger.LogWarning(ex, "Daily prep run failed"); return new PrimeRunOutcome(false, ex.Message); } finally { await _broadcaster.PrepFinishedAsync(success); } } private Task RunPingAsync(CancellationToken ct) => RunQuietAsync(PrimePrompts.BuildPingArgs(), PrimePrompts.PingPrompt, "Ping complete", "Ping", ct); private Task RunCustomAsync(PrimeScheduleDto schedule, CancellationToken ct) { var prompt = schedule.PromptOverride; if (string.IsNullOrWhiteSpace(prompt)) return Task.FromResult(new PrimeRunOutcome(false, "Custom schedule has no prompt")); return RunQuietAsync(PrimePrompts.BuildCustomArgs(MaxTurns), prompt, "Custom run complete", "Custom", ct); } /// Ping and Custom deliberately write no log file and raise no Prep* events — the /// prep log belongs to the MyDay selection and would otherwise be overwritten with noise. private async Task RunQuietAsync( IReadOnlyList args, string prompt, string successMessage, string label, CancellationToken ct) { try { using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(ct); timeoutCts.CancelAfter(FireTimeout); var result = await _claude.RunAsync( arguments: args, prompt: prompt, workingDirectory: Cwd(), onStdoutLine: _ => Task.CompletedTask, ct: timeoutCts.Token); return result.IsSuccess ? new PrimeRunOutcome(true, successMessage) : new PrimeRunOutcome(false, $"exit code {result.ExitCode}"); } catch (OperationCanceledException) when (!ct.IsCancellationRequested) { return new PrimeRunOutcome(false, $"timed out after {FireTimeout.TotalMinutes:0} min"); } catch (Exception ex) { _logger.LogWarning(ex, "Prime {Label} run failed", label); return new PrimeRunOutcome(false, ex.Message); } } private static string Cwd() { var cwd = Paths.AppDataRoot(); Directory.CreateDirectory(cwd); return cwd; } ``` Note the gate message changed from `"Daily prep already running"` to `"Prime run already in progress"` — `FireAsync_returns_already_running_when_gate_held` asserts on the substring "already running", which still matches. - [ ] **Step 6: Run the tests to verify they pass** ```bash dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter FullyQualifiedName~PrimeRunnerTests ``` Expected: all pass, including the five pre-existing ones. - [ ] **Step 7: Commit** ```bash git commit -m "feat(prime): branch PrimeRunner on the schedule action kind" -- \ src/ClaudeDo.Worker/Prime/PrimePrompts.cs \ src/ClaudeDo.Worker/Prime/PrimeScheduleDto.cs \ src/ClaudeDo.Worker/Prime/PrimeScheduler.cs \ src/ClaudeDo.Worker/Prime/PrimeRunner.cs \ tests/ClaudeDo.Worker.Tests/Prime/PrimeRunnerTests.cs ``` --- ### Task 4: Hub wiring and upsert validation **Files:** - Create: `src/ClaudeDo.Worker/Prime/PrimeScheduleValidation.cs` - Modify: `src/ClaudeDo.Worker/Hub/WorkerHub.cs:1029-1057` (`ListPrimeSchedules`, `UpsertPrimeSchedule`) and `:1071-1078` (`RunDailyPrepNow`) - Test: `tests/ClaudeDo.Worker.Tests/Prime/PrimeScheduleValidationTests.cs` The validation lives in its own static class rather than inline in the hub because constructing `WorkerHub` in a test needs a dozen dependencies; a pure function needs none. - [ ] **Step 1: Write the failing test** Create `tests/ClaudeDo.Worker.Tests/Prime/PrimeScheduleValidationTests.cs`: ```csharp using ClaudeDo.Data.Models; using ClaudeDo.Worker.Prime; namespace ClaudeDo.Worker.Tests.Prime; public class PrimeScheduleValidationTests { private static PrimeScheduleDto Dto(PrimeActionKind kind, string? prompt) => new(Guid.NewGuid(), 31, new TimeSpan(7, 0, 0), true, null, prompt, kind); [Theory] [InlineData(null)] [InlineData("")] [InlineData(" ")] public void Custom_without_a_prompt_is_rejected(string? prompt) { var error = PrimeScheduleValidation.Validate(Dto(PrimeActionKind.Custom, prompt)); Assert.NotNull(error); } [Fact] public void Custom_with_a_prompt_is_accepted() { Assert.Null(PrimeScheduleValidation.Validate(Dto(PrimeActionKind.Custom, "do the thing"))); } [Fact] public void Ping_without_a_prompt_is_accepted() { Assert.Null(PrimeScheduleValidation.Validate(Dto(PrimeActionKind.Ping, null))); } [Fact] public void FillMyDay_without_a_prompt_is_accepted() { Assert.Null(PrimeScheduleValidation.Validate(Dto(PrimeActionKind.FillMyDay, null))); } } ``` - [ ] **Step 2: Run the test to verify it fails** ```bash dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter FullyQualifiedName~PrimeScheduleValidationTests ``` Expected: compile error — `PrimeScheduleValidation` does not exist. - [ ] **Step 3: Write the validator** Create `src/ClaudeDo.Worker/Prime/PrimeScheduleValidation.cs`: ```csharp using ClaudeDo.Data.Models; namespace ClaudeDo.Worker.Prime; public static class PrimeScheduleValidation { /// Returns an error message, or null when the schedule is safe to persist. public static string? Validate(PrimeScheduleDto dto) { if (dto.Kind == PrimeActionKind.Custom && string.IsNullOrWhiteSpace(dto.PromptOverride)) return "A custom Prime schedule needs a prompt."; return null; } } ``` - [ ] **Step 4: Run the test to verify it passes** ```bash dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter FullyQualifiedName~PrimeScheduleValidationTests ``` Expected: 6 passed. - [ ] **Step 5: Wire the hub** In `src/ClaudeDo.Worker/Hub/WorkerHub.cs`: `ListPrimeSchedules` — add `e.Kind` to the projection: ```csharp return rows.Select(e => new PrimeScheduleDto( e.Id, (int)e.Days, e.TimeOfDay, e.Enabled, e.LastRunAt, e.PromptOverride, e.Kind)).ToList(); ``` `UpsertPrimeSchedule` — validate first, set `Kind` on the entity, and return it: ```csharp public async Task UpsertPrimeSchedule(PrimeScheduleDto dto) { var error = PrimeScheduleValidation.Validate(dto); if (error is not null) throw new HubException(error); using var ctx = _dbFactory.CreateDbContext(); var repo = new PrimeScheduleRepository(ctx); var existing = await repo.GetAsync(dto.Id); var entity = new ClaudeDo.Data.Models.PrimeScheduleEntity { Id = dto.Id == Guid.Empty ? Guid.NewGuid() : dto.Id, Days = (ClaudeDo.Data.Models.PrimeDays)dto.Days, TimeOfDay = dto.TimeOfDay, Enabled = dto.Enabled, PromptOverride = dto.PromptOverride, Kind = dto.Kind, CreatedAt = existing?.CreatedAt ?? DateTimeOffset.UtcNow, LastRunAt = existing?.LastRunAt, }; await repo.UpsertAsync(entity); _primeSignal.Signal(); return new PrimeScheduleDto(entity.Id, (int)entity.Days, entity.TimeOfDay, entity.Enabled, entity.LastRunAt, entity.PromptOverride, entity.Kind); } ``` `HubException` comes from `Microsoft.AspNetCore.SignalR`; check the file's usings and add it if missing. `RunDailyPrepNow` — pin the synthetic schedule to `FillMyDay`. The "Plan day" button means "fill my day" regardless of how the schedules are configured: ```csharp var schedule = new PrimeScheduleDto( Guid.Empty, 0, TimeSpan.Zero, true, null, null, ClaudeDo.Data.Models.PrimeActionKind.FillMyDay); ``` - [ ] **Step 6: Build and run the whole Worker suite** ```bash dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release ``` Expected: build succeeds, all tests pass. `tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs:141-143` uses the **Ui** DTO in an `IWorkerClient` fake and only ever constructs empty lists, so it needs no change until Task 5 changes the Ui DTO — recheck it after that task. - [ ] **Step 7: Commit** ```bash git commit -m "feat(prime): carry the action kind through the hub and validate custom prompts" -- \ src/ClaudeDo.Worker/Prime/PrimeScheduleValidation.cs \ src/ClaudeDo.Worker/Hub/WorkerHub.cs \ tests/ClaudeDo.Worker.Tests/Prime/PrimeScheduleValidationTests.cs ``` --- ### Task 5: UI viewmodels **Files:** - Modify: `src/ClaudeDo.Ui/Services/PrimeScheduleDto.cs` - Modify: `src/ClaudeDo.Ui/ViewModels/Modals/Settings/PrimeScheduleRowViewModel.cs` - Modify: `src/ClaudeDo.Ui/ViewModels/Modals/Settings/PrimeClaudeTabViewModel.cs` - Test: `tests/ClaudeDo.Ui.Tests/ViewModels/PrimeClaudeTabViewModelTests.cs` - [ ] **Step 1: Extend the Ui DTO** `ClaudeDo.Ui` already references `ClaudeDo.Data`, so it uses the same enum — no duplicate. In `src/ClaudeDo.Ui/Services/PrimeScheduleDto.cs`: ```csharp using ClaudeDo.Data.Models; namespace ClaudeDo.Ui.Services; public sealed record PrimeScheduleDto( Guid Id, int Days, TimeSpan TimeOfDay, bool Enabled, DateTimeOffset? LastRunAt, string? PromptOverride, PrimeActionKind Kind); ``` Leave `PrimeFiredEvent` in the file unchanged. - [ ] **Step 2: Write the failing tests** In `tests/ClaudeDo.Ui.Tests/ViewModels/PrimeClaudeTabViewModelTests.cs`, add `using ClaudeDo.Data.Models;` and replace the `Dto` helper (line 30-31): ```csharp private static PrimeScheduleDto Dto(Guid id, int days, TimeSpan time, PrimeActionKind kind = PrimeActionKind.FillMyDay, string? prompt = null) => new(id, days, time, true, null, prompt, kind); ``` Append these tests to the class: ```csharp [Fact] public void AddSchedule_Defaults_To_Ping() { var vm = new PrimeClaudeTabViewModel(new FakeApi()); vm.AddScheduleCommand.Execute(null); Assert.Equal(PrimeActionKind.Ping, vm.Rows[0].Kind); Assert.True(vm.Rows[0].IsPing); Assert.False(vm.Rows[0].IsFillMyDay); Assert.False(vm.Rows[0].IsCustom); } [Fact] public void Setting_IsCustom_Flips_Kind_And_Clears_The_Others() { var vm = new PrimeClaudeTabViewModel(new FakeApi()); vm.AddScheduleCommand.Execute(null); var row = vm.Rows[0]; row.IsCustom = true; Assert.Equal(PrimeActionKind.Custom, row.Kind); Assert.False(row.IsPing); Assert.False(row.IsFillMyDay); } [Fact] public void Row_RadioGroup_Is_Unique_Per_Row() { var vm = new PrimeClaudeTabViewModel(new FakeApi()); vm.AddScheduleCommand.Execute(null); vm.AddScheduleCommand.Execute(null); Assert.NotEqual(vm.Rows[0].RadioGroup, vm.Rows[1].RadioGroup); } [Fact] public void Validate_Rejects_Custom_Without_A_Prompt() { var vm = new PrimeClaudeTabViewModel(new FakeApi()); vm.AddScheduleCommand.Execute(null); vm.Rows[0].IsCustom = true; vm.Rows[0].PromptOverride = " "; Assert.NotNull(vm.Validate()); } [Fact] public void Validate_Accepts_Custom_With_A_Prompt() { var vm = new PrimeClaudeTabViewModel(new FakeApi()); vm.AddScheduleCommand.Execute(null); vm.Rows[0].IsCustom = true; vm.Rows[0].PromptOverride = "queue the oldest task"; Assert.Null(vm.Validate()); } [Fact] public void AnyFillMyDay_Tracks_Kind_And_Row_Changes() { var vm = new PrimeClaudeTabViewModel(new FakeApi()); Assert.False(vm.AnyFillMyDay); vm.AddScheduleCommand.Execute(null); Assert.False(vm.AnyFillMyDay); vm.Rows[0].IsFillMyDay = true; Assert.True(vm.AnyFillMyDay); vm.RemoveScheduleCommand.Execute(vm.Rows[0]); Assert.False(vm.AnyFillMyDay); } [Fact] public async Task Save_Round_Trips_The_Kind() { var api = new FakeApi(); var vm = new PrimeClaudeTabViewModel(api); vm.AddScheduleCommand.Execute(null); vm.Rows[0].IsCustom = true; vm.Rows[0].PromptOverride = "do the thing"; await vm.SaveAsync(); Assert.Equal(PrimeActionKind.Custom, api.Upserts[0].Kind); } ``` - [ ] **Step 3: Run the tests to verify they fail** ```bash dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release --filter FullyQualifiedName~PrimeClaudeTabViewModelTests ``` Expected: compile errors — `Kind`, `IsPing`, `RadioGroup`, `AnyFillMyDay` do not exist. - [ ] **Step 4: Extend the row viewmodel** In `src/ClaudeDo.Ui/ViewModels/Modals/Settings/PrimeScheduleRowViewModel.cs`, add `using ClaudeDo.Data.Models;` and `using ClaudeDo.Ui.Localization;`, then: Add the observable property next to the others: ```csharp [ObservableProperty] private PrimeActionKind _kind; ``` Add the radio-binding members after `TimeText`: ```csharp /// Per-row group name. A shared GroupName would make every schedule in the /// ItemsControl share one selection. public string RadioGroup => $"prime-kind-{Id}"; // Three booleans rather than an enum-to-bool converter: RadioButton.IsChecked binds // TwoWay to a bool, and compiled bindings stay simple. public bool IsPing { get => Kind == PrimeActionKind.Ping; set { if (value) Kind = PrimeActionKind.Ping; } } public bool IsFillMyDay { get => Kind == PrimeActionKind.FillMyDay; set { if (value) Kind = PrimeActionKind.FillMyDay; } } public bool IsCustom { get => Kind == PrimeActionKind.Custom; set { if (value) Kind = PrimeActionKind.Custom; } } public bool ShowPrompt => Kind != PrimeActionKind.Ping; public string PromptLabel => Kind == PrimeActionKind.Custom ? Loc.T("settings.prime.customPromptLabel") : Loc.T("settings.prime.promptOverrideLabel"); public string PromptPlaceholder => Kind == PrimeActionKind.Custom ? Loc.T("settings.prime.customPromptPlaceholder") : Loc.T("settings.prime.promptOverridePlaceholder"); partial void OnKindChanged(PrimeActionKind value) { OnPropertyChanged(nameof(IsPing)); OnPropertyChanged(nameof(IsFillMyDay)); OnPropertyChanged(nameof(IsCustom)); OnPropertyChanged(nameof(ShowPrompt)); OnPropertyChanged(nameof(PromptLabel)); OnPropertyChanged(nameof(PromptPlaceholder)); } ``` In the constructor, after `PromptOverride = dto.PromptOverride;`: ```csharp Kind = dto.Kind; ``` And `ToDto`: ```csharp public PrimeScheduleDto ToDto() => new(Id, DaysMask(), TimeOfDay, Enabled, LastRunAt, string.IsNullOrWhiteSpace(PromptOverride) ? null : PromptOverride, Kind); ``` - [ ] **Step 5: Extend the tab viewmodel** In `src/ClaudeDo.Ui/ViewModels/Modals/Settings/PrimeClaudeTabViewModel.cs`, add `using System.Collections.Specialized;`, `using System.ComponentModel;` and `using ClaudeDo.Data.Models;`, then: ```csharp [ObservableProperty] private bool _anyFillMyDay; public PrimeClaudeTabViewModel(IPrimeScheduleApi api) { _api = api; Rows.CollectionChanged += OnRowsChanged; } private void OnRowsChanged(object? sender, NotifyCollectionChangedEventArgs e) { if (e.OldItems is not null) foreach (PrimeScheduleRowViewModel r in e.OldItems) r.PropertyChanged -= OnRowChanged; if (e.NewItems is not null) foreach (PrimeScheduleRowViewModel r in e.NewItems) r.PropertyChanged += OnRowChanged; UpdateAnyFillMyDay(); } private void OnRowChanged(object? sender, PropertyChangedEventArgs e) { if (e.PropertyName == nameof(PrimeScheduleRowViewModel.Kind)) UpdateAnyFillMyDay(); } private void UpdateAnyFillMyDay() => AnyFillMyDay = Rows.Any(r => r.Kind == PrimeActionKind.FillMyDay); ``` Replace the existing expression-bodied constructor (line 17) with the block above. In `LoadAsync`, unsubscribe before clearing — `Clear()` raises a Reset with no `OldItems`, so the handler above cannot detach them: ```csharp public async Task LoadAsync() { foreach (var r in Rows) r.PropertyChanged -= OnRowChanged; Rows.Clear(); _initialIds.Clear(); var list = await _api.ListAsync(); foreach (var dto in list) { Rows.Add(new PrimeScheduleRowViewModel(dto, isExisting: true)); _initialIds.Add(dto.Id); } } ``` In `Validate`, add the custom-prompt rule inside the loop, after the time check: ```csharp if (r.Kind == PrimeActionKind.Custom && string.IsNullOrWhiteSpace(r.PromptOverride)) return $"Schedule {r.TimeOfDay:hh\\:mm}: a custom action needs a prompt."; ``` In `AddSchedule`, the new DTO gains the kind: ```csharp var dto = new PrimeScheduleDto( Id: Guid.NewGuid(), Days: 31, // Mon–Fri TimeOfDay: new TimeSpan(7, 0, 0), Enabled: true, LastRunAt: null, PromptOverride: null, Kind: PrimeActionKind.Ping); ``` - [ ] **Step 6: Run the tests to verify they pass** ```bash dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release ``` Expected: build succeeds, all Ui tests pass. If `tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs:155-157` or `tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs:141-143` fail to compile, they construct a `PrimeScheduleDto` positionally — add `PrimeActionKind.Ping` as the last argument. (At plan time both only build empty lists and need no change.) Note on flakiness: `Ui.Tests` has a known order-dependent failure mode. If a test outside `PrimeClaudeTabViewModelTests` fails, re-run that test alone before blaming this change. - [ ] **Step 7: Commit** ```bash git commit -m "feat(prime): add the action kind to the settings viewmodels" -- \ src/ClaudeDo.Ui/Services/PrimeScheduleDto.cs \ src/ClaudeDo.Ui/ViewModels/Modals/Settings/PrimeScheduleRowViewModel.cs \ src/ClaudeDo.Ui/ViewModels/Modals/Settings/PrimeClaudeTabViewModel.cs \ tests/ClaudeDo.Ui.Tests/ViewModels/PrimeClaudeTabViewModelTests.cs ``` --- ### Task 6: Locale keys **Files:** - Modify: `src/ClaudeDo.Localization/locales/en.json:90-104` (the `settings.prime` block) - Modify: `src/ClaudeDo.Localization/locales/de.json:90-104` - [ ] **Step 1: Add the English keys** In `en.json`, inside `settings.prime`, after `"dailyPrepMaxTasks"`: ```json "actionLabel": "Action", "actionPing": "Ping", "actionFillMyDay": "Fill My Day", "actionCustom": "Custom prompt", "actionHint": "Ping only opens the usage window. Fill My Day runs the daily prep. Custom runs your own prompt with the ClaudeDo tools.", "customPromptLabel": "Prompt", "customPromptPlaceholder": "The full prompt to run for this schedule (required)", "maxTasksDisabledHint": "Only applies to Fill My Day schedules.", ``` Also update the tab description, which currently claims every schedule is a ping: ```json "description": "Run a single non-interactive Claude action on the days you choose, at a chosen time. Only runs while ClaudeDo is open. If the app starts within 30 minutes of the target time, it fires immediately.", ``` - [ ] **Step 2: Add the German keys** In `de.json`, inside `settings.prime`, same positions: ```json "actionLabel": "Aktion", "actionPing": "Ping", "actionFillMyDay": "Mein Tag füllen", "actionCustom": "Eigener Prompt", "actionHint": "Ping öffnet nur das Nutzungsfenster. „Mein Tag füllen“ startet die Tagesplanung. „Eigener Prompt“ führt deinen eigenen Prompt mit den ClaudeDo-Tools aus.", "customPromptLabel": "Prompt", "customPromptPlaceholder": "Der vollständige Prompt für diesen Zeitplan (Pflichtfeld)", "maxTasksDisabledHint": "Gilt nur für Zeitpläne mit „Mein Tag füllen“.", ``` ```json "description": "Führt an den von dir gewählten Tagen zu einer bestimmten Zeit eine einzelne nicht-interaktive Claude-Aktion aus. Läuft nur, solange ClaudeDo geöffnet ist. Wenn die App innerhalb von 30 Minuten vor der Zielzeit startet, wird sofort ausgelöst.", ``` - [ ] **Step 3: Run the parity test** ```bash dotnet test tests/ClaudeDo.Localization.Tests/ClaudeDo.Localization.Tests.csproj -c Release ``` Expected: all pass. A failure here means an en/de key mismatch — fix the missing side. - [ ] **Step 4: Commit** ```bash git commit -m "feat(i18n): add Prime action-kind strings in en and de" -- \ src/ClaudeDo.Localization/locales/en.json \ src/ClaudeDo.Localization/locales/de.json ``` --- ### Task 7: Settings view — radio group and conditional fields **Files:** - Modify: `src/ClaudeDo.Ui/Views/Modals/SettingsModalView.axaml:300-316` There is no headless test for this — the verification is a build plus the user's visual pass. - [ ] **Step 1: Add the radio group and make the prompt box conditional** Replace lines 301-305 (the `field-label` TextBlock and the prompt `TextBox` inside the schedule `DataTemplate`) with: ```xml ``` `PlaceholderText` stays — it is the same property the file already uses on this TextBox; only the value changes from the `{loc:Tr}` markup extension to a binding, because the string now depends on the selected kind. - [ ] **Step 2: Add the action hint under the description** After the existing description TextBlock (line 270-271): ```xml ``` - [ ] **Step 3: Gate the max-tasks field** Replace lines 312-316 with: ```xml ``` - [ ] **Step 4: Build** ```bash dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release ``` Expected: Build succeeded. A compiled-binding error here names the property it could not resolve — check it against Task 5. - [ ] **Step 5: Commit** ```bash git commit -m "feat(ui): add the Prime action radio group to settings" -- \ src/ClaudeDo.Ui/Views/Modals/SettingsModalView.axaml ``` --- ### Task 8: Documentation **Files:** - Modify: `src/ClaudeDo.Data/CLAUDE.md:17` (the `PrimeScheduleEntity` line) - Modify: `src/ClaudeDo.Worker/CLAUDE.md` (the "Daily Prep (Prime Claude)" section and the `Prime/` folder line) - Modify: `src/ClaudeDo.Ui/CLAUDE.md` (the `SettingsModalViewModel` row) - [ ] **Step 1: Update `src/ClaudeDo.Data/CLAUDE.md`** Replace the `PrimeScheduleEntity` bullet: ```markdown - **PrimeScheduleEntity** — Id, Days (`[Flags] PrimeDays` weekday bitmask, column `days_of_week`), TimeOfDay, Enabled, LastRunAt, PromptOverride, Kind (`PrimeActionKind`, column `action_kind`, default `ping`), CreatedAt. Recurs on selected weekdays; no date range. `PromptOverride`'s role depends on `Kind`: ignored for `Ping`, appended to the daily-prep prompt for `FillMyDay`, and the entire prompt for `Custom`. ``` - [ ] **Step 2: Update `src/ClaudeDo.Worker/CLAUDE.md`** In the folder layout, the `Prime/` line becomes: ``` Prime/ — "Prime Claude" schedules: PrimeScheduler, PrimeRunner, PrimePrompts, DailyPrepPrompt, PrimeScheduleValidation, NextDueCalculator, PrimeScheduleSignal ``` Rename the "## Daily Prep (Prime Claude)" section to "## Prime Claude" and put this in front of the existing PrimeScheduler/PrimeRunner paragraphs: ```markdown Every schedule carries a `PrimeActionKind` (`Ping` — the default for new schedules — `FillMyDay`, or `Custom`). `PrimeRunner.FireAsync` holds one `SemaphoreSlim` gate for **all** kinds, then branches: | Kind | Prompt | Args | Log + `Prep*` events | |---|---|---|---| | `Ping` | `PrimePrompts.PingPrompt`, fixed | `--max-turns 1 --strict-mcp-config` (no MCP server loads at all) | none | | `FillMyDay` | `DailyPrepPrompt.BuildPrompt` (+ the schedule's prompt as an *addition*) | the two MyDay MCP tools | `daily-prep.log`, truncated per run, + `PrepStarted`/`PrepLine`/`PrepFinished` | | `Custom` | the schedule's prompt *verbatim* | `--allowedTools mcp__claudedo` — no Read/Write/Edit/Bash | none | Ping and Custom deliberately write **no** log and raise **no** `Prep*` events: the prep log belongs to the MyDay selection and would otherwise be overwritten with ping noise. All three kinds still broadcast `PrimeFired` and update `last_run_at`. `WorkerHub.RunDailyPrepNow` (the "Plan day" button) pins its synthetic schedule to `FillMyDay` — the button means "fill my day" no matter how the schedules are configured. `WorkerHub.UpsertPrimeSchedule` runs `PrimeScheduleValidation.Validate` first, so a `Custom` schedule can never be saved without a prompt. ``` - [ ] **Step 3: Update `src/ClaudeDo.Ui/CLAUDE.md`** Append to the `SettingsModalViewModel` table row: ```markdown The Prime Claude tab gives each schedule row a three-way action radio group (Ping / Fill My Day / Custom) — `RadioButton.GroupName` is bound to a **per-row** `RadioGroup` string, since a shared group name would make every schedule share one selection. `DailyPrepMaxTasks` is disabled unless some row is `FillMyDay`. ``` - [ ] **Step 4: Commit** ```bash git commit -m "docs(prime): document the schedule action kinds" -- \ src/ClaudeDo.Data/CLAUDE.md \ src/ClaudeDo.Worker/CLAUDE.md \ src/ClaudeDo.Ui/CLAUDE.md ``` --- ### Task 9: Full verification - [ ] **Step 1: Build both entry points** ```bash dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release ``` Expected: both succeed. - [ ] **Step 2: Run every affected suite** ```bash dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release dotnet test tests/ClaudeDo.Localization.Tests/ClaudeDo.Localization.Tests.csproj -c Release dotnet test tests/ClaudeDo.Data.Tests/ClaudeDo.Data.Tests.csproj -c Release ``` Expected: all green. Report the actual pass counts — do not claim success without the output. - [ ] **Step 3: Report the visual-verification gap** State explicitly that the following were **not** verified and need the user's eyes: - the radio group's layout inside the schedule card - the prompt box appearing/disappearing and its label switching when the kind changes - the greyed-out `DailyPrepMaxTasks` field with its hint - that existing schedules now show **Ping** selected after the migration --- ## Notes for the implementer - **Never `git add -A`.** Every commit in this plan stages explicit paths. The working tree contains unrelated WIP from parallel sessions. - **`.slnx` needs .NET 9.** Build individual `.csproj` files with `-c Release`; a running Worker locks the `Debug` output. - Do not push. Committing is fine; pushing needs the user's explicit go-ahead.