feat(worker): fast-forward finished children into the parent's unit branch

Planning parents get their own worktree on Queue plan, children fork from the
unit head and are absorbed via merge --ff-only before Done (ff failure -> Failed,
interactive child approve -> absorb instead of merge to the target).
This commit is contained in:
mika kuns
2026-10-01 11:25:47 +02:00
parent 724ad90f88
commit 9da522b0c9
8 changed files with 489 additions and 17 deletions
@@ -117,6 +117,7 @@ public sealed class TaskMergeService
private readonly ITaskStateService _state;
private readonly IVerifyCommandRunner _verify;
private readonly ILogger<TaskMergeService> _logger;
private readonly UnitBranchService? _unitBranch;
public TaskMergeService(
IDbContextFactory<ClaudeDoDbContext> dbFactory,
@@ -124,7 +125,8 @@ public sealed class TaskMergeService
HubBroadcaster broadcaster,
ITaskStateService state,
IVerifyCommandRunner verify,
ILogger<TaskMergeService> logger)
ILogger<TaskMergeService> logger,
UnitBranchService? unitBranch = null)
{
_dbFactory = dbFactory;
_git = git;
@@ -132,6 +134,7 @@ public sealed class TaskMergeService
_state = state;
_verify = verify;
_logger = logger;
_unitBranch = unitBranch;
}
private async Task<(TaskEntity Task, ListEntity List, WorktreeEntity? Worktree, string? VerifyCommand)> LoadMergeContextAsync(
@@ -1010,6 +1013,21 @@ public sealed class TaskMergeService
if (task.Status != TaskStatus.WaitingForReview)
return Blocked("task is not waiting for review");
// An interactive child of a unit lands in the unit branch, not on the target; the parent's
// own approve merges the unit. A failed fast-forward keeps the child in review.
if (task.ParentTaskId is not null && _unitBranch is not null)
{
var absorb = await _unitBranch.AbsorbChildAsync(taskId, ct);
if (absorb.Failed) return Blocked(absorb.Error!);
if (absorb.Applicable)
{
var approved = await _state.ApproveReviewAsync(taskId, ct);
return approved.Ok
? new MergeResult(StatusMerged, Array.Empty<string>(), null)
: Blocked(approved.Reason ?? "approve failed");
}
}
if (wt is not null && wt.State == WorktreeState.Kept)
{
// Kept means either RevertMergeAsync undid a previous merge, or the user manually