feat(FN-1036): add mergeDetails collection and storage in merger

- Add MergeDetails type import and mergeDetails field to TaskStore.updateTask signature
- Store merge details (commitSha, filesChanged, insertions, deletions, mergeCommitMessage, resolution info) after successful squash merge
- Capture best-effort commitSha even when branch is not found (mergeConfirmed: false)
- Add comprehensive tests for store updateTask with mergeDetails and merger detail collection
This commit is contained in:
gsxdsm
2026-04-06 21:36:31 -07:00
parent 1c232a17c4
commit ff8a8682c3
4 changed files with 320 additions and 2 deletions

View File

@@ -1,6 +1,6 @@
import { execSync } from "node:child_process";
import { existsSync } from "node:fs";
import { getTaskMergeBlocker, type TaskStore, type MergeResult, type WorkflowStep, type WorkflowStepResult, type Settings } from "@fusion/core";
import { getTaskMergeBlocker, type TaskStore, type MergeResult, type MergeDetails, type WorkflowStep, type WorkflowStepResult, type Settings } from "@fusion/core";
import { createKbAgent, describeModel, promptWithFallback } from "./pi.js";
import type { WorktreePool } from "./worktree-pool.js";
import { AgentLogger } from "./agent-logger.js";
@@ -618,6 +618,26 @@ export async function aiMergeTask(
});
} catch {
result.error = `Branch '${branch}' not found — moving to done without merge`;
// Best-effort: try to capture current HEAD commitSha even though branch is missing
try {
const commitSha = execSync("git rev-parse HEAD", {
cwd: rootDir,
stdio: "pipe",
encoding: "utf-8",
}).trim() || undefined;
if (commitSha) {
await store.updateTask(taskId, {
mergeDetails: {
commitSha,
mergedAt: new Date().toISOString(),
mergeConfirmed: false,
},
});
mergerLog.log(`${taskId}: branch not found but captured commitSha ${commitSha.slice(0, 8)}`);
}
} catch {
// No commit SHA available — task will show summary fallback
}
await completeTask(store, taskId, result);
return result;
}
@@ -792,6 +812,53 @@ export async function aiMergeTask(
throw new Error(`AI merge failed for ${taskId}: all 3 attempts exhausted`);
}
// 5b. Collect merge details and store on task
try {
const commitSha = execSync("git rev-parse HEAD", {
cwd: rootDir,
stdio: "pipe",
encoding: "utf-8",
}).trim() || undefined;
let filesChanged: number | undefined;
let insertions: number | undefined;
let deletions: number | undefined;
try {
const statsOutput = execSync("git show --shortstat --format= HEAD", {
cwd: rootDir,
stdio: "pipe",
encoding: "utf-8",
}).trim();
const normalized = statsOutput.replace(/\n/g, " ");
const filesMatch = normalized.match(/(\d+) files? changed/);
const insertionsMatch = normalized.match(/(\d+) insertions?\(\+\)/);
const deletionsMatch = normalized.match(/(\d+) deletions?\(-\)/);
filesChanged = filesMatch ? Number.parseInt(filesMatch[1], 10) : 0;
insertions = insertionsMatch ? Number.parseInt(insertionsMatch[1], 10) : 0;
deletions = deletionsMatch ? Number.parseInt(deletionsMatch[1], 10) : 0;
} catch { /* non-fatal */ }
const mergeDetails: MergeDetails = {
commitSha,
filesChanged,
insertions,
deletions,
mergeCommitMessage: commitLog,
mergedAt: new Date().toISOString(),
mergeConfirmed: true,
resolutionStrategy: result.resolutionStrategy,
resolutionMethod: result.resolutionMethod,
attemptsMade: result.attemptsMade,
autoResolvedCount: result.autoResolvedCount,
};
await store.updateTask(taskId, { mergeDetails });
mergerLog.log(`${taskId}: merge details stored (commitSha: ${commitSha?.slice(0, 8)})`);
} catch (err: any) {
mergerLog.warn(`${taskId}: failed to collect/store merge details: ${err.message}`);
}
// 6. Delete branch
try {
execSync(`git branch -d "${branch}"`, { cwd: rootDir, stdio: "pipe" });