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.
|
||||
Reference in New Issue
Block a user