refactor(mcp): rewrite external MCP tool descriptions for trigger clarity

Every tool description now leads with what the tool does AND when to reach for
it, since MCP clients rank tools by that text. Per-parameter prose moved onto
the parameters as [Description], exhaustive result-shape enumerations and
design/history rationale dropped, and the repeated boilerplate clauses
(lean-task-ref, batch cap, refused-while-Running) pulled into McpToolDocs,
which also documents the style for future tools.

Tool-level description text: 20494 -> 13605 chars (-34%); combined with the new
parameter descriptions 18517 (-10%).

Closes gaps that caused wrong calls rather than just verbose ones:
- list_task_attachments returns metadata only, no file content
- run_task_now shares continue_task's single override slot and throws when busy
- list_runs is ordered oldest-first and feeds get_run
- workingDir on create_list/update_list is an existing local git repo path,
  unvalidated until the first task run
- get_task_worktree's behind=0 also means the main ref was unreachable

Removes get_task_status_values: a whole tool entry for static reference text.
GetTask's description is now the canonical place for status meanings.
This commit is contained in:
mika kuns
2026-08-07 09:25:45 +02:00
parent 6e9a7cea87
commit c792765ed3
16 changed files with 295 additions and 282 deletions
+134 -160
View File
@@ -26,7 +26,6 @@ public sealed record CancelTaskResult(bool Cancelled, string Id);
// review range (worktree ahead, or HandlerBaseCommit..HandlerHeadCommit for a worktree-less
// child) contributed nothing, so a reviewer sees them before approving instead of after.
public sealed record ReviewTaskResult(TaskRefDto Task, string? MergeStatus, IReadOnlyList<string> MergeConflicts, string? MergeMessage, string? RepoPath = null, IReadOnlyList<TaskRefDto>? EmptyChildren = null);
public sealed record StatusValueDto(string Status, string Meaning);
public sealed record RunTaskNowResult(bool Started, string TaskId);
public sealed record TaskDto(
@@ -161,7 +160,8 @@ public sealed class ExternalMcpService
_planningMerge = planningMerge;
}
[McpServerTool, Description("List all task lists available in ClaudeDo.")]
[McpServerTool, Description(
"List all task lists available in ClaudeDo. Start here — every task tool needs a listId from this call.")]
public async Task<IReadOnlyList<TaskListDto>> ListTaskLists(CancellationToken cancellationToken)
{
var lists = await _lists.GetAllAsync(cancellationToken);
@@ -169,17 +169,17 @@ public sealed class ExternalMcpService
}
[McpServerTool, Description(
"List tasks in a given list. Optionally filter by creator (createdBy) and/or status. " +
"Valid status values: Idle, Queued, Running, WaitingForReview, WaitingForChildren, Done, Failed, Cancelled. " +
"includeDescription=false (default): returns lean task references in `tasks` (no Description/Result) — " +
"use this unless you actually need the description text, since a list of verbosely-described tasks can " +
"otherwise blow past the response size limit. " +
"includeDescription=true: returns full tasks (incl. Description/Result) in `tasksFull` instead; `tasks` is " +
"null in that case.")]
"List the tasks in one list — the usual way to find a taskId. Optionally filter by creator and/or status.")]
public async Task<ListTasksResult> ListTasks(
string listId,
[Description("Only return tasks with this CreatedBy value.")]
string? createdBy = null,
[Description("Only return tasks in this status: Idle, Queued, Running, WaitingForReview, " +
"WaitingForChildren, Done, Failed or Cancelled.")]
string? status = null,
[Description("false (default): lean references in `tasks`, no Description/Result — keep this unless you " +
"need the description text, since verbosely-described tasks can blow past the response size " +
"limit. true: full tasks in `tasksFull` instead (`tasks` is then null).")]
bool includeDescription = false,
CancellationToken cancellationToken = default)
{
@@ -206,10 +206,12 @@ public sealed class ExternalMcpService
}
[McpServerTool, Description(
"Get a single task by id, including its current status and result. " +
"Status lifecycle: Idle → Queued → Running → WaitingForReview → Done | Failed | Cancelled. " +
"A successful run lands in WaitingForReview; use review_task to approve, reject, or cancel. " +
"Done/Failed/Cancelled tasks can be reset to Idle for re-execution.")]
"Get a single task by id, including its current status and result — the canonical reference for what a " +
"status means. Lifecycle: Idle → Queued → Running → WaitingForReview → Done | Failed | Cancelled. " +
"A successful run lands in WaitingForReview; use review_task to approve, reject or cancel it. " +
"Done/Failed/Cancelled tasks can be reset to Idle for re-execution. A Queued task with a blocker waits " +
"for its predecessor before the picker will claim it, and WaitingForChildren is a parent whose own work " +
"is done but whose children are still running.")]
public async Task<TaskDto> GetTask(string taskId, CancellationToken cancellationToken)
{
var task = await _tasks.GetByIdAsync(taskId, cancellationToken)
@@ -227,21 +229,19 @@ public sealed class ExternalMcpService
}
[McpServerTool, Description(
"Create a new task in the given list. Set queueImmediately=true to enqueue it for agent execution. " +
"Set model to the cheapest model that can do the task well — 'haiku' for trivial/mechanical work, " +
"'sonnet' for normal coding (the default), 'opus' only for complex or cross-cutting work. " +
"Leave model null to inherit the list/global default. " +
"Returns { task, possibleDuplicates }: task is a lean reference (id, listId, title, status, " +
"sortOrder, isMyDay) — not the description you just sent. The task is always created — " +
"possibleDuplicates is a non-blocking heads-up (up to 3 open tasks in the same list with a " +
"strongly overlapping title, id/title/status only); check it and mention any hit to the caller, " +
"but do not treat it as an error.")]
"Create a new task in the given list. The task is always created — possibleDuplicates is a non-blocking " +
"heads-up (up to 3 open tasks in the same list with a strongly overlapping title); check it and mention " +
"any hit to the caller, but do not treat it as an error." + McpToolDocs.LeanTaskRef)]
public async Task<AddTaskResult> AddTask(
string listId,
string title,
string? description = null,
string? createdBy = null,
[Description("true: enqueue the task for agent execution right away.")]
bool queueImmediately = false,
[Description("Cheapest model that can do the task well: 'haiku' for trivial/mechanical work, 'sonnet' " +
"for normal coding, 'opus' only for complex or cross-cutting work. null inherits the " +
"list/global default (normally sonnet).")]
string? model = null,
CancellationToken cancellationToken = default)
{
@@ -354,9 +354,8 @@ public sealed class ExternalMcpService
}
[McpServerTool, Description(
"Update an existing task's title, description, and/or commit type. Pass null to leave a field unchanged. " +
"Refuses if the task is currently Running. Returns a lean task reference (id, listId, title, status, " +
"sortOrder, isMyDay) — not the description you just sent.")]
"Update an existing task's title, description, and/or commit type. Pass null to leave a field unchanged." +
McpToolDocs.NotWhileRunning + McpToolDocs.LeanTaskRef)]
public async Task<TaskRefDto> UpdateTask(
string taskId,
string? title = null,
@@ -380,12 +379,12 @@ public sealed class ExternalMcpService
}
[McpServerTool, Description(
"Append a subtask (step) to a task. orderNum defaults to the end. " +
"Refuses if the task is currently Running. Subtasks are surfaced to the agent at run time and shown in the task's Steps list. " +
"Returns a lean task reference (id, listId, title, status, sortOrder, isMyDay), not the task's description.")]
"Append a subtask (step) to a task. Subtasks are surfaced to the agent at run time and shown in the " +
"task's Steps list." + McpToolDocs.NotWhileRunning + McpToolDocs.LeanTaskRef)]
public async Task<TaskRefDto> AddSubtask(
string taskId,
string title,
[Description("Position among the existing steps; defaults to the end.")]
int? orderNum = null,
CancellationToken cancellationToken = default)
{
@@ -419,16 +418,14 @@ public sealed class ExternalMcpService
}
[McpServerTool, Description(
"Update a task's status. Only 'Idle', 'Queued', 'Cancelled' and 'Done' are permitted externally — " +
"use run_task_now for execution control, and review_task to act on a WaitingForReview task. " +
"Settable: Idle (reset to editable), Queued (enqueue for execution), " +
"Cancelled (retire the task without deleting it; it can be reset to Idle later), " +
"Done (mark complete; refused if the task has an active worktree — use review_task to approve " +
"and merge that worktree instead). " +
"Full lifecycle: Idle → Queued → Running → WaitingForReview → Done | Failed | Cancelled. " +
"Returns a lean task reference (id, listId, title, status, sortOrder, isMyDay), not the task's description.")]
"Move a task between the statuses a caller may set directly. Use run_task_now for execution control and " +
"review_task to act on a WaitingForReview task — neither is reachable from here." + McpToolDocs.LeanTaskRef)]
public async Task<TaskRefDto> UpdateTaskStatus(
string taskId,
[Description("'Idle' (reset to editable), 'Queued' (enqueue for execution), 'Cancelled' (retire without " +
"deleting; can be reset to Idle later) or 'Done' (mark complete; refused if the task has an " +
"active worktree — use review_task to approve and merge that worktree instead). No other " +
"value is settable externally.")]
string status,
CancellationToken cancellationToken)
{
@@ -482,27 +479,29 @@ public sealed class ExternalMcpService
}
[McpServerTool, Description(
"Review a task that is WaitingForReview. " +
"decision='approve' review+merge, exactly like the UI's Approve: a childless task merges its worktree into " +
"targetBranch (default: the repo's current branch) then goes Done; a task with children drives the unit merge " +
"(parent worktree if active + each Done child in order); a task without an active worktree approves straight to Done. " +
"mergeStatus 'conflict' means the merge stopped on conflicts (files listed) — by default the merge is cleanly " +
"aborted and you resolve in the ClaudeDo UI; pass leaveConflictsInTree=true to instead leave the conflict " +
"markers in the working tree (repoPath in the result) so you can resolve them and call continue_merge, " +
"or abort_merge to cancel. " +
"decision='reject_rerun' → Queued and re-runs, resuming the agent's session with your feedback as the next turn (feedback is required). " +
"decision='reject_park' → Idle for manual editing (feedback ignored). " +
"decision='cancel' → Cancelled. " +
"Fails if the task is not currently WaitingForReview (except cancel, which also works while Running/Queued). " +
"The result's task field is a lean reference (id, listId, title, status, sortOrder, isMyDay), not the task's description. " +
"emptyChildren (parent approve only) lists the Done children about to be unit-merged whose own review range " +
"contributed nothing (e.g. a child that reported CLAUDEDO_BLOCKED and committed no code) — check it before " +
"trusting that every child actually delivered something.")]
"Act on a task that is WaitingForReview — the only way to approve, reject or retire a reviewed run. " +
"'approve' is review+merge, exactly like the UI's Approve: a childless task merges its worktree into " +
"targetBranch then goes Done; a task with children drives the unit merge (parent worktree if active + each " +
"Done child in order); a task without an active worktree approves straight to Done. Fails if the task is " +
"not WaitingForReview (except 'cancel', which also works while Running/Queued). mergeStatus 'conflict' " +
"means the merge stopped on conflicts, with the files listed. emptyChildren (parent approve only) lists " +
"the Done children about to be unit-merged whose own review range contributed nothing (e.g. a child that " +
"reported CLAUDEDO_BLOCKED and committed no code) — check it before trusting that every child actually " +
"delivered something." + McpToolDocs.LeanTaskRef)]
public async Task<ReviewTaskResult> ReviewTask(
string taskId,
[Description("'approve', 'reject_rerun', 'reject_park' or 'cancel'.")]
string decision,
[Description("Rejection comment. Required for 'reject_rerun', where the task goes Queued and re-runs with " +
"this text as the next turn of the agent's resumed session; ignored for 'reject_park', which " +
"just returns the task to Idle for manual editing.")]
string? feedback = null,
[Description("Branch an approve merges into; defaults to the repo's current branch.")]
string? targetBranch = null,
[Description("What an approve does when the merge hits conflicts. false (default): abort cleanly, leaving " +
"no half-merged state, and you resolve in the ClaudeDo UI. true: leave the conflict markers " +
"in the working tree (repoPath in the result) so you can resolve them and call continue_merge, " +
"or abort_merge to cancel.")]
bool leaveConflictsInTree = false,
CancellationToken cancellationToken = default)
{
@@ -634,7 +633,10 @@ public sealed class ExternalMcpService
}
}
[McpServerTool, Description("Immediately run a task in the override execution slot (bypasses the agent queue). Returns { started: true, taskId } on success.")]
[McpServerTool, Description(
"Run a task immediately in the override execution slot, bypassing the agent queue. That slot is single-" +
"occupancy and shared with continue_task — throws \"Override slot busy\" if something else holds it; " +
"enqueue via update_task_status instead of retrying in a loop.")]
public async Task<RunTaskNowResult> RunTaskNow(string taskId, CancellationToken cancellationToken)
{
try
@@ -653,7 +655,8 @@ public sealed class ExternalMcpService
return new RunTaskNowResult(true, taskId);
}
[McpServerTool, Description("Cancel a running task. Returns { cancelled: true, id } if the task was running and cancellation was requested; cancelled is false if the task was not running.")]
[McpServerTool, Description(
"Cancel a running task, killing its agent process. cancelled=false means the task was not running.")]
public async Task<CancelTaskResult> CancelTask(string taskId, CancellationToken cancellationToken)
{
var cancelled = _queue.CancelTask(taskId);
@@ -661,7 +664,9 @@ public sealed class ExternalMcpService
return new CancelTaskResult(cancelled, taskId);
}
[McpServerTool, Description("Delete a task. Returns { deleted: true, id } on success. Throws if the task is not found or is currently Running — cancel it first.")]
[McpServerTool, Description(
"Delete a task permanently. Prefer update_task_status 'Cancelled' to retire a task you may want back." +
McpToolDocs.NotWhileRunning)]
public async Task<DeleteTaskResult> DeleteTask(string taskId, CancellationToken cancellationToken)
{
var task = await _tasks.GetByIdAsync(taskId, cancellationToken)
@@ -676,31 +681,12 @@ public sealed class ExternalMcpService
return new DeleteTaskResult(true, taskId);
}
// ── Status reference ─────────────────────────────────────────────────────
[McpServerTool, Description("Returns all valid task status values and their meanings. Use before filtering by status or interpreting task state.")]
public Task<IReadOnlyList<StatusValueDto>> GetTaskStatusValues() =>
Task.FromResult<IReadOnlyList<StatusValueDto>>([
new("Idle", "Not yet queued; task is editable and will not run until enqueued."),
new("Queued", "Waiting for an agent execution slot. Tasks with a blocker (BlockedByTaskId) are skipped by the queue picker until their predecessor finishes."),
new("Running", "Agent is actively executing the task; cannot be edited or deleted until cancelled."),
new("WaitingForReview", "Run finished successfully and awaits review. Use review_task: approve (→ Done), reject_rerun (→ Queued, resumes the session with feedback), reject_park (→ Idle), or cancel (→ Cancelled)."),
new("WaitingForChildren", "Planning parent whose child tasks are still running. The parent resumes once all children reach a terminal state."),
new("Done", "Completed successfully and approved; result text is available in the result field. Can be reset to Idle for re-execution."),
new("Failed", "Execution ended with an error; task can be reset to Idle or re-queued directly."),
new("Cancelled", "Cancelled by the user; task can be reset to Idle or re-queued directly."),
]);
// ── Worktree / git tools ──────────────────────────────────────────────────
[McpServerTool, Description(
"Get git worktree details for a task: path, branch, headCommit (current HEAD SHA), " +
"baseCommit (SHA where the branch was created), ahead (commits on branch since base), " +
"behind (commits on main not yet on this branch; 0 if 'main' ref is unreachable), " +
"isDirty (has uncommitted changes in the worktree directory), " +
"mergeCommit (SHA of the merge commit this worktree's branch produced on the target branch, " +
"if it has been merged and that succeeded after this field was introduced; null otherwise — " +
"required by revert_merge). " +
"Get a task's git worktree state — path, branch, base/head commit, ahead/behind counts, isDirty, and the " +
"mergeCommit its branch produced once merged. behind is 0 when the 'main' ref is unreachable, so do not " +
"read 0 as \"up to date\" without checking. A null mergeCommit means revert_merge cannot act on this task. " +
"Throws if the task or its worktree does not exist.")]
public async Task<WorktreeInfoDto> GetTaskWorktree(string taskId, CancellationToken cancellationToken)
{
@@ -718,16 +704,17 @@ public sealed class ExternalMcpService
}
[McpServerTool, Description(
"Get the diff for a task's worktree relative to its base commit. For a worktree-less " +
"list-handler host task (Mission Control's \"Let Claude handle it\"), returns the fixed " +
"HandlerBaseCommit..HandlerHeadCommit range over the list's working dir instead. " +
"stat=false (default): returns the full unified diff, capped at 200 KB (truncated=true when larger). " +
"stat=true: returns a --stat summary (changed files with insertion/deletion counts). " +
"files always lists the changed file paths regardless of stat mode. " +
"totalBytes is the uncapped diff size (useful when truncated=true). " +
"Throws if the task has no worktree/review range, or the relevant directory is missing from disk.")]
"Read what a task actually changed — the diff of its worktree against its base commit (for a worktree-less " +
"list-handler host task, the fixed HandlerBaseCommit..HandlerHeadCommit range over the list's working dir " +
"instead). files lists the changed paths in either mode; truncated=true means the diff was capped and " +
"totalBytes holds its real size. Throws if the task has no worktree/review range, or the relevant " +
"directory is missing from disk.")]
public async Task<TaskDiffDto> GetTaskDiff(
string taskId, bool stat = false, CancellationToken cancellationToken = default)
string taskId,
[Description("false (default): the full unified diff, capped at 200 KB. true: a --stat summary with " +
"per-file insertion/deletion counts — start here when the diff may be large.")]
bool stat = false,
CancellationToken cancellationToken = default)
{
var (repoPath, baseCommit, headCommit) = await LoadDiffRangeAsync(taskId, cancellationToken);
@@ -782,21 +769,21 @@ public sealed class ExternalMcpService
}
[McpServerTool, Description(
"Merge a task's worktree branch into targetBranch (default: main). " +
"noFf=true (default): always creates a merge commit (--no-ff). " +
"dryRun=true: validates preconditions only, does not perform the merge; merged=false in the result means 'not actually merged'. " +
"allowWaitingForReview=true: also allows merging a task in WaitingForReview (default false, which only allows Done). " +
"On success: merged=true, mergeCommit contains the new merge commit SHA. " +
"On conflict: by default the merge is cleanly aborted (no half-merged state left); merged=false and conflicts lists the affected files. " +
"leaveConflictsInTree=true: on conflict the merge is NOT aborted — conflict markers are left in the working " +
"tree at repoPath (conflictsInTree=true in the result) so you can resolve them there and call continue_merge, " +
"or abort_merge to cancel.")]
"Merge a Done task's worktree branch into targetBranch. For a task still in WaitingForReview prefer " +
"review_task, which merges as part of approving. merged=true carries the new mergeCommit SHA; on conflict " +
"merged=false and conflicts lists the affected files.")]
public async Task<MergeTaskResultDto> MergeTask(
string taskId,
string targetBranch = "main",
[Description("true (default): always create a merge commit (--no-ff).")]
bool noFf = true,
[Description("true: validate preconditions only and do not merge — merged=false then means \"not attempted\".")]
bool dryRun = false,
[Description("true: also allow merging a task in WaitingForReview; false (default) allows Done only.")]
bool allowWaitingForReview = false,
[Description("What to do on conflict. false (default): abort cleanly, leaving no half-merged state. true: " +
"leave the conflict markers in the working tree at repoPath (conflictsInTree=true) so you can " +
"resolve them there and call continue_merge, or abort_merge to cancel.")]
bool leaveConflictsInTree = false,
CancellationToken cancellationToken = default)
{
@@ -848,11 +835,9 @@ public sealed class ExternalMcpService
}
[McpServerTool, Description(
"Finish an in-progress conflicted merge after the conflict markers in the working tree (repoPath from " +
"merge_task/review_task) have been resolved. Handles both a single task's merge and a parent/children unit " +
"merge — pass the PARENT task id to continue a unit merge. On success merged=true and the task reaches its " +
"post-merge status (Done when approving). If conflict markers are still present, merged=false and conflicts " +
"lists the affected files — resolve them and call continue_merge again. " +
"Finish an in-progress conflicted merge once you have resolved the conflict markers in the working tree " +
"(repoPath from merge_task/review_task). Pass the PARENT task id to continue a parent/children unit merge. " +
"merged=false with conflicts listed means markers are still present — resolve them and call again. " +
"Throws if there is no in-progress merge for the task; use abort_merge to cancel a paused merge instead.")]
public async Task<MergeContinuationResultDto> ContinueMerge(string taskId, CancellationToken cancellationToken)
{
@@ -920,10 +905,8 @@ public sealed class ExternalMcpService
[McpServerTool, Description(
"Abort an in-progress conflicted merge, discarding the conflict markers and restoring a clean working tree. " +
"Handles both a single task's merge and a parent/children unit merge — pass the PARENT task id to abort a " +
"unit merge. The task keeps its pre-merge status (e.g. WaitingForReview). " +
"Throws if there is no in-progress merge for the task. " +
"Returns a lean task reference (id, listId, title, status, sortOrder, isMyDay), not the task's description.")]
"Pass the PARENT task id to abort a parent/children unit merge. The task keeps its pre-merge status " +
"(e.g. WaitingForReview). Throws if there is no in-progress merge for the task." + McpToolDocs.LeanTaskRef)]
public async Task<TaskRefDto> AbortMerge(string taskId, CancellationToken cancellationToken)
{
_ = await _tasks.GetByIdAsync(taskId, cancellationToken)
@@ -945,21 +928,17 @@ public sealed class ExternalMcpService
}
[McpServerTool, Description(
"Non-destructive merge preview for a task's worktree branch against targetBranch (default: the repo's " +
"current branch), via `git merge-tree --write-tree` — does NOT touch the working tree, index, or HEAD. " +
"status: 'clean' (mergeable; changedFileCount is the size of that merge) or 'conflict' (conflictFiles " +
"lists the paths git would stop on). behind = commits on targetBranch not yet on this task's branch, so " +
"you can spot a stale branch even when the preview itself is clean. " +
"IMPORTANT: a clean preview says nothing about whether the merged result compiles or passes tests — git " +
"can merge two changes cleanly (e.g. one file deletes a symbol another file still references) and still " +
"break the build. " +
"isEmpty=true means the task's review range contributed nothing (no commits ahead of base, or — for a " +
"worktree-less list-handler host task — HandlerBaseCommit == HandlerHeadCommit); do not mistake a small " +
"changedFileCount for an empty one, check isEmpty instead. " +
"Throws a clear error if the task has neither an active worktree nor a handler commit range, or the " +
"list's working directory is missing from disk.")]
"Check whether a task would merge cleanly before touching anything — `git merge-tree --write-tree`, so the " +
"working tree, index and HEAD are untouched. status is 'clean' or 'conflict' (conflictFiles lists where git " +
"would stop); behind counts commits on targetBranch not yet on this branch, which flags a stale branch even " +
"when the preview is clean. IMPORTANT: a clean preview says nothing about whether the result compiles or " +
"passes tests — git can merge two changes cleanly (one file deleting a symbol another still references) and " +
"still break the build. isEmpty=true means the task's review range contributed nothing; check that flag " +
"rather than reading a small changedFileCount as empty. Throws if the task has neither an active worktree " +
"nor a handler commit range, or the list's working directory is missing from disk.")]
public async Task<MergePreviewToolDto> PreviewMerge(
string taskId,
[Description("Branch to preview against; defaults to the repo's current branch.")]
string? targetBranch = null,
CancellationToken cancellationToken = default)
{
@@ -968,18 +947,16 @@ public sealed class ExternalMcpService
}
[McpServerTool, Description(
"Merge preview plus file-overlap check across several tasks at once, all previewed against the same " +
"targetBranch (default: the repo's current branch). For each taskId returns the same fields as " +
"preview_merge (status/conflictFiles/changedFileCount/behind; error is set instead if that task could not " +
"be previewed, and it is then excluded from the overlap computation). overlaps lists, for each file " +
"touched by MORE THAN ONE of the given tasks (via each task's own diff, not the merge preview itself), " +
"which tasks touch it — passing a single taskId always yields an empty overlaps list. " +
"IMPORTANT: file-name overlap is a HINT, not a guarantee of a real collision, and its absence is not a " +
"guarantee of safety — two tasks touching different files entirely (e.g. one deletes a symbol, another " +
"still references it elsewhere) can still collide, and this tool will not flag that case. " +
"isEmpty=true (per entry) means that task's review range contributed nothing — see preview_merge.")]
"Plan a batch merge: preview_merge for several tasks against the same targetBranch, plus a file-overlap " +
"check between them. Per entry you get preview_merge's fields, or error instead when that task could not " +
"be previewed (it is then left out of the overlap computation). overlaps names, for each file touched by " +
"MORE THAN ONE of the given tasks, which tasks touch it — a single taskId always yields no overlaps. " +
"IMPORTANT: overlap is a HINT and its absence is not safety — two tasks touching entirely different files " +
"(one deleting a symbol, another still referencing it) can still collide unflagged, and as with " +
"preview_merge a clean result does not mean the merge builds.")]
public async Task<MergePreviewSetResultDto> PreviewMergeSet(
IReadOnlyList<string> taskIds,
[Description("Branch to preview every task against; defaults to the repo's current branch.")]
string? targetBranch = null,
CancellationToken cancellationToken = default)
{
@@ -1076,18 +1053,17 @@ public sealed class ExternalMcpService
}
[McpServerTool, Description(
"Revert a previously merged task's merge commit on targetBranch (default: main), via `git revert -m 1` — " +
"a new commit, never a reset/rewrite (the target working directory is shared with other sessions). " +
"Requires the task to be Done with a Merged worktree that has a recorded merge commit; tasks merged " +
"before this feature existed have no recorded commit and are refused rather than guessed via git log. " +
"On success: reverted=true, revertCommit is the new commit's SHA, and the task returns to " +
"WaitingForReview so it can be reconsidered. " +
"On a conflicting revert: reverted=false, the revert is aborted immediately (no half-resolved state " +
"left in the tree) and conflicts lists the files that would have conflicted. " +
"Throws if there is no recorded merge commit, the repo is mid-merge/mid-revert, or the target working " +
"tree has uncommitted changes from another session.")]
"Undo a merged task by reverting its merge commit — `git revert -m 1`, always a new commit and never a " +
"reset/rewrite, since the target working directory is shared with other sessions. Requires the task to be " +
"Done with a Merged worktree that has a recorded merge commit (check get_task_worktree's mergeCommit " +
"first). On success the task returns to WaitingForReview so it can be reconsidered. On conflict the revert " +
"is aborted immediately and conflicts lists the files. Throws if there is no recorded merge commit, the " +
"repo is mid-merge/mid-revert, or the target working tree has uncommitted changes from another session.")]
public async Task<RevertMergeResultDto> RevertMerge(
string taskId, string targetBranch = "main", CancellationToken cancellationToken = default)
string taskId,
[Description("Branch carrying the merge commit; defaults to main.")]
string targetBranch = "main",
CancellationToken cancellationToken = default)
{
var result = await _merge.RevertMergeAsync(taskId, targetBranch, cancellationToken);
@@ -1104,10 +1080,8 @@ public sealed class ExternalMcpService
}
[McpServerTool, Description(
"List all ClaudeDo-tracked worktrees. " +
"Each entry: taskId, path, branch, headCommit (empty if path missing on disk), " +
"isDirty (has uncommitted changes), mergedIntoMain (worktree state is Merged). " +
"Only worktrees recorded in the ClaudeDo database are returned.")]
"Survey every worktree ClaudeDo tracks — use it to find leftovers to clean up. Only worktrees recorded in " +
"the ClaudeDo database appear here, and headCommit is empty when the path is missing from disk.")]
public async Task<IReadOnlyList<WorktreeListItemDto>> ListWorktrees(CancellationToken cancellationToken)
{
var rows = await _maintenance.GetOverviewAsync(null, cancellationToken);
@@ -1125,12 +1099,14 @@ public sealed class ExternalMcpService
}
[McpServerTool, Description(
"Remove a task's worktree directory and delete its git branch. " +
"force=false (default): refuses if the worktree has uncommitted changes or the task is Running. " +
"force=true: removes even a dirty worktree (uncommitted changes are lost); task must not be Running. " +
"Returns removed=true on success; branchDeleted reflects whether the branch was also removed.")]
"Remove a task's worktree directory and delete its git branch. branchDeleted reports whether the branch " +
"went too." + McpToolDocs.NotWhileRunning)]
public async Task<CleanupWorktreeResult> CleanupTaskWorktree(
string taskId, bool force = false, CancellationToken cancellationToken = default)
string taskId,
[Description("false (default): refuse a worktree with uncommitted changes. true: remove it anyway, losing " +
"those changes.")]
bool force = false,
CancellationToken cancellationToken = default)
{
using var ctx = _dbFactory.CreateDbContext();
var task = await new TaskRepository(ctx).GetByIdAsync(taskId, cancellationToken)
@@ -1155,10 +1131,9 @@ public sealed class ExternalMcpService
}
[McpServerTool, Description(
"Send a follow-up prompt to an existing Claude session (multi-turn continuation). " +
"The agent resumes using --resume with the session ID from the task's last run. " +
"Runs in the override execution slot; throws if the slot is busy — try again later. " +
"Returns a status string from the execution slot.")]
"Send a follow-up prompt to a task's existing Claude session instead of starting a fresh run — the agent " +
"resumes via --resume with the session id from the task's last run, so its prior context is kept. Uses the " +
"same single-occupancy override slot as run_task_now and throws \"Override slot busy\" when that is taken.")]
public async Task<string> ContinueTask(
string taskId,
string followUpPrompt,
@@ -1187,10 +1162,10 @@ public sealed class ExternalMcpService
// ── Daily prep ───────────────────────────────────────────────────────────
[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).")]
"Daily prep: the open tasks eligible for today's MyDay selection. candidates are Idle, unblocked, " +
"non-manual and in a git repo not excluded from the weekly report; currentMyDay are Idle tasks already " +
"flagged and count toward maxTasks, the hard cap on open MyDay tasks. Add your picks with set_my_day and " +
"never exceed maxTasks.")]
public async Task<DailyPrepDataDto> GetDailyPrepCandidates(CancellationToken cancellationToken)
{
await using var ctx = await _dbFactory.CreateDbContextAsync(cancellationToken);
@@ -1225,14 +1200,13 @@ public sealed class ExternalMcpService
}
[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. " +
"Returns a lean task reference (id, listId, title, status, sortOrder, isMyDay), not the task's description.")]
"Daily prep: set or clear a task's MyDay flag. Setting it is rejected once the MyDay cap " +
"(DailyPrepMaxTasks open MyDay tasks) would be exceeded; clearing is always allowed." +
McpToolDocs.LeanTaskRef)]
public async Task<TaskRefDto> SetMyDay(
string taskId,
bool isMyDay,
[Description("Position in the MyDay list; use consecutive values to keep related tasks together.")]
int? sortOrder = null,
CancellationToken cancellationToken = default)
{