# Prime Recurring Weekday Schedule — 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:** Replace the Prime schedule's date-range model with a recurring weekday model — pick a set of weekdays plus a time, and the ping fires on the next eligible day the worker is running.

**Architecture:** A `[Flags] PrimeDays` weekday bitmask stored as a single `days_of_week` int column replaces `StartDate`/`EndDate`/`WorkdaysOnly`. `NextDueCalculator` walks forward to the next selected weekday; the existing 30-minute catch-up and already-fired-today logic are untouched. UI swaps the range picker + Mon–Fri checkbox for seven toggle buttons. Both SignalR DTO copies carry a single `int Days`.

**Tech Stack:** .NET 8, EF Core (SQLite), Avalonia 12 (CommunityToolkit.Mvvm), SignalR, xUnit.

**Spec:** `docs/superpowers/specs/2026-06-02-prime-recurring-weekdays-design.md`

**Build/test note:** `dotnet build ClaudeDo.slnx` needs .NET 9; on .NET 8 build individual csproj. Commands in this plan use the per-project form.

---

## File Structure

- `src/ClaudeDo.Data/Models/PrimeDays.cs` — **new**, `[Flags]` enum.
- `src/ClaudeDo.Data/Models/PrimeScheduleEntity.cs` — swap fields.
- `src/ClaudeDo.Data/Configuration/PrimeScheduleEntityConfiguration.cs` — column mapping.
- `src/ClaudeDo.Data/Migrations/*` — new migration + snapshot.
- `src/ClaudeDo.Data/Repositories/PrimeScheduleRepository.cs` — upsert fields + ordering.
- `src/ClaudeDo.Worker/Prime/PrimeScheduleDto.cs` — `int Days`.
- `src/ClaudeDo.Worker/Prime/NextDueCalculator.cs` — weekday eligibility.
- `src/ClaudeDo.Worker/Prime/PrimeScheduler.cs` — `ToDto` mapping.
- `src/ClaudeDo.Worker/Hub/WorkerHub.cs` — list/upsert mapping.
- `src/ClaudeDo.Ui/Services/PrimeScheduleDto.cs` — `int Days`.
- `src/ClaudeDo.Ui/ViewModels/Modals/Settings/PrimeScheduleRowViewModel.cs` — 7 day bools.
- `src/ClaudeDo.Ui/ViewModels/Modals/Settings/PrimeClaudeTabViewModel.cs` — defaults + validation.
- `src/ClaudeDo.Ui/Design/IslandStyles.axaml` — `day-toggle` style class.
- `src/ClaudeDo.Ui/Views/Modals/SettingsModalView.axaml` — row template.
- Tests: `NextDueCalculatorTests`, `PrimeSchedulerTests`, `PrimeScheduleRepositoryTests`, `PrimeClaudeTabViewModelTests`.
- Docs: `src/ClaudeDo.Data/CLAUDE.md`, root `CLAUDE.md`.

---

## Task 1: PrimeDays enum + entity + configuration

**Files:**
- Create: `src/ClaudeDo.Data/Models/PrimeDays.cs`
- Modify: `src/ClaudeDo.Data/Models/PrimeScheduleEntity.cs`
- Modify: `src/ClaudeDo.Data/Configuration/PrimeScheduleEntityConfiguration.cs`

- [ ] **Step 1: Create the flags enum**

`src/ClaudeDo.Data/Models/PrimeDays.cs`:

```csharp
namespace ClaudeDo.Data.Models;

[Flags]
public enum PrimeDays
{
    None = 0,
    Monday = 1,
    Tuesday = 2,
    Wednesday = 4,
    Thursday = 8,
    Friday = 16,
    Saturday = 32,
    Sunday = 64,
    Weekdays = Monday | Tuesday | Wednesday | Thursday | Friday, // 31
    All = Weekdays | Saturday | Sunday, // 127
}
```

- [ ] **Step 2: Swap entity fields**

In `src/ClaudeDo.Data/Models/PrimeScheduleEntity.cs`, remove `StartDate`, `EndDate`, `WorkdaysOnly` and add `Days`. Result:

```csharp
namespace ClaudeDo.Data.Models;

public sealed class PrimeScheduleEntity
{
    public Guid Id { get; set; } = Guid.NewGuid();
    public PrimeDays Days { get; set; } = PrimeDays.Weekdays;
    public TimeSpan TimeOfDay { get; set; }
    public bool Enabled { get; set; } = true;
    public DateTimeOffset? LastRunAt { get; set; }
    public string? PromptOverride { get; set; }
    public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
}
```

- [ ] **Step 3: Update entity configuration**

In `src/ClaudeDo.Data/Configuration/PrimeScheduleEntityConfiguration.cs`, replace the `start_date`/`end_date`/`workdays_only` property lines with a `days_of_week` mapping (EF maps the enum to INTEGER automatically):

```csharp
        builder.Property(s => s.Days).HasColumnName("days_of_week")
            .IsRequired().HasDefaultValue(PrimeDays.Weekdays);
        builder.Property(s => s.TimeOfDay).HasColumnName("time_of_day").IsRequired();
        builder.Property(s => s.Enabled).HasColumnName("enabled").IsRequired().HasDefaultValue(true);
```

Leave `Id`, `LastRunAt`, `PromptOverride`, `CreatedAt` mappings unchanged. Add `using ClaudeDo.Data.Models;` if not present (it already is).

- [ ] **Step 4: Build the Data project**

Run: `dotnet build src/ClaudeDo.Data/ClaudeDo.Data.csproj`
Expected: FAILS — `PrimeScheduleRepository`, snapshot, etc. still reference removed fields. That is expected; Tasks 2–3 fix it. (If you prefer a clean build gate, proceed to Task 2 before building.)

- [ ] **Step 5: Commit**

```bash
git add src/ClaudeDo.Data/Models/PrimeDays.cs src/ClaudeDo.Data/Models/PrimeScheduleEntity.cs src/ClaudeDo.Data/Configuration/PrimeScheduleEntityConfiguration.cs
git commit -m "feat(data): model Prime schedule as weekday bitmask"
```

---

## Task 2: Repository

**Files:**
- Modify: `src/ClaudeDo.Data/Repositories/PrimeScheduleRepository.cs`

- [ ] **Step 1: Update `ListAsync` ordering**

The old ordering used `StartDate`. Order by `TimeOfDay`:

```csharp
    public async Task<IReadOnlyList<PrimeScheduleEntity>> ListAsync(CancellationToken ct = default)
    {
        var rows = await _context.PrimeSchedules.AsNoTracking()
            .OrderBy(s => s.TimeOfDay)
            .ToListAsync(ct);
        return rows;
    }
```

- [ ] **Step 2: Update `UpsertAsync` field copy**

Replace the three removed-field assignments with `Days`:

```csharp
        else
        {
            existing.Days = entity.Days;
            existing.TimeOfDay = entity.TimeOfDay;
            existing.Enabled = entity.Enabled;
            existing.PromptOverride = entity.PromptOverride;
        }
```

Leave `GetAsync`, `DeleteAsync`, `UpdateLastRunAsync` unchanged.

- [ ] **Step 3: Commit** (build verified after migration in Task 3)

```bash
git add src/ClaudeDo.Data/Repositories/PrimeScheduleRepository.cs
git commit -m "feat(data): persist weekday bitmask in prime schedule repo"
```

---

## Task 3: EF migration

**Files:**
- Create: `src/ClaudeDo.Data/Migrations/<timestamp>_PrimeWeekdays.cs` (generated)
- Modify: `src/ClaudeDo.Data/Migrations/ClaudeDoDbContextModelSnapshot.cs` (generated)

- [ ] **Step 1: Generate the migration**

Run from repo root:

```bash
dotnet ef migrations add PrimeWeekdays --project src/ClaudeDo.Data/ClaudeDo.Data.csproj --startup-project src/ClaudeDo.Worker/ClaudeDo.Worker.csproj
```

Expected: a new `*_PrimeWeekdays.cs` file and an updated snapshot. (If `dotnet ef` is unavailable, hand-write the migration using the body below.)

- [ ] **Step 2: Replace the generated `Up` body with an explicit backfill**

EF's auto-generated drop/add would discard existing schedules' weekday intent. Edit the new migration's `Up` to add the column, backfill from `workdays_only`, then drop the old columns:

```csharp
        protected override void Up(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.AddColumn<int>(
                name: "days_of_week",
                table: "prime_schedules",
                type: "INTEGER",
                nullable: false,
                defaultValue: 31);

            migrationBuilder.Sql(
                "UPDATE prime_schedules SET days_of_week = CASE WHEN workdays_only = 1 THEN 31 ELSE 127 END;");

            migrationBuilder.DropColumn(name: "start_date", table: "prime_schedules");
            migrationBuilder.DropColumn(name: "end_date", table: "prime_schedules");
            migrationBuilder.DropColumn(name: "workdays_only", table: "prime_schedules");
        }
```

- [ ] **Step 3: Replace the generated `Down` body**

```csharp
        protected override void Down(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.AddColumn<DateOnly>(
                name: "start_date", table: "prime_schedules",
                type: "TEXT", nullable: false, defaultValue: new DateOnly(2000, 1, 1));
            migrationBuilder.AddColumn<DateOnly>(
                name: "end_date", table: "prime_schedules",
                type: "TEXT", nullable: false, defaultValue: new DateOnly(2099, 12, 31));
            migrationBuilder.AddColumn<bool>(
                name: "workdays_only", table: "prime_schedules",
                type: "INTEGER", nullable: false, defaultValue: true);

            migrationBuilder.Sql(
                "UPDATE prime_schedules SET workdays_only = CASE WHEN days_of_week = 127 THEN 0 ELSE 1 END;");

            migrationBuilder.DropColumn(name: "days_of_week", table: "prime_schedules");
        }
```

Add `using System;` at the top of the migration file if `DateOnly` defaults require it (the existing AddPrimeSchedules migration already imports `System`).

- [ ] **Step 4: Build the Data project**

Run: `dotnet build src/ClaudeDo.Data/ClaudeDo.Data.csproj`
Expected: PASS.

- [ ] **Step 5: Commit**

```bash
git add src/ClaudeDo.Data/Migrations
git commit -m "feat(data): migrate prime schedules to days_of_week bitmask"
```

---

## Task 4: Worker DTO + NextDueCalculator (TDD)

**Files:**
- Modify: `src/ClaudeDo.Worker/Prime/PrimeScheduleDto.cs`
- Modify: `src/ClaudeDo.Worker/Prime/NextDueCalculator.cs`
- Test: `tests/ClaudeDo.Worker.Tests/Prime/NextDueCalculatorTests.cs`

- [ ] **Step 1: Update the Worker DTO**

`src/ClaudeDo.Worker/Prime/PrimeScheduleDto.cs`:

```csharp
namespace ClaudeDo.Worker.Prime;

public sealed record PrimeScheduleDto(
    Guid Id,
    int Days,
    TimeSpan TimeOfDay,
    bool Enabled,
    DateTimeOffset? LastRunAt,
    string? PromptOverride);
```

- [ ] **Step 2: Rewrite the calculator tests**

Replace the entire body of `tests/ClaudeDo.Worker.Tests/Prime/NextDueCalculatorTests.cs`. Note: 2026-05-05 is a Tuesday; 2026-05-08 is a Friday; 2026-05-09/10 are Sat/Sun; 2026-05-11 is a Monday.

```csharp
using ClaudeDo.Data.Models;
using ClaudeDo.Worker.Prime;

namespace ClaudeDo.Worker.Tests.Prime;

public class NextDueCalculatorTests
{
    private static PrimeScheduleDto Schedule(
        PrimeDays days, TimeSpan time,
        bool enabled = true, DateTimeOffset? lastRun = null) =>
        new(Guid.NewGuid(), (int)days, time, enabled, lastRun, null);

    [Fact]
    public void Disabled_Schedule_Returns_Null()
    {
        var now = new DateTimeOffset(2026, 5, 5, 6, 0, 0, TimeSpan.FromHours(2));
        var s = Schedule(PrimeDays.All, new(7, 0, 0), enabled: false);
        Assert.Null(NextDueCalculator.Compute(new[] { s }, now, TimeSpan.FromMinutes(30)));
    }

    [Fact]
    public void No_Days_Selected_Returns_Null()
    {
        var now = new DateTimeOffset(2026, 5, 5, 6, 0, 0, TimeSpan.FromHours(2));
        var s = Schedule(PrimeDays.None, new(7, 0, 0));
        Assert.Null(NextDueCalculator.Compute(new[] { s }, now, TimeSpan.FromMinutes(30)));
    }

    [Fact]
    public void Future_Same_Day_Returns_Today_At_Target()
    {
        var now = new DateTimeOffset(2026, 5, 5, 6, 0, 0, TimeSpan.FromHours(2)); // Tue
        var s = Schedule(PrimeDays.All, new(7, 0, 0));
        var r = NextDueCalculator.Compute(new[] { s }, now, TimeSpan.FromMinutes(30));
        Assert.NotNull(r);
        Assert.Equal(new DateTimeOffset(2026, 5, 5, 7, 0, 0, now.Offset), r!.At);
        Assert.False(r.FireImmediately);
    }

    [Fact]
    public void Within_CatchUp_Window_Fires_Immediately()
    {
        var now = new DateTimeOffset(2026, 5, 5, 7, 15, 0, TimeSpan.FromHours(2));
        var s = Schedule(PrimeDays.All, new(7, 0, 0));
        var r = NextDueCalculator.Compute(new[] { s }, now, TimeSpan.FromMinutes(30));
        Assert.NotNull(r);
        Assert.True(r!.FireImmediately);
    }

    [Fact]
    public void Past_CatchUp_Window_Skips_To_Next_Eligible_Day()
    {
        var now = new DateTimeOffset(2026, 5, 5, 9, 0, 0, TimeSpan.FromHours(2)); // Tue
        var s = Schedule(PrimeDays.All, new(7, 0, 0));
        var r = NextDueCalculator.Compute(new[] { s }, now, TimeSpan.FromMinutes(30));
        Assert.NotNull(r);
        Assert.Equal(new DateOnly(2026, 5, 6), DateOnly.FromDateTime(r!.At.LocalDateTime));
    }

    [Fact]
    public void Weekdays_Only_Skips_Weekend()
    {
        var now = new DateTimeOffset(2026, 5, 8, 8, 0, 0, TimeSpan.FromHours(2)); // Fri, past catch-up
        var s = Schedule(PrimeDays.Weekdays, new(7, 0, 0));
        var r = NextDueCalculator.Compute(new[] { s }, now, TimeSpan.FromMinutes(30));
        Assert.NotNull(r);
        Assert.Equal(DayOfWeek.Monday, r!.At.LocalDateTime.DayOfWeek);
        Assert.Equal(new DateOnly(2026, 5, 11), DateOnly.FromDateTime(r.At.LocalDateTime));
    }

    [Fact]
    public void Single_Day_Schedule_Targets_That_Weekday()
    {
        var now = new DateTimeOffset(2026, 5, 5, 8, 0, 0, TimeSpan.FromHours(2)); // Tue, past catch-up
        var s = Schedule(PrimeDays.Friday, new(7, 0, 0));
        var r = NextDueCalculator.Compute(new[] { s }, now, TimeSpan.FromMinutes(30));
        Assert.NotNull(r);
        Assert.Equal(DayOfWeek.Friday, r!.At.LocalDateTime.DayOfWeek);
        Assert.Equal(new DateOnly(2026, 5, 8), DateOnly.FromDateTime(r.At.LocalDateTime));
    }

    [Fact]
    public void Already_Fired_Today_Skips_To_Next_Eligible_Day()
    {
        var now = new DateTimeOffset(2026, 5, 5, 6, 0, 0, TimeSpan.FromHours(2));
        var lastRun = new DateTimeOffset(2026, 5, 5, 7, 1, 0, TimeSpan.FromHours(2));
        var s = Schedule(PrimeDays.All, new(7, 0, 0), lastRun: lastRun);
        var r = NextDueCalculator.Compute(new[] { s }, now, TimeSpan.FromMinutes(30));
        Assert.NotNull(r);
        Assert.Equal(new DateOnly(2026, 5, 6), DateOnly.FromDateTime(r!.At.LocalDateTime));
    }

    [Fact]
    public void Multiple_Schedules_Returns_Earliest()
    {
        var now = new DateTimeOffset(2026, 5, 5, 6, 0, 0, TimeSpan.FromHours(2));
        var early = Schedule(PrimeDays.All, new(7, 0, 0));
        var late = Schedule(PrimeDays.All, new(9, 0, 0));
        var r = NextDueCalculator.Compute(new[] { late, early }, now, TimeSpan.FromMinutes(30));
        Assert.NotNull(r);
        Assert.Equal(early.Id, r!.Schedule.Id);
    }
}
```

- [ ] **Step 3: Run the tests to verify they fail**

Run: `dotnet test tests/ClaudeDo.Worker.Tests --filter FullyQualifiedName~NextDueCalculatorTests`
Expected: FAIL — `PrimeScheduleDto` no longer has `StartDate`/`EndDate`/`workdaysOnly`, and the calculator still references them (compile errors).

- [ ] **Step 4: Rewrite the calculator**

Replace the entire body of `src/ClaudeDo.Worker/Prime/NextDueCalculator.cs`:

```csharp
using ClaudeDo.Data.Models;

namespace ClaudeDo.Worker.Prime;

public sealed record NextDue(PrimeScheduleDto Schedule, DateTimeOffset At, bool FireImmediately);

public static class NextDueCalculator
{
    public static NextDue? Compute(
        IEnumerable<PrimeScheduleDto> schedules,
        DateTimeOffset now,
        TimeSpan catchUp)
    {
        NextDue? best = null;
        foreach (var s in schedules)
        {
            if (!s.Enabled) continue;
            var due = ComputeFor(s, now, catchUp);
            if (due is null) continue;
            if (best is null || due.At < best.At) best = due;
        }
        return best;
    }

    private static NextDue? ComputeFor(PrimeScheduleDto s, DateTimeOffset now, TimeSpan catchUp)
    {
        if ((PrimeDays)s.Days == PrimeDays.None) return null;

        var todayLocal = DateOnly.FromDateTime(now.LocalDateTime);
        var alreadyFiredToday = s.LastRunAt is { } last &&
                                DateOnly.FromDateTime(last.LocalDateTime) == todayLocal;

        if (!alreadyFiredToday && IsEligibleDay(s, todayLocal))
        {
            var todayTarget = ToOffset(todayLocal, s.TimeOfDay, now.Offset);
            if (todayTarget >= now)
                return new NextDue(s, todayTarget, false);
            if (now <= todayTarget + catchUp)
                return new NextDue(s, now, true);
        }

        var d = todayLocal.AddDays(1);
        for (int i = 0; i < 7; i++)
        {
            if (IsEligibleDay(s, d))
                return new NextDue(s, ToOffset(d, s.TimeOfDay, now.Offset), false);
            d = d.AddDays(1);
        }
        return null;
    }

    private static bool IsEligibleDay(PrimeScheduleDto s, DateOnly d) =>
        ((PrimeDays)s.Days & ToFlag(d.DayOfWeek)) != PrimeDays.None;

    private static PrimeDays ToFlag(DayOfWeek dow) => dow switch
    {
        DayOfWeek.Monday => PrimeDays.Monday,
        DayOfWeek.Tuesday => PrimeDays.Tuesday,
        DayOfWeek.Wednesday => PrimeDays.Wednesday,
        DayOfWeek.Thursday => PrimeDays.Thursday,
        DayOfWeek.Friday => PrimeDays.Friday,
        DayOfWeek.Saturday => PrimeDays.Saturday,
        DayOfWeek.Sunday => PrimeDays.Sunday,
        _ => PrimeDays.None,
    };

    private static DateTimeOffset ToOffset(DateOnly day, TimeSpan time, TimeSpan offset) =>
        new(day.Year, day.Month, day.Day, time.Hours, time.Minutes, time.Seconds, offset);
}
```

- [ ] **Step 5: Run the calculator tests**

Run: `dotnet test tests/ClaudeDo.Worker.Tests --filter FullyQualifiedName~NextDueCalculatorTests`
Expected: still FAILS to build — `PrimeScheduler.ToDto` and `WorkerHub` mappings reference removed fields. Proceed to Tasks 5–6, then re-run.

- [ ] **Step 6: Commit**

```bash
git add src/ClaudeDo.Worker/Prime/PrimeScheduleDto.cs src/ClaudeDo.Worker/Prime/NextDueCalculator.cs tests/ClaudeDo.Worker.Tests/Prime/NextDueCalculatorTests.cs
git commit -m "feat(worker): compute prime due-time from weekday bitmask"
```

---

## Task 5: PrimeScheduler.ToDto + scheduler tests

**Files:**
- Modify: `src/ClaudeDo.Worker/Prime/PrimeScheduler.cs:104-105`
- Test: `tests/ClaudeDo.Worker.Tests/Prime/PrimeSchedulerTests.cs`

- [ ] **Step 1: Update the `ToDto` mapping**

Replace the `ToDto` method in `PrimeScheduler.cs`:

```csharp
    private static PrimeScheduleDto ToDto(Data.Models.PrimeScheduleEntity e) =>
        new(e.Id, (int)e.Days, e.TimeOfDay, e.Enabled, e.LastRunAt, e.PromptOverride);
```

- [ ] **Step 2: Update scheduler test fixtures**

In `tests/ClaudeDo.Worker.Tests/Prime/PrimeSchedulerTests.cs`, every `new PrimeScheduleEntity { ... }` initializer sets `StartDate`/`EndDate`/`WorkdaysOnly`. Replace those three lines in each of the three initializers (lines ~48-52, ~89-94, ~131-136) with a single `Days` assignment. Each initializer becomes:

```csharp
            await new PrimeScheduleRepository(ctx).UpsertAsync(new PrimeScheduleEntity
            {
                Id = id,
                Days = PrimeDays.All,
                TimeOfDay = new TimeSpan(7, 0, 0),
                Enabled = true,
                CreatedAt = DateTimeOffset.UtcNow,
            });
```

Add `using ClaudeDo.Data.Models;` to the file's usings if not already present (it is, via line 1).

- [ ] **Step 3: Run scheduler + calculator tests**

Run: `dotnet test tests/ClaudeDo.Worker.Tests --filter "FullyQualifiedName~Prime"`
Expected: still build-fails until `WorkerHub` (Task 6) compiles. After Task 6, this command must PASS.

- [ ] **Step 4: Commit**

```bash
git add src/ClaudeDo.Worker/Prime/PrimeScheduler.cs tests/ClaudeDo.Worker.Tests/Prime/PrimeSchedulerTests.cs
git commit -m "test(worker): adapt prime scheduler tests to weekday model"
```

---

## Task 6: WorkerHub mapping + repository tests

**Files:**
- Modify: `src/ClaudeDo.Worker/Hub/WorkerHub.cs:488-518`
- Test: `tests/ClaudeDo.Worker.Tests/Repositories/PrimeScheduleRepositoryTests.cs`

- [ ] **Step 1: Update `ListPrimeSchedules`**

```csharp
    public async Task<List<PrimeScheduleDto>> ListPrimeSchedules()
    {
        using var ctx = _dbFactory.CreateDbContext();
        var rows = await new PrimeScheduleRepository(ctx).ListAsync();
        return rows.Select(e => new PrimeScheduleDto(
            e.Id, (int)e.Days, e.TimeOfDay, e.Enabled, e.LastRunAt, e.PromptOverride)).ToList();
    }
```

- [ ] **Step 2: Update `UpsertPrimeSchedule`**

```csharp
    public async Task<PrimeScheduleDto> UpsertPrimeSchedule(PrimeScheduleDto dto)
    {
        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,
            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);
    }
```

`DeletePrimeSchedule` is unchanged.

- [ ] **Step 3: Update repository tests**

In `tests/ClaudeDo.Worker.Tests/Repositories/PrimeScheduleRepositoryTests.cs`, replace each entity initializer's `StartDate`/`EndDate`/`WorkdaysOnly` lines with `Days = PrimeDays.Weekdays,` (drop them where only `StartDate`/`EndDate` appear). The three initializers become:

```csharp
            // Upsert_Then_List_RoundTrips
            await new PrimeScheduleRepository(ctx).UpsertAsync(new PrimeScheduleEntity
            {
                Id = id,
                Days = PrimeDays.Weekdays,
                TimeOfDay = new TimeSpan(7, 0, 0),
                Enabled = true,
                CreatedAt = DateTimeOffset.UtcNow,
            });
```

```csharp
            // UpdateLastRunAt_Persists
            await new PrimeScheduleRepository(ctx).UpsertAsync(new PrimeScheduleEntity
            {
                Id = id,
                Days = PrimeDays.Weekdays,
                TimeOfDay = new TimeSpan(7, 0, 0),
                Enabled = true,
                CreatedAt = DateTimeOffset.UtcNow,
            });
```

```csharp
            // Delete_Removes_Row
            await new PrimeScheduleRepository(ctx).UpsertAsync(new PrimeScheduleEntity
            {
                Id = id,
                Days = PrimeDays.All,
                TimeOfDay = TimeSpan.Zero,
                Enabled = true,
                CreatedAt = DateTimeOffset.UtcNow,
            });
```

Add an assertion in `Upsert_Then_List_RoundTrips` after the existing time assertion:

```csharp
        Assert.Equal(PrimeDays.Weekdays, rows[0].Days);
```

- [ ] **Step 4: Build worker + run all worker tests**

Run: `dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj && dotnet test tests/ClaudeDo.Worker.Tests`
Expected: PASS (all Prime + repository tests green).

- [ ] **Step 5: Commit**

```bash
git add src/ClaudeDo.Worker/Hub/WorkerHub.cs tests/ClaudeDo.Worker.Tests/Repositories/PrimeScheduleRepositoryTests.cs
git commit -m "feat(worker): map prime schedule weekday bitmask over the hub"
```

---

## Task 7: UI DTO + ViewModels + tests (TDD)

**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: Update the UI DTO**

`src/ClaudeDo.Ui/Services/PrimeScheduleDto.cs` (keep `PrimeFiredEvent` unchanged):

```csharp
namespace ClaudeDo.Ui.Services;

public sealed record PrimeScheduleDto(
    Guid Id,
    int Days,
    TimeSpan TimeOfDay,
    bool Enabled,
    DateTimeOffset? LastRunAt,
    string? PromptOverride);

public sealed record PrimeFiredEvent(
    Guid ScheduleId,
    bool Success,
    string Message,
    DateTimeOffset FiredAt);
```

- [ ] **Step 2: Rewrite the row VM**

Replace the body of `src/ClaudeDo.Ui/ViewModels/Modals/Settings/PrimeScheduleRowViewModel.cs`:

```csharp
using ClaudeDo.Ui.Services;
using CommunityToolkit.Mvvm.ComponentModel;

namespace ClaudeDo.Ui.ViewModels.Modals.Settings;

public sealed partial class PrimeScheduleRowViewModel : ViewModelBase
{
    private const int Mon = 1, Tue = 2, Wed = 4, Thu = 8, Fri = 16, Sat = 32, Sun = 64;

    public Guid Id { get; }
    public bool IsExisting { get; }

    [ObservableProperty] private bool _enabled;
    [ObservableProperty] private bool _monday;
    [ObservableProperty] private bool _tuesday;
    [ObservableProperty] private bool _wednesday;
    [ObservableProperty] private bool _thursday;
    [ObservableProperty] private bool _friday;
    [ObservableProperty] private bool _saturday;
    [ObservableProperty] private bool _sunday;
    [ObservableProperty] private TimeSpan _timeOfDay;
    [ObservableProperty] private DateTimeOffset? _lastRunAt;

    public string LastRunLabel => LastRunAt is { } v ? v.LocalDateTime.ToString("g") : "—";

    partial void OnLastRunAtChanged(DateTimeOffset? value) => OnPropertyChanged(nameof(LastRunLabel));

    public PrimeScheduleRowViewModel(PrimeScheduleDto dto, bool isExisting)
    {
        Id = dto.Id;
        IsExisting = isExisting;
        Enabled = dto.Enabled;
        Monday = (dto.Days & Mon) != 0;
        Tuesday = (dto.Days & Tue) != 0;
        Wednesday = (dto.Days & Wed) != 0;
        Thursday = (dto.Days & Thu) != 0;
        Friday = (dto.Days & Fri) != 0;
        Saturday = (dto.Days & Sat) != 0;
        Sunday = (dto.Days & Sun) != 0;
        TimeOfDay = dto.TimeOfDay;
        LastRunAt = dto.LastRunAt;
    }

    public int DaysMask()
    {
        int m = 0;
        if (Monday) m |= Mon;
        if (Tuesday) m |= Tue;
        if (Wednesday) m |= Wed;
        if (Thursday) m |= Thu;
        if (Friday) m |= Fri;
        if (Saturday) m |= Sat;
        if (Sunday) m |= Sun;
        return m;
    }

    public PrimeScheduleDto ToDto() =>
        new(Id, DaysMask(), TimeOfDay, Enabled, LastRunAt, null);
}
```

- [ ] **Step 3: Update the tab VM**

In `src/ClaudeDo.Ui/ViewModels/Modals/Settings/PrimeClaudeTabViewModel.cs`, replace `Validate` and `AddSchedule`:

```csharp
    public string? Validate()
    {
        foreach (var r in Rows)
        {
            if (r.DaysMask() == 0)
                return $"Schedule {r.TimeOfDay:hh\\:mm}: select at least one day.";
            if (r.TimeOfDay < TimeSpan.Zero || r.TimeOfDay >= TimeSpan.FromDays(1))
                return "Time must be between 00:00 and 23:59.";
        }
        return null;
    }
```

```csharp
    [RelayCommand]
    private void AddSchedule()
    {
        var dto = new PrimeScheduleDto(
            Id: Guid.NewGuid(),
            Days: 31, // Mon–Fri
            TimeOfDay: new TimeSpan(7, 0, 0),
            Enabled: true,
            LastRunAt: null,
            PromptOverride: null);
        Rows.Add(new PrimeScheduleRowViewModel(dto, isExisting: false));
    }
```

`LoadAsync`, `SaveAsync`, `RemoveSchedule`, `ApplyFiredEvent` are unchanged.

- [ ] **Step 4: Rewrite the tab VM tests**

Replace the body of `tests/ClaudeDo.Ui.Tests/ViewModels/PrimeClaudeTabViewModelTests.cs`:

```csharp
using ClaudeDo.Ui.Services;
using ClaudeDo.Ui.ViewModels.Modals.Settings;

namespace ClaudeDo.Ui.Tests.ViewModels;

public class PrimeClaudeTabViewModelTests
{
    private sealed class FakeApi : IPrimeScheduleApi
    {
        public List<PrimeScheduleDto> Stored { get; } = new();
        public List<PrimeScheduleDto> Upserts { get; } = new();
        public List<Guid> Deletes { get; } = new();
        public Task<List<PrimeScheduleDto>> ListAsync() => Task.FromResult(Stored.ToList());
        public Task<PrimeScheduleDto?> UpsertAsync(PrimeScheduleDto dto)
        {
            Upserts.Add(dto);
            return Task.FromResult<PrimeScheduleDto?>(dto);
        }
        public Task DeleteAsync(Guid id) { Deletes.Add(id); return Task.CompletedTask; }
    }

    private static PrimeScheduleDto Dto(Guid id, int days, TimeSpan time) =>
        new(id, days, time, true, null, null);

    [Fact]
    public async Task Load_Populates_Rows()
    {
        var api = new FakeApi();
        api.Stored.Add(Dto(Guid.NewGuid(), 31, new TimeSpan(7, 0, 0)));
        var vm = new PrimeClaudeTabViewModel(api);
        await vm.LoadAsync();
        Assert.Single(vm.Rows);
    }

    [Fact]
    public void AddSchedule_Appends_Row_With_Defaults()
    {
        var vm = new PrimeClaudeTabViewModel(new FakeApi());
        vm.AddScheduleCommand.Execute(null);
        Assert.Single(vm.Rows);
        Assert.True(vm.Rows[0].Enabled);
        Assert.True(vm.Rows[0].Monday);
        Assert.True(vm.Rows[0].Friday);
        Assert.False(vm.Rows[0].Saturday);
        Assert.Equal(new TimeSpan(7, 0, 0), vm.Rows[0].TimeOfDay);
    }

    [Fact]
    public void Row_Decomposes_And_Recomposes_Days()
    {
        var vm = new PrimeClaudeTabViewModel(new FakeApi());
        vm.AddScheduleCommand.Execute(null);
        var row = vm.Rows[0];
        Assert.Equal(31, row.DaysMask());
        row.Saturday = true;
        Assert.Equal(63, row.DaysMask());
    }

    [Fact]
    public async Task Save_Diffs_New_And_Removed_Rows()
    {
        var api = new FakeApi();
        var keptId = Guid.NewGuid();
        var deletedId = Guid.NewGuid();
        api.Stored.Add(Dto(keptId, 31, new TimeSpan(7, 0, 0)));
        api.Stored.Add(Dto(deletedId, 31, new TimeSpan(8, 0, 0)));

        var vm = new PrimeClaudeTabViewModel(api);
        await vm.LoadAsync();
        vm.RemoveScheduleCommand.Execute(vm.Rows.Single(r => r.Id == deletedId));
        vm.AddScheduleCommand.Execute(null);

        await vm.SaveAsync();

        Assert.Contains(deletedId, api.Deletes);
        Assert.Equal(2, api.Upserts.Count);
    }

    [Fact]
    public void Validate_Reports_No_Days_Selected()
    {
        var vm = new PrimeClaudeTabViewModel(new FakeApi());
        vm.AddScheduleCommand.Execute(null);
        var row = vm.Rows[0];
        row.Monday = row.Tuesday = row.Wednesday = row.Thursday = row.Friday = false;
        Assert.NotNull(vm.Validate());
    }

    [Fact]
    public void Validate_Passes_With_One_Day()
    {
        var vm = new PrimeClaudeTabViewModel(new FakeApi());
        vm.AddScheduleCommand.Execute(null);
        Assert.Null(vm.Validate());
    }
}
```

- [ ] **Step 5: Run UI tests**

Run: `dotnet test tests/ClaudeDo.Ui.Tests --filter FullyQualifiedName~PrimeClaudeTabViewModelTests`
Expected: PASS.

- [ ] **Step 6: Commit**

```bash
git add 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
git commit -m "feat(ui): drive prime schedule rows from weekday toggles"
```

---

## Task 8: XAML — toggle-button row

**Files:**
- Modify: `src/ClaudeDo.Ui/Design/IslandStyles.axaml`
- Modify: `src/ClaudeDo.Ui/Views/Modals/SettingsModalView.axaml`

- [ ] **Step 1: Add a `day-toggle` style class**

Append to `src/ClaudeDo.Ui/Design/IslandStyles.axaml` (inside the root `<Styles>` element, alongside the other style selectors). Uses existing dynamic-resource tokens — no hardcoded colors:

```xml
  <Style Selector="ToggleButton.day-toggle">
    <Setter Property="MinWidth" Value="34"/>
    <Setter Property="Padding" Value="6,4"/>
    <Setter Property="Margin" Value="0"/>
    <Setter Property="HorizontalContentAlignment" Value="Center"/>
    <Setter Property="Background" Value="{DynamicResource DeepBrush}"/>
    <Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
    <Setter Property="BorderBrush" Value="{DynamicResource LineBrush}"/>
    <Setter Property="BorderThickness" Value="1"/>
    <Setter Property="CornerRadius" Value="4"/>
  </Style>
  <Style Selector="ToggleButton.day-toggle:checked /template/ ContentPresenter">
    <Setter Property="Background" Value="{DynamicResource AccentBrush}"/>
  </Style>
```

If `AccentBrush` is not a defined token, use the brush the project uses for primary/selected affordances (check the `primary` button style in this file and reuse that brush). Final visual pass is the user's.

- [ ] **Step 2: Replace the Prime row template**

In `src/ClaudeDo.Ui/Views/Modals/SettingsModalView.axaml`, replace the `<Grid ...>` inside the Prime `DataTemplate` (currently columns `Auto,*,Auto,Auto,Auto,Auto` with the `ThemedDatePicker` and Mon–Fri checkbox) with:

```xml
                      <Grid ColumnDefinitions="Auto,*,Auto,Auto,Auto" ColumnSpacing="8">
                        <CheckBox Grid.Column="0" IsChecked="{Binding Enabled, Mode=TwoWay}" VerticalAlignment="Center"/>
                        <StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="4" VerticalAlignment="Center">
                          <ToggleButton Classes="day-toggle" Content="Mo" IsChecked="{Binding Monday, Mode=TwoWay}"/>
                          <ToggleButton Classes="day-toggle" Content="Tu" IsChecked="{Binding Tuesday, Mode=TwoWay}"/>
                          <ToggleButton Classes="day-toggle" Content="We" IsChecked="{Binding Wednesday, Mode=TwoWay}"/>
                          <ToggleButton Classes="day-toggle" Content="Th" IsChecked="{Binding Thursday, Mode=TwoWay}"/>
                          <ToggleButton Classes="day-toggle" Content="Fr" IsChecked="{Binding Friday, Mode=TwoWay}"/>
                          <ToggleButton Classes="day-toggle" Content="Sa" IsChecked="{Binding Saturday, Mode=TwoWay}"/>
                          <ToggleButton Classes="day-toggle" Content="Su" IsChecked="{Binding Sunday, Mode=TwoWay}"/>
                        </StackPanel>
                        <TextBox Grid.Column="2" Width="64"
                                 Text="{Binding TimeOfDay, Mode=TwoWay, Converter={StaticResource TimeSpanToHhmm}}"
                                 VerticalAlignment="Center"/>
                        <TextBlock Classes="meta" Grid.Column="3" Text="{Binding LastRunLabel}" VerticalAlignment="Center"
                                   MinWidth="80"/>
                        <Button Classes="icon-btn" Grid.Column="4" Content="✕"
                                Command="{Binding $parent[ItemsControl].((vm:SettingsModalViewModel)DataContext).Prime.RemoveScheduleCommand}"
                                CommandParameter="{Binding}"/>
                      </Grid>
```

- [ ] **Step 3: Update the explainer text**

Replace the intro `TextBlock` Text in the Prime tab (`SettingsModalView.axaml`):

```xml
                         Text="Prime your Claude usage window by firing a single non-interactive ping 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, the ping fires immediately."/>
```

- [ ] **Step 4: Remove the now-unused range converter (only if unreferenced)**

The `DateOnlyToDateTime` resource on line 23 was used only by the range picker. Grep the file: if `DateOnlyToDateTime` has no other reference, remove the `<conv:DateOnlyToDateTimeConverter x:Key="DateOnlyToDateTime"/>` line. Keep `TimeSpanToHhmm` (still used).

Run: `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj`
Expected: PASS.

- [ ] **Step 5: Manual UI check**

Start the worker, then the app. Open Settings → Prime Claude. Verify: a row shows 7 toggle buttons with Mon–Fri lit by default; toggling Sat/Sun persists after Save+reopen; clearing all days shows the validation error on Save. (UI correctness can only be confirmed in the running app — state so explicitly if it cannot be run.)

- [ ] **Step 6: Commit**

```bash
git add src/ClaudeDo.Ui/Design/IslandStyles.axaml src/ClaudeDo.Ui/Views/Modals/SettingsModalView.axaml
git commit -m "feat(ui): replace prime date range with weekday toggle buttons"
```

---

## Task 9: Docs

**Files:**
- Modify: `src/ClaudeDo.Data/CLAUDE.md`
- Modify: `CLAUDE.md`

- [ ] **Step 1: Update the Data CLAUDE.md**

In `src/ClaudeDo.Data/CLAUDE.md`, the Models section has no PrimeSchedule line today; add one under Models, and confirm the `prime_schedules` table mention in the Schema section stays accurate:

```markdown
- **PrimeScheduleEntity** — Id, Days (`[Flags] PrimeDays` weekday bitmask, stored as `days_of_week` int), TimeOfDay, Enabled, LastRunAt, PromptOverride, CreatedAt. Recurs on the selected weekdays; no date range.
```

- [ ] **Step 2: Update the root CLAUDE.md if Prime is described**

Grep `CLAUDE.md` for "Prime"; if there is a Prime description mentioning a date range, update it to "recurring weekday schedule". If there is no such line, make no change.

- [ ] **Step 3: Full test sweep**

Run: `dotnet test tests/ClaudeDo.Worker.Tests && dotnet test tests/ClaudeDo.Ui.Tests`
Expected: PASS.

- [ ] **Step 4: Commit**

```bash
git add src/ClaudeDo.Data/CLAUDE.md CLAUDE.md
git commit -m "docs: describe recurring-weekday Prime schedule"
```

---

## Self-Review Notes

- **Spec coverage:** data model (T1), scheduling logic (T4), UI toggles (T7–T8), migration+backfill (T3), both DTOs (T4/T7), tests (T4–T7), out-of-scope items excluded. ✓
- **Type consistency:** entity `PrimeDays Days`; both DTOs `int Days`; hub/scheduler cast `(int)`/`(PrimeDays)` at boundaries; calculator casts `(PrimeDays)s.Days`; row VM exposes 7 bools + `DaysMask()`. ✓
- **Build ripple:** a single type change breaks several projects at once, so some intermediate steps note expected build failures; the gating green builds are T3 Step 4 (Data), T6 Step 4 (Worker + tests), T8 Step 4 (App). ✓
```
