using System.ComponentModel; using ClaudeDo.Data.Repositories; using ClaudeDo.Worker.Lifecycle; using ModelContextProtocol.Server; using TaskStatus = ClaudeDo.Data.Models.TaskStatus; namespace ClaudeDo.Worker.External; public sealed record ResetFailedTaskResult(bool Reset, string TaskId, int Number); [McpServerToolType] public sealed class LifecycleMcpTools { private readonly TaskRepository _tasks; private readonly TaskResetService _reset; private readonly ExternalMcpService _svc; private readonly InteractiveReviewSubmissionService _reviewSubmission; public LifecycleMcpTools( TaskRepository tasks, TaskResetService reset, ExternalMcpService svc, InteractiveReviewSubmissionService reviewSubmission) { _tasks = tasks; _reset = reset; _svc = svc; _reviewSubmission = reviewSubmission; } [McpServerTool, Description( "Discards a failed task's worktree (and all uncommitted work in it) and resets it to Idle for a fresh " + "run. Only tasks with Status=Failed are accepted; other statuses throw. Check failureReason from " + "get_task/batch_get_tasks first: for failureReason=\"max_turns\" the worktree's work is usually still " + "good and continue_task (resume the session, keep the worktree) is almost always the right call instead " + "— reach for this tool only for a real error, not a task that just ran out of turns.")] public async Task ResetFailedTask(string taskId, CancellationToken cancellationToken) { taskId = await TaskIdResolver.ResolveAsync(_tasks, taskId, cancellationToken); var task = await _tasks.GetByIdAsync(taskId, cancellationToken) ?? throw new InvalidOperationException($"Task {taskId} not found."); if (task.Status != TaskStatus.Failed) throw new InvalidOperationException($"Task {taskId} is {task.Status}, not Failed. Only failed tasks can be reset via this tool."); await _reset.ResetAsync(taskId, cancellationToken); return new ResetFailedTaskResult(true, taskId, task.Number); } [McpServerTool, Description( "Submit a task into the review pipeline directly, without a headless agent run — the way a list " + "handler ('Let Claude handle it') submits its OWN handler task once its run is done, since " + "update_task_status doesn't reach WaitingForReview. Only Idle or Failed tasks are accepted; a " + "Running/Queued/WaitingForReview/WaitingForChildren task throws. For a worktree-less handler task this " + "stamps the review range's HandlerHeadCommit from the list's working dir HEAD (nothing to commit — the " + "handler already committed straight to the list's working dir); for a task with an active worktree it " + "commits any uncommitted changes there first. Either way the task then transitions to WaitingForReview " + "and its diff becomes reviewable via review_task/get_task_diff." + McpToolDocs.LeanTaskRef + McpToolDocs.TaskNumberHint)] public async Task SubmitTaskForReview(string taskId, CancellationToken cancellationToken) { taskId = await TaskIdResolver.ResolveAsync(_tasks, taskId, cancellationToken); try { await _reviewSubmission.SubmitAsync(taskId, cancellationToken); } catch (KeyNotFoundException) { throw new InvalidOperationException($"Task {taskId} not found."); } return await _svc.GetTaskRefAsync(taskId, cancellationToken); } }