refactor(worker): extract OverrideSlotService and reorganize Worker/Services into domain folders

Slice 5 of the worker state consolidation refactor.

OverrideSlotService (new in Worker/Queue/) owns RunNow, ContinueTask,
and the override-slot piece of CancelTask. QueueService keeps the
queue-slot guard for "task is already running" rejection and delegates
to OverrideSlotService for execution; CancelTask tries the override
slot first, then the queue slot. QueueSlotState is extracted to its own
file.

Folder reorg (via git mv to preserve history):
- Worker/Queue/      QueueService, OverrideSlotService, QueueSlotState
                     (alongside existing waker/picker)
- Worker/Lifecycle/  StaleTaskRecovery, TaskResetService, TaskMergeService
- Worker/Worktrees/  WorktreeMaintenanceService
- Worker/Agents/     AgentFileService, DefaultAgentSeeder

Worker/Services/ folder removed. All consumers updated to the new
namespaces (Program.cs, WorkerHub, ExternalMcpService,
PlanningMergeOrchestrator, all Worker tests).

OverrideSlotService is registered as a DI singleton in both the main
worker app and the external MCP app.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mika Kuns
2026-04-27 14:42:13 +02:00
parent 4ab906ff0b
commit ff7c239959
25 changed files with 200 additions and 109 deletions

View File

@@ -0,0 +1,76 @@
using ClaudeDo.Data.Models;
namespace ClaudeDo.Worker.Agents;
public sealed class AgentFileService
{
private readonly string _agentsDir;
public AgentFileService(string agentsDir)
{
_agentsDir = agentsDir;
}
public Task<List<AgentInfo>> ScanAsync(CancellationToken ct = default)
{
var agents = new List<AgentInfo>();
if (!Directory.Exists(_agentsDir))
return Task.FromResult(agents);
foreach (var file in Directory.EnumerateFiles(_agentsDir, "*.md"))
{
ct.ThrowIfCancellationRequested();
var (name, description) = ParseFrontmatter(file);
agents.Add(new AgentInfo(name, description, file));
}
agents.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase));
return Task.FromResult(agents);
}
public async Task<string> ReadAsync(string path, CancellationToken ct = default)
{
return await File.ReadAllTextAsync(path, ct);
}
public async Task WriteAsync(string path, string content, CancellationToken ct = default)
{
var dir = Path.GetDirectoryName(path);
if (dir is not null) Directory.CreateDirectory(dir);
await File.WriteAllTextAsync(path, content, ct);
}
public Task DeleteAsync(string path, CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested();
if (File.Exists(path)) File.Delete(path);
return Task.CompletedTask;
}
private static (string name, string description) ParseFrontmatter(string filePath)
{
var fileName = Path.GetFileNameWithoutExtension(filePath);
string name = fileName;
string description = "";
try
{
using var reader = new StreamReader(filePath);
var firstLine = reader.ReadLine();
if (firstLine?.Trim() != "---")
return (name, description);
while (reader.ReadLine() is { } line)
{
if (line.Trim() == "---") break;
if (line.StartsWith("name:"))
name = line["name:".Length..].Trim();
else if (line.StartsWith("description:"))
description = line["description:".Length..].Trim();
}
}
catch { /* Can't read file -- use filename fallback */ }
return (name, description);
}
}