feat(daily-prep): run daily prep from PrimeRunner via allowed MCP tools

This commit is contained in:
mika kuns
2026-06-03 16:24:09 +02:00
parent fd7f8ac78f
commit 20b3a29d08
4 changed files with 187 additions and 20 deletions

View File

@@ -1,53 +1,80 @@
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.FromSeconds(60);
private readonly IClaudeProcess _claude;
private readonly ILogger<PrimeRunner> _logger;
private static readonly TimeSpan FireTimeout = TimeSpan.FromMinutes(5);
private const int MaxTurns = 30;
public PrimeRunner(IClaudeProcess claude, ILogger<PrimeRunner> logger)
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)
{
var cwd = Paths.AppDataRoot();
Directory.CreateDirectory(cwd);
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
timeoutCts.CancelAfter(FireTimeout);
if (!await _gate.WaitAsync(0, ct))
return new PrimeRunOutcome(false, "Daily prep already running");
try
{
var prompt = schedule.PromptOverride ?? "ping";
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: "-p --max-turns 1",
arguments: args,
prompt: prompt,
workingDirectory: cwd,
onStdoutLine: _ => Task.CompletedTask,
ct: timeoutCts.Token);
if (IsSuccess(result))
return new PrimeRunOutcome(true, "Primed Claude");
return new PrimeRunOutcome(false, FailureMessage(result));
return result.IsSuccess
? new PrimeRunOutcome(true, "Daily prep complete")
: new PrimeRunOutcome(false, $"exit code {result.ExitCode}");
}
catch (OperationCanceledException) when (timeoutCts.IsCancellationRequested && !ct.IsCancellationRequested)
catch (OperationCanceledException) when (!ct.IsCancellationRequested)
{
return new PrimeRunOutcome(false, $"timed out after {FireTimeout.TotalSeconds:0}s");
return new PrimeRunOutcome(false, $"timed out after {FireTimeout.TotalMinutes:0} min");
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Prime fire failed");
_logger.LogWarning(ex, "Daily prep run failed");
return new PrimeRunOutcome(false, ex.Message);
}
finally
{
_gate.Release();
}
}
private static bool IsSuccess(RunResult result) => result.IsSuccess;
private static string FailureMessage(RunResult result) => $"exit code {result.ExitCode}";
}