feat(core): add mergeAdvanceAutoSync project setting
Schema for what the merger should do in other worktrees still checked out on the integration branch when it advances the branch ref. Modes: off — legacy (user pulls manually) ff-only — fast-forward only when other worktree is clean stash-and-ff — Smart Pull pipeline (default) Threads through DEFAULT_PROJECT_SETTINGS, PROJECT_SETTINGS_KEYS (auto via Object.keys), the docs settings table, and parity + persistence tests. Merger consumption lands in the follow-up engine change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
14
.changeset/merge-advance-auto-sync-setting.md
Normal file
14
.changeset/merge-advance-auto-sync-setting.md
Normal file
@@ -0,0 +1,14 @@
|
||||
---
|
||||
"@fusion/core": minor
|
||||
---
|
||||
|
||||
feat(core): add `mergeAdvanceAutoSync` project setting (`"off" | "ff-only" | "stash-and-ff"`)
|
||||
|
||||
Adds the schema for a new project setting that controls what happens in **other** worktrees still checked out on the integration branch when the merger advances the branch ref. Previously the merger only updated `refs/heads/<branch>` and left every other checkout's index and working tree pinned at the old tip, so `git status` in the user's project-root checkout reported the new commits as inverted "staged changes to be committed."
|
||||
|
||||
Modes (default `"stash-and-ff"`):
|
||||
- `"off"` — preserve the legacy behavior; user must `git pull` or click the Merge Advance Notice banner Pull button.
|
||||
- `"ff-only"` — auto-fast-forward only clean worktrees; dirty worktrees stay untouched and the banner still surfaces.
|
||||
- `"stash-and-ff"` — run the Smart Pull pipeline (stash → fast-forward → pop). Pop conflicts emit `merge:auto-sync` audit events with `outcome: "stash-pop-conflict"` and surface through the existing dashboard stash-conflict modal.
|
||||
|
||||
Schema-only in this changeset; the merger hook that consumes the setting lands in the follow-up engine change.
|
||||
@@ -203,6 +203,7 @@ Defaults from `DEFAULT_PROJECT_SETTINGS`; key scope from `PROJECT_SETTINGS_KEYS`
|
||||
| `mergeStrategy` | `"direct" \| "pull-request"` | `"direct"` | Completion mode (local direct merge vs PR-first). |
|
||||
| `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. |
|
||||
| `mergeAdvanceAutoSync` | `"off" \| "ff-only" \| "stash-and-ff"` | `"stash-and-ff"` | After the merger advances the integration-branch ref, what to do in **other** worktrees still on that branch (typically your project-root checkout). `off` leaves them alone; users must `git pull` or click the Merge Advance Notice banner's Pull button to bring their checkout forward — this is the surprise behavior that made `git status` look like the merge had been reverted. `ff-only` auto-fast-forwards only when the other worktree's index and working tree are clean; dirty worktrees stay untouched and the banner still surfaces for manual pull. `stash-and-ff` (default) runs the Smart Pull pipeline (stash → fast-forward → pop) so local edits survive across the auto-sync. Pop conflicts emit `merge:auto-sync` audit events with `outcome: "stash-pop-conflict"` and surface through the dashboard's existing stash-conflict modal. Only applies to direct merges. |
|
||||
| `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). |
|
||||
| `prerebaseHotFiles` | `string[]` | `[`"AGENTS.md"`, `"packages/core/src/store.ts"`, `"packages/core/src/db.ts"`, `"packages/engine/src/executor.ts"`, `"packages/engine/src/scheduler.ts"`, `"packages/engine/src/merger.ts"`, `"packages/dashboard/app/styles.css"`]` | Exact-path trigger list for auto-prerebase. If any listed file appears in `<task.baseCommitSha>..localMainHead`, merger runs prerebase first, then continues through the existing Stage 1/2 cascade. Empty array disables hot-file triggering. |
|
||||
|
||||
@@ -157,6 +157,12 @@ describe("settings key parity", () => {
|
||||
expect(isGlobalSettingsKey("directMergeCommitStrategy")).toBe(false);
|
||||
});
|
||||
|
||||
it("defaults mergeAdvanceAutoSync to stash-and-ff and keeps it project-scoped", () => {
|
||||
expect(DEFAULT_PROJECT_SETTINGS.mergeAdvanceAutoSync).toBe("stash-and-ff");
|
||||
expect(isProjectSettingsKey("mergeAdvanceAutoSync")).toBe(true);
|
||||
expect(isGlobalSettingsKey("mergeAdvanceAutoSync")).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps integrationBranch project-scoped", () => {
|
||||
expect(DEFAULT_PROJECT_SETTINGS.integrationBranch).toBeUndefined();
|
||||
expect(isProjectSettingsKey("integrationBranch")).toBe(true);
|
||||
|
||||
@@ -111,6 +111,15 @@ describe("TaskStore", () => {
|
||||
const settings = await harness.store().getSettings();
|
||||
expect(settings.directMergeCommitStrategy).toBe("always-rebase");
|
||||
});
|
||||
|
||||
it("defaults mergeAdvanceAutoSync to stash-and-ff and persists updates", async () => {
|
||||
const defaults = await harness.store().getSettings();
|
||||
expect(defaults.mergeAdvanceAutoSync).toBe("stash-and-ff");
|
||||
|
||||
await harness.store().updateSettings({ mergeAdvanceAutoSync: "off" });
|
||||
const settings = await harness.store().getSettings();
|
||||
expect(settings.mergeAdvanceAutoSync).toBe("off");
|
||||
});
|
||||
});
|
||||
|
||||
// ── Planning/Validator Model Settings ────────────────────────────
|
||||
|
||||
@@ -197,6 +197,7 @@ export const DEFAULT_PROJECT_SETTINGS = {
|
||||
mergeStrategy: "direct",
|
||||
directMergeCommitStrategy: "always-squash",
|
||||
mergeIntegrationWorktree: "reuse-task-worktree",
|
||||
mergeAdvanceAutoSync: "stash-and-ff",
|
||||
integrationBranch: undefined,
|
||||
requirePrApproval: false,
|
||||
pushAfterMerge: false,
|
||||
|
||||
@@ -213,6 +213,12 @@ export function normalizeMergeIntegrationWorktreeMode(
|
||||
|
||||
export const DIRECT_MERGE_COMMIT_STRATEGIES = ["auto", "always-squash", "always-rebase"] as const;
|
||||
export type DirectMergeCommitStrategy = (typeof DIRECT_MERGE_COMMIT_STRATEGIES)[number];
|
||||
|
||||
export const MERGE_ADVANCE_AUTO_SYNC_MODES = ["off", "ff-only", "stash-and-ff"] as const;
|
||||
export type MergeAdvanceAutoSyncMode = (typeof MERGE_ADVANCE_AUTO_SYNC_MODES)[number];
|
||||
export function normalizeMergeAdvanceAutoSyncMode(value: unknown): MergeAdvanceAutoSyncMode {
|
||||
return value === "off" || value === "ff-only" || value === "stash-and-ff" ? value : "stash-and-ff";
|
||||
}
|
||||
/** How merge conflicts are resolved when the AI agent can't (or shouldn't) decide.
|
||||
*
|
||||
* Both `smart-*` strategies share the same cascade: pre-merge fetch +
|
||||
@@ -2743,6 +2749,21 @@ export interface ProjectSettings {
|
||||
* - "cwd-main": legacy alias for "cwd-integration-branch" (normalized at read time)
|
||||
* Auto-merge only; manual/direct merge entrypoints outside auto-merge are unchanged. */
|
||||
mergeIntegrationWorktree?: MergeIntegrationWorktreeMode;
|
||||
/** After the merger advances the integration branch ref, what to do in *other*
|
||||
* worktrees that have the same branch checked out (typically the user's
|
||||
* project-root checkout that sat at the previous tip).
|
||||
* - "off": do nothing; the user must `git pull` (or click the Merge Advance
|
||||
* Notice banner's Pull button) to bring their checkout forward.
|
||||
* - "ff-only": fast-forward the other worktree only when its index and
|
||||
* working tree are clean. Dirty worktrees are left alone and the banner
|
||||
* still surfaces for manual handling.
|
||||
* - "stash-and-ff" (default): run the Smart Pull pipeline
|
||||
* (stash → fast-forward → pop) so local edits survive across the
|
||||
* auto-sync. Pop conflicts surface as `merge:auto-sync` audit events with
|
||||
* `outcome: "stash-pop-conflict"` and are forwarded to the dashboard's
|
||||
* existing stash-conflict modal.
|
||||
* Only applies to direct merges (`mergeStrategy === "direct"`). */
|
||||
mergeAdvanceAutoSync?: MergeAdvanceAutoSyncMode;
|
||||
/** Explicit integration branch name (e.g. `main`, `master`, `trunk`, `develop`).
|
||||
* Resolution order: `integrationBranch` → `baseBranch` → `origin/HEAD` → `main`.
|
||||
* This value is used as the `projectDefaultBranch` input to `resolveTaskMergeTarget`. */
|
||||
|
||||
Reference in New Issue
Block a user