docs(FN-4333): document postMergeAuditMode + add mode=off/warn integration tests

Adopts the documentation and integration test additions from the
Fusion executor's parallel work on FN-4333 (sleek-oak worktree). The
core hot-fix already landed in ccd718acd.

- AGENTS.md: update the post-squash audit paragraph to describe the
  three modes and the verified-tree short-circuit.
- docs/settings-reference.md: add postMergeAuditMode row.
- packages/engine/src/__tests__/merger-merge-lifecycle.test.ts: add
  integration tests covering postMergeAuditMode=off (skips audit) and
  postMergeAuditMode=warn (continues despite dirty findings).

Fusion-Task-Id: FN-4333
This commit is contained in:
Fusion
2026-05-13 09:53:45 -07:00
parent 936427053d
commit 121198852f
3 changed files with 65 additions and 1 deletions

View File

@@ -225,7 +225,7 @@ Two rules, learned the hard way (FN-2370 silently reverted three commits' work):
2. **Prefer rebase-and-merge over squash for branches spanning multiple substantive commits.** Fusion's direct merger now defaults `directMergeCommitStrategy="auto"`, which keeps squash for branches with 01 substantive commits but automatically switches multi-substantive branches to a history-preserving rebase/cherry-pick path. Use the project setting `directMergeCommitStrategy` or the task-level `**Direct Merge Commit Strategy:** auto|always-squash|always-rebase` PROMPT line when you need to force a route.
After any squash that auto-resolved conflicts, the merger now runs the post-squash audit as a blocking gate before auto-completing the task. Flagged merges stay in `in-review` for inspection, and only a clean audit proceeds to `done`.
After any squash that auto-resolved conflicts, the merger runs the post-squash audit before auto-completing the task. Outcome depends on `postMergeAuditMode`: `block` refuses completion on findings, `warn` logs findings and continues, and `off` skips the audit. For rebase-strategy merges, overlap-only findings are also auto-cleared when deterministic verification has already proven the merged tree.
Before those auto-resolved squash commits are written, the merger also runs a per-file diff-volume gate: it compares each file's staged squash delta against the branch's net delta vs its merge-base, and blocks the merge in `in-review` when a non-allowlisted file loses too much branch volume. This is the pre-commit guard against FN-3936-style silent drops where fallback resolution kept a branch's commit message but discarded the branch's main file edits.

View File

@@ -190,6 +190,7 @@ Defaults from `DEFAULT_PROJECT_SETTINGS`; key scope from `PROJECT_SETTINGS_KEYS`
| `mergeDiffVolumeThreshold` | `number` | `0.2` | Minimum staged-to-branch-net ratio allowed for a non-allowlisted file during auto-resolved squash finalization. Applied at merge time and clamped to `0..1`. |
| `mergeDiffVolumeAllowlist` | `string[]` | `[]` | Additional glob patterns skipped by the pre-commit diff-volume gate, beyond the built-in generated-file and lockfile allowlists. |
| `mergeStrategyOverlapBehavior` | `"flip-to-prefer-branch" \| "warn-only" \| "ignore"` | `"flip-to-prefer-branch"` | Safety control for `mergeConflictStrategy="smart-prefer-main"`. Before the Attempt 3 `-X ours` fallback, Fusion checks whether the task branch and recent `main` history overlap on the same files (30-commit lookback, matching the squash audit heuristics). `flip-to-prefer-branch` makes overlapping files prefer the task branch so hardening is not silently discarded (the FN-3936 class of regression). `warn-only` logs the overlap but keeps the legacy main-wins fallback. `ignore` disables the overlap guard and preserves legacy behavior exactly. |
| `postMergeAuditMode` | `"block" \| "warn" \| "off"` | `"block"` | Controls the post-merge audit gate. **Block** refuses to auto-complete merges that show duplicate-subject or touched-file overlap risks (most conservative). **Warn** logs findings but auto-completes the merge. **Off** skips the audit entirely. Default: Block. Switching to Warn or Off is recommended only if you trust your branches don't silently drop edits. Regardless of mode, rebase-strategy overlap-only findings are auto-cleared when deterministic merge verification has already proven the tree (FN-4333). |
### Per-task direct-merge override

View File

@@ -2806,6 +2806,69 @@ describe("aiMergeTask post-squash audit gate", () => {
expect(store.updateTask).toHaveBeenCalledWith("FN-050", { status: null });
});
it("skips the post-merge audit entirely when postMergeAuditMode=off (FN-4333)", async () => {
setupAutoResolvedMergeExecSync();
mockedAuditSquashMerge.mockResolvedValue({
strategy: "squash",
squashSha: "mergedcommit123",
parentSha: "parent123",
auditTargetLabel: "mergedcommit123",
squashSubject: "feat: squash merge",
lookback: 30,
branchSubjects: ["feat: duplicate subject"],
recentMainSubjects: ["feat: duplicate subject"],
duplicateSubjects: [{ type: "duplicate-subject", subject: "feat: duplicate subject" }],
touchedFiles: [],
touchedFileOverlaps: [],
findings: [{ type: "duplicate-subject", subject: "feat: duplicate subject" }],
issueCount: 1,
clean: false,
});
const store = createAuditStore({ postMergeAuditMode: "off" });
const result = await aiMergeTask(store, "/tmp/root", "FN-050");
expect(result.merged).toBe(true);
expect(mockedAuditSquashMerge).not.toHaveBeenCalled();
expect(
vi.mocked(store.appendAgentLog).mock.calls.some(([, message]) => String(message).includes("audit blocked auto-completion")),
).toBe(false);
});
it("continues when postMergeAuditMode=warn even with dirty audit findings (FN-4333)", async () => {
setupAutoResolvedMergeExecSync();
mockedAuditSquashMerge.mockResolvedValue({
strategy: "squash",
squashSha: "mergedcommit123",
parentSha: "parent123",
auditTargetLabel: "mergedcommit123",
squashSubject: "feat: squash merge",
lookback: 30,
branchSubjects: ["feat: duplicate subject"],
recentMainSubjects: ["feat: duplicate subject"],
duplicateSubjects: [{ type: "duplicate-subject", subject: "feat: duplicate subject" }],
touchedFiles: [],
touchedFileOverlaps: [],
findings: [{ type: "duplicate-subject", subject: "feat: duplicate subject" }],
issueCount: 1,
clean: false,
});
const store = createAuditStore({ postMergeAuditMode: "warn" });
const result = await aiMergeTask(store, "/tmp/root", "FN-050");
expect(result.merged).toBe(true);
expect(store.moveTask).toHaveBeenCalledWith("FN-050", "done");
expect(
vi.mocked(store.appendAgentLog).mock.calls.some(([, message, type]) => type === "tool_error" && String(message).includes("audit blocked auto-completion")),
).toBe(false);
expect(
vi.mocked(store.appendAgentLog).mock.calls.some(
([, message]) => String(message).includes("post-squash audit found 1 risk(s) — continuing"),
),
).toBe(true);
});
it("skips the post-squash audit when the squash merge is empty", async () => {
setupEmptySquashMergeExecSync();
const store = createAuditStore();