refactor(tags): remove tag entity and all references
Drops TagEntity, TagRepository, and tag wiring across data layer, worker, and UI. Adds RemoveTags migration to clean up schema. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -11,8 +11,6 @@ namespace ClaudeDo.Worker.External;
|
||||
|
||||
public sealed record TaskListDto(string Id, string Name, string? WorkingDir);
|
||||
|
||||
public sealed record TagDto(long Id, string Name);
|
||||
|
||||
public sealed record TaskDto(
|
||||
string Id,
|
||||
string ListId,
|
||||
@@ -32,7 +30,6 @@ public sealed class ExternalMcpService
|
||||
private readonly ListRepository _lists;
|
||||
private readonly QueueService _queue;
|
||||
private readonly HubBroadcaster _broadcaster;
|
||||
private readonly TagRepository _tags;
|
||||
private readonly ITaskStateService _state;
|
||||
|
||||
public ExternalMcpService(
|
||||
@@ -40,14 +37,12 @@ public sealed class ExternalMcpService
|
||||
ListRepository lists,
|
||||
QueueService queue,
|
||||
HubBroadcaster broadcaster,
|
||||
TagRepository tags,
|
||||
ITaskStateService state)
|
||||
{
|
||||
_tasks = tasks;
|
||||
_lists = lists;
|
||||
_queue = queue;
|
||||
_broadcaster = broadcaster;
|
||||
_tags = tags;
|
||||
_state = state;
|
||||
}
|
||||
|
||||
@@ -91,14 +86,13 @@ public sealed class ExternalMcpService
|
||||
return ToDto(task);
|
||||
}
|
||||
|
||||
[McpServerTool, Description("Create a new task in the given list. Set queueImmediately=true to enqueue it for agent execution. Optional tags are attached on creation; missing tag names auto-create.")]
|
||||
[McpServerTool, Description("Create a new task in the given list. Set queueImmediately=true to enqueue it for agent execution.")]
|
||||
public async Task<TaskDto> AddTask(
|
||||
string listId,
|
||||
string title,
|
||||
string? description,
|
||||
string createdBy,
|
||||
bool queueImmediately,
|
||||
IReadOnlyList<string>? tags,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(listId))
|
||||
@@ -124,9 +118,6 @@ public sealed class ExternalMcpService
|
||||
};
|
||||
await _tasks.AddAsync(entity, cancellationToken);
|
||||
|
||||
if (tags is not null && tags.Count > 0)
|
||||
await _tasks.SetTagsAsync(entity.Id, tags, cancellationToken);
|
||||
|
||||
if (queueImmediately)
|
||||
{
|
||||
// Routes through TaskStateService so the queue is woken automatically.
|
||||
@@ -140,13 +131,12 @@ public sealed class ExternalMcpService
|
||||
return ToDto(entity);
|
||||
}
|
||||
|
||||
[McpServerTool, Description("Update an existing task's title, description, commit type, and/or tags. Pass null to leave a field unchanged. Tags are replaced as a full set when non-null. Refuses if the task is currently Running.")]
|
||||
[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.")]
|
||||
public async Task<TaskDto> UpdateTask(
|
||||
string taskId,
|
||||
string? title,
|
||||
string? description,
|
||||
string? commitType,
|
||||
IReadOnlyList<string>? tags,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var task = await _tasks.GetByIdAsync(taskId, cancellationToken)
|
||||
@@ -159,9 +149,6 @@ public sealed class ExternalMcpService
|
||||
if (commitType is not null) task.CommitType = commitType;
|
||||
await _tasks.UpdateAsync(task, cancellationToken);
|
||||
|
||||
if (tags is not null)
|
||||
await _tasks.SetTagsAsync(taskId, tags, cancellationToken);
|
||||
|
||||
var reload = (await _tasks.GetByIdAsync(taskId, cancellationToken))!;
|
||||
await _broadcaster.TaskUpdated(taskId);
|
||||
return ToDto(reload);
|
||||
@@ -239,30 +226,6 @@ public sealed class ExternalMcpService
|
||||
await _broadcaster.TaskUpdated(taskId);
|
||||
}
|
||||
|
||||
[McpServerTool, Description("Replace the full tag set on an existing task. Missing tag names auto-create. Refuses if the task is Running.")]
|
||||
public async Task<TaskDto> SetTaskTags(
|
||||
string taskId,
|
||||
IReadOnlyList<string> tags,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var task = await _tasks.GetByIdAsync(taskId, cancellationToken)
|
||||
?? throw new InvalidOperationException($"Task {taskId} not found.");
|
||||
if (task.Status == TaskStatus.Running)
|
||||
throw new InvalidOperationException("Cannot retag a running task. Cancel it first.");
|
||||
|
||||
await _tasks.SetTagsAsync(taskId, tags, cancellationToken);
|
||||
var reload = (await _tasks.GetByIdAsync(taskId, cancellationToken))!;
|
||||
await _broadcaster.TaskUpdated(taskId);
|
||||
return ToDto(reload);
|
||||
}
|
||||
|
||||
[McpServerTool, Description("List all known tags. Useful for discovering existing tag names (including 'agent' which marks tasks for auto-execution) before tagging.")]
|
||||
public async Task<IReadOnlyList<TagDto>> ListTags(CancellationToken cancellationToken)
|
||||
{
|
||||
var tags = await _tags.GetAllAsync(cancellationToken);
|
||||
return tags.Select(t => new TagDto(t.Id, t.Name)).ToList();
|
||||
}
|
||||
|
||||
private static TaskDto ToDto(TaskEntity t) => new(
|
||||
t.Id,
|
||||
t.ListId,
|
||||
|
||||
@@ -331,41 +331,6 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
||||
if (!result.Ok) throw new HubException(result.Reason ?? "set status failed");
|
||||
}
|
||||
|
||||
public async Task SetTaskTags(string taskId, string[] tagNames)
|
||||
{
|
||||
await using var ctx = await _dbFactory.CreateDbContextAsync();
|
||||
var entity = await ctx.Tasks.Include(t => t.Tags).FirstOrDefaultAsync(t => t.Id == taskId);
|
||||
if (entity is null) throw new HubException("task not found");
|
||||
|
||||
var desired = (tagNames ?? Array.Empty<string>())
|
||||
.Select(n => n?.Trim().ToLowerInvariant() ?? "")
|
||||
.Where(n => n.Length > 0)
|
||||
.ToHashSet();
|
||||
|
||||
foreach (var t in entity.Tags.Where(t => !desired.Contains(t.Name)).ToList())
|
||||
entity.Tags.Remove(t);
|
||||
|
||||
var existingByName = await ctx.Tags
|
||||
.Where(t => desired.Contains(t.Name))
|
||||
.ToListAsync();
|
||||
foreach (var name in desired)
|
||||
{
|
||||
if (entity.Tags.Any(t => t.Name == name)) continue;
|
||||
var tag = existingByName.FirstOrDefault(t => t.Name == name)
|
||||
?? new TagEntity { Name = name };
|
||||
if (tag.Id == 0) ctx.Tags.Add(tag);
|
||||
entity.Tags.Add(tag);
|
||||
}
|
||||
await ctx.SaveChangesAsync();
|
||||
await _broadcaster.TaskUpdated(taskId);
|
||||
}
|
||||
|
||||
public async Task<List<string>> GetAllTags()
|
||||
{
|
||||
await using var ctx = await _dbFactory.CreateDbContextAsync();
|
||||
return await ctx.Tags.OrderBy(t => t.Name).Select(t => t.Name).ToListAsync();
|
||||
}
|
||||
|
||||
public async Task UpdateTaskAgentSettings(UpdateTaskAgentSettingsDto dto)
|
||||
{
|
||||
using var ctx = _dbFactory.CreateDbContext();
|
||||
|
||||
@@ -28,7 +28,6 @@ public sealed class PlanningChainCoordinator
|
||||
// chain leaves history alone but still reshapes the tail.
|
||||
// - Running children abort the operation — the chain cannot be reshaped while
|
||||
// one of its members is mid-flight.
|
||||
// The "agent" tag is auto-attached to every child so the picker can claim them.
|
||||
// Returns the number of children placed in the chain.
|
||||
internal async Task<int> SetupChainAsync(string parentTaskId, CancellationToken ct = default)
|
||||
{
|
||||
@@ -37,7 +36,6 @@ public sealed class PlanningChainCoordinator
|
||||
?? throw new InvalidOperationException($"Task {parentTaskId} not found.");
|
||||
|
||||
var children = await ctx.Tasks
|
||||
.Include(t => t.Tags)
|
||||
.Where(t => t.ParentTaskId == parentTaskId)
|
||||
.OrderBy(t => t.SortOrder).ThenBy(t => t.CreatedAt)
|
||||
.ToListAsync(ct);
|
||||
@@ -49,18 +47,6 @@ public sealed class PlanningChainCoordinator
|
||||
throw new InvalidOperationException(
|
||||
$"Child {running.Id} is running; cannot reshape chain.");
|
||||
|
||||
// Worker queue picker requires the "agent" tag — attach it so children are pickable.
|
||||
var agentTag = await ctx.Tags.FirstOrDefaultAsync(t => t.Name == "agent", ct);
|
||||
if (agentTag is not null)
|
||||
{
|
||||
foreach (var c in children)
|
||||
{
|
||||
if (!c.Tags.Any(t => t.Id == agentTag.Id))
|
||||
c.Tags.Add(agentTag);
|
||||
}
|
||||
await ctx.SaveChangesAsync(ct);
|
||||
}
|
||||
|
||||
// Re-shape over Idle and Queued children only; leave Done/Failed/Cancelled
|
||||
// (terminal) results in place.
|
||||
var sequenceable = children
|
||||
|
||||
@@ -8,7 +8,7 @@ using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
|
||||
|
||||
namespace ClaudeDo.Worker.Planning;
|
||||
|
||||
public sealed record ChildTaskDto(string TaskId, string Title, string? Description, string Status, IReadOnlyList<string> Tags);
|
||||
public sealed record ChildTaskDto(string TaskId, string Title, string? Description, string Status);
|
||||
public sealed record CreatedChildDto(string TaskId, string Status);
|
||||
|
||||
[McpServerToolType]
|
||||
@@ -41,12 +41,11 @@ public sealed class PlanningMcpService
|
||||
public async Task<CreatedChildDto> CreateChildTask(
|
||||
string title,
|
||||
string? description,
|
||||
IReadOnlyList<string>? tags,
|
||||
string? commitType,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var ctx = _contextAccessor.Current;
|
||||
var child = await _tasks.CreateChildAsync(ctx.ParentTaskId, title, description, tags, commitType, cancellationToken);
|
||||
var child = await _tasks.CreateChildAsync(ctx.ParentTaskId, title, description, commitType, cancellationToken);
|
||||
await BroadcastTaskUpdatedAsync(child.Id, cancellationToken);
|
||||
await BroadcastTaskUpdatedAsync(ctx.ParentTaskId, cancellationToken);
|
||||
return new CreatedChildDto(child.Id, child.Status.ToString());
|
||||
@@ -58,24 +57,19 @@ public sealed class PlanningMcpService
|
||||
{
|
||||
var ctx = _contextAccessor.Current;
|
||||
var children = await _tasks.GetChildrenAsync(ctx.ParentTaskId, cancellationToken);
|
||||
var list = new List<ChildTaskDto>(children.Count);
|
||||
foreach (var c in children)
|
||||
{
|
||||
var tagList = await _tasks.GetTagsAsync(c.Id, cancellationToken);
|
||||
list.Add(new ChildTaskDto(c.Id, c.Title, c.Description, c.Status.ToString(), tagList.Select(t => t.Name).ToList()));
|
||||
}
|
||||
return list;
|
||||
return children
|
||||
.Select(c => new ChildTaskDto(c.Id, c.Title, c.Description, c.Status.ToString()))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static readonly TaskStatus[] EditableStatuses =
|
||||
{ TaskStatus.Idle, TaskStatus.Queued };
|
||||
|
||||
[McpServerTool, Description("Update a child task in the active planning session. Can change title, description, tags (replaces the full set), commit type, and status. Status must be one of: Idle, Queued.")]
|
||||
[McpServerTool, Description("Update a child task in the active planning session. Can change title, description, commit type, and status. Status must be one of: Idle, Queued.")]
|
||||
public async Task<ChildTaskDto> UpdateChildTask(
|
||||
string taskId,
|
||||
string? title,
|
||||
string? description,
|
||||
IReadOnlyList<string>? tags,
|
||||
string? commitType,
|
||||
string? status,
|
||||
CancellationToken cancellationToken)
|
||||
@@ -101,13 +95,12 @@ public sealed class PlanningMcpService
|
||||
newStatus = parsed;
|
||||
}
|
||||
|
||||
await _tasks.UpdateChildAsync(taskId, title, description, commitType, tags, newStatus, cancellationToken);
|
||||
await _tasks.UpdateChildAsync(taskId, title, description, commitType, newStatus, cancellationToken);
|
||||
|
||||
var reload = (await _tasks.GetByIdAsync(taskId, cancellationToken))!;
|
||||
var tagList = await _tasks.GetTagsAsync(reload.Id, cancellationToken);
|
||||
await BroadcastTaskUpdatedAsync(reload.Id, cancellationToken);
|
||||
await BroadcastTaskUpdatedAsync(ctx.ParentTaskId, cancellationToken);
|
||||
return new ChildTaskDto(reload.Id, reload.Title, reload.Description, reload.Status.ToString(), tagList.Select(t => t.Name).ToList());
|
||||
return new ChildTaskDto(reload.Id, reload.Title, reload.Description, reload.Status.ToString());
|
||||
}
|
||||
|
||||
[McpServerTool, Description("Delete a child task in the active planning session.")]
|
||||
|
||||
@@ -185,7 +185,6 @@ if (cfg.ExternalMcpPort > 0)
|
||||
sp.GetRequiredService<IDbContextFactory<ClaudeDoDbContext>>().CreateDbContext());
|
||||
externalBuilder.Services.AddScoped<TaskRepository>();
|
||||
externalBuilder.Services.AddScoped<ListRepository>();
|
||||
externalBuilder.Services.AddScoped<TagRepository>();
|
||||
externalBuilder.Services.AddScoped<ExternalMcpService>();
|
||||
externalBuilder.Services.AddMcpServer()
|
||||
.WithHttpTransport()
|
||||
|
||||
@@ -362,19 +362,14 @@ public sealed class TaskRunner
|
||||
TaskEntity task, ListConfigEntity? listConfig, string? resumeSessionId, CancellationToken ct)
|
||||
{
|
||||
AppSettingsEntity global;
|
||||
bool isAgentTask;
|
||||
using (var ctx = _dbFactory.CreateDbContext())
|
||||
{
|
||||
var settingsRepo = new AppSettingsRepository(ctx);
|
||||
global = await settingsRepo.GetAsync(ct);
|
||||
|
||||
var taskRepo = new TaskRepository(ctx);
|
||||
var tags = await taskRepo.GetEffectiveTagsAsync(task.Id, ct);
|
||||
isAgentTask = tags.Any(t => string.Equals(t.Name, "agent", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
var systemFile = PromptFiles.ReadOrNull(PromptKind.System);
|
||||
var agentFile = isAgentTask ? PromptFiles.ReadOrNull(PromptKind.Agent) : null;
|
||||
var agentFile = PromptFiles.ReadOrNull(PromptKind.Agent);
|
||||
|
||||
var instructions = MergeInstructions(
|
||||
systemFile, global.DefaultClaudeInstructions, listConfig?.SystemPrompt, task.SystemPrompt, agentFile);
|
||||
|
||||
Reference in New Issue
Block a user