feat(worker): approve a unit parent like a single task, orchestrator only for legacy units

This commit is contained in:
mika kuns
2026-10-01 11:25:48 +02:00
parent 9da522b0c9
commit 1f373a5a67
3 changed files with 26 additions and 11 deletions
+18 -7
View File
@@ -12,6 +12,7 @@ using ClaudeDo.Worker.Hub;
using ClaudeDo.Worker.Lifecycle;
using ClaudeDo.Worker.Planning;
using ClaudeDo.Worker.Queue;
using ClaudeDo.Worker.Runner;
using ClaudeDo.Worker.State;
using ClaudeDo.Worker.Worktrees;
using Microsoft.EntityFrameworkCore;
@@ -678,16 +679,23 @@ public sealed class ExternalMcpService
{
// Approve is the single review+merge action — mirror the hub's ApproveReview
// routing instead of only flipping the status (which left branches unmerged).
bool hasChildren;
bool hasChildren, legacyUnit;
await using (var ctx = await _dbFactory.CreateDbContextAsync(cancellationToken))
hasChildren = await ctx.Tasks.AnyAsync(t => t.ParentTaskId == taskId, cancellationToken);
if (hasChildren)
{
// Compute before the merge starts -- children are still Done with their own
// pre-merge worktree/commit range at this point, so "did it contribute anything"
// reflects the review range the reviewer is about to approve.
hasChildren = await ctx.Tasks.AnyAsync(t => t.ParentTaskId == taskId, cancellationToken);
legacyUnit = hasChildren && await UnitBranchService.HasUnabsorbedChildrenAsync(ctx, taskId, cancellationToken);
}
// Compute before the merge starts -- children are still Done with their own
// pre-merge worktree/commit range at this point, so "did it contribute anything"
// reflects the review range the reviewer is about to approve.
if (hasChildren)
emptyChildren = await GetEmptyDoneChildrenAsync(taskId, cancellationToken);
// Absorbed children are already in the parent's unit branch: the parent merges like a
// single task. Only a legacy unit (a Done child still Active) takes the unit merge.
if (legacyUnit)
{
// externallyDriven: true — this call came from an MCP session, not the UI's
// Approve button. A unit-merge conflict must not auto-open the in-app resolver;
// the driving session resolves it via continue_merge/abort_merge instead.
@@ -796,6 +804,9 @@ public sealed class ExternalMcpService
{
if (child.Worktree is not null)
{
// Absorbed into the unit branch (no merge commit): empty iff it never committed.
if (child.Worktree is { State: WorktreeState.Merged, MergeCommit: null })
return child.Worktree.HeadCommit is null;
if (!Directory.Exists(child.Worktree.Path)) return false;
var files = ParseDiffStatFileNames(
await _git.DiffStatAsync(child.Worktree.Path, child.Worktree.BaseCommit, "HEAD", ct: ct));