fix(worker): block approve of a planning parent with an unfinished child, surface git stderr on ff failure

This commit is contained in:
mika kuns
2026-10-01 11:36:45 +02:00
parent a353897345
commit acd188bca9
5 changed files with 40 additions and 3 deletions
+3
View File
@@ -105,6 +105,9 @@ review. The child row goes `Merged` with `MergeCommit` null (so `revert_merge` o
whole unit), its worktree and branch are removed, the parent row's `HeadCommit`/`DiffStat` move
along. A child that can't fast-forward (e.g. started in parallel via Run now) is `Failed` — no
merge fallback; in the approve path it stays `WaitingForReview` with a `blocked` result.
A planning parent is only approvable once **every** child is `Done` — `ApproveAndMergeAsync`
blocks with `subtask X is not Done (status …)`, the orchestrator's rule, so a failed/cancelled child
never lands half a plan on the target (improvement parents have no such gate, as before).
`UnitBranchService` never writes task status (it would close a DI cycle through
`PlanningChainCoordinator`). ⚠️ `QueuePicker` counts `worktrees.state='merged'` as a satisfied
dependency, so an absorbed child satisfies a `dependsOn` edge although nothing reached `main` yet.
@@ -1013,6 +1013,20 @@ public sealed class TaskMergeService
if (task.Status != TaskStatus.WaitingForReview)
return Blocked("task is not waiting for review");
// A plan lands whole or not at all -- same rule (and message) as PlanningMergeOrchestrator.StartAsync,
// which a unit-branch parent no longer passes through.
if (task.PlanningPhase != PlanningPhase.None)
{
using var ctx = _dbFactory.CreateDbContext();
var notDone = await ctx.Tasks.AsNoTracking()
.Where(c => c.ParentTaskId == taskId && c.Status != TaskStatus.Done)
.OrderBy(c => c.SortOrder)
.Select(c => new { c.Id, c.Status })
.FirstOrDefaultAsync(ct);
if (notDone is not null)
return Blocked($"subtask {notDone.Id} is not Done (status {notDone.Status})");
}
// 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)
@@ -88,8 +88,9 @@ public sealed class UnitBranchService
var (code, stderr) = await _git.MergeFastForwardOnlyAsync(parentWt.Path, childWt.BranchName, ct);
if (code != 0)
return new AbsorbResult(true,
$"Unit branch '{parentWt.BranchName}' cannot be fast-forwarded to '{childWt.BranchName}' — the child " +
$"did not start from the unit's current head (e.g. it was run in parallel via Run now). git: {stderr}");
$"Unit branch '{parentWt.BranchName}' cannot be fast-forwarded to '{childWt.BranchName}': {stderr.Trim()} " +
"(usually the child did not start from the unit's current head, e.g. run in parallel via Run now, " +
"or the unit worktree has local changes)");
var head = await _git.RevParseHeadAsync(parentWt.Path, ct);
var diffStat = await _git.DiffStatAsync(parentWt.Path, parentWt.BaseCommit, head, ct: ct);