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); } }