Merge claudedo/01556d1f8af54fe2afa4bced3a209121
This commit is contained in:
+3
-2
@@ -7,7 +7,7 @@ public sealed record BatchAddTaskInput(string Title, string? Description = null,
|
|||||||
public sealed record BatchSetMyDayInput(string TaskId, bool IsMyDay, int? SortOrder = null);
|
public sealed record BatchSetMyDayInput(string TaskId, bool IsMyDay, int? SortOrder = null);
|
||||||
|
|
||||||
public sealed record BatchGetTaskResult(string Id, bool Found, TaskDto? Task, string? Error);
|
public sealed record BatchGetTaskResult(string Id, bool Found, TaskDto? Task, string? Error);
|
||||||
public sealed record BatchAddTaskResult(int Index, string Title, bool Ok, TaskDto? Task, string? Error);
|
public sealed record BatchAddTaskResult(int Index, string Title, bool Ok, TaskRefDto? Task, string? Error);
|
||||||
public sealed record BatchTaskResult(string TaskId, bool Ok, string? Error);
|
public sealed record BatchTaskResult(string TaskId, bool Ok, string? Error);
|
||||||
public sealed record BatchCancelResult(string TaskId, bool Ok, bool Cancelled, string? Error);
|
public sealed record BatchCancelResult(string TaskId, bool Ok, bool Cancelled, string? Error);
|
||||||
public sealed record BatchCleanupResult(string TaskId, bool Ok, bool Removed, bool BranchDeleted, string? Error);
|
public sealed record BatchCleanupResult(string TaskId, bool Ok, bool Removed, bool BranchDeleted, string? Error);
|
||||||
@@ -62,7 +62,8 @@ public sealed class BatchMcpTools
|
|||||||
"Create many tasks in one list at once. Each item: { title, description?, model? } " +
|
"Create many tasks in one list at once. Each item: { title, description?, model? } " +
|
||||||
"(model: haiku|sonnet|opus, blank = inherit list/global default). " +
|
"(model: haiku|sonnet|opus, blank = inherit list/global default). " +
|
||||||
"queueImmediately enqueues every created task. " +
|
"queueImmediately enqueues every created task. " +
|
||||||
"Returns one result per item: { index, title, ok, task, error }. Max 100 items.")]
|
"Returns one result per item: { index, title, ok, task, error }; task is a lean reference " +
|
||||||
|
"(id, listId, title, status, sortOrder, isMyDay), not the description you just sent. Max 100 items.")]
|
||||||
public async Task<IReadOnlyList<BatchAddTaskResult>> BatchAddTasks(
|
public async Task<IReadOnlyList<BatchAddTaskResult>> BatchAddTasks(
|
||||||
string listId,
|
string listId,
|
||||||
BatchAddTaskInput[] tasks,
|
BatchAddTaskInput[] tasks,
|
||||||
|
|||||||
+48
-21
@@ -19,7 +19,7 @@ namespace ClaudeDo.Worker.External;
|
|||||||
public sealed record TaskListDto(string Id, string Name, string? WorkingDir);
|
public sealed record TaskListDto(string Id, string Name, string? WorkingDir);
|
||||||
public sealed record DeleteTaskResult(bool Deleted, string Id);
|
public sealed record DeleteTaskResult(bool Deleted, string Id);
|
||||||
public sealed record CancelTaskResult(bool Cancelled, string Id);
|
public sealed record CancelTaskResult(bool Cancelled, string Id);
|
||||||
public sealed record ReviewTaskResult(TaskDto Task, string? MergeStatus, IReadOnlyList<string> MergeConflicts, string? MergeMessage, string? RepoPath = null);
|
public sealed record ReviewTaskResult(TaskRefDto Task, string? MergeStatus, IReadOnlyList<string> MergeConflicts, string? MergeMessage, string? RepoPath = null);
|
||||||
public sealed record StatusValueDto(string Status, string Meaning);
|
public sealed record StatusValueDto(string Status, string Meaning);
|
||||||
public sealed record RunTaskNowResult(bool Started, string TaskId);
|
public sealed record RunTaskNowResult(bool Started, string TaskId);
|
||||||
|
|
||||||
@@ -37,6 +37,16 @@ public sealed record TaskDto(
|
|||||||
bool IsMyDay,
|
bool IsMyDay,
|
||||||
int SortOrder);
|
int SortOrder);
|
||||||
|
|
||||||
|
// Lean counterpart to TaskDto for writing/status-changing tools: echoes back what changed
|
||||||
|
// without re-sending Description/Result, which the caller just sent or already has.
|
||||||
|
public sealed record TaskRefDto(
|
||||||
|
string Id,
|
||||||
|
string ListId,
|
||||||
|
string Title,
|
||||||
|
string Status,
|
||||||
|
int SortOrder,
|
||||||
|
bool IsMyDay);
|
||||||
|
|
||||||
public sealed record WorktreeInfoDto(
|
public sealed record WorktreeInfoDto(
|
||||||
string Path, string Branch, string HeadCommit, string BaseCommit,
|
string Path, string Branch, string HeadCommit, string BaseCommit,
|
||||||
int Ahead, int Behind, bool IsDirty, string? MergeCommit = null);
|
int Ahead, int Behind, bool IsDirty, string? MergeCommit = null);
|
||||||
@@ -171,8 +181,9 @@ public sealed class ExternalMcpService
|
|||||||
"Create a new task in the given list. Set queueImmediately=true to enqueue it for agent execution. " +
|
"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, " +
|
"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. " +
|
"'sonnet' for normal coding (the default), 'opus' only for complex or cross-cutting work. " +
|
||||||
"Leave model null to inherit the list/global default.")]
|
"Leave model null to inherit the list/global default. " +
|
||||||
public async Task<TaskDto> AddTask(
|
"Returns a lean task reference (id, listId, title, status, sortOrder, isMyDay) — not the description you just sent.")]
|
||||||
|
public async Task<TaskRefDto> AddTask(
|
||||||
string listId,
|
string listId,
|
||||||
string title,
|
string title,
|
||||||
string? description = null,
|
string? description = null,
|
||||||
@@ -214,11 +225,14 @@ public sealed class ExternalMcpService
|
|||||||
}
|
}
|
||||||
|
|
||||||
await _broadcaster.TaskUpdated(entity.Id);
|
await _broadcaster.TaskUpdated(entity.Id);
|
||||||
return ToDto(entity);
|
return ToRefDto(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
[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.")]
|
[McpServerTool, Description(
|
||||||
public async Task<TaskDto> UpdateTask(
|
"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.")]
|
||||||
|
public async Task<TaskRefDto> UpdateTask(
|
||||||
string taskId,
|
string taskId,
|
||||||
string? title = null,
|
string? title = null,
|
||||||
string? description = null,
|
string? description = null,
|
||||||
@@ -237,13 +251,14 @@ public sealed class ExternalMcpService
|
|||||||
|
|
||||||
var reload = (await _tasks.GetByIdAsync(taskId, cancellationToken))!;
|
var reload = (await _tasks.GetByIdAsync(taskId, cancellationToken))!;
|
||||||
await _broadcaster.TaskUpdated(taskId);
|
await _broadcaster.TaskUpdated(taskId);
|
||||||
return ToDto(reload);
|
return ToRefDto(reload);
|
||||||
}
|
}
|
||||||
|
|
||||||
[McpServerTool, Description(
|
[McpServerTool, Description(
|
||||||
"Append a subtask (step) to a task. orderNum defaults to the end. " +
|
"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.")]
|
"Refuses if the task is currently Running. Subtasks are surfaced to the agent at run time and shown in the task's Steps list. " +
|
||||||
public async Task<TaskDto> AddSubtask(
|
"Returns a lean task reference (id, listId, title, status, sortOrder, isMyDay), not the task's description.")]
|
||||||
|
public async Task<TaskRefDto> AddSubtask(
|
||||||
string taskId,
|
string taskId,
|
||||||
string title,
|
string title,
|
||||||
int? orderNum = null,
|
int? orderNum = null,
|
||||||
@@ -275,7 +290,7 @@ public sealed class ExternalMcpService
|
|||||||
}, cancellationToken);
|
}, cancellationToken);
|
||||||
|
|
||||||
await _broadcaster.TaskUpdated(taskId);
|
await _broadcaster.TaskUpdated(taskId);
|
||||||
return ToDto(task);
|
return ToRefDto(task);
|
||||||
}
|
}
|
||||||
|
|
||||||
[McpServerTool, Description(
|
[McpServerTool, Description(
|
||||||
@@ -285,8 +300,9 @@ public sealed class ExternalMcpService
|
|||||||
"Cancelled (retire the task without deleting it; it can be reset to Idle later), " +
|
"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 " +
|
"Done (mark complete; refused if the task has an active worktree — use review_task to approve " +
|
||||||
"and merge that worktree instead). " +
|
"and merge that worktree instead). " +
|
||||||
"Full lifecycle: Idle → Queued → Running → WaitingForReview → Done | Failed | Cancelled.")]
|
"Full lifecycle: Idle → Queued → Running → WaitingForReview → Done | Failed | Cancelled. " +
|
||||||
public async Task<TaskDto> UpdateTaskStatus(
|
"Returns a lean task reference (id, listId, title, status, sortOrder, isMyDay), not the task's description.")]
|
||||||
|
public async Task<TaskRefDto> UpdateTaskStatus(
|
||||||
string taskId,
|
string taskId,
|
||||||
string status,
|
string status,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
@@ -337,7 +353,7 @@ public sealed class ExternalMcpService
|
|||||||
}
|
}
|
||||||
|
|
||||||
var reload = (await _tasks.GetByIdAsync(taskId, cancellationToken))!;
|
var reload = (await _tasks.GetByIdAsync(taskId, cancellationToken))!;
|
||||||
return ToDto(reload);
|
return ToRefDto(reload);
|
||||||
}
|
}
|
||||||
|
|
||||||
[McpServerTool, Description(
|
[McpServerTool, Description(
|
||||||
@@ -352,7 +368,8 @@ public sealed class ExternalMcpService
|
|||||||
"decision='reject_rerun' → Queued and re-runs, resuming the agent's session with your feedback as the next turn (feedback is required). " +
|
"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='reject_park' → Idle for manual editing (feedback ignored). " +
|
||||||
"decision='cancel' → Cancelled. " +
|
"decision='cancel' → Cancelled. " +
|
||||||
"Fails if the task is not currently WaitingForReview (except cancel, which also works while Running/Queued).")]
|
"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.")]
|
||||||
public async Task<ReviewTaskResult> ReviewTask(
|
public async Task<ReviewTaskResult> ReviewTask(
|
||||||
string taskId,
|
string taskId,
|
||||||
string decision,
|
string decision,
|
||||||
@@ -433,7 +450,7 @@ public sealed class ExternalMcpService
|
|||||||
}
|
}
|
||||||
|
|
||||||
return new ReviewTaskResult(
|
return new ReviewTaskResult(
|
||||||
ToDto((await _tasks.GetByIdAsync(taskId, cancellationToken))!),
|
ToRefDto((await _tasks.GetByIdAsync(taskId, cancellationToken))!),
|
||||||
mergeStatus, mergeConflicts, mergeMessage, repoPath);
|
mergeStatus, mergeConflicts, mergeMessage, repoPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -725,8 +742,9 @@ public sealed class ExternalMcpService
|
|||||||
"Abort an in-progress conflicted merge, discarding the conflict markers and restoring a clean working tree. " +
|
"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 " +
|
"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). " +
|
"unit merge. The task keeps its pre-merge status (e.g. WaitingForReview). " +
|
||||||
"Throws if there is no in-progress merge for the task.")]
|
"Throws if there is no in-progress merge for the task. " +
|
||||||
public async Task<TaskDto> AbortMerge(string taskId, CancellationToken cancellationToken)
|
"Returns a lean task reference (id, listId, title, status, sortOrder, isMyDay), not the task's description.")]
|
||||||
|
public async Task<TaskRefDto> AbortMerge(string taskId, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
_ = await _tasks.GetByIdAsync(taskId, cancellationToken)
|
_ = await _tasks.GetByIdAsync(taskId, cancellationToken)
|
||||||
?? throw new InvalidOperationException($"Task {taskId} not found.");
|
?? throw new InvalidOperationException($"Task {taskId} not found.");
|
||||||
@@ -743,7 +761,7 @@ public sealed class ExternalMcpService
|
|||||||
}
|
}
|
||||||
|
|
||||||
await _broadcaster.TaskUpdated(taskId);
|
await _broadcaster.TaskUpdated(taskId);
|
||||||
return ToDto((await _tasks.GetByIdAsync(taskId, cancellationToken))!);
|
return ToRefDto((await _tasks.GetByIdAsync(taskId, cancellationToken))!);
|
||||||
}
|
}
|
||||||
|
|
||||||
[McpServerTool, Description(
|
[McpServerTool, Description(
|
||||||
@@ -997,8 +1015,9 @@ public sealed class ExternalMcpService
|
|||||||
"Daily prep: set or clear a task's MyDay flag, optionally setting its sortOrder " +
|
"Daily prep: set or clear a task's MyDay flag, optionally setting its sortOrder " +
|
||||||
"(use consecutive sortOrder values to keep related tasks together). " +
|
"(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); " +
|
"Setting isMyDay=true is rejected if it would exceed the MyDay cap (DailyPrepMaxTasks open MyDay tasks); " +
|
||||||
"clearing (isMyDay=false) is always allowed.")]
|
"clearing (isMyDay=false) is always allowed. " +
|
||||||
public async Task<TaskDto> SetMyDay(
|
"Returns a lean task reference (id, listId, title, status, sortOrder, isMyDay), not the task's description.")]
|
||||||
|
public async Task<TaskRefDto> SetMyDay(
|
||||||
string taskId,
|
string taskId,
|
||||||
bool isMyDay,
|
bool isMyDay,
|
||||||
int? sortOrder = null,
|
int? sortOrder = null,
|
||||||
@@ -1025,7 +1044,7 @@ public sealed class ExternalMcpService
|
|||||||
await ctx.SaveChangesAsync(cancellationToken);
|
await ctx.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
await _broadcaster.TaskUpdated(taskId);
|
await _broadcaster.TaskUpdated(taskId);
|
||||||
return ToDto(task);
|
return ToRefDto(task);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static DailyPrepCandidateDto ToCandidate(TaskEntity t) => new(
|
private static DailyPrepCandidateDto ToCandidate(TaskEntity t) => new(
|
||||||
@@ -1120,6 +1139,14 @@ public sealed class ExternalMcpService
|
|||||||
t.FinishedAt,
|
t.FinishedAt,
|
||||||
t.IsMyDay,
|
t.IsMyDay,
|
||||||
t.SortOrder);
|
t.SortOrder);
|
||||||
|
|
||||||
|
private static TaskRefDto ToRefDto(TaskEntity t) => new(
|
||||||
|
t.Id,
|
||||||
|
t.ListId,
|
||||||
|
t.Title,
|
||||||
|
t.Status.ToString(),
|
||||||
|
t.SortOrder,
|
||||||
|
t.IsMyDay);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static class DailyPrepFilter
|
internal static class DailyPrepFilter
|
||||||
|
|||||||
@@ -189,6 +189,41 @@ public sealed class ExternalMcpServiceTests : IDisposable
|
|||||||
Assert.Equal("new title", loaded!.Title);
|
Assert.Equal("new title", loaded!.Title);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateTask_ReturnsLeanReference_NotFullTaskDto()
|
||||||
|
{
|
||||||
|
// TaskRefDto has no Description/Result properties -- a writing tool that echoes it back
|
||||||
|
// would re-send the (possibly long) description the caller just sent.
|
||||||
|
Assert.DoesNotContain(nameof(TaskDto.Description), typeof(TaskRefDto).GetProperties().Select(p => p.Name));
|
||||||
|
Assert.DoesNotContain("Result", typeof(TaskRefDto).GetProperties().Select(p => p.Name));
|
||||||
|
|
||||||
|
var listId = await SeedListAsync();
|
||||||
|
var task = await SeedTaskAsync(listId, "old title");
|
||||||
|
task.Description = "a long description the caller already has";
|
||||||
|
await _tasks.UpdateAsync(task, CancellationToken.None);
|
||||||
|
var sut = BuildSut(CreateQueue());
|
||||||
|
|
||||||
|
var dto = await sut.UpdateTask(task.Id, "new title", null, null, CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.Equal(task.Id, dto.Id);
|
||||||
|
Assert.Equal(listId, dto.ListId);
|
||||||
|
Assert.Equal("new title", dto.Title);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetTask_ReturnsFullDtoIncludingDescription()
|
||||||
|
{
|
||||||
|
var listId = await SeedListAsync();
|
||||||
|
var task = await SeedTaskAsync(listId, "with description");
|
||||||
|
task.Description = "the full description text";
|
||||||
|
await _tasks.UpdateAsync(task, CancellationToken.None);
|
||||||
|
var sut = BuildSut(CreateQueue());
|
||||||
|
|
||||||
|
var dto = await sut.GetTask(task.Id, CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.Equal("the full description text", dto.Description);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task UpdateTask_OnRunning_Throws()
|
public async Task UpdateTask_OnRunning_Throws()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user