From ff7c239959448c5c8d452dd8849424746adf9f03 Mon Sep 17 00:00:00 2001 From: Mika Kuns Date: Mon, 27 Apr 2026 14:42:13 +0200 Subject: [PATCH] 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) --- .../{Services => Agents}/AgentFileService.cs | 2 +- .../DefaultAgentSeeder.cs | 2 +- .../External/ExternalMcpService.cs | 2 +- src/ClaudeDo.Worker/Hub/WorkerHub.cs | 4 +- .../StaleTaskRecovery.cs | 2 +- .../TaskMergeService.cs | 2 +- .../TaskResetService.cs | 2 +- .../Planning/PlanningMergeOrchestrator.cs | 2 +- src/ClaudeDo.Worker/Program.cs | 8 +- .../Queue/OverrideSlotService.cs | 134 ++++++++++++++++++ .../{Services => Queue}/QueueService.cs | 105 +++----------- src/ClaudeDo.Worker/Queue/QueueSlotState.cs | 8 ++ .../WorktreeMaintenanceService.cs | 2 +- .../External/ExternalMcpServiceTests.cs | 5 +- .../Hub/AgentSettingsHubTests.cs | 2 +- .../Hub/PlanningHubTests.cs | 5 +- .../PlanningMergeOrchestratorTests.cs | 2 +- .../Services/AgentFileServiceTests.cs | 2 +- .../Services/DefaultAgentSeederTests.cs | 2 +- .../Services/QueueServiceSlotGuardTests.cs | 4 +- .../Services/QueueServiceTests.cs | 4 +- .../Services/StaleTaskRecoveryTests.cs | 2 +- .../Services/TaskMergeServiceTests.cs | 2 +- .../Services/TaskResetServiceTests.cs | 2 +- .../WorktreeMaintenanceServiceTests.cs | 2 +- 25 files changed, 200 insertions(+), 109 deletions(-) rename src/ClaudeDo.Worker/{Services => Agents}/AgentFileService.cs (98%) rename src/ClaudeDo.Worker/{Services => Agents}/DefaultAgentSeeder.cs (97%) rename src/ClaudeDo.Worker/{Services => Lifecycle}/StaleTaskRecovery.cs (95%) rename src/ClaudeDo.Worker/{Services => Lifecycle}/TaskMergeService.cs (99%) rename src/ClaudeDo.Worker/{Services => Lifecycle}/TaskResetService.cs (98%) create mode 100644 src/ClaudeDo.Worker/Queue/OverrideSlotService.cs rename src/ClaudeDo.Worker/{Services => Queue}/QueueService.cs (53%) create mode 100644 src/ClaudeDo.Worker/Queue/QueueSlotState.cs rename src/ClaudeDo.Worker/{Services => Worktrees}/WorktreeMaintenanceService.cs (99%) diff --git a/src/ClaudeDo.Worker/Services/AgentFileService.cs b/src/ClaudeDo.Worker/Agents/AgentFileService.cs similarity index 98% rename from src/ClaudeDo.Worker/Services/AgentFileService.cs rename to src/ClaudeDo.Worker/Agents/AgentFileService.cs index adc598a..69069a7 100644 --- a/src/ClaudeDo.Worker/Services/AgentFileService.cs +++ b/src/ClaudeDo.Worker/Agents/AgentFileService.cs @@ -1,6 +1,6 @@ using ClaudeDo.Data.Models; -namespace ClaudeDo.Worker.Services; +namespace ClaudeDo.Worker.Agents; public sealed class AgentFileService { diff --git a/src/ClaudeDo.Worker/Services/DefaultAgentSeeder.cs b/src/ClaudeDo.Worker/Agents/DefaultAgentSeeder.cs similarity index 97% rename from src/ClaudeDo.Worker/Services/DefaultAgentSeeder.cs rename to src/ClaudeDo.Worker/Agents/DefaultAgentSeeder.cs index 895197b..94a1469 100644 --- a/src/ClaudeDo.Worker/Services/DefaultAgentSeeder.cs +++ b/src/ClaudeDo.Worker/Agents/DefaultAgentSeeder.cs @@ -1,6 +1,6 @@ using Microsoft.Extensions.Logging; -namespace ClaudeDo.Worker.Services; +namespace ClaudeDo.Worker.Agents; public sealed record SeedResult(int Copied, int Skipped); diff --git a/src/ClaudeDo.Worker/External/ExternalMcpService.cs b/src/ClaudeDo.Worker/External/ExternalMcpService.cs index d13e396..e447438 100644 --- a/src/ClaudeDo.Worker/External/ExternalMcpService.cs +++ b/src/ClaudeDo.Worker/External/ExternalMcpService.cs @@ -2,7 +2,7 @@ using System.ComponentModel; using ClaudeDo.Data.Models; using ClaudeDo.Data.Repositories; using ClaudeDo.Worker.Hub; -using ClaudeDo.Worker.Services; +using ClaudeDo.Worker.Queue; using ClaudeDo.Worker.State; using ModelContextProtocol.Server; using TaskStatus = ClaudeDo.Data.Models.TaskStatus; diff --git a/src/ClaudeDo.Worker/Hub/WorkerHub.cs b/src/ClaudeDo.Worker/Hub/WorkerHub.cs index c96d9d6..eb19de7 100644 --- a/src/ClaudeDo.Worker/Hub/WorkerHub.cs +++ b/src/ClaudeDo.Worker/Hub/WorkerHub.cs @@ -2,9 +2,11 @@ using System.Reflection; using ClaudeDo.Data; using ClaudeDo.Data.Models; using ClaudeDo.Data.Repositories; +using ClaudeDo.Worker.Agents; +using ClaudeDo.Worker.Lifecycle; using ClaudeDo.Worker.Planning; using ClaudeDo.Worker.Queue; -using ClaudeDo.Worker.Services; +using ClaudeDo.Worker.Worktrees; using Microsoft.AspNetCore.SignalR; using Microsoft.EntityFrameworkCore; diff --git a/src/ClaudeDo.Worker/Services/StaleTaskRecovery.cs b/src/ClaudeDo.Worker/Lifecycle/StaleTaskRecovery.cs similarity index 95% rename from src/ClaudeDo.Worker/Services/StaleTaskRecovery.cs rename to src/ClaudeDo.Worker/Lifecycle/StaleTaskRecovery.cs index c45ab4f..d39af7a 100644 --- a/src/ClaudeDo.Worker/Services/StaleTaskRecovery.cs +++ b/src/ClaudeDo.Worker/Lifecycle/StaleTaskRecovery.cs @@ -1,6 +1,6 @@ using ClaudeDo.Worker.State; -namespace ClaudeDo.Worker.Services; +namespace ClaudeDo.Worker.Lifecycle; public sealed class StaleTaskRecovery : IHostedService { diff --git a/src/ClaudeDo.Worker/Services/TaskMergeService.cs b/src/ClaudeDo.Worker/Lifecycle/TaskMergeService.cs similarity index 99% rename from src/ClaudeDo.Worker/Services/TaskMergeService.cs rename to src/ClaudeDo.Worker/Lifecycle/TaskMergeService.cs index bc2b1bb..8fd8010 100644 --- a/src/ClaudeDo.Worker/Services/TaskMergeService.cs +++ b/src/ClaudeDo.Worker/Lifecycle/TaskMergeService.cs @@ -6,7 +6,7 @@ using ClaudeDo.Worker.Hub; using Microsoft.EntityFrameworkCore; using TaskStatus = ClaudeDo.Data.Models.TaskStatus; -namespace ClaudeDo.Worker.Services; +namespace ClaudeDo.Worker.Lifecycle; public sealed record MergeResult( string Status, diff --git a/src/ClaudeDo.Worker/Services/TaskResetService.cs b/src/ClaudeDo.Worker/Lifecycle/TaskResetService.cs similarity index 98% rename from src/ClaudeDo.Worker/Services/TaskResetService.cs rename to src/ClaudeDo.Worker/Lifecycle/TaskResetService.cs index 314dff5..c4316a7 100644 --- a/src/ClaudeDo.Worker/Services/TaskResetService.cs +++ b/src/ClaudeDo.Worker/Lifecycle/TaskResetService.cs @@ -7,7 +7,7 @@ using ClaudeDo.Worker.State; using Microsoft.EntityFrameworkCore; using TaskStatus = ClaudeDo.Data.Models.TaskStatus; -namespace ClaudeDo.Worker.Services; +namespace ClaudeDo.Worker.Lifecycle; public sealed class TaskResetService { diff --git a/src/ClaudeDo.Worker/Planning/PlanningMergeOrchestrator.cs b/src/ClaudeDo.Worker/Planning/PlanningMergeOrchestrator.cs index 6c46750..809db11 100644 --- a/src/ClaudeDo.Worker/Planning/PlanningMergeOrchestrator.cs +++ b/src/ClaudeDo.Worker/Planning/PlanningMergeOrchestrator.cs @@ -3,7 +3,7 @@ using ClaudeDo.Data; using ClaudeDo.Data.Git; using ClaudeDo.Data.Models; using ClaudeDo.Worker.Hub; -using ClaudeDo.Worker.Services; +using ClaudeDo.Worker.Lifecycle; using Microsoft.EntityFrameworkCore; using TaskStatus = ClaudeDo.Data.Models.TaskStatus; diff --git a/src/ClaudeDo.Worker/Program.cs b/src/ClaudeDo.Worker/Program.cs index 25315b3..94a5c79 100644 --- a/src/ClaudeDo.Worker/Program.cs +++ b/src/ClaudeDo.Worker/Program.cs @@ -1,14 +1,16 @@ using ClaudeDo.Data; using ClaudeDo.Data.Git; using ClaudeDo.Data.Repositories; +using ClaudeDo.Worker.Agents; using ClaudeDo.Worker.Config; using ClaudeDo.Worker.External; using ClaudeDo.Worker.Hub; +using ClaudeDo.Worker.Lifecycle; using ClaudeDo.Worker.Planning; using ClaudeDo.Worker.Queue; using ClaudeDo.Worker.Runner; -using ClaudeDo.Worker.Services; using ClaudeDo.Worker.State; +using ClaudeDo.Worker.Worktrees; using Microsoft.EntityFrameworkCore; var cfg = WorkerConfig.Load(); @@ -69,6 +71,9 @@ builder.Services.AddSingleton(sp => new DefaultAgentSeeder( agentsDir, sp.GetService>())); +// Override slot owns RunNow / ContinueTask. Queue slot is the BackgroundService. +builder.Services.AddSingleton(); + // QueueService: singleton + hosted service (same instance). builder.Services.AddSingleton(); builder.Services.AddHostedService(sp => sp.GetRequiredService()); @@ -141,6 +146,7 @@ if (cfg.ExternalMcpPort > 0) externalBuilder.Services.AddSingleton(cfg); externalBuilder.Services.AddSingleton(app.Services.GetRequiredService()); externalBuilder.Services.AddSingleton(app.Services.GetRequiredService()); + externalBuilder.Services.AddSingleton(app.Services.GetRequiredService()); externalBuilder.Services.AddSingleton(app.Services.GetRequiredService>()); externalBuilder.Services.AddSingleton(app.Services.GetRequiredService()); externalBuilder.Services.AddSingleton(app.Services.GetRequiredService()); diff --git a/src/ClaudeDo.Worker/Queue/OverrideSlotService.cs b/src/ClaudeDo.Worker/Queue/OverrideSlotService.cs new file mode 100644 index 0000000..4b4fb45 --- /dev/null +++ b/src/ClaudeDo.Worker/Queue/OverrideSlotService.cs @@ -0,0 +1,134 @@ +using ClaudeDo.Data; +using ClaudeDo.Data.Repositories; +using ClaudeDo.Worker.Runner; +using Microsoft.EntityFrameworkCore; + +namespace ClaudeDo.Worker.Queue; + +public sealed class OverrideSlotService +{ + private readonly IDbContextFactory _dbFactory; + private readonly TaskRunner _runner; + private readonly ILogger _logger; + + private readonly object _lock = new(); + private volatile QueueSlotState? _slot; + + public OverrideSlotService( + IDbContextFactory dbFactory, + TaskRunner runner, + ILogger logger) + { + _dbFactory = dbFactory; + _runner = runner; + _logger = logger; + } + + public QueueSlotState? CurrentSlot => _slot; + + public async Task RunNow(string taskId) + { + using (var context = _dbFactory.CreateDbContext()) + { + var taskRepo = new TaskRepository(context); + var exists = await taskRepo.GetByIdAsync(taskId); + if (exists is null) + throw new KeyNotFoundException($"Task '{taskId}' not found."); + } + + lock (_lock) + { + if (_slot is not null) + throw new InvalidOperationException("override slot busy"); + + var cts = new CancellationTokenSource(); + _slot = new QueueSlotState { TaskId = taskId, StartedAt = DateTime.UtcNow, Cts = cts }; + + _ = RunInSlotAsync(taskId, cts.Token).ContinueWith(t => + { + if (t.IsFaulted) + _logger.LogError(t.Exception, "RunInSlotAsync failed for task {TaskId}", taskId); + lock (_lock) { _slot = null; } + cts.Dispose(); + }, TaskScheduler.Default); + } + } + + public async Task ContinueTask(string taskId, string followUpPrompt) + { + using var context = _dbFactory.CreateDbContext(); + var taskRepo = new TaskRepository(context); + var task = await taskRepo.GetByIdAsync(taskId) + ?? throw new KeyNotFoundException($"Task '{taskId}' not found."); + + if (task.Status == Data.Models.TaskStatus.Running) + throw new InvalidOperationException("task is already running"); + + lock (_lock) + { + if (_slot is not null) + throw new InvalidOperationException("override slot busy"); + + var cts = new CancellationTokenSource(); + _slot = new QueueSlotState { TaskId = taskId, StartedAt = DateTime.UtcNow, Cts = cts }; + + _ = RunContinueInSlotAsync(taskId, followUpPrompt, cts.Token).ContinueWith(t => + { + if (t.IsFaulted) + _logger.LogError(t.Exception, "RunContinueInSlotAsync failed for task {TaskId}", taskId); + lock (_lock) { _slot = null; } + cts.Dispose(); + }, TaskScheduler.Default); + } + + return taskId; + } + + public bool TryCancel(string taskId) + { + lock (_lock) + { + if (_slot is not null && _slot.TaskId == taskId) + { + _slot.Cts.Cancel(); + return true; + } + } + return false; + } + + private async Task RunInSlotAsync(string taskId, CancellationToken ct) + { + try + { + _logger.LogInformation("Starting task {TaskId} in override slot", taskId); + + Data.Models.TaskEntity task; + using (var context = _dbFactory.CreateDbContext()) + { + var taskRepo = new TaskRepository(context); + task = await taskRepo.GetByIdAsync(taskId, ct) + ?? throw new KeyNotFoundException($"Task '{taskId}' not found."); + } + + await _runner.RunAsync(task, "override", ct); + } + catch (Exception ex) + { + _logger.LogError(ex, "Override slot runner error for task {TaskId}", taskId); + } + } + + private async Task RunContinueInSlotAsync(string taskId, string followUpPrompt, CancellationToken ct) + { + try + { + _logger.LogInformation("Continuing task {TaskId} in override slot", taskId); + await _runner.ContinueAsync(taskId, followUpPrompt, "override", ct); + } + catch (Exception ex) + { + _logger.LogError(ex, "Continue runner error for task {TaskId}", taskId); + } + } +} diff --git a/src/ClaudeDo.Worker/Services/QueueService.cs b/src/ClaudeDo.Worker/Queue/QueueService.cs similarity index 53% rename from src/ClaudeDo.Worker/Services/QueueService.cs rename to src/ClaudeDo.Worker/Queue/QueueService.cs index ba42d46..55b035a 100644 --- a/src/ClaudeDo.Worker/Services/QueueService.cs +++ b/src/ClaudeDo.Worker/Queue/QueueService.cs @@ -2,18 +2,10 @@ using ClaudeDo.Data; using ClaudeDo.Data.Models; using ClaudeDo.Data.Repositories; using ClaudeDo.Worker.Config; -using ClaudeDo.Worker.Queue; using ClaudeDo.Worker.Runner; using Microsoft.EntityFrameworkCore; -namespace ClaudeDo.Worker.Services; - -public sealed class QueueSlotState -{ - public required string TaskId { get; init; } - public required DateTime StartedAt { get; init; } - public required CancellationTokenSource Cts { get; init; } -} +namespace ClaudeDo.Worker.Queue; public sealed class QueueService : BackgroundService { @@ -23,10 +15,10 @@ public sealed class QueueService : BackgroundService private readonly ILogger _logger; private readonly QueueWaker _waker; private readonly IQueuePicker _picker; + private readonly OverrideSlotService _override; private readonly object _lock = new(); private volatile QueueSlotState? _queueSlot; - private volatile QueueSlotState? _overrideSlot; public QueueService( IDbContextFactory dbFactory, @@ -34,7 +26,8 @@ public sealed class QueueService : BackgroundService WorkerConfig cfg, ILogger logger, QueueWaker waker, - IQueuePicker picker) + IQueuePicker picker, + OverrideSlotService overrideSlot) { _dbFactory = dbFactory; _runner = runner; @@ -42,6 +35,7 @@ public sealed class QueueService : BackgroundService _logger = logger; _waker = waker; _picker = picker; + _override = overrideSlot; } public IReadOnlyList<(string slot, string taskId, DateTime startedAt)> GetActive() @@ -49,75 +43,36 @@ public sealed class QueueService : BackgroundService var list = new List<(string, string, DateTime)>(); var q = _queueSlot; if (q is not null) list.Add(("queue", q.TaskId, q.StartedAt)); - var o = _overrideSlot; + var o = _override.CurrentSlot; if (o is not null) list.Add(("override", o.TaskId, o.StartedAt)); return list; } - public async Task RunNow(string taskId) + public Task RunNow(string taskId) { - using (var context = _dbFactory.CreateDbContext()) - { - var taskRepo = new TaskRepository(context); - var exists = await taskRepo.GetByIdAsync(taskId); - if (exists is null) - throw new KeyNotFoundException($"Task '{taskId}' not found."); - } - - lock (_lock) - { - if (_queueSlot?.TaskId == taskId) - throw new InvalidOperationException("task is already running in queue slot"); - if (_overrideSlot is not null) - throw new InvalidOperationException("override slot busy"); - - var cts = new CancellationTokenSource(); - _overrideSlot = new QueueSlotState { TaskId = taskId, StartedAt = DateTime.UtcNow, Cts = cts }; - - _ = RunInSlotAsync(taskId, "override", cts.Token).ContinueWith(t => - { - if (t.IsFaulted) - _logger.LogError(t.Exception, "RunInSlotAsync failed for task {TaskId}", taskId); - lock (_lock) { _overrideSlot = null; } - cts.Dispose(); - }, TaskScheduler.Default); - } + EnsureNotInQueueSlot(taskId); + return _override.RunNow(taskId); } - public async Task ContinueTask(string taskId, string followUpPrompt) + public Task ContinueTask(string taskId, string followUpPrompt) { - using var context = _dbFactory.CreateDbContext(); - var taskRepo = new TaskRepository(context); - var task = await taskRepo.GetByIdAsync(taskId) - ?? throw new KeyNotFoundException($"Task '{taskId}' not found."); - - if (task.Status == Data.Models.TaskStatus.Running) - throw new InvalidOperationException("task is already running"); + EnsureNotInQueueSlot(taskId); + return _override.ContinueTask(taskId, followUpPrompt); + } + private void EnsureNotInQueueSlot(string taskId) + { lock (_lock) { if (_queueSlot?.TaskId == taskId) throw new InvalidOperationException("task is already running in queue slot"); - if (_overrideSlot is not null) - throw new InvalidOperationException("override slot busy"); - - var cts = new CancellationTokenSource(); - _overrideSlot = new QueueSlotState { TaskId = taskId, StartedAt = DateTime.UtcNow, Cts = cts }; - - _ = RunContinueInSlotAsync(taskId, followUpPrompt, cts.Token).ContinueWith(t => - { - if (t.IsFaulted) - _logger.LogError(t.Exception, "RunContinueInSlotAsync failed for task {TaskId}", taskId); - lock (_lock) { _overrideSlot = null; } - cts.Dispose(); - }, TaskScheduler.Default); } - - return taskId; } public bool CancelTask(string taskId) { + if (_override.TryCancel(taskId)) return true; + lock (_lock) { if (_queueSlot is not null && _queueSlot.TaskId == taskId) @@ -125,11 +80,6 @@ public sealed class QueueService : BackgroundService _queueSlot.Cts.Cancel(); return true; } - if (_overrideSlot is not null && _overrideSlot.TaskId == taskId) - { - _overrideSlot.Cts.Cancel(); - return true; - } } return false; } @@ -162,7 +112,7 @@ public sealed class QueueService : BackgroundService var cts = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken); _queueSlot = new QueueSlotState { TaskId = task.Id, StartedAt = DateTime.UtcNow, Cts = cts }; - _ = RunInSlotAsync(task.Id, "queue", cts.Token).ContinueWith(t => + _ = RunInSlotAsync(task.Id, cts.Token).ContinueWith(t => { if (t.IsFaulted) _logger.LogError(t.Exception, "RunInSlotAsync failed for task {TaskId} in queue slot", task.Id); @@ -185,11 +135,11 @@ public sealed class QueueService : BackgroundService _logger.LogInformation("QueueService stopping"); } - private async Task RunInSlotAsync(string taskId, string slot, CancellationToken ct) + private async Task RunInSlotAsync(string taskId, CancellationToken ct) { try { - _logger.LogInformation("Starting task {TaskId} in {Slot} slot", taskId, slot); + _logger.LogInformation("Starting task {TaskId} in queue slot", taskId); TaskEntity task; using (var context = _dbFactory.CreateDbContext()) @@ -199,24 +149,11 @@ public sealed class QueueService : BackgroundService ?? throw new KeyNotFoundException($"Task '{taskId}' not found."); } - await _runner.RunAsync(task, slot, ct); + await _runner.RunAsync(task, "queue", ct); } catch (Exception ex) { _logger.LogError(ex, "Slot runner error for task {TaskId}", taskId); } } - - private async Task RunContinueInSlotAsync(string taskId, string followUpPrompt, CancellationToken ct) - { - try - { - _logger.LogInformation("Continuing task {TaskId} in override slot", taskId); - await _runner.ContinueAsync(taskId, followUpPrompt, "override", ct); - } - catch (Exception ex) - { - _logger.LogError(ex, "Continue runner error for task {TaskId}", taskId); - } - } } diff --git a/src/ClaudeDo.Worker/Queue/QueueSlotState.cs b/src/ClaudeDo.Worker/Queue/QueueSlotState.cs new file mode 100644 index 0000000..f3431c5 --- /dev/null +++ b/src/ClaudeDo.Worker/Queue/QueueSlotState.cs @@ -0,0 +1,8 @@ +namespace ClaudeDo.Worker.Queue; + +public sealed class QueueSlotState +{ + public required string TaskId { get; init; } + public required DateTime StartedAt { get; init; } + public required CancellationTokenSource Cts { get; init; } +} diff --git a/src/ClaudeDo.Worker/Services/WorktreeMaintenanceService.cs b/src/ClaudeDo.Worker/Worktrees/WorktreeMaintenanceService.cs similarity index 99% rename from src/ClaudeDo.Worker/Services/WorktreeMaintenanceService.cs rename to src/ClaudeDo.Worker/Worktrees/WorktreeMaintenanceService.cs index 32e95c6..a6d11ce 100644 --- a/src/ClaudeDo.Worker/Services/WorktreeMaintenanceService.cs +++ b/src/ClaudeDo.Worker/Worktrees/WorktreeMaintenanceService.cs @@ -3,7 +3,7 @@ using ClaudeDo.Data.Git; using ClaudeDo.Data.Models; using Microsoft.EntityFrameworkCore; -namespace ClaudeDo.Worker.Services; +namespace ClaudeDo.Worker.Worktrees; public sealed class WorktreeMaintenanceService { diff --git a/tests/ClaudeDo.Worker.Tests/External/ExternalMcpServiceTests.cs b/tests/ClaudeDo.Worker.Tests/External/ExternalMcpServiceTests.cs index ce5e3f5..d03ffd0 100644 --- a/tests/ClaudeDo.Worker.Tests/External/ExternalMcpServiceTests.cs +++ b/tests/ClaudeDo.Worker.Tests/External/ExternalMcpServiceTests.cs @@ -5,8 +5,8 @@ using ClaudeDo.Data.Repositories; using ClaudeDo.Worker.Config; using ClaudeDo.Worker.External; using ClaudeDo.Worker.Hub; +using ClaudeDo.Worker.Queue; using ClaudeDo.Worker.Runner; -using ClaudeDo.Worker.Services; using ClaudeDo.Worker.Tests.Infrastructure; using ClaudeDo.Worker.Tests.Services; using Microsoft.AspNetCore.SignalR; @@ -117,7 +117,8 @@ public sealed class ExternalMcpServiceTests : IDisposable NullLogger.Instance, TaskStateServiceBuilder.Build(dbFactory).State); var waker = new ClaudeDo.Worker.Queue.QueueWaker(); var picker = new ClaudeDo.Worker.Queue.QueuePicker(dbFactory); - return new QueueService(dbFactory, runner, cfg, NullLogger.Instance, waker, picker); + var overrideSlot = new OverrideSlotService(dbFactory, runner, NullLogger.Instance); + return new QueueService(dbFactory, runner, cfg, NullLogger.Instance, waker, picker, overrideSlot); } [Fact] diff --git a/tests/ClaudeDo.Worker.Tests/Hub/AgentSettingsHubTests.cs b/tests/ClaudeDo.Worker.Tests/Hub/AgentSettingsHubTests.cs index d7e4e20..73f98bd 100644 --- a/tests/ClaudeDo.Worker.Tests/Hub/AgentSettingsHubTests.cs +++ b/tests/ClaudeDo.Worker.Tests/Hub/AgentSettingsHubTests.cs @@ -57,7 +57,7 @@ public sealed class AgentSettingsHubTests : IDisposable Directory.CreateDirectory(targetDir); await File.WriteAllTextAsync(Path.Combine(bundleDir, "code-reviewer.md"), "body"); - var seeder = new ClaudeDo.Worker.Services.DefaultAgentSeeder(bundleDir, targetDir); + var seeder = new ClaudeDo.Worker.Agents.DefaultAgentSeeder(bundleDir, targetDir); var result = await seeder.SeedMissingAsync(); Assert.Equal(1, result.Copied); diff --git a/tests/ClaudeDo.Worker.Tests/Hub/PlanningHubTests.cs b/tests/ClaudeDo.Worker.Tests/Hub/PlanningHubTests.cs index cd9e4db..a8250ee 100644 --- a/tests/ClaudeDo.Worker.Tests/Hub/PlanningHubTests.cs +++ b/tests/ClaudeDo.Worker.Tests/Hub/PlanningHubTests.cs @@ -4,8 +4,11 @@ using ClaudeDo.Data.Models; using ClaudeDo.Data.Repositories; using ClaudeDo.Worker.Config; using ClaudeDo.Worker.Hub; +using ClaudeDo.Worker.Agents; +using ClaudeDo.Worker.Lifecycle; using ClaudeDo.Worker.Planning; -using ClaudeDo.Worker.Services; +using ClaudeDo.Worker.Queue; +using ClaudeDo.Worker.Worktrees; using ClaudeDo.Worker.Tests.Infrastructure; using Microsoft.AspNetCore.SignalR; using Xunit; diff --git a/tests/ClaudeDo.Worker.Tests/Planning/PlanningMergeOrchestratorTests.cs b/tests/ClaudeDo.Worker.Tests/Planning/PlanningMergeOrchestratorTests.cs index 34b0cb3..a1d7b43 100644 --- a/tests/ClaudeDo.Worker.Tests/Planning/PlanningMergeOrchestratorTests.cs +++ b/tests/ClaudeDo.Worker.Tests/Planning/PlanningMergeOrchestratorTests.cs @@ -2,8 +2,8 @@ using ClaudeDo.Data; using ClaudeDo.Data.Git; using ClaudeDo.Data.Models; using ClaudeDo.Worker.Hub; +using ClaudeDo.Worker.Lifecycle; using ClaudeDo.Worker.Planning; -using ClaudeDo.Worker.Services; using ClaudeDo.Worker.Tests.Infrastructure; using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Logging.Abstractions; diff --git a/tests/ClaudeDo.Worker.Tests/Services/AgentFileServiceTests.cs b/tests/ClaudeDo.Worker.Tests/Services/AgentFileServiceTests.cs index d857da2..a9bcb12 100644 --- a/tests/ClaudeDo.Worker.Tests/Services/AgentFileServiceTests.cs +++ b/tests/ClaudeDo.Worker.Tests/Services/AgentFileServiceTests.cs @@ -1,4 +1,4 @@ -using ClaudeDo.Worker.Services; +using ClaudeDo.Worker.Agents; namespace ClaudeDo.Worker.Tests.Services; diff --git a/tests/ClaudeDo.Worker.Tests/Services/DefaultAgentSeederTests.cs b/tests/ClaudeDo.Worker.Tests/Services/DefaultAgentSeederTests.cs index a5b53b6..c4e5aaa 100644 --- a/tests/ClaudeDo.Worker.Tests/Services/DefaultAgentSeederTests.cs +++ b/tests/ClaudeDo.Worker.Tests/Services/DefaultAgentSeederTests.cs @@ -1,4 +1,4 @@ -using ClaudeDo.Worker.Services; +using ClaudeDo.Worker.Agents; namespace ClaudeDo.Worker.Tests.Services; diff --git a/tests/ClaudeDo.Worker.Tests/Services/QueueServiceSlotGuardTests.cs b/tests/ClaudeDo.Worker.Tests/Services/QueueServiceSlotGuardTests.cs index 0b701b7..70a1e20 100644 --- a/tests/ClaudeDo.Worker.Tests/Services/QueueServiceSlotGuardTests.cs +++ b/tests/ClaudeDo.Worker.Tests/Services/QueueServiceSlotGuardTests.cs @@ -5,7 +5,6 @@ using ClaudeDo.Worker.Config; using ClaudeDo.Worker.Hub; using ClaudeDo.Worker.Queue; using ClaudeDo.Worker.Runner; -using ClaudeDo.Worker.Services; using ClaudeDo.Worker.Tests.Infrastructure; using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Logging.Abstractions; @@ -60,7 +59,8 @@ public sealed class QueueServiceSlotGuardTests : IDisposable NullLogger.Instance, TaskStateServiceBuilder.Build(dbFactory).State); _waker = new QueueWaker(); var picker = new QueuePicker(dbFactory); - var service = new QueueService(dbFactory, runner, _cfg, NullLogger.Instance, _waker, picker); + var overrideSlot = new OverrideSlotService(dbFactory, runner, NullLogger.Instance); + var service = new QueueService(dbFactory, runner, _cfg, NullLogger.Instance, _waker, picker, overrideSlot); return (service, fake); } diff --git a/tests/ClaudeDo.Worker.Tests/Services/QueueServiceTests.cs b/tests/ClaudeDo.Worker.Tests/Services/QueueServiceTests.cs index 6f1de6b..b7f1749 100644 --- a/tests/ClaudeDo.Worker.Tests/Services/QueueServiceTests.cs +++ b/tests/ClaudeDo.Worker.Tests/Services/QueueServiceTests.cs @@ -6,7 +6,6 @@ using ClaudeDo.Worker.Config; using ClaudeDo.Worker.Hub; using ClaudeDo.Worker.Queue; using ClaudeDo.Worker.Runner; -using ClaudeDo.Worker.Services; using ClaudeDo.Worker.Tests.Infrastructure; using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Logging.Abstractions; @@ -61,7 +60,8 @@ public sealed class QueueServiceTests : IDisposable NullLogger.Instance, TaskStateServiceBuilder.Build(dbFactory).State); _waker = new QueueWaker(); var picker = new QueuePicker(dbFactory); - var service = new QueueService(dbFactory, runner, _cfg, NullLogger.Instance, _waker, picker); + var overrideSlot = new OverrideSlotService(dbFactory, runner, NullLogger.Instance); + var service = new QueueService(dbFactory, runner, _cfg, NullLogger.Instance, _waker, picker, overrideSlot); return (service, fake); } diff --git a/tests/ClaudeDo.Worker.Tests/Services/StaleTaskRecoveryTests.cs b/tests/ClaudeDo.Worker.Tests/Services/StaleTaskRecoveryTests.cs index 993a513..20eb7d0 100644 --- a/tests/ClaudeDo.Worker.Tests/Services/StaleTaskRecoveryTests.cs +++ b/tests/ClaudeDo.Worker.Tests/Services/StaleTaskRecoveryTests.cs @@ -1,7 +1,7 @@ using ClaudeDo.Data; using ClaudeDo.Data.Models; using ClaudeDo.Data.Repositories; -using ClaudeDo.Worker.Services; +using ClaudeDo.Worker.Lifecycle; using ClaudeDo.Worker.Tests.Infrastructure; using Microsoft.Extensions.Logging.Abstractions; using TaskStatus = ClaudeDo.Data.Models.TaskStatus; diff --git a/tests/ClaudeDo.Worker.Tests/Services/TaskMergeServiceTests.cs b/tests/ClaudeDo.Worker.Tests/Services/TaskMergeServiceTests.cs index 33963d0..5efbea5 100644 --- a/tests/ClaudeDo.Worker.Tests/Services/TaskMergeServiceTests.cs +++ b/tests/ClaudeDo.Worker.Tests/Services/TaskMergeServiceTests.cs @@ -3,7 +3,7 @@ using ClaudeDo.Data.Models; using ClaudeDo.Data.Repositories; using ClaudeDo.Worker.Hub; using ClaudeDo.Worker.Runner; -using ClaudeDo.Worker.Services; +using ClaudeDo.Worker.Lifecycle; using ClaudeDo.Worker.Tests.Infrastructure; using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Logging.Abstractions; diff --git a/tests/ClaudeDo.Worker.Tests/Services/TaskResetServiceTests.cs b/tests/ClaudeDo.Worker.Tests/Services/TaskResetServiceTests.cs index 5ef3054..d2d424e 100644 --- a/tests/ClaudeDo.Worker.Tests/Services/TaskResetServiceTests.cs +++ b/tests/ClaudeDo.Worker.Tests/Services/TaskResetServiceTests.cs @@ -3,7 +3,7 @@ using ClaudeDo.Data.Repositories; using ClaudeDo.Worker.Config; using ClaudeDo.Worker.Hub; using ClaudeDo.Worker.Runner; -using ClaudeDo.Worker.Services; +using ClaudeDo.Worker.Lifecycle; using ClaudeDo.Worker.Tests.Infrastructure; using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Logging.Abstractions; diff --git a/tests/ClaudeDo.Worker.Tests/Services/WorktreeMaintenanceServiceTests.cs b/tests/ClaudeDo.Worker.Tests/Services/WorktreeMaintenanceServiceTests.cs index 3caddaa..1bc71e5 100644 --- a/tests/ClaudeDo.Worker.Tests/Services/WorktreeMaintenanceServiceTests.cs +++ b/tests/ClaudeDo.Worker.Tests/Services/WorktreeMaintenanceServiceTests.cs @@ -1,7 +1,7 @@ using ClaudeDo.Data.Git; using ClaudeDo.Data.Models; using ClaudeDo.Data.Repositories; -using ClaudeDo.Worker.Services; +using ClaudeDo.Worker.Worktrees; using ClaudeDo.Worker.Tests.Infrastructure; using Microsoft.Extensions.Logging.Abstractions;