docs(daily-prep): add design specs and implementation plans
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
517
docs/superpowers/plans/2026-06-03-daily-prep-live-view.md
Normal file
517
docs/superpowers/plans/2026-06-03-daily-prep-live-view.md
Normal file
@@ -0,0 +1,517 @@
|
|||||||
|
# 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).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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-79` — `FireAsync`; discard lambda at ~55-60; ctor at ~19-29.
|
||||||
|
- `src/ClaudeDo.Worker/Hub/WorkerHub.cs:542-549` — `RunDailyPrepNow` (uses `_broadcaster`); DailyNote CRUD at 559-583 (shows the db-context pattern this hub uses).
|
||||||
|
- `src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs:19` — `TaskMessageEvent`; `:55` — `RunDailyPrepNowAsync`.
|
||||||
|
- `src/ClaudeDo.Ui/Services/WorkerClient.cs:99-122` — `TaskStarted/Finished/Message` hub.On; `:170-173` — `PrimeFired` hub.On (the pattern to copy).
|
||||||
|
- `src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs` — `IsNotesMode` ~56, `Log` ~193, ctor/subscriptions ~272-337, `OnTaskMessage` ~339-363 (stdout→`StreamLineFormatter`→`Log`), `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-75` — `ItemsControl ItemsSource="{Binding Log}"` + the `LogLineViewModel` item template to reuse.
|
||||||
|
- `src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs` — `NotesRequested` ~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; `:225` — `PrimeFired` 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" }`.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[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).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter PrimeRunner
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Extend `IPrimeBroadcaster`:**
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
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`):
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
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:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
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.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[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):
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
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.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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`):
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
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):
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
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.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[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:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
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:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<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.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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.**
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[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:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[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}"`):
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<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:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
Tasks.PrepRequested += () => Details.ShowPrep();
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 6: Run tests + build App.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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.
|
||||||
736
docs/superpowers/plans/2026-06-03-daily-prep.md
Normal file
736
docs/superpowers/plans/2026-06-03-daily-prep.md
Normal file
@@ -0,0 +1,736 @@
|
|||||||
|
# Daily Prep ("Prime Claude") 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:** Turn the Prime Time warm-up into a daily preparation where Claude reads open tasks and moves an effort-aware, capped subset into MyDay, triggered by the Prime schedule and a manual button.
|
||||||
|
|
||||||
|
**Architecture:** Agentic. Two new tools on the always-on `ExternalMcpService` (`get_daily_prep_candidates`, `set_my_day` with a server-side cap-guard). The existing `PrimeRunner` is rewritten to launch a headless `claude -p` run with a fixed parameterized prompt and `--allowedTools` for those two tools, relying on the already-registered `claudedo` MCP (no separate `--mcp-config`). A new `DailyPrepMaxTasks` app setting drives the cap. A manual hub method reuses the same runner with a single-flight guard.
|
||||||
|
|
||||||
|
**Tech Stack:** .NET 8, ASP.NET Core, EF Core (SQLite), SignalR, ModelContextProtocol, Avalonia (CommunityToolkit.Mvvm), xUnit.
|
||||||
|
|
||||||
|
**Spec:** `docs/superpowers/specs/2026-06-03-daily-prep-design.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Deviation from spec (deliberate, to minimize churn)
|
||||||
|
|
||||||
|
The spec proposed renaming `IPrimeRunner`/`PrimeRunner`/`PrimeScheduler` → `DailyPrep*`. **We keep the existing names and the `FireAsync(PrimeScheduleDto, ct)` signature** and only rewrite the runner body. This avoids touching the scheduler, DI registration, `IPrimeBroadcaster`, and the existing Prime tests for a pure rename. The per-schedule `PromptOverride` field becomes unused by the runner (left in the DB/UI untouched).
|
||||||
|
|
||||||
|
## Build & test commands (this repo)
|
||||||
|
|
||||||
|
`.slnx` needs .NET 9; on .NET 8 build/test individual projects. Use `-c Release` if a running Worker locks `Debug`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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.Data.Tests/ClaudeDo.Data.Tests.csproj -c Release
|
||||||
|
```
|
||||||
|
|
||||||
|
Tests use **real SQLite + real git** (project convention). Mirror the setup already present in the test file you are extending.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
**Create**
|
||||||
|
- `src/ClaudeDo.Data/Migrations/<timestamp>_DailyPrepMaxTasks.cs` (+ Designer, via `dotnet ef`)
|
||||||
|
- `src/ClaudeDo.Worker/Prime/DailyPrepPrompt.cs` — pure prompt + args builder (easy to unit-test)
|
||||||
|
|
||||||
|
**Modify**
|
||||||
|
- `src/ClaudeDo.Data/Models/AppSettingsEntity.cs` — add `DailyPrepMaxTasks`
|
||||||
|
- `src/ClaudeDo.Data/Configuration/AppSettingsEntityConfiguration.cs` — map column
|
||||||
|
- `src/ClaudeDo.Data/Repositories/AppSettingsRepository.cs` — persist field in `UpdateAsync`
|
||||||
|
- `src/ClaudeDo.Worker/External/ExternalMcpService.cs` — add 2 tools + DTOs
|
||||||
|
- `src/ClaudeDo.Worker/Prime/PrimeRunner.cs` — rewrite body to daily prep + single-flight
|
||||||
|
- `src/ClaudeDo.Worker/Hub/WorkerHub.cs` — add `DailyPrepMaxTasks` to AppSettings DTO + `RunDailyPrepNow`
|
||||||
|
- `src/ClaudeDo.Ui/Services/WorkerClient.cs` — mirror `DailyPrepMaxTasks` in the UI AppSettings DTO + add `RunDailyPrepNow` call
|
||||||
|
- `src/ClaudeDo.Ui/ViewModels/Modals/Settings/PrimeClaudeTabViewModel.cs` (+ its view) — numeric editor for `DailyPrepMaxTasks`
|
||||||
|
- MyDay list header view + its ViewModel — "Tag vorbereiten" button + command
|
||||||
|
|
||||||
|
**Test**
|
||||||
|
- `tests/ClaudeDo.Data.Tests/...AppSettings...` — new field persists / default 5
|
||||||
|
- `tests/ClaudeDo.Worker.Tests/External/ExternalMcpServiceTests.cs` — candidate filter + set_my_day + cap-guard
|
||||||
|
- `tests/ClaudeDo.Worker.Tests/Prime/DailyPrepPromptTests.cs` — prompt/args content
|
||||||
|
- `tests/ClaudeDo.Worker.Tests/Prime/PrimeRunnerTests.cs` (if present) — single-flight + success/failure via `IClaudeProcess` fake
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 1: `DailyPrepMaxTasks` app setting
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/ClaudeDo.Data/Models/AppSettingsEntity.cs`
|
||||||
|
- Modify: `src/ClaudeDo.Data/Configuration/AppSettingsEntityConfiguration.cs`
|
||||||
|
- Modify: `src/ClaudeDo.Data/Repositories/AppSettingsRepository.cs`
|
||||||
|
- Create (via `dotnet ef`): `src/ClaudeDo.Data/Migrations/<timestamp>_DailyPrepMaxTasks.cs`
|
||||||
|
- Test: `tests/ClaudeDo.Data.Tests` (extend existing AppSettings repository test, or add `AppSettingsRepositoryTests.cs`)
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing test**
|
||||||
|
|
||||||
|
In a Data.Tests file (mirror the existing repo test harness that opens a real SQLite `ClaudeDoDbContext`):
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[Fact]
|
||||||
|
public async Task DailyPrepMaxTasks_defaults_to_5_and_persists()
|
||||||
|
{
|
||||||
|
await using var ctx = NewContext(); // existing helper that migrates a temp sqlite db
|
||||||
|
var repo = new AppSettingsRepository(ctx);
|
||||||
|
|
||||||
|
var initial = await repo.GetAsync();
|
||||||
|
Assert.Equal(5, initial.DailyPrepMaxTasks);
|
||||||
|
|
||||||
|
initial.DailyPrepMaxTasks = 8;
|
||||||
|
await repo.UpdateAsync(initial);
|
||||||
|
|
||||||
|
var reloaded = await repo.GetAsync();
|
||||||
|
Assert.Equal(8, reloaded.DailyPrepMaxTasks);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run it — expect FAIL** (`AppSettingsEntity` has no `DailyPrepMaxTasks`).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet test tests/ClaudeDo.Data.Tests/ClaudeDo.Data.Tests.csproj -c Release --filter DailyPrepMaxTasks_defaults_to_5_and_persists
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add the property** to `AppSettingsEntity.cs` after `StandupWeekday`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Max number of open tasks the daily prep ("Prime Claude") may place in MyDay.
|
||||||
|
public int DailyPrepMaxTasks { get; set; } = 5;
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Map the column** in `AppSettingsEntityConfiguration.cs`, after the `StandupWeekday` mapping (before `builder.HasData(...)`):
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
builder.Property(s => s.DailyPrepMaxTasks)
|
||||||
|
.HasColumnName("daily_prep_max_tasks").IsRequired().HasDefaultValue(5);
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Persist it** in `AppSettingsRepository.UpdateAsync`, after the `StandupWeekday` assignment:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
row.DailyPrepMaxTasks = updated.DailyPrepMaxTasks < 1 ? 1 : updated.DailyPrepMaxTasks;
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 6: Generate the migration** (regenerates the model snapshot — do NOT hand-edit the snapshot):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet ef migrations add DailyPrepMaxTasks \
|
||||||
|
-p src/ClaudeDo.Data/ClaudeDo.Data.csproj \
|
||||||
|
-s src/ClaudeDo.Worker/ClaudeDo.Worker.csproj
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify the generated `Up` contains an `AddColumn<int>("daily_prep_max_tasks", ... defaultValue: 5)` and an `UpdateData` setting the singleton row's `daily_prep_max_tasks` to 5. If `dotnet ef` is unavailable, hand-write the migration mirroring `20260603072822_WeeklyReport.cs` **and** add the matching `Property<int>("DailyPrepMaxTasks").HasColumnName("daily_prep_max_tasks")` line to `ClaudeDoDbContextModelSnapshot.cs` under the `AppSettingsEntity` builder.
|
||||||
|
|
||||||
|
- [ ] **Step 7: Run the test — expect PASS.**
|
||||||
|
|
||||||
|
- [ ] **Step 8: Commit.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/ClaudeDo.Data tests/ClaudeDo.Data.Tests
|
||||||
|
git commit -m "feat(daily-prep): add DailyPrepMaxTasks app setting"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 2: `get_daily_prep_candidates` MCP tool
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/ClaudeDo.Worker/External/ExternalMcpService.cs`
|
||||||
|
- Test: `tests/ClaudeDo.Worker.Tests/External/ExternalMcpServiceTests.cs`
|
||||||
|
|
||||||
|
Read `ExternalMcpServiceTests.cs` first and reuse its existing harness (how it builds an `ExternalMcpService` with a real SQLite context, `ListRepository`, `TaskRepository`, fake `HubBroadcaster`, etc.). The new tool reads **all** lists/tasks itself via the injected `_dbFactory`, so it needs no new constructor args.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing test.** Seed: a list with `WorkingDir = @"D:\work\repo"` holding two `Idle` tasks (one blocked, one not) and one `Done` task; a second list with `WorkingDir = @"C:\Private\secret"` holding one `Idle` task; a third list with `WorkingDir = null` holding one `Idle` task; and one `Idle` task with `IsMyDay = true` in the first list. Set `AppSettings.ReportExcludedPaths = "[\"C:\\\\Private\"]"`.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[Fact]
|
||||||
|
public async Task GetDailyPrepCandidates_filters_by_status_block_and_excluded_repo()
|
||||||
|
{
|
||||||
|
// ... seed as described, using the file's existing seed helpers ...
|
||||||
|
var svc = NewService();
|
||||||
|
|
||||||
|
var result = await svc.GetDailyPrepCandidates(CancellationToken.None);
|
||||||
|
|
||||||
|
// Only the non-blocked, Idle, non-MyDay task in the non-excluded repo is a candidate.
|
||||||
|
Assert.Single(result.Candidates);
|
||||||
|
Assert.Equal("idle-unblocked", result.Candidates[0].Id);
|
||||||
|
// The Idle MyDay task is reported separately, not as a candidate.
|
||||||
|
Assert.Single(result.CurrentMyDay);
|
||||||
|
Assert.Equal(1, result.MaxTasks > 0 ? 1 : 1); // MaxTasks comes from AppSettings (default 5)
|
||||||
|
Assert.Equal(5, result.MaxTasks);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run it — expect FAIL** (method missing).
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add the DTOs** near the other record declarations at the top of `ExternalMcpService.cs`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public sealed record DailyPrepCandidateDto(
|
||||||
|
string Id, string ListId, string ListName, string Title, string? Description,
|
||||||
|
bool IsStarred, DateTime? ScheduledFor, DateTime CreatedAt);
|
||||||
|
|
||||||
|
public sealed record DailyPrepDataDto(
|
||||||
|
int MaxTasks,
|
||||||
|
IReadOnlyList<DailyPrepCandidateDto> Candidates,
|
||||||
|
IReadOnlyList<DailyPrepCandidateDto> CurrentMyDay);
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Add the tool method** to the `ExternalMcpService` class body:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[McpServerTool, Description(
|
||||||
|
"Daily prep: returns the open tasks eligible for today's MyDay selection. " +
|
||||||
|
"candidates = Idle, not blocked, in a git repo not excluded from the weekly report, and not already in MyDay. " +
|
||||||
|
"currentMyDay = Idle tasks already flagged IsMyDay (count them toward the cap). " +
|
||||||
|
"maxTasks = the hard cap on total open MyDay tasks. Use set_my_day to add tasks (never exceed maxTasks).")]
|
||||||
|
public async Task<DailyPrepDataDto> GetDailyPrepCandidates(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await using var ctx = await _dbFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
var settings = await new AppSettingsRepository(ctx).GetAsync(cancellationToken);
|
||||||
|
var excludes = DailyPrepFilter.ParseExcludes(settings.ReportExcludedPaths);
|
||||||
|
var maxTasks = settings.DailyPrepMaxTasks < 1 ? 1 : settings.DailyPrepMaxTasks;
|
||||||
|
|
||||||
|
var idle = await ctx.Tasks
|
||||||
|
.AsNoTracking()
|
||||||
|
.Include(t => t.List)
|
||||||
|
.Where(t => t.Status == TaskStatus.Idle)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
var currentMyDay = idle
|
||||||
|
.Where(t => t.IsMyDay)
|
||||||
|
.OrderBy(t => t.SortOrder)
|
||||||
|
.Select(ToCandidate)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var candidates = idle
|
||||||
|
.Where(t => !t.IsMyDay
|
||||||
|
&& t.BlockedByTaskId == null
|
||||||
|
&& DailyPrepFilter.IsIncludedRepo(t.List?.WorkingDir, excludes))
|
||||||
|
.OrderBy(t => t.CreatedAt)
|
||||||
|
.Select(ToCandidate)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return new DailyPrepDataDto(maxTasks, candidates, currentMyDay);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DailyPrepCandidateDto ToCandidate(TaskEntity t) => new(
|
||||||
|
t.Id, t.ListId, t.List?.Name ?? "", t.Title, t.Description,
|
||||||
|
t.IsStarred, t.ScheduledFor, t.CreatedAt);
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Add the filter helper** as a small static class at the bottom of `ExternalMcpService.cs` (single-consumer helper lives beside its consumer, per repo convention):
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
internal static class DailyPrepFilter
|
||||||
|
{
|
||||||
|
public static string[] ParseExcludes(string? json)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(json)) return [];
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var list = System.Text.Json.JsonSerializer.Deserialize<List<string>>(json);
|
||||||
|
return list is null ? [] : list.Select(Normalize).Where(p => p.Length > 0).ToArray();
|
||||||
|
}
|
||||||
|
catch (System.Text.Json.JsonException) { return []; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsIncludedRepo(string? workingDir, string[] excludes)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(workingDir)) return false; // not a repo → excluded
|
||||||
|
var norm = Normalize(workingDir);
|
||||||
|
return !excludes.Any(p => norm.StartsWith(p, StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Normalize(string path) =>
|
||||||
|
path.Trim().Replace('/', '\\').TrimEnd('\\');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Add `using ClaudeDo.Data.Repositories;` if not already present (it is, via existing usings).
|
||||||
|
|
||||||
|
- [ ] **Step 6: Run the test — expect PASS.**
|
||||||
|
|
||||||
|
- [ ] **Step 7: Commit.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/ClaudeDo.Worker/External/ExternalMcpService.cs tests/ClaudeDo.Worker.Tests/External/ExternalMcpServiceTests.cs
|
||||||
|
git commit -m "feat(daily-prep): add get_daily_prep_candidates MCP tool"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 3: `set_my_day` MCP tool with cap-guard
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/ClaudeDo.Worker/External/ExternalMcpService.cs`
|
||||||
|
- Test: `tests/ClaudeDo.Worker.Tests/External/ExternalMcpServiceTests.cs`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing tests.**
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[Fact]
|
||||||
|
public async Task SetMyDay_sets_flag_and_sort_order()
|
||||||
|
{
|
||||||
|
var svc = NewService();
|
||||||
|
var id = await SeedIdleTask("My task"); // existing/added helper returning task id
|
||||||
|
|
||||||
|
var dto = await svc.SetMyDay(id, isMyDay: true, sortOrder: 3, CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.True(dto.IsMyDay);
|
||||||
|
Assert.Equal(3, dto.SortOrder);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SetMyDay_rejects_when_cap_reached()
|
||||||
|
{
|
||||||
|
// AppSettings.DailyPrepMaxTasks = 1 (set in seed)
|
||||||
|
var svc = NewService();
|
||||||
|
var first = await SeedIdleTask("a");
|
||||||
|
var second = await SeedIdleTask("b");
|
||||||
|
await svc.SetMyDay(first, true, null, CancellationToken.None);
|
||||||
|
|
||||||
|
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
|
||||||
|
() => svc.SetMyDay(second, true, null, CancellationToken.None));
|
||||||
|
Assert.Contains("limit", ex.Message, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SetMyDay_unset_is_always_allowed()
|
||||||
|
{
|
||||||
|
var svc = NewService();
|
||||||
|
var id = await SeedIdleTask("a");
|
||||||
|
await svc.SetMyDay(id, true, null, CancellationToken.None);
|
||||||
|
|
||||||
|
var dto = await svc.SetMyDay(id, false, null, CancellationToken.None);
|
||||||
|
Assert.False(dto.IsMyDay);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`SetMyDay` returns the existing `TaskDto`. Add a `SortOrder` field to `TaskDto` — see Step 3a. (`SeedIdleTask` / the `DailyPrepMaxTasks=1` seed reuse the file's existing seeding helpers.)
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run — expect FAIL.**
|
||||||
|
|
||||||
|
- [ ] **Step 3a: Add `SortOrder` to `TaskDto`** (record + `ToDto`) so the result reflects ordering:
|
||||||
|
|
||||||
|
In the `TaskDto` record add `int SortOrder` as the last positional member, and in `ToDto(TaskEntity t)` add `t.SortOrder` as the last argument. (Update any test that constructs `TaskDto` positionally — search the test project.)
|
||||||
|
|
||||||
|
- [ ] **Step 3b: Add the tool method:**
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[McpServerTool, Description(
|
||||||
|
"Daily prep: set or clear a task's MyDay flag, optionally setting its sortOrder " +
|
||||||
|
"(use consecutive sortOrder values to keep related tasks together). " +
|
||||||
|
"Setting isMyDay=true is rejected if it would exceed the MyDay cap (DailyPrepMaxTasks open MyDay tasks); " +
|
||||||
|
"clearing (isMyDay=false) is always allowed.")]
|
||||||
|
public async Task<TaskDto> SetMyDay(
|
||||||
|
string taskId,
|
||||||
|
bool isMyDay,
|
||||||
|
int? sortOrder,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await using var ctx = await _dbFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
var task = await ctx.Tasks.FirstOrDefaultAsync(t => t.Id == taskId, cancellationToken)
|
||||||
|
?? throw new InvalidOperationException($"Task {taskId} not found.");
|
||||||
|
|
||||||
|
if (isMyDay && !task.IsMyDay)
|
||||||
|
{
|
||||||
|
var settings = await new AppSettingsRepository(ctx).GetAsync(cancellationToken);
|
||||||
|
var max = settings.DailyPrepMaxTasks < 1 ? 1 : settings.DailyPrepMaxTasks;
|
||||||
|
var openMyDay = await ctx.Tasks.CountAsync(
|
||||||
|
t => t.IsMyDay && t.Status == TaskStatus.Idle, cancellationToken);
|
||||||
|
if (openMyDay >= max)
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"MyDay limit {max} reached. Clear a task before adding another.");
|
||||||
|
}
|
||||||
|
|
||||||
|
task.IsMyDay = isMyDay;
|
||||||
|
if (sortOrder is not null) task.SortOrder = sortOrder.Value;
|
||||||
|
await ctx.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
await _broadcaster.TaskUpdated(taskId);
|
||||||
|
return ToDto(task);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run — expect PASS.**
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/ClaudeDo.Worker/External/ExternalMcpService.cs tests/ClaudeDo.Worker.Tests
|
||||||
|
git commit -m "feat(daily-prep): add set_my_day MCP tool with cap-guard"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 4: Rewrite `PrimeRunner` to run the daily prep
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `src/ClaudeDo.Worker/Prime/DailyPrepPrompt.cs`
|
||||||
|
- Modify: `src/ClaudeDo.Worker/Prime/PrimeRunner.cs`
|
||||||
|
- Test: `tests/ClaudeDo.Worker.Tests/Prime/DailyPrepPromptTests.cs`, and extend `PrimeRunnerTests.cs` if it exists
|
||||||
|
|
||||||
|
The runner needs the cap `X` (read from `AppSettings`) and today's date. Inject `IDbContextFactory<ClaudeDoDbContext>` into `PrimeRunner` (it is resolvable in the main app DI) and an `IPrimeClock` for the date (already registered).
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write failing prompt/args tests.**
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class DailyPrepPromptTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Build_prompt_contains_cap_and_date()
|
||||||
|
{
|
||||||
|
var prompt = DailyPrepPrompt.BuildPrompt(maxTasks: 5, today: new DateOnly(2026, 6, 3));
|
||||||
|
Assert.Contains("5", prompt);
|
||||||
|
Assert.Contains("2026-06-03", prompt);
|
||||||
|
Assert.Contains("get_daily_prep_candidates", prompt);
|
||||||
|
Assert.Contains("set_my_day", prompt);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Build_args_allows_only_the_two_tools()
|
||||||
|
{
|
||||||
|
var args = DailyPrepPrompt.BuildArgs(maxTurns: 30);
|
||||||
|
Assert.Contains("--output-format stream-json", args);
|
||||||
|
Assert.Contains("--max-turns 30", args);
|
||||||
|
Assert.Contains("--allowedTools", args);
|
||||||
|
Assert.Contains("mcp__claudedo__get_daily_prep_candidates", args);
|
||||||
|
Assert.Contains("mcp__claudedo__set_my_day", args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run — expect FAIL.**
|
||||||
|
|
||||||
|
- [ ] **Step 3: Create `DailyPrepPrompt.cs`:**
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
namespace ClaudeDo.Worker.Prime;
|
||||||
|
|
||||||
|
public static class DailyPrepPrompt
|
||||||
|
{
|
||||||
|
public const string CandidatesTool = "mcp__claudedo__get_daily_prep_candidates";
|
||||||
|
public const string SetMyDayTool = "mcp__claudedo__set_my_day";
|
||||||
|
|
||||||
|
public static string BuildArgs(int maxTurns) =>
|
||||||
|
"-p --output-format stream-json --verbose --permission-mode acceptEdits " +
|
||||||
|
$"--max-turns {maxTurns} " +
|
||||||
|
$"--allowedTools {CandidatesTool} {SetMyDayTool}";
|
||||||
|
|
||||||
|
public static string BuildPrompt(int maxTasks, DateOnly today) =>
|
||||||
|
$"""
|
||||||
|
Du bereitest meinen Arbeitstag fuer {today:yyyy-MM-dd} vor.
|
||||||
|
|
||||||
|
1. Rufe {CandidatesTool} auf.
|
||||||
|
2. Behalte bereits als MyDay markierte offene Tasks (currentMyDay) — entferne sie nicht.
|
||||||
|
3. Fuelle bis maximal {maxTasks} offene Tasks GESAMT in MyDay auf (currentMyDay zaehlt mit). Niemals mehr.
|
||||||
|
4. Schaetze pro Kandidat grob den Aufwand und waehle eine machbare Mischung (nicht nur Grossbrocken).
|
||||||
|
Priorisiere isStarred, faellige (scheduledFor) und aeltere Tasks.
|
||||||
|
5. Lege thematisch verwandte Tasks durch aufeinanderfolgende sortOrder-Werte nebeneinander.
|
||||||
|
6. Setze die Auswahl via {SetMyDayTool}(taskId, true, sortOrder). Markiere nichts ausserhalb der Kandidatenliste.
|
||||||
|
|
||||||
|
Wenn es keine Kandidaten gibt, tue nichts.
|
||||||
|
""";
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run prompt tests — expect PASS.**
|
||||||
|
|
||||||
|
- [ ] **Step 5: Rewrite `PrimeRunner.cs`:**
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using ClaudeDo.Data;
|
||||||
|
using ClaudeDo.Data.Repositories;
|
||||||
|
using ClaudeDo.Worker.Runner;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Worker.Prime;
|
||||||
|
|
||||||
|
public sealed class PrimeRunner : IPrimeRunner
|
||||||
|
{
|
||||||
|
private static readonly TimeSpan FireTimeout = TimeSpan.FromMinutes(5);
|
||||||
|
private const int MaxTurns = 30;
|
||||||
|
|
||||||
|
private readonly IClaudeProcess _claude;
|
||||||
|
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
|
||||||
|
private readonly IPrimeClock _clock;
|
||||||
|
private readonly ILogger<PrimeRunner> _logger;
|
||||||
|
private readonly SemaphoreSlim _gate = new(1, 1);
|
||||||
|
|
||||||
|
public PrimeRunner(
|
||||||
|
IClaudeProcess claude,
|
||||||
|
IDbContextFactory<ClaudeDoDbContext> dbFactory,
|
||||||
|
IPrimeClock clock,
|
||||||
|
ILogger<PrimeRunner> logger)
|
||||||
|
{
|
||||||
|
_claude = claude;
|
||||||
|
_dbFactory = dbFactory;
|
||||||
|
_clock = clock;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<PrimeRunOutcome> FireAsync(PrimeScheduleDto schedule, CancellationToken ct)
|
||||||
|
{
|
||||||
|
if (!await _gate.WaitAsync(0, ct))
|
||||||
|
return new PrimeRunOutcome(false, "Daily prep already running");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
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: _ => Task.CompletedTask,
|
||||||
|
ct: timeoutCts.Token);
|
||||||
|
|
||||||
|
return result.IsSuccess
|
||||||
|
? 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
|
||||||
|
{
|
||||||
|
_gate.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 6: Fix the DI registration is unchanged** (`AddSingleton<IPrimeRunner, PrimeRunner>()` already works — the new ctor deps `IDbContextFactory` and `IPrimeClock` are registered). Build the Worker.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 7: Update/extend `PrimeRunnerTests.cs`** (if present) to match the new ctor: construct `PrimeRunner` with a fake `IClaudeProcess`, a real temp-SQLite `IDbContextFactory`, a fake `IPrimeClock`, and a logger. Add:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[Fact]
|
||||||
|
public async Task FireAsync_returns_already_running_when_gate_held()
|
||||||
|
{
|
||||||
|
var runner = NewRunner(claudeDelay: TimeSpan.FromSeconds(2));
|
||||||
|
var schedule = new PrimeScheduleDto(Guid.Empty, 0, TimeSpan.Zero, true, null, null);
|
||||||
|
|
||||||
|
var first = runner.FireAsync(schedule, CancellationToken.None);
|
||||||
|
var second = await runner.FireAsync(schedule, CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.False(second.Success);
|
||||||
|
Assert.Contains("already running", second.Message, StringComparison.OrdinalIgnoreCase);
|
||||||
|
await first;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If no `PrimeRunnerTests.cs` exists, create one. The fake `IClaudeProcess` should optionally delay (to keep the gate held) and return a successful `RunResult { ExitCode = 0, ResultMarkdown = "ok" }`.
|
||||||
|
|
||||||
|
- [ ] **Step 8: Run — expect PASS.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter "DailyPrepPrompt|PrimeRunner"
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 9: Commit.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/ClaudeDo.Worker/Prime tests/ClaudeDo.Worker.Tests/Prime
|
||||||
|
git commit -m "feat(daily-prep): run daily prep from PrimeRunner via allowed MCP tools"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 5: Hub — `RunDailyPrepNow` + expose `DailyPrepMaxTasks`
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/ClaudeDo.Worker/Hub/WorkerHub.cs`
|
||||||
|
- Modify: `src/ClaudeDo.Ui/Services/WorkerClient.cs`
|
||||||
|
|
||||||
|
Read `WorkerHub.cs` first. It already exposes a `GetAppSettings`/`UpdateAppSettings` pair backed by a DTO record (the one carrying `ReportExcludedPaths`, `StandupWeekday`).
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add `DailyPrepMaxTasks` to the hub AppSettings DTO record** (the record near the top of `WorkerHub.cs` that lists `ReportExcludedPaths`). Add `int DailyPrepMaxTasks` as a member. In the read mapping (`GetAppSettings`, where `row.ReportExcludedPaths` is read) add `row.DailyPrepMaxTasks`; in the write mapping (`UpdateAppSettings`, where `ReportExcludedPaths = dto.ReportExcludedPaths`) add `DailyPrepMaxTasks = dto.DailyPrepMaxTasks`.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add the hub method.** Inject `IPrimeRunner` and `HubBroadcaster` if the hub does not already have them (the hub is constructed by SignalR via DI; both are registered singletons). Then:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public async Task<bool> RunDailyPrepNow()
|
||||||
|
{
|
||||||
|
var schedule = new PrimeScheduleDto(Guid.Empty, 0, TimeSpan.Zero, true, null, null);
|
||||||
|
var firedAt = DateTimeOffset.Now;
|
||||||
|
var outcome = await _primeRunner.FireAsync(schedule, Context.ConnectionAborted);
|
||||||
|
await _broadcaster.PrimeFired(Guid.Empty, outcome.Success, outcome.Message, firedAt);
|
||||||
|
return outcome.Success;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Add `using ClaudeDo.Worker.Prime;` to `WorkerHub.cs` if missing.
|
||||||
|
|
||||||
|
> **Caution (memory):** changing the `WorkerHub` constructor breaks hand-rolled hub-test fakes in `ClaudeDo.Worker.Tests` and possibly `ClaudeDo.Ui.Tests`. After editing, build the test projects and fix every `new WorkerHub(...)` / fake `IWorkerClient` construction the compiler flags.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Mirror the DTO in the UI** (`WorkerClient.cs`, the AppSettings DTO around line 498): add `int DailyPrepMaxTasks` to the record (same position as in the hub DTO). Add a `RunDailyPrepNow` client call:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public Task<bool> RunDailyPrepNowAsync() =>
|
||||||
|
_connection.InvokeAsync<bool>("RunDailyPrepNow");
|
||||||
|
```
|
||||||
|
|
||||||
|
(Match the exact connection field/name and the async-wrapper style used by neighbouring calls like `GenerateWeekReport`.)
|
||||||
|
|
||||||
|
- [ ] **Step 4: Build Worker + App + test projects; fix any broken fakes.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/ClaudeDo.Worker/Hub/WorkerHub.cs src/ClaudeDo.Ui/Services/WorkerClient.cs tests
|
||||||
|
git commit -m "feat(daily-prep): add RunDailyPrepNow hub method and expose DailyPrepMaxTasks"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 6: Settings UI — edit `DailyPrepMaxTasks`
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/ClaudeDo.Ui/ViewModels/Modals/Settings/PrimeClaudeTabViewModel.cs`
|
||||||
|
- Modify: the Prime Claude tab markup in `src/ClaudeDo.Ui/Views/Modals/SettingsModalView.axaml`
|
||||||
|
- Modify: `src/ClaudeDo.Ui/ViewModels/Modals/SettingsModalViewModel.cs` (load/save wiring, where other AppSettings fields are mapped)
|
||||||
|
|
||||||
|
Read these three files first; mirror how an existing numeric AppSetting (e.g. `MaxParallelExecutions` or `WorktreeAutoCleanupDays`) is loaded from the hub DTO, bound, and saved back.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add an observable property** to `PrimeClaudeTabViewModel.cs`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[ObservableProperty] private int _dailyPrepMaxTasks = 5;
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Wire load/save** in `SettingsModalViewModel.cs`: where the AppSettings DTO is read into the tabs, set `PrimeClaude.DailyPrepMaxTasks = dto.DailyPrepMaxTasks;`. Where the DTO is written, include `DailyPrepMaxTasks = PrimeClaude.DailyPrepMaxTasks`. (Use the exact tab property name for the Prime Claude tab in that VM.)
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add the editor** in the Prime Claude tab of `SettingsModalView.axaml`, near the schedule list:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
|
||||||
|
<TextBlock Text="{x:Static loc:L.Settings_DailyPrepMaxTasks}" VerticalAlignment="Center"/>
|
||||||
|
<NumericUpDown Minimum="1" Maximum="50" Increment="1" Width="100"
|
||||||
|
Value="{Binding PrimeClaude.DailyPrepMaxTasks}"/>
|
||||||
|
</StackPanel>
|
||||||
|
```
|
||||||
|
|
||||||
|
Add the `Settings_DailyPrepMaxTasks` key to both `locales/en.json` and `locales/de.json` (en: "Max tasks per day", de: "Max. Aufgaben pro Tag"). If the tab does not use localized labels yet, use a plain `Text="Max tasks per day"` string to match its current style.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Build the App; smoke-build the UI.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/ClaudeDo.Ui src/ClaudeDo.Localization
|
||||||
|
git commit -m "feat(daily-prep): add DailyPrepMaxTasks editor to Prime Claude settings"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 7: MyDay header — "Tag vorbereiten" button
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: the ViewModel backing the MyDay list view (the one that exposes the smart-list header/toolbar; find it under `src/ClaudeDo.Ui/ViewModels/Islands/` — likely the tasks/list island VM that has access to `IWorkerClient`)
|
||||||
|
- Modify: the corresponding view (`.axaml`) that renders the list header
|
||||||
|
|
||||||
|
Read the island VM + view first. Find where the active list is known to be `smart:my-day` so the button can be shown only there (mirror any existing conditional header content). The VM already holds a worker-client reference used by other commands (e.g. RunNow) — reuse it.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add the command** to the island VM:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[RelayCommand]
|
||||||
|
private async Task PrepareDayAsync()
|
||||||
|
{
|
||||||
|
await _workerClient.RunDailyPrepNowAsync();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
(Use the VM's existing worker-client field name. The MyDay list refreshes automatically via the `TaskUpdated` broadcast the tools emit, so no manual reload is needed.)
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add an `IsMyDayList` (or reuse existing selected-list) guard** so the button only appears on the MyDay smart list. If the VM already exposes the selected list id, add:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public bool IsMyDayList => SelectedListId == "smart:my-day";
|
||||||
|
```
|
||||||
|
|
||||||
|
and raise its change notification wherever `SelectedListId` changes (mirror existing patterns; if a `[NotifyPropertyChangedFor]` or manual `OnPropertyChanged` is already used for the selection, add this property to it).
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add the button** to the list header in the view, visible only on MyDay:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<Button Content="{x:Static loc:L.MyDay_PrepareDay}"
|
||||||
|
Command="{Binding PrepareDayCommand}"
|
||||||
|
IsVisible="{Binding IsMyDayList}"/>
|
||||||
|
```
|
||||||
|
|
||||||
|
Add `MyDay_PrepareDay` to `locales/en.json` ("Prepare day") and `locales/de.json` ("Tag vorbereiten"), or a plain string if the view is not localized.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Build the App.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Manual smoke (cannot be unit-tested):** start the Worker and App, open MyDay, click "Tag vorbereiten", confirm tasks appear (capped) and the button is hidden on other lists. Report results explicitly — do not claim UI success without running it.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/ClaudeDo.Ui src/ClaudeDo.Localization
|
||||||
|
git commit -m "feat(daily-prep): add Prepare-day button to MyDay header"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Final verification
|
||||||
|
|
||||||
|
- [ ] `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release`
|
||||||
|
- [ ] `dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release`
|
||||||
|
- [ ] `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release`
|
||||||
|
- [ ] `dotnet test tests/ClaudeDo.Data.Tests/ClaudeDo.Data.Tests.csproj -c Release`
|
||||||
|
- [ ] `dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release`
|
||||||
|
- [ ] End-to-end manual run: schedule fires (or button) → Claude calls the two tools → MyDay gets a capped subset; re-run keeps existing MyDay and tops up without exceeding the cap.
|
||||||
|
|
||||||
|
## Notes / risks
|
||||||
|
|
||||||
|
- Relies on the globally registered `claudedo` MCP (installer `RegisterMcpStep`). If absent, the prep run produces 0 changes — acceptable for v1.
|
||||||
|
- `--permission-mode acceptEdits` + explicit `--allowedTools` pre-approves exactly the two tools so the headless run never blocks on a permission prompt.
|
||||||
|
- The cap-guard counts `Idle && IsMyDay` tasks; it is the source of truth for the "never move everything in" invariant regardless of Claude's behavior.
|
||||||
|
- Future phase (out of scope): external ticket sources (Jira) feed into `get_daily_prep_candidates` behind a task-source abstraction.
|
||||||
182
docs/superpowers/specs/2026-06-03-daily-prep-design.md
Normal file
182
docs/superpowers/specs/2026-06-03-daily-prep-design.md
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
# Daily Prep ("Prime Claude") — Design
|
||||||
|
|
||||||
|
Date: 2026-06-03
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Turn the existing Prime Time warm-up into a **daily preparation** ("Tagesvorbereitung").
|
||||||
|
At a scheduled time (or on demand), Claude reads the open tasks, estimates effort,
|
||||||
|
and selects a focused subset into the MyDay list — capped so it never moves
|
||||||
|
everything in. Claude does the reasoning itself (agentic), via the already-registered
|
||||||
|
ClaudeDo MCP. This replaces the current `"ping"` behavior entirely.
|
||||||
|
|
||||||
|
A later phase will feed external tickets (Jira, possibly a second system) into the
|
||||||
|
same candidate pool; that is out of scope for this spec.
|
||||||
|
|
||||||
|
## Goals
|
||||||
|
|
||||||
|
- Scheduled and manual ("Tag vorbereiten" button) daily prep.
|
||||||
|
- Claude picks a subset of open tasks into MyDay, ordered so related tasks sit together.
|
||||||
|
- Effort-aware selection, hard-capped at `X` open MyDay tasks.
|
||||||
|
- Keep existing MyDay tasks across re-runs; only top up to `X`.
|
||||||
|
- Candidates limited to tasks in repos that are **not** excluded from the weekly report.
|
||||||
|
|
||||||
|
## Non-Goals
|
||||||
|
|
||||||
|
- External ticket integration (Jira etc.) — future phase.
|
||||||
|
- Group labels/headers in the MyDay view — grouping is ordering-only via `SortOrder`.
|
||||||
|
- A user-editable prep prompt — the prompt is fixed, parameterized.
|
||||||
|
|
||||||
|
## Key Decisions
|
||||||
|
|
||||||
|
| Topic | Decision |
|
||||||
|
| --- | --- |
|
||||||
|
| Who reasons | Agentic — Claude decides via MCP tools. |
|
||||||
|
| MyDay model | `TaskEntity.IsMyDay` flag (smart list `smart:my-day`). |
|
||||||
|
| Grouping | Ordering only via existing `SortOrder` (no new field, no migration for grouping). |
|
||||||
|
| Selection | Effort estimate, hard cap `X` tasks/day. |
|
||||||
|
| Candidates | `Status == Idle`, `BlockedByTaskId == null`, list `WorkingDir` not under `ReportExcludedPaths`. |
|
||||||
|
| Re-run | Keep existing MyDay tasks; top up to `X`. |
|
||||||
|
| Trigger | Existing Prime schedule **and** a manual button. |
|
||||||
|
| Ping | Removed — daily prep replaces it. |
|
||||||
|
| Prompt | Fixed, with injected parameters (`X`, today's date). |
|
||||||
|
| Tool access | Reuse the globally registered `claudedo` MCP — **no** separate `--mcp-config`. |
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### 1. MCP tools (extend `ExternalMcpService`, port 47822)
|
||||||
|
|
||||||
|
The worker already exposes `ExternalMcpService` as the `claudedo` MCP server. Add two tools;
|
||||||
|
they automatically surface as `mcp__claudedo__get_daily_prep_candidates` and
|
||||||
|
`mcp__claudedo__set_my_day`.
|
||||||
|
|
||||||
|
- **`get_daily_prep_candidates()`** → JSON containing:
|
||||||
|
- `candidates[]`: open, non-blocked tasks in non-excluded repos, each with
|
||||||
|
`id, title, description, listName, isStarred, scheduledFor, age` (age derived from `CreatedAt`).
|
||||||
|
- `currentMyDay[]`: currently-`IsMyDay` open tasks (so Claude sees remaining capacity).
|
||||||
|
- Filter: `Status == Idle` AND `BlockedByTaskId == null` AND the task's list `WorkingDir`
|
||||||
|
does not start with any prefix in `AppSettings.ReportExcludedPaths`
|
||||||
|
(default `["C:\\Private"]`; case-insensitive prefix match, same semantics as the weekly report).
|
||||||
|
|
||||||
|
- **`set_my_day(taskId, isMyDay, sortOrder?)`** →
|
||||||
|
- Sets `IsMyDay` and (optionally) `SortOrder` on the task via `TaskRepository`.
|
||||||
|
- Broadcasts `TaskUpdated` via `HubBroadcaster` so the UI updates live.
|
||||||
|
- **Cap-guard:** when `isMyDay == true`, count current open (`Idle`) tasks with
|
||||||
|
`IsMyDay == true`. If `count >= X`, reject with an error message
|
||||||
|
("MyDay limit {X} reached"). `isMyDay == false` is always allowed.
|
||||||
|
`X = AppSettings.DailyPrepMaxTasks`. This guarantees the "never move everything in"
|
||||||
|
invariant server-side, independent of Claude's behavior.
|
||||||
|
|
||||||
|
### 2. `DailyPrepRunner` (replaces ping logic)
|
||||||
|
|
||||||
|
Rename `IPrimeRunner`/`PrimeRunner` → `IDailyPrepRunner`/`DailyPrepRunner` (the `"ping"`
|
||||||
|
concept is gone). It:
|
||||||
|
|
||||||
|
- Loads `AppSettings` (`X = DailyPrepMaxTasks`).
|
||||||
|
- Builds the fixed prompt with injected parameters (`X`, today's date).
|
||||||
|
- Invokes `claude -p --output-format stream-json --verbose` with:
|
||||||
|
- `--permission-mode` set so the headless run won't block on permission prompts,
|
||||||
|
- `--allowedTools mcp__claudedo__get_daily_prep_candidates mcp__claudedo__set_my_day`,
|
||||||
|
- `--max-turns 30` (constant), timeout 5 min (constant; larger than the old 60s ping).
|
||||||
|
- **No `--mcp-config`** — relies on the globally registered `claudedo` MCP (the worker runs
|
||||||
|
as the user via the per-user logon Scheduled Task, so the headless run inherits the
|
||||||
|
user-scope registration and its auth).
|
||||||
|
- Returns an outcome (e.g. number of tasks added) for broadcasting.
|
||||||
|
|
||||||
|
### 3. Scheduler
|
||||||
|
|
||||||
|
`PrimeScheduler` is unchanged in structure — it now calls `IDailyPrepRunner` instead of the
|
||||||
|
ping runner. `NextDueCalculator` and the schedule model are untouched.
|
||||||
|
|
||||||
|
### 4. Manual trigger
|
||||||
|
|
||||||
|
- Worker hub method `RunDailyPrepNow()` invokes the same `DailyPrepRunner`.
|
||||||
|
- UI button **"Tag vorbereiten"** in the MyDay list header.
|
||||||
|
- **Single-flight guard:** if a prep run is already in progress, the trigger reports
|
||||||
|
"already running" and does not start a parallel run (applies to both schedule and button).
|
||||||
|
|
||||||
|
### 5. Parameter config
|
||||||
|
|
||||||
|
- New field **`DailyPrepMaxTasks`** (int, default `5`) on `AppSettingsEntity`.
|
||||||
|
- Plumbing: EF config + migration, `AppSettingsRepository`, `WorkerHub` AppSettings DTO,
|
||||||
|
UI DTO mirror + `WorkerClient`, and a numeric editor in the Prime Claude settings tab.
|
||||||
|
- `ReportExcludedPaths` is reused as-is (already on `AppSettings`).
|
||||||
|
|
||||||
|
## Data Flow
|
||||||
|
|
||||||
|
1. Trigger (schedule due **or** button) → `DailyPrepRunner.RunAsync`.
|
||||||
|
2. Runner loads `AppSettings` (`X`), builds prompt, launches Claude.
|
||||||
|
3. Claude → `get_daily_prep_candidates` → DB query returns filtered candidates + current MyDay.
|
||||||
|
4. Claude estimates effort, tops up to **X total**, calls `set_my_day(id, true, sortOrder)`
|
||||||
|
for each chosen task (consecutive `sortOrder` for related tasks).
|
||||||
|
5. `ExternalMcpService` writes `IsMyDay`/`SortOrder`, broadcasts `TaskUpdated` → MyDay list
|
||||||
|
updates live.
|
||||||
|
6. Runner updates `LastRunAt`, broadcasts "prep done" (count added).
|
||||||
|
|
||||||
|
## Fixed Prompt (parameterized)
|
||||||
|
|
||||||
|
Content (parameters in `{}`):
|
||||||
|
|
||||||
|
> Du bereitest meinen Arbeitstag für **{today}** vor.
|
||||||
|
> 1. Rufe `get_daily_prep_candidates` auf.
|
||||||
|
> 2. Behalte bereits als MyDay markierte offene Tasks.
|
||||||
|
> 3. Fülle bis **maximal {X} offene Tasks gesamt** in MyDay auf — niemals mehr.
|
||||||
|
> 4. Schätze pro Task grob den Aufwand; wähle eine machbare Mischung (nicht nur Großbrocken).
|
||||||
|
> Priorisiere `isStarred`, fällige (`scheduledFor`) und ältere Tasks.
|
||||||
|
> 5. Lege thematisch verwandte Tasks durch aufeinanderfolgende `sortOrder`-Werte nebeneinander.
|
||||||
|
> 6. Setze die Auswahl via `set_my_day(id, true, sortOrder)`. Markiere nichts außerhalb der
|
||||||
|
> Kandidatenliste.
|
||||||
|
|
||||||
|
Injected parameters: `{today}` (date) and `{X}` (= `DailyPrepMaxTasks`).
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
- No candidates → Claude marks nothing; runner reports "0 added".
|
||||||
|
- Claude run fails / times out → log + failure broadcast (existing scheduler event channel);
|
||||||
|
`LastRunAt` is set on attempt, as today, to avoid tight retry loops.
|
||||||
|
- `set_my_day` on an invalid/ineligible id → tool returns an error string; Claude adapts.
|
||||||
|
- Cap exceeded → tool returns an error; Claude stops adding.
|
||||||
|
- Concurrent trigger → single-flight guard reports "already running".
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Real SQLite + real git (project convention).
|
||||||
|
|
||||||
|
- `get_daily_prep_candidates`: only `Idle`; blocked excluded; tasks in excluded repos
|
||||||
|
(`ReportExcludedPaths`) excluded; current MyDay tasks included.
|
||||||
|
- `set_my_day`: sets flag + `SortOrder`; broadcasts `TaskUpdated`; cap-guard rejects at limit;
|
||||||
|
unset always allowed.
|
||||||
|
- `DailyPrepRunner`: prompt contains `{X}` + date; args contain `--allowedTools` +
|
||||||
|
permission-mode + `--max-turns`; success/failure outcomes via an `IClaudeProcess` fake.
|
||||||
|
- Rename `IPrimeRunner` → `IDailyPrepRunner` requires syncing `PrimeScheduler` tests/fakes.
|
||||||
|
|
||||||
|
## Files to Create / Modify (high level)
|
||||||
|
|
||||||
|
**Data**
|
||||||
|
- `Models/AppSettingsEntity.cs` — add `DailyPrepMaxTasks`.
|
||||||
|
- `Configuration/AppSettingsEntityConfiguration.cs` — map new column.
|
||||||
|
- `Migrations/` — new migration for `daily_prep_max_tasks`.
|
||||||
|
- `Repositories/AppSettingsRepository.cs` — persist new field.
|
||||||
|
|
||||||
|
**Worker**
|
||||||
|
- `External/ExternalMcpService.cs` — add `get_daily_prep_candidates`, `set_my_day` (+ cap-guard).
|
||||||
|
- `Prime/PrimeRunner.cs` → `DailyPrepRunner.cs`; `Prime/Interfaces/IPrimeRunner.cs`
|
||||||
|
→ `IDailyPrepRunner.cs`; prompt builder + arg builder.
|
||||||
|
- `Prime/PrimeScheduler.cs` — depend on `IDailyPrepRunner`.
|
||||||
|
- `Hub/WorkerHub.cs` — AppSettings DTO field; `RunDailyPrepNow()`.
|
||||||
|
- `Program.cs` — DI registration update.
|
||||||
|
|
||||||
|
**UI**
|
||||||
|
- `Services/WorkerClient.cs` + AppSettings DTO mirror — new field; `RunDailyPrepNow` call.
|
||||||
|
- Prime Claude settings tab VM/view — numeric editor for `DailyPrepMaxTasks`.
|
||||||
|
- MyDay list header — "Tag vorbereiten" button + command (Lists/IslandsShell VM).
|
||||||
|
|
||||||
|
**Tests**
|
||||||
|
- `ClaudeDo.Worker.Tests` — MCP tools, runner, scheduler fakes.
|
||||||
|
- `ClaudeDo.Data.Tests` — AppSettings persistence (if covered there).
|
||||||
|
- `ClaudeDo.Ui.Tests` — settings VM / button wiring as applicable.
|
||||||
|
|
||||||
|
## Future Phase (out of scope)
|
||||||
|
|
||||||
|
External ticket sources (Jira, possibly a second system) feed into the candidate pool used by
|
||||||
|
`get_daily_prep_candidates`, behind a task-source abstraction. Designed separately.
|
||||||
151
docs/superpowers/specs/2026-06-03-daily-prep-live-view-design.md
Normal file
151
docs/superpowers/specs/2026-06-03-daily-prep-live-view-design.md
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
# Daily Prep — Live Output View + Clear Day — Design
|
||||||
|
|
||||||
|
Date: 2026-06-03
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Two follow-ups to the daily-prep ("Prime Claude") feature:
|
||||||
|
|
||||||
|
1. **Live output view.** While Claude prepares the day, there is no feedback. Add a
|
||||||
|
live, human-readable view of the prep run's output, shown as a new content mode in
|
||||||
|
the existing right-hand **Details island** (mirroring how Daily Notes works — a mode
|
||||||
|
swap, not a separate window/column).
|
||||||
|
2. **Clear Day button.** A MyDay-header button that clears the MyDay selection
|
||||||
|
immediately.
|
||||||
|
|
||||||
|
## Goals
|
||||||
|
|
||||||
|
- See the prep run's progress live, rendered with the same friendly terminal renderer
|
||||||
|
used for task runs (assistant text + tool calls like `set_my_day …`, not raw NDJSON).
|
||||||
|
- Both manual (button) and scheduled prep runs stream into the log.
|
||||||
|
- The manual button opens the prep view; a scheduled run fills the log silently and is
|
||||||
|
opened via a dedicated "Vorbereitungs-Log" button (the existing `PrimeStatus` footer
|
||||||
|
remains the hint that a run happened).
|
||||||
|
- A "Tag leeren" button clears all MyDay tasks (any status) with no confirmation.
|
||||||
|
|
||||||
|
## Non-Goals
|
||||||
|
|
||||||
|
- No new island/column and no popup/overlay — reuse the Details island as a mode swap.
|
||||||
|
- No persistence of prep output across app restarts (in-memory log only).
|
||||||
|
- No undo for Clear Day (re-runnable via "Tag vorbereiten").
|
||||||
|
|
||||||
|
## Key Decisions
|
||||||
|
|
||||||
|
| Topic | Decision |
|
||||||
|
| --- | --- |
|
||||||
|
| Rendering | Reuse the existing `SessionTerminalView` / `StreamLineFormatter` renderer. |
|
||||||
|
| Location | New `IsPrepMode` content panel inside the Details island (like `IsNotesMode`). |
|
||||||
|
| Lifecycle | Manual click opens the view (UI-local); `PrepStarted/PrepLine/PrepFinished` events fill the log regardless of current mode; scheduled runs do not auto-open. |
|
||||||
|
| Open after schedule | Dedicated "Vorbereitungs-Log" header button + existing `PrimeStatus` footer hint. |
|
||||||
|
| Clear Day scope | All MyDay tasks regardless of status. |
|
||||||
|
| Clear Day confirm | None — clear directly. |
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Feature A — Live prep output
|
||||||
|
|
||||||
|
**Worker**
|
||||||
|
- Extend `IPrimeBroadcaster` (`src/ClaudeDo.Worker/Prime/Interfaces/IPrimeBroadcaster.cs`)
|
||||||
|
with `PrepStartedAsync()`, `PrepLineAsync(string line)`, `PrepFinishedAsync(bool success)`.
|
||||||
|
- Implement in `HubBroadcaster` (`src/ClaudeDo.Worker/Hub/HubBroadcaster.cs`) sending
|
||||||
|
SignalR events `PrepStarted`, `PrepLine` (string), `PrepFinished` (bool).
|
||||||
|
- `PrimeRunner` (`src/ClaudeDo.Worker/Prime/PrimeRunner.cs`): inject `IPrimeBroadcaster`.
|
||||||
|
In `FireAsync`, after the single-flight gate is entered and a run will actually happen:
|
||||||
|
call `PrepStartedAsync()` before `RunAsync`; replace the discard lambda with
|
||||||
|
`async line => await _broadcaster.PrepLineAsync(line)`; call
|
||||||
|
`PrepFinishedAsync(result.IsSuccess)` after. The "already running" early-return path
|
||||||
|
emits nothing (no run occurs). Both scheduled and manual runs go through `FireAsync`,
|
||||||
|
so both stream.
|
||||||
|
|
||||||
|
**UI**
|
||||||
|
- `WorkerClient` (`src/ClaudeDo.Ui/Services/WorkerClient.cs`): register
|
||||||
|
`_hub.On<…>("PrepStarted"/"PrepLine"/"PrepFinished", …)` each via
|
||||||
|
`Dispatcher.UIThread.Post`, raising `PrepStartedEvent` / `PrepLineEvent(string)` /
|
||||||
|
`PrepFinishedEvent(bool)`. Declare these on `IWorkerClient`.
|
||||||
|
- `DetailsIslandViewModel`: add `IsPrepMode` (bool), `IsPrepRunning` (bool), a dedicated
|
||||||
|
`PrepLog` (`ObservableCollection<LogLineViewModel>`), and `ShowPrep()` (calls
|
||||||
|
`Bind(null)`, sets `IsNotesMode=false`, `IsPrepMode=true`). Subscribe to the three prep
|
||||||
|
events in the ctor (always active, independent of mode):
|
||||||
|
- `PrepStarted` → clear `PrepLog`, `IsPrepRunning=true`.
|
||||||
|
- `PrepLine` → format the line with the same `StreamLineFormatter` path used by the
|
||||||
|
stdout branch of `OnTaskMessage`, append a `LogLineViewModel` to `PrepLog`.
|
||||||
|
- `PrepFinished` → `IsPrepRunning=false` (optionally append a status line).
|
||||||
|
Mode exclusivity: the normal task-details panel becomes visible on
|
||||||
|
`!IsNotesMode && !IsPrepMode`; `ShowNotes()` also sets `IsPrepMode=false`; `Bind(task)`
|
||||||
|
resets both flags.
|
||||||
|
- `DetailsIslandView.axaml`: add a third `<Panel IsVisible="{Binding IsPrepMode}">` in the
|
||||||
|
body grid alongside the existing details/notes panels, rendering `PrepLog` in the
|
||||||
|
terminal style (reuse the `LogLineViewModel` item template used by `SessionTerminalView`).
|
||||||
|
|
||||||
|
**Wiring**
|
||||||
|
- `TasksIslandViewModel`: add a `PrepRequested` event (mirror `NotesRequested`).
|
||||||
|
`PrepareDayCommand` raises `PrepRequested` in addition to calling
|
||||||
|
`RunDailyPrepNowAsync()`. Add `ShowPrepLogCommand` that raises `PrepRequested`. Add the
|
||||||
|
"Vorbereitungs-Log" button to the MyDay header (`IsVisible="{Binding IsMyDayList}"`).
|
||||||
|
- `IslandsShellViewModel`: wire `Tasks.PrepRequested += () => Details.ShowPrep()`.
|
||||||
|
|
||||||
|
### Feature B — Clear Day
|
||||||
|
|
||||||
|
**Worker**
|
||||||
|
- `WorkerHub.ClearMyDay()` (`src/ClaudeDo.Worker/Hub/WorkerHub.cs`): query ids where
|
||||||
|
`IsMyDay == true`; `ExecuteUpdateAsync` setting `is_my_day = false`; broadcast
|
||||||
|
`TaskUpdated(id)` for each affected id (the UI reloads the current list on `TaskUpdated`).
|
||||||
|
|
||||||
|
**UI**
|
||||||
|
- `IWorkerClient.ClearMyDayAsync()` + `WorkerClient` impl invoking `"ClearMyDay"`.
|
||||||
|
- `TasksIslandViewModel.ClearDayCommand` calls `_worker.ClearMyDayAsync()` (no confirm).
|
||||||
|
Add the "Tag leeren" button to the MyDay header next to "Tag vorbereiten".
|
||||||
|
|
||||||
|
## Data Flow (live view)
|
||||||
|
|
||||||
|
1. Trigger (schedule or button) → `PrimeRunner.FireAsync`.
|
||||||
|
2. `PrepStartedAsync()` → SignalR `PrepStarted` → `WorkerClient.PrepStartedEvent` →
|
||||||
|
`DetailsIslandViewModel` clears `PrepLog`, sets `IsPrepRunning`.
|
||||||
|
3. Each Claude stdout line → `PrepLineAsync(line)` → `PrepLine` → formatted, appended to
|
||||||
|
`PrepLog` (visible if the user is in prep mode; filled silently otherwise).
|
||||||
|
4. Run ends → `PrepFinishedAsync(success)` → `PrepFinished` → `IsPrepRunning=false`.
|
||||||
|
5. Manual button click also raised `PrepRequested` → `Details.ShowPrep()` (view open).
|
||||||
|
After a scheduled run, the user clicks "Vorbereitungs-Log" to open it.
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
- Prep run fails/times out → `PrepFinished(false)`; the existing `PrimeFired` footer
|
||||||
|
status still reports failure.
|
||||||
|
- "Already running" → no prep events emitted (no run happened); existing behavior intact.
|
||||||
|
- `ClearMyDay` with zero MyDay tasks → no-op, no broadcasts.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
- Worker: `PrimeRunner` streams `PrepStarted` → N×`PrepLine` → `PrepFinished` (fake
|
||||||
|
`IClaudeProcess` invokes `onStdoutLine` with sample lines; fake `IPrimeBroadcaster`
|
||||||
|
records calls). `WorkerHub.ClearMyDay` clears all IsMyDay rows and broadcasts per id
|
||||||
|
(real SQLite, mirror existing hub tests).
|
||||||
|
- UI: `DetailsIslandViewModel` appends to `PrepLog` on `PrepLineEvent` and `ShowPrep()`
|
||||||
|
sets the mode flags (mutual exclusivity with notes); `TasksIslandViewModel.ClearDayCommand`
|
||||||
|
calls `ClearMyDayAsync` (stub worker client).
|
||||||
|
|
||||||
|
## Files (high level)
|
||||||
|
|
||||||
|
**Modify**
|
||||||
|
- `src/ClaudeDo.Worker/Prime/Interfaces/IPrimeBroadcaster.cs`
|
||||||
|
- `src/ClaudeDo.Worker/Hub/HubBroadcaster.cs`
|
||||||
|
- `src/ClaudeDo.Worker/Prime/PrimeRunner.cs`
|
||||||
|
- `src/ClaudeDo.Worker/Hub/WorkerHub.cs` (ClearMyDay)
|
||||||
|
- `src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs`
|
||||||
|
- `src/ClaudeDo.Ui/Services/WorkerClient.cs`
|
||||||
|
- `src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs`
|
||||||
|
- `src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml`
|
||||||
|
- `src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs`
|
||||||
|
- `src/ClaudeDo.Ui/Views/Islands/TasksIslandView.axaml`
|
||||||
|
- `src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs`
|
||||||
|
- `src/ClaudeDo.Localization/locales/en.json`, `de.json` (button labels)
|
||||||
|
|
||||||
|
**Test**
|
||||||
|
- `tests/ClaudeDo.Worker.Tests/Prime/PrimeRunnerTests.cs`
|
||||||
|
- `tests/ClaudeDo.Worker.Tests/Hub/…` (ClearMyDay)
|
||||||
|
- `tests/ClaudeDo.Ui.Tests/…` (DetailsIslandViewModel prep events; TasksIslandViewModel ClearDay) + `StubWorkerClient`
|
||||||
|
|
||||||
|
## Known fragility
|
||||||
|
|
||||||
|
Changing `IWorkerClient` / `WorkerClient` / VM constructors breaks hand-rolled fakes
|
||||||
|
(`StubWorkerClient`, `FakeWorkerClient`) in both test projects — update all of them.
|
||||||
Reference in New Issue
Block a user