feat(FN-2947): merge fusion/fn-2947

- Add AI-powered merge commit summarization with toggle in Settings (`packages/core/src/ai-summarize.ts`, `packages/dashboard/app/components/SettingsModal.tsx`, `packages/engine/src/merger.ts`)
- Introduce `generateMergeCommitSummary` utility in `@fusion/core` that calls the configured model with a diff prompt; results appear in the commit log for merge commits
- Add `aiMergeSummaryEnabled` project setting and `useAiMergeSummaryEnabled` hook in dashboard
- Update `@runfusion/fusion` CLI extension to surface the new setting
- Add `POST /api/tasks/:id/workflow/pre-merge` and `POST /api/tasks/:id/planning-subtask` routes (`packages/dashboard/src/routes/`)
- Rework InsightsView with two-pane layout for better readability (`packages/dashboard/app/components/InsightsView.tsx`, `packages/dashboard/app/components/InsightsView.css`)
- Fix `task plan` command to surface errors when planning fails (`packages/cli/src/commands/task.ts`)
- Add tests for `ai-summarize`, `agent-heartbeat`, `agent-tools`, and dashboard API routes; refresh onboarding and agents-view mobile tests
- Minor CSS polish: reduce AgentsView panel hover gap, improve CommitDiffTab layout, add ScriptsModal styles
- Changelog and version bumps for `@runfusion/fusion` v0.9.1, CLI alias, and all workspace packages

Commits merged:
- feat(FN-2947): complete Step 7 — add changeset and docs deliverables
- fix(FN-2947): clean unused aiSummary path
- test(FN-2947): complete Step 5 — cover AI merge summary flow
- feat(FN-2947): complete Step 4 — add merge summary settings toggle
- feat(FN-2947): complete Step 3 — wire AI merge summaries into merger
- feat(FN-2947): complete Step 2 — add merge summary setting
- feat(FN-2947): complete Step 1 — add merge commit summarizer
- feat(FN-2970): merge fusion/fn-2970
- feat(FN-2956): merge fusion/fn-2956
- feat(FN-2923): merge fusion/fn-2923
- feat(FN-2945): merge fusion/fn-2945
- chore(release): v0.9.1
- fix(FN-XXX): improve git manager diff layout
- fix(dashboard): rework Insights view with two-pane layout
- fix(FN-XXX): keep experimental views off by default

Files changed:
.changeset/active-agents-no-stuck-connecting.md    |  13 --
 .changeset/active-agents-panel-hoist-heartbeat.md  |  13 --
 .changeset/add-ai-merge-commit-summary.md          |   5 +
 .changeset/fix-agent-heartbeat-terminal-links.md   |   5 -
 .changeset/show-planning-tasks-immediately.md      |   5 -
 CHANGELOG.md                                       |  92 ++++++++++
 docs/settings-reference.md                         |   3 +
 package.json                                       |   2 +-
 packages/cli-alias/CHANGELOG.md                    |  15 ++
 packages/cli-alias/package.json                    |   2 +-
 packages/cli/CHANGELOG.md                          |  12 ++
 packages/cli/package.json                          |   2 +-
 packages/cli/src/__tests__/task-plan.test.ts       |   1 +
 packages/cli/src/commands/__tests__/task.test.ts   |  15 +-
 packages/cli/src/commands/task.ts                  |  15 +-
 packages/cli/src/extension.ts                      |   9 +
 packages/core/CHANGELOG.md                         |   7 +
 packages/core/package.json                         |   2 +-
 packages/core/src/__tests__/ai-summarize.test.ts   |  64 +++++++
 packages/core/src/ai-summarize.ts                  | 114 ++++++++++++
 packages/core/src/index.ts                         |   3 +
 packages/core/src/settings-schema.ts               |   1 +
 packages/core/src/types.ts                         |   4 +
 packages/dashboard/CHANGELOG.md                    |  14 ++
 packages/dashboard/app/App.tsx                     |  28 ++-
 .../app/__tests__/agent-css-classes.test.ts        |   3 +-
 packages/dashboard/app/__tests__/api.test.ts       |  13 ++
 packages/dashboard/app/api/legacy.ts               |   2 +
 packages/dashboard/app/components/AgentsView.css   |  22 +--
 .../dashboard/app/components/CommitDiffTab.tsx     |   2 +-
 .../dashboard/app/components/GitManagerModal.tsx   |  71 ++++----
 packages/dashboard/app/components/InsightsView.css | 200 ++++++++++++++++++---
 packages/dashboard/app/components/InsightsView.tsx | 111 ++++++++----
 .../app/components/ModelOnboardingModal.tsx        |   5 +-
 packages/dashboard/app/components/ScriptsModal.css |  89 +++++++++
 .../dashboard/app/components/SettingsModal.css     |  11 +-
 .../dashboard/app/components/SettingsModal.tsx     |  22 ++-
 .../dashboard/app/components/TaskDetailModal.css   |   5 +
 packages/dashboard/app/components/TodoView.tsx     |   2 +
 .../app/components/__tests__/App.test.tsx          |  33 ++++
 .../app/components/__tests__/InsightsView.test.tsx |  25 ++-
 .../__tests__/ModelOnboardingModal.test.tsx        |   4 +-
 .../app/components/__tests__/QuickChatFAB.test.tsx |  14 +-
 .../__tests__/SettingsModalNodeRouting.test.tsx    |  12 +-
 .../app/components/__tests__/TodoView.test.tsx     |   4 +-
 .../__tests__/agents-view-mobile.test.tsx          |   5 +-
 .../components/__tests__/onboarding-flow.test.tsx  |   2 +-
 .../app/hooks/__tests__/useAppSettings.test.ts     |   1 +
 .../app/hooks/__tests__/useTaskHandlers.test.ts    |   4 +-
 packages/dashboard/app/hooks/useAppSettings.ts     |  12 ++
 packages/dashboard/app/hooks/useTaskHandlers.ts    |   4 +-
 packages/dashboard/package.json                    |   2 +-
 packages/dashboard/src/__tests__/routes.test.ts    |  14 ++
 packages/dashboard/src/routes.ts                   |   4 +
 .../dashboard/src/routes/register-git-github.ts    |  12 ++
 .../src/routes/register-planning-subtask-routes.ts |   3 +
 .../src/routes/register-task-workflow-routes.ts    |   7 +
 packages/desktop/CHANGELOG.md                      |   7 +
 packages/desktop/package.json                      |   2 +-
 packages/engine/CHANGELOG.md                       |  11 ++
 packages/engine/package.json                       |   2 +-
 .../engine/src/__tests__/agent-heartbeat.test.ts   |  10 ++
 .../src/__tests__/agent-tools-delegation.test.ts   |   2 +
 packages/engine/src/__tests__/agent-tools.test.ts  |  37 ++++
 packages/engine/src/__tests__/cron-runner.test.ts  |   4 +
 packages/engine/src/__tests__/merger.test.ts       |  75 ++++++++
 .../src/__tests__/node-routing-policy.test.ts      |  25 ++-
 .../src/__tests__/pr-comment-handler.test.ts       |   8 +
 .../src/__tests__/scheduler-node-routing.test.ts   |  18 +-
 packages/engine/src/__tests__/triage.test.ts       |   2 +
 packages/engine/src/agent-heartbeat.ts             |  13 +-
 packages/engine/src/agent-tools.ts                 |  13 +-
 packages/engine/src/cron-runner.ts                 |   7 +-
 packages/engine/src/executor.ts                    |   2 +-
 packages/engine/src/merger.ts                      | 181 ++++++++-----------
 packages/engine/src/mission-execution-loop.ts      |   8 +
 packages/engine/src/pr-comment-handler.ts          |   5 +
 packages/engine/src/project-engine.ts              |   8 +
 packages/engine/src/routine-runner.ts              |   4 +
 packages/engine/src/scheduler.ts                   |   8 +-
 packages/engine/src/triage.ts                      |   4 +
 packages/mobile/CHANGELOG.md                       |   7 +
 packages/mobile/package.json                       |   2 +-
 packages/pi-claude-cli/CHANGELOG.md                |   7 +
 packages/pi-claude-cli/package.json                |   2 +-
 packages/plugin-sdk/CHANGELOG.md                   |  10 ++
 packages/plugin-sdk/package.json                   |   2 +-
 .../examples/fusion-plugin-auto-label/CHANGELOG.md |   8 +
 .../examples/fusion-plugin-auto-label/package.json |   2 +-
 .../examples/fusion-plugin-ci-status/CHANGELOG.md  |   8 +
 .../examples/fusion-plugin-ci-status/package.json  |   2 +-
 .../fusion-plugin-notification/CHANGELOG.md        |   8 +
 .../fusion-plugin-notification/package.json        |   2 +-
 .../fusion-plugin-settings-demo/CHANGELOG.md       |   8 +
 .../fusion-plugin-settings-demo/package.json       |   2 +-
 plugins/fusion-plugin-hermes-runtime/CHANGELOG.md  |   8 +
 plugins/fusion-plugin-hermes-runtime/package.json  |   2 +-
 .../fusion-plugin-openclaw-runtime/CHANGELOG.md    |   8 +
 .../fusion-plugin-openclaw-runtime/package.json    |   2 +-
 .../fusion-plugin-paperclip-runtime/CHANGELOG.md   |   8 +
 .../fusion-plugin-paperclip-runtime/package.json   |   2 +-
 101 files changed, 1388 insertions(+), 334 deletions(-)

Fusion-Task-Id: FN-2947
This commit is contained in:
Fusion
2026-04-29 17:36:20 -07:00
committed by gsxdsm
parent 883560776d
commit 5c82c3f34a
10 changed files with 360 additions and 110 deletions

View File

@@ -118,6 +118,7 @@ import {
import { mergerLog } from "../logger.js";
import { createFnAgent } from "../pi.js";
import { execSync, exec } from "node:child_process";
import * as core from "@fusion/core";
import { type TaskStore, type Task, type MergeResult, DEFAULT_SETTINGS } from "@fusion/core";
const mockedCreateFnAgent = vi.mocked(createFnAgent);
@@ -5077,6 +5078,80 @@ describe("aiMergeTask — merge details collection", () => {
expect(mergeDetails.attemptsMade).toBe(1);
});
it("stores AI summary in mergeDetails when useAiMergeCommitSummary is enabled", async () => {
const store = createMockStore(
{ id: "FN-050", worktree: "/tmp/root/.worktrees/KB-050" },
[{ id: "FN-050", worktree: "/tmp/root/.worktrees/KB-050", column: "in-review" } as Task],
);
(store.getSettings as ReturnType<typeof vi.fn>).mockResolvedValue({
...DEFAULT_SETTINGS,
useAiMergeCommitSummary: true,
});
vi.spyOn(core, "summarizeMergeCommit").mockResolvedValue("AI summary of merged work.");
mockedExecSync.mockImplementation((cmd: any) => {
const cmdStr = String(cmd);
if (cmdStr.includes("rev-parse --verify")) return Buffer.from("abc123");
if (cmdStr === "git rev-parse HEAD" || cmdStr.startsWith("git rev-parse HEAD ")) return "mergedcommit123456789";
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 "";
if (cmdStr.includes("diff --cached --quiet")) return "1";
if (cmdStr.includes("git commit")) return Buffer.from("");
if (cmdStr.includes("show --shortstat")) return "1 file changed, 1 insertion(+)";
if (cmdStr.includes("branch -d") || cmdStr.includes("branch -D")) return Buffer.from("");
if (cmdStr.includes("worktree remove")) return Buffer.from("");
return Buffer.from("");
});
const result = await aiMergeTask(store, "/tmp/root", "FN-050");
expect(result.merged).toBe(true);
const updateCalls = (store.updateTask as ReturnType<typeof vi.fn>).mock.calls;
const mergeDetailsCall = updateCalls.find((call: any[]) => call[1]?.mergeDetails !== undefined);
expect(mergeDetailsCall?.[1].mergeDetails.mergeCommitMessage).toBe("AI summary of merged work.");
});
it("falls back to raw commit log when AI merge summary returns null", async () => {
const store = createMockStore(
{ id: "FN-050", worktree: "/tmp/root/.worktrees/KB-050" },
[{ id: "FN-050", worktree: "/tmp/root/.worktrees/KB-050", column: "in-review" } as Task],
);
(store.getSettings as ReturnType<typeof vi.fn>).mockResolvedValue({
...DEFAULT_SETTINGS,
useAiMergeCommitSummary: true,
});
vi.spyOn(core, "summarizeMergeCommit").mockResolvedValue(null);
mockedExecSync.mockImplementation((cmd: any) => {
const cmdStr = String(cmd);
if (cmdStr.includes("rev-parse --verify")) return Buffer.from("abc123");
if (cmdStr === "git rev-parse HEAD" || cmdStr.startsWith("git rev-parse HEAD ")) return "mergedcommit123456789";
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 "";
if (cmdStr.includes("diff --cached --quiet")) return "1";
if (cmdStr.includes("git commit")) return Buffer.from("");
if (cmdStr.includes("show --shortstat")) return "1 file changed, 1 insertion(+)";
if (cmdStr.includes("branch -d") || cmdStr.includes("branch -D")) return Buffer.from("");
if (cmdStr.includes("worktree remove")) return Buffer.from("");
return Buffer.from("");
});
const result = await aiMergeTask(store, "/tmp/root", "FN-050");
expect(result.merged).toBe(true);
const updateCalls = (store.updateTask as ReturnType<typeof vi.fn>).mock.calls;
const mergeDetailsCall = updateCalls.find((call: any[]) => call[1]?.mergeDetails !== undefined);
expect(mergeDetailsCall?.[1].mergeDetails.mergeCommitMessage).toBe("- feat: something");
});
it("stores partial mergeDetails when branch is not found", async () => {
const store = createMockStore(
{ id: "FN-050", worktree: "/tmp/root/.worktrees/KB-050" },

View File

@@ -137,9 +137,10 @@ import {
getTaskMergeBlocker,
normalizeMergeConflictStrategy,
resolveProjectDefaultModel,
resolveTitleSummarizerSettingsModel,
resolveAgentPrompt,
summarizeCommitBody,
summarizeCommitSubject,
summarizeMergeCommit,
type TaskStore,
type MergeResult,
type MergeDetails,
@@ -1054,19 +1055,31 @@ function resetMergeWithWarn(rootDir: string, taskId: string, label: string): voi
}
}
async function generateAiMergeSummary(
commitLog: string,
diffStat: string,
settings: Settings,
rootDir: string,
): Promise<string | null> {
try {
const resolved = resolveTitleSummarizerSettingsModel(settings);
return await summarizeMergeCommit(
commitLog,
diffStat,
rootDir,
resolved.provider,
resolved.modelId,
);
} catch (err: unknown) {
const message = err instanceof Error ? err.message : String(err);
mergerLog.warn(`AI merge summary failed; using deterministic fallback (${message})`);
return null;
}
}
/**
* Build the canonical merge commit message from the branch's step commits.
* Subject is always `feat[(taskId)]: merge <branch>`. Body has three parts so
* `git log` shows what actually landed instead of a bare "merge":
* 1. AI-generated summary (via the title-summarizer model lane), built from
* the branch's step-commit subjects + diffstat. Best-effort — bounded by
* timeout, falls through silently on any failure.
* 2. The raw step-commit list (always included as ground truth so a reader
* can verify the AI summary against the actual commits).
* 3. The diffstat block so file-level changes are visible inline.
*
* The AI summary is additive context, never the sole source of truth — the
* step commits and diffstat below it are deterministic.
* Subject is always `feat[(taskId)]: merge <branch>`.
*/
async function buildDeterministicMergeMessage(params: {
taskId: string;
@@ -1074,13 +1087,11 @@ async function buildDeterministicMergeMessage(params: {
commitLog: string;
diffStat?: string;
includeTaskId: boolean;
rootDir?: string;
settings?: Settings;
signal?: AbortSignal;
aiSummary?: string | null;
}): Promise<{ subjectArg: string; bodyArg: string }> {
const { taskId, branch, commitLog, diffStat, includeTaskId, rootDir, settings, signal } = params;
const { taskId, branch, commitLog, diffStat, includeTaskId, aiSummary } = params;
const prefix = includeTaskId ? `feat(${taskId})` : "feat";
const fallbackSubject = `${prefix}: merge ${branch}`;
const subject = `${prefix}: merge ${branch}`;
const trimmedCommitLog = commitLog?.trim() ?? "";
const trimmedDiffStat = diffStat?.trim() ?? "";
@@ -1089,64 +1100,12 @@ async function buildDeterministicMergeMessage(params: {
? trimmedCommitLog
: `- merge ${branch}`;
// Best-effort AI summary using the title-summarizer lane (small/fast model).
// Falls back to the project default when not configured. Any failure (no
// runtime, timeout, empty response) returns null and we skip the summary.
// Subject and body are generated in parallel so the extra subject call
// doesn't serialize merge time.
let aiSummary: string | null = null;
let aiSubject: string | null = null;
if (rootDir && settings && (trimmedCommitLog.length > 0 || trimmedDiffStat.length > 0)) {
const useTitleSummarizer =
!!settings.titleSummarizerProvider && !!settings.titleSummarizerModelId;
const provider = useTitleSummarizer
? settings.titleSummarizerProvider!
: (settings.defaultProviderOverride && settings.defaultModelIdOverride
? settings.defaultProviderOverride
: settings.defaultProvider);
const modelId = useTitleSummarizer
? settings.titleSummarizerModelId!
: (settings.defaultProviderOverride && settings.defaultModelIdOverride
? settings.defaultModelIdOverride
: settings.defaultModelId);
const [bodyResult, subjectResult] = await Promise.all([
summarizeCommitBody(trimmedDiffStat, rootDir, provider, modelId, {
branch,
taskId,
commitLog: trimmedCommitLog,
signal,
}).catch(() => null),
summarizeCommitSubject(trimmedDiffStat, rootDir, provider, modelId, {
branch,
taskId,
commitLog: trimmedCommitLog,
signal,
}).catch(() => null),
]);
aiSummary = bodyResult;
aiSubject = subjectResult;
}
// Compose subject: prefer the AI summary; fall back to the legacy
// `merge <branch>` form on any failure so a wedged summarizer can never
// block a merge. Hard cap at 72 chars (subject + prefix) — git's soft
// limit is 72; the AI is already capped at 60 by sanitizeCommitSubject.
let subject = fallbackSubject;
if (aiSubject && aiSubject.length > 0) {
const candidate = `${prefix}: ${aiSubject}`;
subject = candidate.length > 72 ? candidate.slice(0, 72).trimEnd() : candidate;
}
const sections: string[] = [];
if (aiSummary && aiSummary.trim().length > 0) {
sections.push(aiSummary.trim());
}
sections.push(`Commits merged:\n${commitsSection}`);
if (trimmedDiffStat.length > 0) {
sections.push(`Files changed:\n${trimmedDiffStat}`);
}
const body = sections.join("\n\n");
const body = aiSummary?.trim().length
? aiSummary.trim()
: [
`Commits merged:\n${commitsSection}`,
trimmedDiffStat.length > 0 ? `Files changed:\n${trimmedDiffStat}` : "",
].filter(Boolean).join("\n\n");
// -m args are double-quoted in the shell command, so escape backslashes,
// double quotes, dollar signs, and backticks.
@@ -1185,6 +1144,7 @@ async function commitOrAmendMergeWithFixes(
diffStat?: string,
settings?: Settings,
signal?: AbortSignal,
aiSummary?: string | null,
): Promise<boolean> {
try {
// Stage everything (squash state + verification fixes the agent left
@@ -1259,9 +1219,7 @@ async function commitOrAmendMergeWithFixes(
commitLog: messageCommitLog,
diffStat: messageDiffStat,
includeTaskId,
rootDir,
settings,
signal,
aiSummary,
});
const trailerArg = buildTaskIdTrailerArg(taskId);
@@ -2002,31 +1960,33 @@ async function resolveSafeCommitBody(opts: {
const cleanStat = opts.diffStat.trim();
if (cleanStat.length > 0) {
// Prefer the dedicated title-summarization model — a small, fast tier
// intended for short summarization. Falls back to the project / global
// default model when the summarizer lane isn't configured. The core
// `summarizeCommitBody` helper handles missing-runtime / timeout / empty
// response gracefully and returns null.
const useTitleSummarizer =
!!opts.settings.titleSummarizerProvider && !!opts.settings.titleSummarizerModelId;
const provider = useTitleSummarizer
? opts.settings.titleSummarizerProvider!
: (opts.settings.defaultProviderOverride && opts.settings.defaultModelIdOverride
? opts.settings.defaultProviderOverride
: opts.settings.defaultProvider);
const modelId = useTitleSummarizer
? opts.settings.titleSummarizerModelId!
: (opts.settings.defaultProviderOverride && opts.settings.defaultModelIdOverride
? opts.settings.defaultModelIdOverride
: opts.settings.defaultModelId);
if (opts.settings.useAiMergeCommitSummary) {
// Prefer the dedicated title-summarization model — a small, fast tier
// intended for short summarization. Falls back to the project / global
// default model when the summarizer lane isn't configured. The core
// `summarizeCommitBody` helper handles missing-runtime / timeout / empty
// response gracefully and returns null.
const useTitleSummarizer =
!!opts.settings.titleSummarizerProvider && !!opts.settings.titleSummarizerModelId;
const provider = useTitleSummarizer
? opts.settings.titleSummarizerProvider!
: (opts.settings.defaultProviderOverride && opts.settings.defaultModelIdOverride
? opts.settings.defaultProviderOverride
: opts.settings.defaultProvider);
const modelId = useTitleSummarizer
? opts.settings.titleSummarizerModelId!
: (opts.settings.defaultProviderOverride && opts.settings.defaultModelIdOverride
? opts.settings.defaultModelIdOverride
: opts.settings.defaultModelId);
const ai = await summarizeCommitBody(cleanStat, opts.rootDir, provider, modelId, {
branch: opts.branch,
taskId: opts.taskId,
signal: opts.signal,
timeoutMs: opts.aiTimeoutMs,
}).catch(() => null);
if (ai && ai.trim().length > 0) return ai.trim();
const ai = await summarizeCommitBody(cleanStat, opts.rootDir, provider, modelId, {
branch: opts.branch,
taskId: opts.taskId,
signal: opts.signal,
timeoutMs: opts.aiTimeoutMs,
}).catch(() => null);
if (ai && ai.trim().length > 0) return ai.trim();
}
return `Files changed:\n\n${cleanStat}`;
}
@@ -3220,6 +3180,10 @@ export async function aiMergeTask(
diffStat = "(unable to read diff)";
}
const aiMergeSummary = settings.useAiMergeCommitSummary
? await generateAiMergeSummary(commitLog, diffStat, settings, rootDir)
: null;
// 4b. Validate diff scope against task's declared File Scope
try {
const scopeResult = await validateDiffScope(store, taskId, diffStat, settings.strictScopeEnforcement);
@@ -3308,6 +3272,7 @@ export async function aiMergeTask(
branch,
commitLog,
diffStat,
aiSummary: aiMergeSummary,
includeTaskId,
sourceIssueRef,
smartConflictResolution,
@@ -3452,6 +3417,7 @@ export async function aiMergeTask(
diffStat,
settings,
options.signal,
aiMergeSummary,
);
if (!finalized) {
// Phantom-merge guard: refused to fabricate a commit. Reset
@@ -3557,6 +3523,7 @@ export async function aiMergeTask(
diffStat,
settings,
options.signal,
aiMergeSummary,
);
if (!finalized) {
// Phantom-merge guard: the verification fix passed but no
@@ -3746,7 +3713,7 @@ export async function aiMergeTask(
filesChanged: recordedFilesChanged,
insertions: recordedInsertions,
deletions: recordedDeletions,
mergeCommitMessage: commitLog,
mergeCommitMessage: aiMergeSummary || commitLog,
mergedAt: new Date().toISOString(),
mergeConfirmed: true,
resolutionStrategy: result.resolutionStrategy,
@@ -4063,6 +4030,7 @@ interface MergeAttemptParams {
branch: string;
commitLog: string;
diffStat: string;
aiSummary?: string | null;
includeTaskId: boolean;
sourceIssueRef?: string;
smartConflictResolution: boolean;
@@ -4110,6 +4078,7 @@ async function executeMergeAttempt(
branch,
commitLog,
diffStat,
aiSummary,
includeTaskId,
sourceIssueRef,
smartConflictResolution,
@@ -4441,9 +4410,7 @@ async function executeMergeAttempt(
commitLog: actualContext.commitLog || commitLog,
diffStat: actualContext.diffStat || diffStat,
includeTaskId,
rootDir,
settings: params.settings,
signal: options.signal,
aiSummary,
});
const trailerArg = buildTaskIdTrailerArg(taskId);
await execAsync(