using ClaudeDo.Data; using ClaudeDo.Data.Models; using Microsoft.EntityFrameworkCore; using TaskStatus = ClaudeDo.Data.Models.TaskStatus; namespace ClaudeDo.Worker.Planning; public sealed class PlanningChainCoordinator { private readonly IDbContextFactory _dbFactory; public PlanningChainCoordinator(IDbContextFactory dbFactory) => _dbFactory = dbFactory; public async Task QueueSubtasksSequentiallyAsync(string parentTaskId, CancellationToken ct = default) { await using var ctx = await _dbFactory.CreateDbContextAsync(ct); var parent = await ctx.Tasks.FirstOrDefaultAsync(t => t.Id == parentTaskId, ct) ?? throw new InvalidOperationException($"Task {parentTaskId} not found."); var children = await ctx.Tasks .Where(t => t.ParentTaskId == parentTaskId) .OrderBy(t => t.SortOrder).ThenBy(t => t.CreatedAt) .ToListAsync(ct); if (children.Count == 0) throw new InvalidOperationException("Parent has no subtasks."); var bad = children.FirstOrDefault(c => c.Status != TaskStatus.Manual && c.Status != TaskStatus.Planned); if (bad is not null) throw new InvalidOperationException( $"Child {bad.Id} is in status {bad.Status}; expected Manual or Planned."); for (int i = 0; i < children.Count; i++) children[i].Status = i == 0 ? TaskStatus.Queued : TaskStatus.Waiting; await ctx.SaveChangesAsync(ct); } public async Task OnChildFinishedAsync( string childTaskId, TaskStatus finalStatus, CancellationToken ct = default) { if (finalStatus != TaskStatus.Done) return null; await using var ctx = await _dbFactory.CreateDbContextAsync(ct); var child = await ctx.Tasks .AsNoTracking() .FirstOrDefaultAsync(t => t.Id == childTaskId, ct); if (child?.ParentTaskId is null) return null; var next = await ctx.Tasks .Where(t => t.ParentTaskId == child.ParentTaskId && t.SortOrder > child.SortOrder && t.Status == TaskStatus.Waiting) .OrderBy(t => t.SortOrder) .FirstOrDefaultAsync(ct); if (next is null) return null; next.Status = TaskStatus.Queued; await ctx.SaveChangesAsync(ct); return next.Id; } }