From 469e68bbc8533458fe04d95a4244d74b6fb96637 Mon Sep 17 00:00:00 2001 From: mika kuns Date: Thu, 4 Jun 2026 16:53:42 +0200 Subject: [PATCH] feat(merge): fold parent branch into combined-diff for improvement parents --- .../Planning/PlanningAggregator.cs | 42 ++++++++++++------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/src/ClaudeDo.Worker/Planning/PlanningAggregator.cs b/src/ClaudeDo.Worker/Planning/PlanningAggregator.cs index 314b4e9..44f9f50 100644 --- a/src/ClaudeDo.Worker/Planning/PlanningAggregator.cs +++ b/src/ClaudeDo.Worker/Planning/PlanningAggregator.cs @@ -43,17 +43,24 @@ public sealed class PlanningAggregator string planningTaskId, CancellationToken ct) { using var ctx = _dbFactory.CreateDbContext(); - var children = await ctx.Tasks + var parent = await ctx.Tasks .Include(t => t.Worktree) - .Where(t => t.ParentTaskId == planningTaskId) - .OrderBy(t => t.SortOrder) - .ToListAsync(ct); + .Include(t => t.Children).ThenInclude(c => c.Worktree) + .SingleOrDefaultAsync(t => t.Id == planningTaskId, ct); + if (parent is null) return new List(); + + var nodes = new List(); + // An improvement parent carries its own code branch — fold it in first so the + // combined diff matches what the tree-merge will produce. + if (parent.PlanningPhase == PlanningPhase.None && parent.Worktree is { State: WorktreeState.Active }) + nodes.Add(parent); + nodes.AddRange(parent.Children.OrderBy(c => c.SortOrder)); var result = new List(); - foreach (var child in children) + foreach (var node in nodes) { - if (child.Worktree is null) continue; - var wt = child.Worktree; + if (node.Worktree is null) continue; + var wt = node.Worktree; var head = wt.HeadCommit ?? await _git.RevParseHeadAsync(wt.Path, ct); string unified; try @@ -62,11 +69,11 @@ public sealed class PlanningAggregator } catch (Exception ex) { - _logger.LogWarning(ex, "diff failed for subtask {Id}", child.Id); + _logger.LogWarning(ex, "diff failed for node {Id}", node.Id); unified = ""; } result.Add(new SubtaskDiff( - child.Id, child.Title, wt.BranchName, wt.BaseCommit, head, wt.DiffStat, unified)); + node.Id, node.Title, wt.BranchName, wt.BaseCommit, head, wt.DiffStat, unified)); } return result; } @@ -87,12 +94,18 @@ public sealed class PlanningAggregator await GitRawAsync(repoDir, ct, "checkout", "-b", integrationBranch); - foreach (var child in childSubtasks) + var nodes = new List(); + // Fold the improvement parent's own branch in first (planning parents have none). + if (planning.PlanningPhase == PlanningPhase.None && planning.Worktree is { State: WorktreeState.Active }) + nodes.Add(planning); + nodes.AddRange(childSubtasks); + + foreach (var node in nodes) { - if (child.Worktree is null) continue; + if (node.Worktree is null) continue; var (code, _) = await _git.MergeNoFfAsync( - repoDir, child.Worktree.BranchName, - $"Integrate subtask: {child.Title}", ct); + repoDir, node.Worktree.BranchName, + $"Integrate: {node.Title}", ct); if (code != 0) { List files; @@ -104,7 +117,7 @@ public sealed class PlanningAggregator try { await _git.BranchDeleteAsync(repoDir, integrationBranch, force: true, ct); } catch { } return new CombinedDiffResult.Failed( - new CombinedDiffFailure(child.Id, files)); + new CombinedDiffFailure(node.Id, files)); } } @@ -135,6 +148,7 @@ public sealed class PlanningAggregator using var ctx = _dbFactory.CreateDbContext(); var planning = await ctx.Tasks .Include(t => t.List) + .Include(t => t.Worktree) .Include(t => t.Children).ThenInclude(c => c.Worktree) .SingleOrDefaultAsync(t => t.Id == planningTaskId, ct) ?? throw new KeyNotFoundException($"Planning task '{planningTaskId}' not found.");