Files
ClaudeDo/docs/superpowers/plans/2026-06-03-daily-prep-live-view.md
2026-06-04 08:42:41 +02:00

22 KiB

Daily Prep — Live Output View + Clear Day — 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: Stream the daily-prep run's output into a live, human-readable view (a new mode in the Details island), and add a "Clear Day" button that empties MyDay.

Architecture: The worker broadcasts PrepStarted/PrepLine/PrepFinished over SignalR (mirroring TaskStarted/TaskMessage/TaskFinished). PrimeRunner forwards each Claude stdout line instead of discarding it. The UI WorkerClient re-raises these as events; DetailsIslandViewModel gains a PrepLog + IsPrepMode panel rendered with the existing terminal renderer. A ClearMyDay hub method bulk-clears IsMyDay. MyDay header gets "Vorbereitungs-Log" and "Tag leeren" buttons.

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

Spec: docs/superpowers/specs/2026-06-03-daily-prep-live-view-design.md


Build & test commands

.slnx needs .NET 9; build/test individual csproj with -c Release (a running Worker may lock Debug).

dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release
dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release

UI cannot be GUI-smoke-tested headlessly — note that explicitly where it applies; the human verifies visuals.

Reference anchors (verify before editing — line numbers drift)

  • src/ClaudeDo.Worker/Prime/Interfaces/IPrimeBroadcaster.cs — currently only PrimeFiredAsync.
  • src/ClaudeDo.Worker/Hub/HubBroadcaster.cs:13-57 — broadcast methods; PrimeFired at ~52-56.
  • src/ClaudeDo.Worker/Prime/PrimeRunner.cs:31-79FireAsync; discard lambda at ~55-60; ctor at ~19-29.
  • src/ClaudeDo.Worker/Hub/WorkerHub.cs:542-549RunDailyPrepNow (uses _broadcaster); DailyNote CRUD at 559-583 (shows the db-context pattern this hub uses).
  • src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs:19TaskMessageEvent; :55RunDailyPrepNowAsync.
  • src/ClaudeDo.Ui/Services/WorkerClient.cs:99-122TaskStarted/Finished/Message hub.On; :170-173PrimeFired hub.On (the pattern to copy).
  • src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.csIsNotesMode ~56, Log ~193, ctor/subscriptions ~272-337, OnTaskMessage ~339-363 (stdout→StreamLineFormatterLog), ShowNotes ~478-483.
  • src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml:131-302 — body grid; task panel IsVisible="{Binding !IsNotesMode}", notes panel IsVisible="{Binding IsNotesMode}"; SessionTerminalView embedded ~295.
  • src/ClaudeDo.Ui/Views/Islands/SessionTerminalView.axaml:54-75ItemsControl ItemsSource="{Binding Log}" + the LogLineViewModel item template to reuse.
  • src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.csNotesRequested ~29, OpenNotesCommand+PrepareDayCommand ~33-45, ShowNotesRow/IsMyDayList ~65-66, both set in LoadForList ~212-213.
  • src/ClaudeDo.Ui/Views/Islands/TasksIslandView.axaml:69-84 — Notes + PrepareDay buttons (styling to copy).
  • src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs:199-201 — island event wiring; :225PrimeFired subscription.
  • Fakes to keep in sync: tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs, tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs (FakeWorkerClient).

Task 1: Worker — prep output broadcast + streaming

Files:

  • Modify: src/ClaudeDo.Worker/Prime/Interfaces/IPrimeBroadcaster.cs

  • Modify: src/ClaudeDo.Worker/Hub/HubBroadcaster.cs

  • Modify: src/ClaudeDo.Worker/Prime/PrimeRunner.cs

  • Test: tests/ClaudeDo.Worker.Tests/Prime/PrimeRunnerTests.cs

  • Step 1: Write the failing test. Extend PrimeRunnerTests with a fake IPrimeBroadcaster that records calls. The fake IClaudeProcess should invoke onStdoutLine with two sample lines and return RunResult { ExitCode = 0, ResultMarkdown = "ok" }.

[Fact]
public async Task FireAsync_streams_started_lines_and_finished()
{
    var broadcaster = new RecordingPrimeBroadcaster();
    var claude = new FakeClaudeProcess(emitLines: new[] { "{\"a\":1}", "{\"b\":2}" }, exitCode: 0, result: "ok");
    var runner = NewRunner(claude, broadcaster); // build with temp-sqlite dbFactory + fake clock + logger + broadcaster
    var schedule = new PrimeScheduleDto(Guid.Empty, 0, TimeSpan.Zero, true, null, null);

    var outcome = await runner.FireAsync(schedule, CancellationToken.None);

    Assert.True(outcome.Success);
    Assert.Equal(1, broadcaster.StartedCount);
    Assert.Equal(new[] { "{\"a\":1}", "{\"b\":2}" }, broadcaster.Lines);
    Assert.Single(broadcaster.FinishedResults);
    Assert.True(broadcaster.FinishedResults[0]);
}

RecordingPrimeBroadcaster implements IPrimeBroadcaster: StartedCount, List<string> Lines, List<bool> FinishedResults, and a no-op PrimeFiredAsync. If the existing FakeClaudeProcess cannot emit lines, add an optional emitLines parameter that loops await onStdoutLine(line) before returning.

  • Step 2: Run — expect FAIL (interface methods + ctor param missing).
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter PrimeRunner
  • Step 3: Extend IPrimeBroadcaster:
public interface IPrimeBroadcaster
{
    Task PrimeFiredAsync(Guid scheduleId, bool success, string message, DateTimeOffset firedAt);
    Task PrepStartedAsync();
    Task PrepLineAsync(string line);
    Task PrepFinishedAsync(bool success);
}

(Keep the existing PrimeFiredAsync signature exactly as it is in the current file.)

  • Step 4: Implement in HubBroadcaster (add next to PrimeFired):
    public Task PrepStarted() => _hub.Clients.All.SendAsync("PrepStarted");
    public Task PrepLine(string line) => _hub.Clients.All.SendAsync("PrepLine", line);
    public Task PrepFinished(bool success) => _hub.Clients.All.SendAsync("PrepFinished", success);

    Task IPrimeBroadcaster.PrepStartedAsync() => PrepStarted();
    Task IPrimeBroadcaster.PrepLineAsync(string line) => PrepLine(line);
    Task IPrimeBroadcaster.PrepFinishedAsync(bool success) => PrepFinished(success);

(Match the existing explicit-interface style used for PrimeFiredAsync.)

  • Step 5: Wire PrimeRunner. Add IPrimeBroadcaster _broadcaster as a ctor param (and field). Rewrite the body of FireAsync after the gate check to:
        if (!await _gate.WaitAsync(0, ct))
            return new PrimeRunOutcome(false, "Daily prep already running");

        var success = false;
        try
        {
            await _broadcaster.PrepStartedAsync();

            var cwd = Paths.AppDataRoot();
            Directory.CreateDirectory(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);
            var args = DailyPrepPrompt.BuildArgs(MaxTurns);

            using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
            timeoutCts.CancelAfter(FireTimeout);

            var result = await _claude.RunAsync(
                arguments: args,
                prompt: prompt,
                workingDirectory: cwd,
                onStdoutLine: line => _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);
            _gate.Release();
        }

DI is unchanged: AddSingleton<IPrimeRunner, PrimeRunner>() resolves IPrimeBroadcaster (registered as sp => sp.GetRequiredService<HubBroadcaster>()).

  • Step 6: Update existing PrimeRunnerTests ctor calls to pass the recording broadcaster; build + run.
dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter PrimeRunner
  • Step 7: Commit.
git add src/ClaudeDo.Worker/Prime src/ClaudeDo.Worker/Hub/HubBroadcaster.cs tests/ClaudeDo.Worker.Tests/Prime
git commit -m "feat(daily-prep): stream prep output via PrepStarted/PrepLine/PrepFinished"

Task 2: Worker — ClearMyDay hub method

Files:

  • Modify: src/ClaudeDo.Worker/Hub/WorkerHub.cs

  • Test: a new/existing hub test under tests/ClaudeDo.Worker.Tests/Hub/ (mirror an existing hub test that seeds a real SQLite db and constructs WorkerHub)

  • Step 1: Write the failing test. Seed three tasks: two with IsMyDay=true (one Idle, one Done), one with IsMyDay=false. Construct WorkerHub the way existing hub tests do (the same null! argument list, plus a recording HubBroadcaster/clients). Call ClearMyDay(); assert both MyDay rows are now false, the third is untouched, and the returned count is 2.

[Fact]
public async Task ClearMyDay_clears_all_isMyDay_tasks()
{
    // seed via the test's db helper ...
    var hub = NewHub(/* ... */);
    var cleared = await hub.ClearMyDay();

    Assert.Equal(2, cleared);
    await using var ctx = NewContext();
    Assert.False(await ctx.Tasks.AnyAsync(t => t.IsMyDay));
}
  • Step 2: Run — expect FAIL.

  • Step 3: Add the method to WorkerHub (use the same db-context acquisition the neighbouring hub methods use — e.g. _dbFactory/repository field name found in the file — and the existing _broadcaster field):

public async Task<int> ClearMyDay()
{
    await using var ctx = await _dbFactory.CreateDbContextAsync();
    var ids = await ctx.Tasks.Where(t => t.IsMyDay).Select(t => t.Id).ToListAsync();
    if (ids.Count == 0) return 0;

    await ctx.Tasks.Where(t => t.IsMyDay)
        .ExecuteUpdateAsync(s => s.SetProperty(t => t.IsMyDay, false));

    foreach (var id in ids)
        await _broadcaster.TaskUpdated(id);

    return ids.Count;
}

If WorkerHub does not already have an IDbContextFactory<ClaudeDoDbContext> field, use whatever data-access dependency the other hub methods use (read the file). Do NOT add a new ctor param unless unavoidable (it would break hub-test fakes — if you must, update all new WorkerHub(...) call sites).

  • Step 4: Run — expect PASS. Build Worker.

  • Step 5: Commit.

git add src/ClaudeDo.Worker/Hub/WorkerHub.cs tests/ClaudeDo.Worker.Tests/Hub
git commit -m "feat(daily-prep): add ClearMyDay hub method"

Task 3: UI — WorkerClient prep events + ClearMyDayAsync

Files:

  • Modify: src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs

  • Modify: src/ClaudeDo.Ui/Services/WorkerClient.cs

  • Modify fakes: tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs, tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs (FakeWorkerClient)

  • Step 1: Declare on IWorkerClient (near TaskMessageEvent / RunDailyPrepNowAsync):

event Action? PrepStartedEvent;
event Action<string>? PrepLineEvent;
event Action<bool>? PrepFinishedEvent;
Task ClearMyDayAsync();
  • Step 2: Implement in WorkerClient. Add the events; register hub callbacks mirroring the PrimeFired registration (~line 170):
public event Action? PrepStartedEvent;
public event Action<string>? PrepLineEvent;
public event Action<bool>? PrepFinishedEvent;

// in the hub-wiring section:
_hub.On("PrepStarted", () => Dispatcher.UIThread.Post(() => PrepStartedEvent?.Invoke()));
_hub.On<string>("PrepLine", line => Dispatcher.UIThread.Post(() => PrepLineEvent?.Invoke(line)));
_hub.On<bool>("PrepFinished", ok => Dispatcher.UIThread.Post(() => PrepFinishedEvent?.Invoke(ok)));

public Task ClearMyDayAsync() => _connection.InvokeAsync("ClearMyDay");

(Use the exact connection field name and async-call style of neighbouring methods like RunDailyPrepNowAsync / GenerateWeekReport. ClearMyDay returns int on the hub; invoking it as a void InvokeAsync("ClearMyDay") is fine, or InvokeAsync<int> if you want the count.)

  • Step 3: Update the fakes. Add the three events (as public event … auto-implemented) and ClearMyDayAsync() => Task.CompletedTask to both StubWorkerClient and FakeWorkerClient. For the ClearDay command test (Task 5), give StubWorkerClient a ClearMyDayCalls counter incremented in ClearMyDayAsync.

  • Step 4: Build App + both test projects; fix any remaining fake gaps.

dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release
dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release
  • Step 5: Commit.
git add src/ClaudeDo.Ui/Services tests
git commit -m "feat(daily-prep): expose prep stream events and ClearMyDay on the UI worker client"

Task 4: UI — Details island prep mode + live log

Files:

  • Modify: src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs

  • Modify: src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml

  • Test: tests/ClaudeDo.Ui.Tests/...DetailsIslandViewModel... (mirror existing Details VM tests; if none, add a small test file)

  • Step 1: Write the failing test. Construct DetailsIslandViewModel with a StubWorkerClient (mirror existing construction). Then:

[Fact]
public void PrepLine_event_appends_to_PrepLog()
{
    var stub = new StubWorkerClient();
    var vm = NewDetailsVm(stub);

    stub.RaisePrepLine("{\"type\":\"assistant\",\"text\":\"hi\"}"); // helper that invokes PrepLineEvent
    Assert.NotEmpty(vm.PrepLog);
}

[Fact]
public void ShowPrep_sets_prep_mode_and_clears_notes_mode()
{
    var vm = NewDetailsVm(new StubWorkerClient());
    vm.ShowPrep();
    Assert.True(vm.IsPrepMode);
    Assert.False(vm.IsNotesMode);
}

Add RaisePrepStarted/RaisePrepLine/RaisePrepFinished helpers to StubWorkerClient that invoke the corresponding events.

  • Step 2: Run — expect FAIL.

  • Step 3: Implement in DetailsIslandViewModel:

    • Add [ObservableProperty] private bool _isPrepMode; and [ObservableProperty] private bool _isPrepRunning;.
    • Add public ObservableCollection<LogLineViewModel> PrepLog { get; } = new();.
    • In the ctor, subscribe: _worker.PrepStartedEvent += OnPrepStarted; _worker.PrepLineEvent += OnPrepLine; _worker.PrepFinishedEvent += OnPrepFinished; (guard with the same _worker is not null pattern used for other events).
    • Handlers:
private void OnPrepStarted()
{
    PrepLog.Clear();
    IsPrepRunning = true;
}

private void OnPrepLine(string line) => AppendStdoutLine(PrepLog, line);

private void OnPrepFinished(bool success) => IsPrepRunning = false;
  • Factor the stdout-formatting currently inside OnTaskMessage into a reusable private void AppendStdoutLine(ObservableCollection<LogLineViewModel> target, string line) that runs the line through StreamLineFormatter and appends LogLineViewModel(s). Have OnTaskMessage's stdout branch call AppendStdoutLine(Log, strippedLine) so both paths share one implementation. (Events arrive already on the UI thread via Dispatcher.UIThread.Post in WorkerClient, so direct collection mutation is correct.)

  • Add public void ShowPrep() mirroring ShowNotes(): call Bind(null), set IsNotesMode = false, IsPrepMode = true.

  • In ShowNotes() add IsPrepMode = false. In Bind(...) reset both IsNotesMode and IsPrepMode to false (find where IsNotesMode is reset; add IsPrepMode beside it).

  • Step 4: Update DetailsIslandView.axaml.

    • Change the task-details panel visibility from IsVisible="{Binding !IsNotesMode}" to a converter-free multi-condition. Avalonia lacks && in bindings, so add a computed property public bool IsTaskDetailVisible => !IsNotesMode && !IsPrepMode; to the VM (raise its change notification from the OnIsNotesModeChanged/OnIsPrepModeChanged partial methods generated by [ObservableProperty]) and bind the task panel to IsVisible="{Binding IsTaskDetailVisible}".
    • Add a third panel after the notes panel:
<Panel IsVisible="{Binding IsPrepMode}">
    <DockPanel>
        <TextBlock DockPanel.Dock="Top" Margin="16,12"
                   Text="{loc:Tr details.prepTitle}" Classes="h2"/>
        <ScrollViewer>
            <ItemsControl ItemsSource="{Binding PrepLog}"/>
        </ScrollViewer>
    </DockPanel>
</Panel>

The ItemsControl reuses the implicit LogLineViewModel DataTemplate that SessionTerminalView relies on. If that template is defined locally inside SessionTerminalView.axaml (not in a shared resource), either move it to a shared ResourceDictionary (e.g. App resources) and reference it from both, or set the ItemsControl.ItemTemplate to a copy of that template. Prefer sharing over copying. Add details.prepTitle ("Daily prep" / "Tagesvorbereitung") to both locale json files.

  • Step 5: Run UI tests — expect PASS; build App.
dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release
  • Step 6: Commit.
git add src/ClaudeDo.Ui src/ClaudeDo.Localization tests/ClaudeDo.Ui.Tests
git commit -m "feat(daily-prep): add live prep-output mode to the Details island"

Task 5: UI — MyDay buttons + shell wiring

Files:

  • Modify: src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs

  • Modify: src/ClaudeDo.Ui/Views/Islands/TasksIslandView.axaml

  • Modify: src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs

  • Modify: src/ClaudeDo.Localization/locales/en.json, de.json

  • Test: tests/ClaudeDo.Worker.Tests/UiVm/... or tests/ClaudeDo.Ui.Tests/... (TasksIslandViewModel)

  • Step 1: Write the failing tests.

[Fact]
public async Task ClearDayCommand_calls_worker()
{
    var stub = new StubWorkerClient();
    var vm = NewTasksVm(stub);
    await vm.ClearDayCommand.ExecuteAsync(null);
    Assert.Equal(1, stub.ClearMyDayCalls);
}

[Fact]
public async Task PrepareDayCommand_raises_PrepRequested()
{
    var vm = NewTasksVm(new StubWorkerClient());
    var raised = false;
    vm.PrepRequested += () => raised = true;
    await vm.PrepareDayCommand.ExecuteAsync(null);
    Assert.True(raised);
}
  • Step 2: Run — expect FAIL.

  • Step 3: Implement in TasksIslandViewModel:

    • Add public event Action? PrepRequested; next to NotesRequested.
    • In PrepareDayAsync (the existing [RelayCommand]), raise PrepRequested?.Invoke(); in addition to the existing RunDailyPrepNowAsync() call.
    • Add:
[RelayCommand]
private void ShowPrepLog() => PrepRequested?.Invoke();

[RelayCommand]
private async Task ClearDayAsync()
{
    if (_worker is null) return;
    try { await _worker.ClearMyDayAsync(); }
    catch { /* worker offline; broadcast will reconcile on return */ }
}
  • Step 4: Add the two buttons to the MyDay header in TasksIslandView.axaml, immediately after the existing "Prepare day" button (~line 84), copying its styling (DockPanel.Dock="Top" Classes="btn" HorizontalAlignment="Stretch" HorizontalContentAlignment="Left" Margin="16,0,16,8" IsVisible="{Binding IsMyDayList}"):
<Button DockPanel.Dock="Top" Classes="btn" HorizontalAlignment="Stretch"
        HorizontalContentAlignment="Left" Margin="16,0,16,8"
        IsVisible="{Binding IsMyDayList}"
        Command="{Binding ShowPrepLogCommand}"
        Content="{loc:Tr tasks.prepLog}"/>
<Button DockPanel.Dock="Top" Classes="btn" HorizontalAlignment="Stretch"
        HorizontalContentAlignment="Left" Margin="16,0,16,8"
        IsVisible="{Binding IsMyDayList}"
        Command="{Binding ClearDayCommand}"
        Content="{loc:Tr tasks.clearDay}"/>

Add tasks.prepLog (en "Prep log" / de "Vorbereitungs-Log") and tasks.clearDay (en "Clear day" / de "Tag leeren") to both locale json files.

  • Step 5: Wire the shell. In IslandsShellViewModel where Tasks.NotesRequested is wired (~line 201), add:
Tasks.PrepRequested += () => Details.ShowPrep();
  • Step 6: Run tests + build App.
dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release
  • Step 7: Manual smoke (human, not headless): start Worker + App, open MyDay, click "Tag vorbereiten" → Details island opens in prep mode and streams readable lines; click "Tag leeren" → MyDay empties; after a scheduled run, "Vorbereitungs-Log" opens the filled log. Confirm the three buttons only appear on MyDay.

  • Step 8: Commit.

git add src/ClaudeDo.Ui src/ClaudeDo.Localization tests
git commit -m "feat(daily-prep): add Prep-log and Clear-day buttons to MyDay header"

Final verification

  • Build Worker + App (Release).
  • dotnet test Worker.Tests, Ui.Tests, Localization.Tests — all green.
  • Manual: prep streams live into the Details island (manual opens it; scheduled fills it silently, opened via the button); Clear Day empties MyDay immediately.

Notes / risks

  • Mode flags IsNotesMode / IsPrepMode are mutually exclusive; the task-details panel uses the computed IsTaskDetailVisible. Verify all three modes switch cleanly.
  • Reusing the LogLineViewModel template: prefer promoting it to a shared resource over copying, to avoid drift between the session terminal and the prep log.
  • ClearMyDay broadcasts one TaskUpdated per affected id; MyDay is small (capped), so this is fine.
  • Keep PrimeRunner's "already running" early-return emitting no prep events.