diff --git a/src/ClaudeDo.Worker/Program.cs b/src/ClaudeDo.Worker/Program.cs index 7ee48435..2b8d2166 100644 --- a/src/ClaudeDo.Worker/Program.cs +++ b/src/ClaudeDo.Worker/Program.cs @@ -182,10 +182,13 @@ builder.Services.AddScoped(sp => builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddMcpServer() .WithHttpTransport() .WithTools() - .WithTools(); + .WithTools() + .WithTools(); // OnlineInboxConfig and OnlineTokenStore are always registered so hub methods work // even when sync is disabled. The sync stack (api client, auth, hosted service) is diff --git a/src/ClaudeDo.Worker/Runner/TaskRunFindingsMcpTools.cs b/src/ClaudeDo.Worker/Runner/TaskRunFindingsMcpTools.cs new file mode 100644 index 00000000..5a4340e0 --- /dev/null +++ b/src/ClaudeDo.Worker/Runner/TaskRunFindingsMcpTools.cs @@ -0,0 +1,52 @@ +using System.ComponentModel; +using ClaudeDo.Worker.Findings; +using ClaudeDo.Worker.Git; +using ModelContextProtocol.Server; + +namespace ClaudeDo.Worker.Runner; + +public sealed record SaveTaskFindingResult( + bool Saved, string Slug, string Path, int TotalFindings, bool NearCapacity); + +[McpServerToolType] +public sealed class TaskRunFindingsMcpTools +{ + private readonly IFindingsStore _store; + private readonly IFindingsStoreLocator _locator; + private readonly TaskRunMcpContextAccessor _ctx; + + public TaskRunFindingsMcpTools( + IFindingsStore store, IFindingsStoreLocator locator, TaskRunMcpContextAccessor ctx) + { + _store = store; + _locator = locator; + _ctx = ctx; + } + + [McpServerTool, Description( + "Record a durable trap you just hit, so future runs read it instead of losing the same " + + "turns. Only for findings that are lasting, non-obvious and behaviour-changing (\"X looks " + + "like Y but is Z — do W instead\"); a bug you fixed is git history, not a finding. When " + + "unsure, don't. Re-using a slug overwrites it. Writes to this task's own project; " + + "nearCapacity=true means prune before adding more.")] + public async Task SaveFinding( + [Description("Stable lowercase kebab-case id, max 60 chars, e.g. 'conpty-arg-quoting'. Same slug overwrites.")] string slug, + [Description("One full sentence stating the trap itself — this is the index line other agents scan.")] string title, + [Description("2-6 sentences: what happens, why it does not look like that, what to do instead.")] string body, + [Description("Repo-relative path or subsystem the finding applies to, e.g. 'src/ClaudeDo.Worker/Planning'.")] string scope = "", + CancellationToken cancellationToken = default) + { + var taskId = _ctx.Current.CallerTaskId; + var target = await _locator.ResolveForTaskAsync(taskId, cancellationToken); + var head = await GitHead.ShortAsync(target.WorkingDir, cancellationToken); + + var outcome = await _store.SaveAsync( + target.WorkingDir, + new FindingInput(slug, title, body, scope, SourceTaskId: taskId, VerifiedAgainst: head), + cancellationToken, + target.Tracked); + + return new SaveTaskFindingResult( + true, outcome.Slug, outcome.Path, outcome.TotalFindings, outcome.NearCapacity); + } +}