From 3008c36921ab80a9f8f3a0abd2e6252b96cf0528 Mon Sep 17 00:00:00 2001 From: mika kuns Date: Fri, 24 Apr 2026 18:28:38 +0200 Subject: [PATCH] feat(worker): register planning services and add Merge-all hub methods Co-Authored-By: Claude Sonnet 4.6 --- src/ClaudeDo.Worker/Hub/WorkerHub.cs | 58 ++++++++++++++++++- .../Planning/PlanningWireDtos.cs | 17 ++++++ src/ClaudeDo.Worker/Program.cs | 2 + .../Hub/PlanningHubTests.cs | 2 +- 4 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 src/ClaudeDo.Worker/Planning/PlanningWireDtos.cs diff --git a/src/ClaudeDo.Worker/Hub/WorkerHub.cs b/src/ClaudeDo.Worker/Hub/WorkerHub.cs index 0371cee..f38e8e0 100644 --- a/src/ClaudeDo.Worker/Hub/WorkerHub.cs +++ b/src/ClaudeDo.Worker/Hub/WorkerHub.cs @@ -46,6 +46,8 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub private readonly TaskMergeService _mergeService; private readonly PlanningSessionManager _planning; private readonly IPlanningTerminalLauncher _launcher; + private readonly PlanningAggregator _planningAggregator; + private readonly PlanningMergeOrchestrator _planningMergeOrchestrator; public WorkerHub( QueueService queue, @@ -57,7 +59,9 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub TaskResetService resetService, TaskMergeService mergeService, PlanningSessionManager planning, - IPlanningTerminalLauncher launcher) + IPlanningTerminalLauncher launcher, + PlanningAggregator planningAggregator, + PlanningMergeOrchestrator planningMergeOrchestrator) { _queue = queue; _agentService = agentService; @@ -69,6 +73,8 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub _mergeService = mergeService; _planning = planning; _launcher = launcher; + _planningAggregator = planningAggregator; + _planningMergeOrchestrator = planningMergeOrchestrator; } public string Ping() => $"pong v{Version}"; @@ -330,5 +336,55 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub public Task GetPendingDraftCountAsync(string taskId) => _planning.GetPendingDraftCountAsync(taskId, Context.ConnectionAborted); + public async Task> GetPlanningAggregate(string planningTaskId) + { + try + { + var diffs = await _planningAggregator.GetAggregatedDiffAsync(planningTaskId, CancellationToken.None); + return diffs.Select(d => new SubtaskDiffDto( + d.SubtaskId, d.Title, d.BranchName, d.BaseCommit, d.HeadCommit, d.DiffStat, d.UnifiedDiff)).ToList(); + } + catch (KeyNotFoundException) { throw new HubException("planning task not found"); } + catch (InvalidOperationException ex) { throw new HubException(ex.Message); } + } + + public async Task BuildPlanningIntegrationBranch(string planningTaskId, string targetBranch) + { + try + { + var result = await _planningAggregator.BuildIntegrationBranchAsync( + planningTaskId, targetBranch ?? "", CancellationToken.None); + return result switch + { + CombinedDiffResult.Ok ok => new CombinedDiffResultDto( + true, ok.Value.IntegrationBranch, ok.Value.UnifiedDiff, null, null), + CombinedDiffResult.Failed f => new CombinedDiffResultDto( + false, null, null, f.Value.FirstConflictSubtaskId, f.Value.ConflictedFiles), + _ => throw new InvalidOperationException("unknown result type"), + }; + } + catch (KeyNotFoundException) { throw new HubException("planning task not found"); } + catch (InvalidOperationException ex) { throw new HubException(ex.Message); } + } + + public async Task MergeAllPlanning(string planningTaskId, string targetBranch) + { + try { await _planningMergeOrchestrator.StartAsync(planningTaskId, targetBranch ?? "", CancellationToken.None); } + catch (KeyNotFoundException) { throw new HubException("planning task not found"); } + catch (InvalidOperationException ex) { throw new HubException(ex.Message); } + } + + public async Task ContinuePlanningMerge(string planningTaskId) + { + try { await _planningMergeOrchestrator.ContinueAsync(planningTaskId, CancellationToken.None); } + catch (InvalidOperationException ex) { throw new HubException(ex.Message); } + } + + public async Task AbortPlanningMerge(string planningTaskId) + { + try { await _planningMergeOrchestrator.AbortAsync(planningTaskId, CancellationToken.None); } + catch (InvalidOperationException ex) { throw new HubException(ex.Message); } + } + private static string? Nullify(string? s) => string.IsNullOrWhiteSpace(s) ? null : s; } diff --git a/src/ClaudeDo.Worker/Planning/PlanningWireDtos.cs b/src/ClaudeDo.Worker/Planning/PlanningWireDtos.cs new file mode 100644 index 0000000..86f0f07 --- /dev/null +++ b/src/ClaudeDo.Worker/Planning/PlanningWireDtos.cs @@ -0,0 +1,17 @@ +namespace ClaudeDo.Worker.Planning; + +public sealed record SubtaskDiffDto( + string SubtaskId, + string Title, + string BranchName, + string BaseCommit, + string HeadCommit, + string? DiffStat, + string UnifiedDiff); + +public sealed record CombinedDiffResultDto( + bool Success, + string? IntegrationBranch, + string? UnifiedDiff, + string? FirstConflictSubtaskId, + IReadOnlyList? ConflictedFiles); diff --git a/src/ClaudeDo.Worker/Program.cs b/src/ClaudeDo.Worker/Program.cs index 433742d..7b73972 100644 --- a/src/ClaudeDo.Worker/Program.cs +++ b/src/ClaudeDo.Worker/Program.cs @@ -36,6 +36,8 @@ builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); // Agent file management. var agentsDir = Path.Combine(ClaudeDo.Data.Paths.AppDataRoot(), "agents"); diff --git a/tests/ClaudeDo.Worker.Tests/Hub/PlanningHubTests.cs b/tests/ClaudeDo.Worker.Tests/Hub/PlanningHubTests.cs index c58e8f4..b35186b 100644 --- a/tests/ClaudeDo.Worker.Tests/Hub/PlanningHubTests.cs +++ b/tests/ClaudeDo.Worker.Tests/Hub/PlanningHubTests.cs @@ -50,7 +50,7 @@ public sealed class PlanningHubTests : IDisposable { var hub = new WorkerHub( null!, null!, null!, null!, null!, null!, null!, null!, - _planning, _launcher); + _planning, _launcher, null!, null!); hub.Clients = new FakeHubCallerClients(_proxy); hub.Context = new FakeHubCallerContext(); return hub;