FN-5644: default AI merge commit body to summarize changes

Fusion-Task-Id: FN-5644

Fusion-Task-Lineage: 5cf0133b-2a14-4bb4-a1c4-2d5bef5793bc
This commit is contained in:
gsxdsm
2026-05-28 23:45:40 -07:00
parent 6c44ed16e7
commit 230efa1cdc
7 changed files with 50 additions and 9 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
Update `useAiMergeCommitSummary` docs/JSDoc to match the intended default of `true`, including that merge commit summaries include a subject plus body summary (narrative + bullets + diff-stat).
Also fixes AI merge-mode prompt guidance so AI-authored squash commits include a summarized body instead of subject-only commit messages.

View File

@@ -388,7 +388,7 @@ Default notes:
| `memoryBackupDir` | `string` | `".fusion/backups/memory"` | Relative memory backup directory path. | | `memoryBackupDir` | `string` | `".fusion/backups/memory"` | Relative memory backup directory path. |
| `memoryBackupScope` | `"project" \| "agents" \| "all"` | `"all"` | Backup scope: project memory, agent memory, or both. | | `memoryBackupScope` | `"project" \| "agents" \| "all"` | `"all"` | Backup scope: project memory, agent memory, or both. |
| `autoSummarizeTitles` | `boolean` | `false` | Auto-generate titles for long untitled descriptions across dashboard/API task creation and agent/tool-created tasks. | | `autoSummarizeTitles` | `boolean` | `false` | Auto-generate titles for long untitled descriptions across dashboard/API task creation and agent/tool-created tasks. |
| `useAiMergeCommitSummary` | `boolean` | `false` | Use AI-generated merge commit summaries instead of raw step-commit subject lists. | | `useAiMergeCommitSummary` | `boolean` | `true` | Use AI-generated merge commit summaries (subject + bullet body + diff-stat) instead of raw step-commit subject lists. |
| `titleSummarizerProvider` | `string` | `undefined` | Provider for title summarization. | | `titleSummarizerProvider` | `string` | `undefined` | Provider for title summarization. |
| `titleSummarizerModelId` | `string` | `undefined` | Model ID for title summarization. | | `titleSummarizerModelId` | `string` | `undefined` | Model ID for title summarization. |
| `titleSummarizerFallbackProvider` | `string` | `undefined` | Fallback provider for title summarization. | | `titleSummarizerFallbackProvider` | `string` | `undefined` | Fallback provider for title summarization. |

View File

@@ -74,6 +74,11 @@ describe("settings defaults invariants", () => {
expect("githubCloseSourceIssueOnDone" in DEFAULT_GLOBAL_SETTINGS).toBe(false); expect("githubCloseSourceIssueOnDone" in DEFAULT_GLOBAL_SETTINGS).toBe(false);
}); });
it("defaults AI merge commit summaries to enabled", () => {
// FN-5642/FN-5644 intentionally default this on for subject + body summary coverage.
expect(DEFAULT_PROJECT_SETTINGS.useAiMergeCommitSummary).toBe(true);
});
describe("recycleWorktrees default", () => { describe("recycleWorktrees default", () => {
it("keeps recycleWorktrees explicitly false in project defaults", () => { it("keeps recycleWorktrees explicitly false in project defaults", () => {
expect(DEFAULT_PROJECT_SETTINGS.recycleWorktrees).toBe(false); expect(DEFAULT_PROJECT_SETTINGS.recycleWorktrees).toBe(false);

View File

@@ -3378,8 +3378,9 @@ export interface ProjectSettings {
* Default: false. */ * Default: false. */
autoSummarizeTitles?: boolean; autoSummarizeTitles?: boolean;
/** When true, merge commit messages include an AI-generated summary of the /** When true, merge commit messages include an AI-generated summary of the
* changes instead of just listing step commit subjects. Uses the title * changes instead of just listing step commit subjects. Body composition
* summarizer model. Default: false. */ * includes a narrative line, bullet summary, and `git diff --stat` when
* available. Uses the title summarizer model. Default: true. */
useAiMergeCommitSummary?: boolean; useAiMergeCommitSummary?: boolean;
/** AI model provider for title summarization (when autoSummarizeTitles is enabled). /** AI model provider for title summarization (when autoSummarizeTitles is enabled).
* Must be set together with `titleSummarizerModelId`. Falls back to planningProvider, * Must be set together with `titleSummarizerModelId`. Falls back to planningProvider,

View File

@@ -3583,7 +3583,7 @@ export function SettingsModal({
AI merge commit summaries AI merge commit summaries
</label> </label>
<small> <small>
When enabled, merge commit messages will include an AI-generated summary of the changes instead of just listing step commit subjects. Uses the title summarization model. When enabled, merge commit messages include an AI-generated subject plus body summary (narrative + bullets + diff-stat) instead of just listing step commit subjects. Uses the title summarization model.
</small> </small>
</div> </div>

View File

@@ -8,6 +8,7 @@ import {
landSquash, landSquash,
parseReviewVerdict, parseReviewVerdict,
buildMergeSystemPrompt, buildMergeSystemPrompt,
buildMergePrompt,
buildReviewSystemPrompt, buildReviewSystemPrompt,
REVIEW_VERDICT_MARKER, REVIEW_VERDICT_MARKER,
AiMergeBlockedError, AiMergeBlockedError,
@@ -116,9 +117,11 @@ describe("parseReviewVerdict", () => {
expect(buildMergeSystemPrompt().toLowerCase()).toContain("conflict"); expect(buildMergeSystemPrompt().toLowerCase()).toContain("conflict");
}); });
it("merge system prompt enforces new-breakage verification + uses the editable merger prompt", () => { it("merge system prompt enforces new-breakage verification + commit body summary guidance", () => {
expect(buildMergeSystemPrompt().toLowerCase()).toContain("type-check"); expect(buildMergeSystemPrompt().toLowerCase()).toContain("type-check");
expect(buildMergeSystemPrompt()).toMatch(/new failure/i); expect(buildMergeSystemPrompt()).toMatch(/new failure/i);
expect(buildMergeSystemPrompt()).toMatch(/bullet list of key changes/i);
expect(buildMergeSystemPrompt()).toMatch(/Files changed:/i);
// A custom 'merger' role prompt is incorporated as the base, while the hard // A custom 'merger' role prompt is incorporated as the base, while the hard
// rules (verification + trailers) are still appended. // rules (verification + trailers) are still appended.
const cfg = { const cfg = {
@@ -129,6 +132,23 @@ describe("parseReviewVerdict", () => {
expect(p).toContain("CUSTOM MERGER PERSONA"); expect(p).toContain("CUSTOM MERGER PERSONA");
expect(p).toContain("Verify before committing"); expect(p).toContain("Verify before committing");
}); });
it("merge prompt requires subject, body summary, and diff-stat in commit message", () => {
const prompt = buildMergePrompt({
taskId: "FN-1",
branch: "fusion/fn-1",
integrationBranch: "main",
tipSha: "0123456789abcdef0123456789abcdef01234567",
taskTitle: "Do the thing",
includeTaskId: true,
trailers: ["Fusion-Task-Id: FN-1"],
});
expect(prompt).toMatch(/Build a merge body from the staged squash diff/i);
expect(prompt).toMatch(/bullet list of key changes/i);
expect(prompt).toMatch(/Files changed:/i);
expect(prompt).toMatch(/git diff --stat/i);
expect(prompt).toMatch(/git commit -m "FN-1: <concise imperative summary of the squashed changes>" -m/i);
});
}); });
describe("runAiMerge", () => { describe("runAiMerge", () => {

View File

@@ -225,6 +225,10 @@ export function buildMergeSystemPrompt(agentPrompts?: AgentPromptsConfig): strin
" - The subject line must CONCISELY SUMMARIZE the squashed changes in", " - The subject line must CONCISELY SUMMARIZE the squashed changes in",
" imperative mood (e.g. \"add X\", \"fix Y\") based on the actual diff — do", " imperative mood (e.g. \"add X\", \"fix Y\") based on the actual diff — do",
" not just restate the task title.", " not just restate the task title.",
" - The commit BODY must include:",
" 1) one short narrative summary line,",
" 2) a bullet list of key changes, and",
" 3) a `Files changed:` section populated from `git diff --stat`.",
" - Include the task-id prefix and the trailer lines EXACTLY as given in the", " - Include the task-id prefix and the trailer lines EXACTLY as given in the",
" task instructions (they associate the commit with the board task).", " task instructions (they associate the commit with the board task).",
].filter((l) => l !== undefined).join("\n"); ].filter((l) => l !== undefined).join("\n");
@@ -253,11 +257,15 @@ export function buildMergePrompt(input: {
"Steps:", "Steps:",
` 1. Run: git merge --squash ${input.branch}`, ` 1. Run: git merge --squash ${input.branch}`,
" 2. If there are conflicts, resolve them (favor the task's intent), then `git add` the resolved files.", " 2. If there are conflicts, resolve them (favor the task's intent), then `git add` the resolved files.",
" 3. Commit the staged result as a SINGLE commit whose subject summarizes the", " 3. Build a merge body from the staged squash diff:",
` actual changes${input.taskTitle ? ` (task title hint: ${JSON.stringify(input.taskTitle)})` : ""}, including the required trailers:`, " - one short narrative summary line",
` git commit -m ${subjectShape}${trailerArgs}`, " - bullet list of key changes",
" - `Files changed:` + the output of `git diff --stat`",
" 4. Commit the staged result as a SINGLE commit whose subject summarizes the",
` actual changes${input.taskTitle ? ` (task title hint: ${JSON.stringify(input.taskTitle)})` : ""}, including the body above and required trailers:`,
` git commit -m ${subjectShape} -m "<narrative + bullet list + Files changed: ...>"${trailerArgs}`,
" Keep the trailer line(s) verbatim — they link the commit to the board task.", " Keep the trailer line(s) verbatim — they link the commit to the board task.",
" 4. Verify `git log --oneline ${tip}..HEAD` shows exactly one new commit and `git status` is clean.".replace("${tip}", short(input.tipSha)), " 5. Verify `git log --oneline ${tip}..HEAD` shows exactly one new commit and `git status` is clean.".replace("${tip}", short(input.tipSha)),
"", "",
"If `git merge --squash` reports the branch is already up to date (nothing to", "If `git merge --squash` reports the branch is already up to date (nothing to",
"merge), do nothing and leave HEAD unchanged.", "merge), do nothing and leave HEAD unchanged.",