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:
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