feat(worker): add PlanningMergeOrchestrator happy path with merge event broadcasts
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -32,4 +32,19 @@ public sealed class HubBroadcaster
|
||||
|
||||
public Task WorkerLog(string message, WorkerLogLevel level, DateTime timestampUtc) =>
|
||||
_hub.Clients.All.SendAsync("WorkerLog", message, level, timestampUtc);
|
||||
|
||||
public Task PlanningMergeStarted(string planningTaskId, string targetBranch) =>
|
||||
_hub.Clients.All.SendAsync("PlanningMergeStarted", planningTaskId, targetBranch);
|
||||
|
||||
public Task PlanningSubtaskMerged(string planningTaskId, string subtaskId) =>
|
||||
_hub.Clients.All.SendAsync("PlanningSubtaskMerged", planningTaskId, subtaskId);
|
||||
|
||||
public Task PlanningMergeConflict(string planningTaskId, string subtaskId, IReadOnlyList<string> files) =>
|
||||
_hub.Clients.All.SendAsync("PlanningMergeConflict", planningTaskId, subtaskId, files);
|
||||
|
||||
public Task PlanningMergeAborted(string planningTaskId) =>
|
||||
_hub.Clients.All.SendAsync("PlanningMergeAborted", planningTaskId);
|
||||
|
||||
public Task PlanningCompleted(string planningTaskId) =>
|
||||
_hub.Clients.All.SendAsync("PlanningCompleted", planningTaskId);
|
||||
}
|
||||
|
||||
8
src/ClaudeDo.Worker/Planning/PlanningMergeEvents.cs
Normal file
8
src/ClaudeDo.Worker/Planning/PlanningMergeEvents.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace ClaudeDo.Worker.Planning;
|
||||
|
||||
public sealed record PlanningMergeStarted(string PlanningTaskId, string TargetBranch);
|
||||
public sealed record PlanningSubtaskMerged(string PlanningTaskId, string SubtaskId);
|
||||
public sealed record PlanningMergeConflict(
|
||||
string PlanningTaskId, string SubtaskId, IReadOnlyList<string> ConflictedFiles);
|
||||
public sealed record PlanningMergeAborted(string PlanningTaskId);
|
||||
public sealed record PlanningCompleted(string PlanningTaskId);
|
||||
119
src/ClaudeDo.Worker/Planning/PlanningMergeOrchestrator.cs
Normal file
119
src/ClaudeDo.Worker/Planning/PlanningMergeOrchestrator.cs
Normal file
@@ -0,0 +1,119 @@
|
||||
using System.Collections.Concurrent;
|
||||
using ClaudeDo.Data;
|
||||
using ClaudeDo.Data.Models;
|
||||
using ClaudeDo.Worker.Hub;
|
||||
using ClaudeDo.Worker.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
|
||||
|
||||
namespace ClaudeDo.Worker.Planning;
|
||||
|
||||
public sealed class PlanningMergeOrchestrator
|
||||
{
|
||||
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
|
||||
private readonly TaskMergeService _merge;
|
||||
private readonly PlanningAggregator _aggregator;
|
||||
private readonly HubBroadcaster _broadcaster;
|
||||
private readonly ILogger<PlanningMergeOrchestrator> _logger;
|
||||
|
||||
private sealed class State
|
||||
{
|
||||
public required string TargetBranch { get; init; }
|
||||
public required Queue<string> RemainingSubtaskIds { get; init; }
|
||||
public string? CurrentSubtaskId { get; set; }
|
||||
}
|
||||
|
||||
private readonly ConcurrentDictionary<string, State> _states = new();
|
||||
|
||||
public PlanningMergeOrchestrator(
|
||||
IDbContextFactory<ClaudeDoDbContext> dbFactory,
|
||||
TaskMergeService merge,
|
||||
PlanningAggregator aggregator,
|
||||
HubBroadcaster broadcaster,
|
||||
ILogger<PlanningMergeOrchestrator> logger)
|
||||
{
|
||||
_dbFactory = dbFactory;
|
||||
_merge = merge;
|
||||
_aggregator = aggregator;
|
||||
_broadcaster = broadcaster;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task StartAsync(string planningTaskId, string targetBranch, CancellationToken ct)
|
||||
{
|
||||
List<TaskEntity> children;
|
||||
using (var ctx = _dbFactory.CreateDbContext())
|
||||
{
|
||||
children = await ctx.Tasks
|
||||
.Include(t => t.Worktree)
|
||||
.Where(t => t.ParentTaskId == planningTaskId)
|
||||
.OrderBy(t => t.SortOrder)
|
||||
.ToListAsync(ct);
|
||||
}
|
||||
|
||||
var queue = new Queue<string>(
|
||||
children
|
||||
.Where(c => c.Worktree is not null && c.Worktree.State != WorktreeState.Merged)
|
||||
.Select(c => c.Id));
|
||||
|
||||
_states[planningTaskId] = new State
|
||||
{
|
||||
TargetBranch = targetBranch,
|
||||
RemainingSubtaskIds = queue,
|
||||
};
|
||||
|
||||
await _broadcaster.PlanningMergeStarted(planningTaskId, targetBranch);
|
||||
await DrainAsync(planningTaskId, ct);
|
||||
}
|
||||
|
||||
private async Task DrainAsync(string planningTaskId, CancellationToken ct)
|
||||
{
|
||||
if (!_states.TryGetValue(planningTaskId, out var state)) return;
|
||||
|
||||
while (state.RemainingSubtaskIds.TryDequeue(out var subtaskId))
|
||||
{
|
||||
state.CurrentSubtaskId = subtaskId;
|
||||
var result = await _merge.MergeAsync(
|
||||
subtaskId,
|
||||
state.TargetBranch,
|
||||
removeWorktree: true,
|
||||
commitMessage: "Merge subtask",
|
||||
leaveConflictsInTree: true,
|
||||
ct);
|
||||
|
||||
if (result.Status == TaskMergeService.StatusConflict)
|
||||
{
|
||||
await _broadcaster.PlanningMergeConflict(planningTaskId, subtaskId, result.ConflictFiles);
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.Status != TaskMergeService.StatusMerged)
|
||||
{
|
||||
await _broadcaster.PlanningMergeConflict(
|
||||
planningTaskId, subtaskId,
|
||||
new[] { result.ErrorMessage ?? "merge blocked" });
|
||||
return;
|
||||
}
|
||||
|
||||
await _broadcaster.PlanningSubtaskMerged(planningTaskId, subtaskId);
|
||||
}
|
||||
|
||||
state.CurrentSubtaskId = null;
|
||||
await FinalizePlanningDoneAsync(planningTaskId, ct);
|
||||
_states.TryRemove(planningTaskId, out _);
|
||||
await _broadcaster.PlanningCompleted(planningTaskId);
|
||||
}
|
||||
|
||||
private async Task FinalizePlanningDoneAsync(string planningTaskId, CancellationToken ct)
|
||||
{
|
||||
using var ctx = _dbFactory.CreateDbContext();
|
||||
var planning = await ctx.Tasks.SingleOrDefaultAsync(t => t.Id == planningTaskId, ct);
|
||||
if (planning is null) return;
|
||||
planning.Status = TaskStatus.Done;
|
||||
planning.FinishedAt = DateTime.UtcNow;
|
||||
await ctx.SaveChangesAsync(ct);
|
||||
|
||||
try { await _aggregator.CleanupIntegrationBranchAsync(planningTaskId, ct); }
|
||||
catch (Exception ex) { _logger.LogWarning(ex, "integration branch cleanup failed"); }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user