fix: use merge-base for diffStat in merger and show files changed on done task cards

The merger was using `git diff HEAD..branch --stat` which includes
artifacts from other tasks when branches fork from older main commits.
Switch to `git diff $(merge-base)..branch --stat` so commit messages
only describe the branch's own changes.

Also surface the "files changed" button on done task cards using
mergeDetails, opening the same ChangedFilesModal with commit-backed
diffs (matching the Changes tab in the task modal).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-04-04 08:33:53 -07:00
parent 8ec7983c3c
commit e9fddc44cb
9 changed files with 74 additions and 17 deletions

View File

@@ -94,6 +94,7 @@ function setupHappyPathExecSync() {
const cmdStr = String(cmd);
if (cmdStr.includes("rev-parse --verify")) return Buffer.from("abc123");
if (cmdStr.includes("git log")) return "- feat: something" as any;
if (cmdStr.includes("merge-base")) return Buffer.from("abc123");
if (cmdStr.includes("git diff") && cmdStr.includes("--stat")) return "1 file changed" as any;
if (cmdStr.includes("merge --squash")) return Buffer.from("");
// Post-squash check: --quiet means "did squash stage anything?" → "1" = yes
@@ -280,6 +281,7 @@ describe("aiMergeTask — empty squash merge (branch already merged via dep)", (
const cmdStr = String(cmd);
if (cmdStr.includes("rev-parse --verify")) return Buffer.from("abc123");
if (cmdStr.includes("git log")) return "- feat: something" as any;
if (cmdStr.includes("merge-base")) return Buffer.from("abc123");
if (cmdStr.includes("git diff") && cmdStr.includes("--stat")) return "1 file changed" as any;
if (cmdStr.includes("merge --squash")) return Buffer.from("");
// Squash staged nothing → "0"
@@ -304,6 +306,7 @@ describe("aiMergeTask — empty squash merge (branch already merged via dep)", (
const cmdStr = String(cmd);
if (cmdStr.includes("rev-parse --verify")) return Buffer.from("abc123");
if (cmdStr.includes("git log")) return "- feat: something" as any;
if (cmdStr.includes("merge-base")) return Buffer.from("abc123");
if (cmdStr.includes("git diff") && cmdStr.includes("--stat")) return "1 file changed" as any;
if (cmdStr.includes("merge --squash")) return Buffer.from("");
if (cmdStr.includes("diff --cached --quiet")) return "0" as any;
@@ -381,6 +384,7 @@ describe("aiMergeTask — includeTaskIdInCommit setting", () => {
const cmdStr = String(cmd);
if (cmdStr.includes("rev-parse --verify")) return Buffer.from("abc123");
if (cmdStr.includes("git log")) return "- feat: something" as any;
if (cmdStr.includes("merge-base")) return Buffer.from("abc123");
if (cmdStr.includes("git diff") && cmdStr.includes("--stat")) return "1 file changed" as any;
if (cmdStr.includes("merge --squash")) return Buffer.from("");
if (cmdStr.includes("diff --cached")) return "1" as any;
@@ -409,6 +413,7 @@ describe("aiMergeTask — includeTaskIdInCommit setting", () => {
const cmdStr = String(cmd);
if (cmdStr.includes("rev-parse --verify")) return Buffer.from("abc123");
if (cmdStr.includes("git log")) return "- feat: something" as any;
if (cmdStr.includes("merge-base")) return Buffer.from("abc123");
if (cmdStr.includes("git diff") && cmdStr.includes("--stat")) return "1 file changed" as any;
if (cmdStr.includes("merge --squash")) return Buffer.from("");
if (cmdStr.includes("diff --cached")) return "1" as any;
@@ -1102,6 +1107,7 @@ describe("aiMergeTask — retry logic with escalating strategies", () => {
const cmdStr = String(cmd);
if (cmdStr.includes("rev-parse --verify")) return Buffer.from("abc123");
if (cmdStr.includes("git log")) return "- feat: something" as any;
if (cmdStr.includes("merge-base")) return Buffer.from("abc123");
if (cmdStr.includes("git diff") && cmdStr.includes("--stat")) return "1 file changed" as any;
if (cmdStr.includes("merge --squash") || cmdStr.includes("merge -X")) return Buffer.from("");
// Post-squash check: "1" = has staged changes
@@ -1133,6 +1139,7 @@ describe("aiMergeTask — retry logic with escalating strategies", () => {
const cmdStr = String(cmd);
if (cmdStr.includes("rev-parse --verify")) return Buffer.from("abc123");
if (cmdStr.includes("git log")) return "- feat: something";
if (cmdStr.includes("merge-base")) return Buffer.from("abc123");
if (cmdStr.includes("--stat")) return "1 file changed";
if (cmdStr.includes("merge --squash")) return Buffer.from("");
// No conflicts
@@ -1169,6 +1176,7 @@ describe("aiMergeTask — retry logic with escalating strategies", () => {
const cmdStr = String(cmd);
if (cmdStr.includes("rev-parse")) return Buffer.from("abc123");
if (cmdStr.includes("git log")) return "- feat: something";
if (cmdStr.includes("merge-base")) return Buffer.from("abc123");
if (cmdStr.includes("--stat")) return "1 file changed";
if (cmdStr.includes("merge --squash")) {
@@ -1218,6 +1226,7 @@ describe("aiMergeTask — retry logic with escalating strategies", () => {
const cmdStr = String(cmd);
if (cmdStr.includes("rev-parse")) return Buffer.from("abc123");
if (cmdStr.includes("git log")) return "- feat: something";
if (cmdStr.includes("merge-base")) return Buffer.from("abc123");
if (cmdStr.includes("--stat")) return "1 file changed";
if (cmdStr.includes("merge --squash")) {
@@ -1268,6 +1277,7 @@ describe("aiMergeTask — retry logic with escalating strategies", () => {
const cmdStr = String(cmd);
if (cmdStr.includes("rev-parse")) return Buffer.from("abc123");
if (cmdStr.includes("git log")) return "- feat: something";
if (cmdStr.includes("merge-base")) return Buffer.from("abc123");
if (cmdStr.includes("--stat")) return "1 file changed";
// First two regular squash merges fail with conflicts
@@ -1339,6 +1349,7 @@ describe("aiMergeTask — retry logic with escalating strategies", () => {
const cmdStr = String(cmd);
if (cmdStr.includes("rev-parse")) return Buffer.from("abc123");
if (cmdStr.includes("git log")) return "- feat: something";
if (cmdStr.includes("merge-base")) return Buffer.from("abc123");
if (cmdStr.includes("--stat")) return "1 file changed";
if (cmdStr.includes("merge --squash") || cmdStr.includes("merge -X theirs")) {
@@ -1385,6 +1396,7 @@ describe("aiMergeTask — retry logic with escalating strategies", () => {
const cmdStr = String(cmd);
if (cmdStr.includes("rev-parse")) return Buffer.from("abc123");
if (cmdStr.includes("git log")) return "- feat: something";
if (cmdStr.includes("merge-base")) return Buffer.from("abc123");
if (cmdStr.includes("--stat")) return "1 file changed";
if (cmdStr.includes("merge --squash")) return Buffer.from("");
if (cmdStr.includes("diff --name-only --diff-filter=U")) return ""; // No conflicts
@@ -1693,6 +1705,7 @@ describe("aiMergeTask — build verification", () => {
const cmdStr = String(cmd);
if (cmdStr.includes("rev-parse --verify")) return Buffer.from("abc123");
if (cmdStr.includes("git log")) return "- feat: something" as any;
if (cmdStr.includes("merge-base")) return Buffer.from("abc123");
if (cmdStr.includes("git diff") && cmdStr.includes("--stat")) return "1 file changed" as any;
if (cmdStr.includes("merge --squash")) return Buffer.from("");
if (cmdStr.includes("diff --cached --quiet")) return "1" as any;
@@ -1740,6 +1753,7 @@ describe("aiMergeTask — build verification", () => {
const cmdStr = String(cmd);
if (cmdStr.includes("rev-parse")) return Buffer.from("abc123");
if (cmdStr.includes("git log")) return "- feat: something" as any;
if (cmdStr.includes("merge-base")) return Buffer.from("abc123");
if (cmdStr.includes("--stat")) return "1 file changed" as any;
if (cmdStr.includes("merge --squash")) return Buffer.from("");
// After commit, diff shows clean
@@ -1784,6 +1798,7 @@ describe("aiMergeTask — build verification", () => {
const cmdStr = String(cmd);
if (cmdStr.includes("rev-parse")) return Buffer.from("abc123");
if (cmdStr.includes("git log")) return "- feat: something" as any;
if (cmdStr.includes("merge-base")) return Buffer.from("abc123");
if (cmdStr.includes("--stat")) return "1 file changed" as any;
if (cmdStr.includes("merge --squash")) return Buffer.from("");
// After commit, diff shows clean
@@ -1840,6 +1855,7 @@ describe("aiMergeTask — build verification", () => {
// Default happy path for other commands
if (cmdStr.includes("rev-parse")) return Buffer.from("abc123");
if (cmdStr.includes("git log")) return "- feat: something" as any;
if (cmdStr.includes("merge-base")) return Buffer.from("abc123");
if (cmdStr.includes("--stat")) return "1 file changed" as any;
if (cmdStr.includes("merge --squash")) return Buffer.from("");
// Staged changes present (agent didn't commit due to build failure)

View File

@@ -1,6 +1,6 @@
import { execSync } from "node:child_process";
import { existsSync } from "node:fs";
import type { TaskStore, MergeResult } from "@fusion/core";
import { getTaskMergeBlocker, type TaskStore, type MergeResult } from "@fusion/core";
import { createKbAgent, promptWithFallback } from "./pi.js";
import type { WorktreePool } from "./worktree-pool.js";
import { AgentLogger } from "./agent-logger.js";
@@ -585,10 +585,9 @@ export async function aiMergeTask(
): Promise<MergeResult> {
// 1. Validate task state
const task = await store.getTask(taskId);
if (task.column !== "in-review") {
throw new Error(
`Cannot merge ${taskId}: task is in '${task.column}', must be in 'in-review'`,
);
const mergeBlocker = getTaskMergeBlocker(task);
if (mergeBlocker) {
throw new Error(`Cannot merge ${taskId}: ${mergeBlocker}`);
}
const branch = task.branch || `kb/${taskId.toLowerCase()}`;
@@ -635,7 +634,11 @@ export async function aiMergeTask(
commitLog = "(unable to read commit log)";
}
try {
diffStat = execSync(`git diff HEAD..${branch} --stat`, {
const mergeBase = execSync(`git merge-base HEAD ${branch}`, {
cwd: rootDir,
encoding: "utf-8",
}).trim();
diffStat = execSync(`git diff ${mergeBase}..${branch} --stat`, {
cwd: rootDir,
encoding: "utf-8",
}).trim();