31 KiB
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.
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, viadotnet ef)src/ClaudeDo.Worker/Prime/DailyPrepPrompt.cs— pure prompt + args builder (easy to unit-test)
Modify
src/ClaudeDo.Data/Models/AppSettingsEntity.cs— addDailyPrepMaxTaskssrc/ClaudeDo.Data/Configuration/AppSettingsEntityConfiguration.cs— map columnsrc/ClaudeDo.Data/Repositories/AppSettingsRepository.cs— persist field inUpdateAsyncsrc/ClaudeDo.Worker/External/ExternalMcpService.cs— add 2 tools + DTOssrc/ClaudeDo.Worker/Prime/PrimeRunner.cs— rewrite body to daily prep + single-flightsrc/ClaudeDo.Worker/Hub/WorkerHub.cs— addDailyPrepMaxTasksto AppSettings DTO +RunDailyPrepNowsrc/ClaudeDo.Ui/Services/WorkerClient.cs— mirrorDailyPrepMaxTasksin the UI AppSettings DTO + addRunDailyPrepNowcallsrc/ClaudeDo.Ui/ViewModels/Modals/Settings/PrimeClaudeTabViewModel.cs(+ its view) — numeric editor forDailyPrepMaxTasks- MyDay list header view + its ViewModel — "Tag vorbereiten" button + command
Test
tests/ClaudeDo.Data.Tests/...AppSettings...— new field persists / default 5tests/ClaudeDo.Worker.Tests/External/ExternalMcpServiceTests.cs— candidate filter + set_my_day + cap-guardtests/ClaudeDo.Worker.Tests/Prime/DailyPrepPromptTests.cs— prompt/args contenttests/ClaudeDo.Worker.Tests/Prime/PrimeRunnerTests.cs(if present) — single-flight + success/failure viaIClaudeProcessfake
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 addAppSettingsRepositoryTests.cs) -
Step 1: Write the failing test
In a Data.Tests file (mirror the existing repo test harness that opens a real SQLite ClaudeDoDbContext):
[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 (
AppSettingsEntityhas noDailyPrepMaxTasks).
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.csafterStandupWeekday:
// 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 theStandupWeekdaymapping (beforebuilder.HasData(...)):
builder.Property(s => s.DailyPrepMaxTasks)
.HasColumnName("daily_prep_max_tasks").IsRequired().HasDefaultValue(5);
- Step 5: Persist it in
AppSettingsRepository.UpdateAsync, after theStandupWeekdayassignment:
row.DailyPrepMaxTasks = updated.DailyPrepMaxTasks < 1 ? 1 : updated.DailyPrepMaxTasks;
- Step 6: Generate the migration (regenerates the model snapshot — do NOT hand-edit the snapshot):
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.
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 twoIdletasks (one blocked, one not) and oneDonetask; a second list withWorkingDir = @"C:\Private\secret"holding oneIdletask; a third list withWorkingDir = nullholding oneIdletask; and oneIdletask withIsMyDay = truein the first list. SetAppSettings.ReportExcludedPaths = "[\"C:\\\\Private\"]".
[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:
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
ExternalMcpServiceclass body:
[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):
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.
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.
[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
SortOrdertoTaskDto(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:
[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.
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 extendPrimeRunnerTests.csif 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.
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:
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:
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 depsIDbContextFactoryandIPrimeClockare registered). Build the Worker.
dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release
- Step 7: Update/extend
PrimeRunnerTests.cs(if present) to match the new ctor: constructPrimeRunnerwith a fakeIClaudeProcess, a real temp-SQLiteIDbContextFactory, a fakeIPrimeClock, and a logger. Add:
[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.
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter "DailyPrepPrompt|PrimeRunner"
- Step 9: Commit.
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
DailyPrepMaxTasksto the hub AppSettings DTO record (the record near the top ofWorkerHub.csthat listsReportExcludedPaths). Addint DailyPrepMaxTasksas a member. In the read mapping (GetAppSettings, whererow.ReportExcludedPathsis read) addrow.DailyPrepMaxTasks; in the write mapping (UpdateAppSettings, whereReportExcludedPaths = dto.ReportExcludedPaths) addDailyPrepMaxTasks = dto.DailyPrepMaxTasks. -
Step 2: Add the hub method. Inject
IPrimeRunnerandHubBroadcasterif the hub does not already have them (the hub is constructed by SignalR via DI; both are registered singletons). Then:
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
WorkerHubconstructor breaks hand-rolled hub-test fakes inClaudeDo.Worker.Testsand possiblyClaudeDo.Ui.Tests. After editing, build the test projects and fix everynew WorkerHub(...)/ fakeIWorkerClientconstruction the compiler flags.
- Step 3: Mirror the DTO in the UI (
WorkerClient.cs, the AppSettings DTO around line 498): addint DailyPrepMaxTasksto the record (same position as in the hub DTO). Add aRunDailyPrepNowclient call:
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.
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.
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:
[ObservableProperty] private int _dailyPrepMaxTasks = 5;
-
Step 2: Wire load/save in
SettingsModalViewModel.cs: where the AppSettings DTO is read into the tabs, setPrimeClaude.DailyPrepMaxTasks = dto.DailyPrepMaxTasks;. Where the DTO is written, includeDailyPrepMaxTasks = 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:
<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.
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release
- Step 5: Commit.
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 toIWorkerClient) - 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:
[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:
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:
<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.
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.
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 Releasedotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Releasedotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Releasedotnet test tests/ClaudeDo.Data.Tests/ClaudeDo.Data.Tests.csproj -c Releasedotnet 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
claudedoMCP (installerRegisterMcpStep). If absent, the prep run produces 0 changes — acceptable for v1. --permission-mode acceptEdits+ explicit--allowedToolspre-approves exactly the two tools so the headless run never blocks on a permission prompt.- The cap-guard counts
Idle && IsMyDaytasks; 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_candidatesbehind a task-source abstraction.