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:
gsxdsm
2026-05-23 14:19:50 -07:00
parent 6083de214a
commit a201f56f09
6 changed files with 52 additions and 0 deletions

View File

@@ -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);

View File

@@ -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 ────────────────────────────

View File

@@ -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,

View File

@@ -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`. */