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:
76
src/ClaudeDo.Worker/Agents/AgentFileService.cs
Normal file
76
src/ClaudeDo.Worker/Agents/AgentFileService.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
60
src/ClaudeDo.Worker/Agents/DefaultAgentSeeder.cs
Normal file
60
src/ClaudeDo.Worker/Agents/DefaultAgentSeeder.cs
Normal file
@@ -0,0 +1,60 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ClaudeDo.Worker.Agents;
|
||||
|
||||
public sealed record SeedResult(int Copied, int Skipped);
|
||||
|
||||
public sealed class DefaultAgentSeeder
|
||||
{
|
||||
private readonly string _bundleDir;
|
||||
private readonly string _targetDir;
|
||||
private readonly ILogger<DefaultAgentSeeder>? _logger;
|
||||
|
||||
public DefaultAgentSeeder(string bundleDir, string targetDir, ILogger<DefaultAgentSeeder>? logger = null)
|
||||
{
|
||||
_bundleDir = bundleDir;
|
||||
_targetDir = targetDir;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<SeedResult> SeedMissingAsync(CancellationToken ct = default)
|
||||
{
|
||||
if (!Directory.Exists(_bundleDir))
|
||||
{
|
||||
_logger?.LogWarning("DefaultAgents bundle dir not found: {Dir}", _bundleDir);
|
||||
return new SeedResult(0, 0);
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(_targetDir);
|
||||
|
||||
int copied = 0;
|
||||
int skipped = 0;
|
||||
|
||||
foreach (var src in Directory.EnumerateFiles(_bundleDir, "*.md"))
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
var fileName = Path.GetFileName(src);
|
||||
var dst = Path.Combine(_targetDir, fileName);
|
||||
|
||||
if (File.Exists(dst))
|
||||
{
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var input = File.OpenRead(src);
|
||||
using var output = File.Create(dst);
|
||||
await input.CopyToAsync(output, ct);
|
||||
copied++;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogWarning(ex, "Failed to copy default agent {File}", fileName);
|
||||
}
|
||||
}
|
||||
|
||||
return new SeedResult(copied, skipped);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user