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:
@@ -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" },
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user