737 lines
31 KiB
Markdown
737 lines
31 KiB
Markdown
# 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.
|