# 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/<timestamp>_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;

/// <summary>What a Prime schedule actually does when it fires.</summary>
public enum PrimeActionKind
{
    /// <summary>One throwaway turn that only opens the usage window. Default for new schedules.</summary>
    Ping,
    /// <summary>Run the daily prep prompt and let Claude fill MyDay.</summary>
    FillMyDay,
    /// <summary>Run the schedule's own prompt with the ClaudeDo MCP tools available.</summary>
    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<PrimeActionKind, string> 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/<timestamp>_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 `<timestamp>_AddPrimeActionKind.cs`. It must be exactly one `AddColumn`:

```csharp
migrationBuilder.AddColumn<string>(
    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<string>? 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;

/// <summary>Prompts and CLI args for the non-daily-prep Prime actions.
/// The daily-prep ones live in <see cref="DailyPrepPrompt"/>.</summary>
public static class PrimePrompts
{
    /// <summary>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.</summary>
    public const string PingPrompt = "Reply with the single word: ok";

    /// <summary>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.</summary>
    public static IReadOnlyList<string> BuildPingArgs() =>
    [
        "-p", "--output-format", "stream-json", "--verbose",
        "--permission-mode", "default",
        "--max-turns", "1",
        "--strict-mcp-config",
    ];

    /// <summary>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.</summary>
    public static IReadOnlyList<string> 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<PrimeRunOutcome> 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<PrimeRunOutcome> 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<PrimeRunOutcome> RunPingAsync(CancellationToken ct) =>
        RunQuietAsync(PrimePrompts.BuildPingArgs(), PrimePrompts.PingPrompt, "Ping complete", "Ping", ct);

    private Task<PrimeRunOutcome> 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);
    }

    /// <summary>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.</summary>
    private async Task<PrimeRunOutcome> RunQuietAsync(
        IReadOnlyList<string> 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
{
    /// <summary>Returns an error message, or null when the schedule is safe to persist.</summary>
    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<PrimeScheduleDto> 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
    /// <summary>Per-row group name. A shared GroupName would make every schedule in the
    /// ItemsControl share one selection.</summary>
    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
                        <TextBlock Classes="field-label" Text="{loc:Tr settings.prime.actionLabel}"/>
                        <StackPanel Orientation="Horizontal" Spacing="16">
                          <RadioButton GroupName="{Binding RadioGroup}"
                                       Content="{loc:Tr settings.prime.actionPing}"
                                       IsChecked="{Binding IsPing, Mode=TwoWay}"/>
                          <RadioButton GroupName="{Binding RadioGroup}"
                                       Content="{loc:Tr settings.prime.actionFillMyDay}"
                                       IsChecked="{Binding IsFillMyDay, Mode=TwoWay}"/>
                          <RadioButton GroupName="{Binding RadioGroup}"
                                       Content="{loc:Tr settings.prime.actionCustom}"
                                       IsChecked="{Binding IsCustom, Mode=TwoWay}"/>
                        </StackPanel>
                        <TextBlock Classes="field-label" Text="{Binding PromptLabel}"
                                   IsVisible="{Binding ShowPrompt}"/>
                        <TextBox Text="{Binding PromptOverride, Mode=TwoWay}"
                                 PlaceholderText="{Binding PromptPlaceholder}"
                                 IsVisible="{Binding ShowPrompt}"
                                 AcceptsReturn="True" TextWrapping="Wrap"
                                 MinHeight="48" MaxHeight="120"/>
```

`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
              <TextBlock Classes="meta" TextWrapping="Wrap"
                         Text="{loc:Tr settings.prime.actionHint}"/>
```

- [ ] **Step 3: Gate the max-tasks field**

Replace lines 312-316 with:

```xml
              <StackPanel Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
                <TextBlock Classes="field-label" Text="{loc:Tr settings.prime.dailyPrepMaxTasks}" VerticalAlignment="Center"/>
                <NumericUpDown Minimum="1" Maximum="50" Increment="1" Width="100" FormatString="0"
                               IsEnabled="{Binding Prime.AnyFillMyDay}"
                               Value="{Binding Prime.DailyPrepMaxTasks, Mode=TwoWay, Converter={StaticResource KeepLastNumber}}"/>
                <TextBlock Classes="meta" VerticalAlignment="Center"
                           Text="{loc:Tr settings.prime.maxTasksDisabledHint}"
                           IsVisible="{Binding !Prime.AnyFillMyDay}"/>
              </StackPanel>
```

- [ ] **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.
