feat(FN-5255): flip directMergeCommitStrategy default from squash to direct

Changes the default merge strategy from squash to direct by flipping `directMergeCommitStrategy` in the settings schema and types, with the core implementation in `merger-ref-update-advance.ts`. Also aligns a heartbeat executor test assertion with the FN-5060 deduplication shape.

Fusion-Task-Id: FN-5255

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Fusion-Task-Id: FN-5255
This commit is contained in:
gsxdsm
2026-05-23 11:01:05 -07:00
parent ec1269fd84
commit 8f5c1f97ad
10 changed files with 19 additions and 11 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
New projects now default `directMergeCommitStrategy` to `"always-squash"` for direct merges.
Existing projects keep their persisted setting value.
Per-project Settings UI controls and per-task `**Direct Merge Commit Strategy:** ...` PROMPT overrides are unchanged.

View File

@@ -122,7 +122,7 @@ Use `superviseSpawn(...)` from `@fusion/core` for managed child processes; do no
### Merging Branches Into Main
1. **Drop duplicate commits before merging.** Rebase away duplicates already on main.
2. **Rebase over squash for multi-commit branches.** `directMergeCommitStrategy="auto"` squashes only tiny branches.
2. **Squash is now the project default; history-preserving merge paths require opt-in.** New projects default `directMergeCommitStrategy="always-squash"`. To preserve multi-commit history, explicitly set project `directMergeCommitStrategy` to `"auto"` or `"always-rebase"`, or set a per-task `**Direct Merge Commit Strategy:** ...` override in `PROMPT.md`.
3. **Empty cherry-picks are no-ops.** Do not create empty commits.
4. **Already-on-main classifier applies.** Allow finalize/self-healing recovery when lineage is landed.
5. **Contamination auto-recovery is bounded.** First pass can auto-drop upstream foreign commits; repeated/ambiguous cases escalate.

View File

@@ -201,7 +201,7 @@ Defaults from `DEFAULT_PROJECT_SETTINGS`; key scope from `PROJECT_SETTINGS_KEYS`
| `overlapIgnorePaths` | `string[]` | `[]` | Optional project-relative file or directory paths to exclude from overlap blocking (for example `docs` or `generated/openapi.json`). Entries are trimmed, deduplicated, and must not be absolute or contain `..` traversal. |
| `autoMerge` | `boolean` | `true` | Auto-finalize tasks from `in-review`. |
| `mergeStrategy` | `"direct" \| "pull-request"` | `"direct"` | Completion mode (local direct merge vs PR-first). |
| `directMergeCommitStrategy` | `"auto" \| "always-squash" \| "always-rebase"` | `"auto"` | Direct-merge commit routing mode. `auto` keeps the legacy squash path for branches with zero or one substantive commit, but switches multi-substantive direct merges to a history-preserving rebase-and-merge/cherry-pick path so commit boundaries, subjects, and `Fusion-Task-Id` trailers survive on `main`. `always-squash` forces the legacy squash path; `always-rebase` always preserves per-commit history. Only applies when `mergeStrategy="direct"`. |
| `directMergeCommitStrategy` | `"auto" \| "always-squash" \| "always-rebase"` | `"always-squash"` | Direct-merge commit routing mode. `always-squash` (default) forces the legacy squash path. `auto` keeps the legacy squash path for branches with zero or one substantive commit, but switches multi-substantive direct merges to a history-preserving rebase-and-merge/cherry-pick path so commit boundaries, subjects, and `Fusion-Task-Id` trailers survive on `main`. `always-rebase` always preserves per-commit history. Only applies when `mergeStrategy="direct"`. |
| `mergeIntegrationWorktree` | `"reuse-task-worktree" \| "cwd-main"` | `"reuse-task-worktree"` | Auto-merge integration-root mode for direct merges only. `reuse-task-worktree` runs the rebase/conflict/audit/finalize cascade inside the task worktree so project-root `HEAD` and dirty state stay untouched. `cwd-main` preserves the legacy project-root integration path as an escape hatch. When `worktrunk.enabled=true`, worktrunk-managed merge/worktree handling still takes precedence and this setting is effectively advisory until the native path is used. |
| `integrationBranch` | `string` | `undefined` | Optional canonical project integration branch override. Resolution order for merge/self-healing/branch-conflict defaults is `integrationBranch` → legacy `baseBranch` → `origin/HEAD` symbolic ref → fallback `"main"`. This resolved value is used as `projectDefaultBranch` for `resolveTaskMergeTarget(...)`; task-level overrides still come from task metadata. |
| `prerebaseAutoEnabled` | `boolean` | `true` | Master switch for pre-merge auto-prerebase policy. When enabled, merger checks divergence from `<task.baseCommitSha>` to local `main` and may rebase before Stage 1/2 rebases. Ignored when `worktrunk.enabled=true` (worktrunk-managed path defers this layer). |
@@ -235,7 +235,7 @@ Accepted values:
Override precedence for direct merges is:
1. Task `PROMPT.md` line `**Direct Merge Commit Strategy:** ...`
2. Project `directMergeCommitStrategy`
3. Default `"auto"`
3. Default `"always-squash"`
### Sandbox settings

View File

@@ -151,8 +151,8 @@ describe("settings key parity", () => {
expect(DEFAULT_PROJECT_SETTINGS.completionDocumentationMode).toBe("off");
});
it("keeps directMergeCommitStrategy project-scoped with auto default", () => {
expect(DEFAULT_PROJECT_SETTINGS.directMergeCommitStrategy).toBe("auto");
it("defaults directMergeCommitStrategy to always-squash and keeps it project-scoped", () => {
expect(DEFAULT_PROJECT_SETTINGS.directMergeCommitStrategy).toBe("always-squash");
expect(isProjectSettingsKey("directMergeCommitStrategy")).toBe(true);
expect(isGlobalSettingsKey("directMergeCommitStrategy")).toBe(false);
});

View File

@@ -103,9 +103,9 @@ describe("TaskStore", () => {
expect(settings.mergeStrategy).toBe("pull-request");
});
it("defaults directMergeCommitStrategy to auto and persists updates", async () => {
it("defaults directMergeCommitStrategy to always-squash and persists updates", async () => {
const defaults = await harness.store().getSettings();
expect(defaults.directMergeCommitStrategy).toBe("auto");
expect(defaults.directMergeCommitStrategy).toBe("always-squash");
await harness.store().updateSettings({ directMergeCommitStrategy: "always-rebase" });
const settings = await harness.store().getSettings();

View File

@@ -195,7 +195,7 @@ export const DEFAULT_PROJECT_SETTINGS = {
autoMerge: true,
testMode: undefined,
mergeStrategy: "direct",
directMergeCommitStrategy: "auto",
directMergeCommitStrategy: "always-squash",
mergeIntegrationWorktree: "reuse-task-worktree",
integrationBranch: undefined,
requirePrApproval: false,

View File

@@ -2733,7 +2733,7 @@ export interface ProjectSettings {
* - "auto": squash single-substantive branches, preserve history for multi-substantive branches
* - "always-squash": always use the legacy squash path for direct merges
* - "always-rebase": always preserve individual branch commits during direct merges
* Only applies when mergeStrategy is "direct". Default: "auto". */
* Only applies when mergeStrategy is "direct". Default: "always-squash". */
directMergeCommitStrategy?: DirectMergeCommitStrategy;
/** Auto-merge integration-root mode.
* - "reuse-task-worktree" (default): run the auto-merge cascade in the task worktree

View File

@@ -2461,6 +2461,7 @@ describe("aiMergeTask post-squash audit gate", () => {
mergeIntegrationWorktree: "cwd-main" as const,
testCommand: "pnpm test",
mergeConflictStrategy: "ai-only",
directMergeCommitStrategy: "auto",
worktreeRebaseBeforeMerge: false,
worktreeRebaseLocalBase: false,
...overrides,

View File

@@ -163,7 +163,7 @@ export async function advanceIntegrationBranchRef(args: {
["merge-base", "--is-ancestor", expectedCurrentSha, newSha],
rootDir,
);
} catch (error: unknown) {
} catch (_error: unknown) {
const diagnostic = `newSha ${newSha} is not a descendant of ${expectedCurrentSha} on ${ref}`;
await emitRefAdvance({
succeeded: false,

View File

@@ -5824,7 +5824,7 @@ function resolveDirectMergeCommitStrategy(
return { strategy: promptOverride, source: "prompt" };
}
return {
strategy: settings.directMergeCommitStrategy ?? "auto",
strategy: settings.directMergeCommitStrategy ?? "always-squash",
source: "project",
};
}