diff --git a/src/resources/extensions/gsd/auto.ts b/src/resources/extensions/gsd/auto.ts index 17cb3102e..71676aa53 100644 --- a/src/resources/extensions/gsd/auto.ts +++ b/src/resources/extensions/gsd/auto.ts @@ -250,9 +250,9 @@ const STATE_REBUILD_MIN_INTERVAL_MS = 30_000; export function shouldUseWorktreeIsolation(): boolean { const prefs = loadEffectiveGSDPreferences()?.preferences?.git; - if (prefs?.isolation === "none") return false; - if (prefs?.isolation === "branch") return false; - return true; // default: worktree + if (prefs?.isolation === "worktree") return true; + // Default is false — worktree isolation requires explicit opt-in + return false; } /** Crash recovery prompt — set by startAuto, consumed by the main loop */ diff --git a/src/resources/extensions/gsd/doctor-checks.ts b/src/resources/extensions/gsd/doctor-checks.ts index 20fee0fe0..0b0d05033 100644 --- a/src/resources/extensions/gsd/doctor-checks.ts +++ b/src/resources/extensions/gsd/doctor-checks.ts @@ -25,7 +25,7 @@ export async function checkGitHealth( issues: DoctorIssue[], fixesApplied: string[], shouldFix: (code: DoctorIssueCode) => boolean, - isolationMode: "none" | "worktree" | "branch" = "worktree", + isolationMode: "none" | "worktree" | "branch" = "none", ): Promise { // Degrade gracefully if not a git repo if (!nativeIsRepo(basePath)) { diff --git a/src/resources/extensions/gsd/doctor.ts b/src/resources/extensions/gsd/doctor.ts index 5c301bd79..f723edd0a 100644 --- a/src/resources/extensions/gsd/doctor.ts +++ b/src/resources/extensions/gsd/doctor.ts @@ -360,8 +360,8 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean; // Git health checks — timed const t0git = Date.now(); const isolationMode: "none" | "worktree" | "branch" = options?.isolationMode ?? - (prefs?.preferences?.git?.isolation === "none" ? "none" : - prefs?.preferences?.git?.isolation === "branch" ? "branch" : "worktree"); + (prefs?.preferences?.git?.isolation === "worktree" ? "worktree" : + prefs?.preferences?.git?.isolation === "branch" ? "branch" : "none"); await checkGitHealth(basePath, issues, fixesApplied, shouldFix, isolationMode); const gitMs = Date.now() - t0git; diff --git a/src/resources/extensions/gsd/preferences-types.ts b/src/resources/extensions/gsd/preferences-types.ts index b57e2514f..9b0083866 100644 --- a/src/resources/extensions/gsd/preferences-types.ts +++ b/src/resources/extensions/gsd/preferences-types.ts @@ -34,7 +34,7 @@ export const MODE_DEFAULTS: Record> = { push_branches: false, pre_merge_check: false, merge_strategy: "squash", - isolation: "worktree", + isolation: "none", }, unique_milestone_ids: false, }, @@ -44,7 +44,7 @@ export const MODE_DEFAULTS: Record> = { push_branches: true, pre_merge_check: true, merge_strategy: "squash", - isolation: "worktree", + isolation: "none", }, unique_milestone_ids: true, }, diff --git a/src/resources/extensions/gsd/preferences.ts b/src/resources/extensions/gsd/preferences.ts index 509ac7f61..df207d1f8 100644 --- a/src/resources/extensions/gsd/preferences.ts +++ b/src/resources/extensions/gsd/preferences.ts @@ -497,13 +497,17 @@ export function resolvePreDispatchHooks(): PreDispatchHookConfig[] { /** * Resolve the effective git isolation mode from preferences. - * Returns "worktree" (default), "branch", or "none". + * Returns "none" (default), "worktree", or "branch". + * + * Default is "none" so GSD works out of the box without preferences.md. + * Worktree isolation requires explicit opt-in because it depends on git + * branch infrastructure that must be set up before use. */ export function getIsolationMode(): "none" | "worktree" | "branch" { const prefs = loadEffectiveGSDPreferences()?.preferences?.git; - if (prefs?.isolation === "none") return "none"; + if (prefs?.isolation === "worktree") return "worktree"; if (prefs?.isolation === "branch") return "branch"; - return "worktree"; // default + return "none"; // default — no isolation, work on current branch } export function resolveParallelConfig(prefs: GSDPreferences | undefined): import("./types.js").ParallelConfig { diff --git a/src/resources/extensions/gsd/tests/none-mode-gates.test.ts b/src/resources/extensions/gsd/tests/none-mode-gates.test.ts index 400288348..bdadcfc1d 100644 --- a/src/resources/extensions/gsd/tests/none-mode-gates.test.ts +++ b/src/resources/extensions/gsd/tests/none-mode-gates.test.ts @@ -70,18 +70,20 @@ try { } }); -// Test 4: shouldUseWorktreeIsolation returns true for no prefs (default) +// Test 4: shouldUseWorktreeIsolation returns false for no prefs (default: none) +// Worktree isolation requires explicit opt-in — default is "none" so GSD +// works out of the box without preferences.md (#2480). // Skip if global prefs exist — they override the default and this test // cannot control ~/.gsd/preferences.md. -test('shouldUseWorktreeIsolation returns true for no prefs (default)', () => { +test('shouldUseWorktreeIsolation returns false for no prefs (default: none)', () => { const globalPrefsExist = existsSync(join(homedir(), ".gsd", "preferences.md")) || existsSync(join(homedir(), ".gsd", "PREFERENCES.md")); if (!globalPrefsExist) { try { removeRunnerPreferences(); // ensure no prefs file invalidateAllCaches(); - assert.deepStrictEqual(shouldUseWorktreeIsolation(), true, "shouldUseWorktreeIsolation() with no prefs (default worktree)"); + assert.deepStrictEqual(shouldUseWorktreeIsolation(), false, "shouldUseWorktreeIsolation() with no prefs (default none)"); } finally { invalidateAllCaches(); } @@ -89,6 +91,21 @@ test('shouldUseWorktreeIsolation returns true for no prefs (default)', () => { } }); +// Test 5: getIsolationMode returns "none" when no preferences.md exists (#2480) +test('getIsolationMode returns "none" with no prefs (default)', () => { + const globalPrefsExist = existsSync(join(homedir(), ".gsd", "preferences.md")) + || existsSync(join(homedir(), ".gsd", "PREFERENCES.md")); + if (!globalPrefsExist) { + try { + removeRunnerPreferences(); + invalidateAllCaches(); + assert.deepStrictEqual(getIsolationMode(), "none", "getIsolationMode() with no prefs defaults to none"); + } finally { + invalidateAllCaches(); + } + } +}); + test('getIsolationMode returns "none" with none prefs', () => { try { writeRunnerPreferences("none"); @@ -100,6 +117,28 @@ try { } }); +test('getIsolationMode returns "worktree" with worktree prefs', () => { +try { + writeRunnerPreferences("worktree"); + invalidateAllCaches(); + assert.deepStrictEqual(getIsolationMode(), "worktree", "getIsolationMode() with worktree prefs"); +} finally { + removeRunnerPreferences(); + invalidateAllCaches(); +} +}); + +test('getIsolationMode returns "branch" with branch prefs', () => { +try { + writeRunnerPreferences("branch"); + invalidateAllCaches(); + assert.deepStrictEqual(getIsolationMode(), "branch", "getIsolationMode() with branch prefs"); +} finally { + removeRunnerPreferences(); + invalidateAllCaches(); +} +}); + test('getActiveAutoWorktreeContext returns null at baseline', () => { assert.deepStrictEqual(getActiveAutoWorktreeContext(), null, "getActiveAutoWorktreeContext() returns null without enterAutoWorktree()"); }); diff --git a/src/resources/extensions/gsd/tests/preferences.test.ts b/src/resources/extensions/gsd/tests/preferences.test.ts index 26ac7261d..8c8e3d198 100644 --- a/src/resources/extensions/gsd/tests/preferences.test.ts +++ b/src/resources/extensions/gsd/tests/preferences.test.ts @@ -41,18 +41,16 @@ test("git.merge_to_main produces deprecation warning", () => { }); -test("getIsolationMode defaults to worktree when preferences have no isolation setting", () => { +test("getIsolationMode defaults to none when preferences have no isolation setting", () => { // Validate the default via validatePreferences: when no isolation is set, - // preferences.git.isolation is undefined, and getIsolationMode returns "worktree". - // We test the function's logic by verifying its documented default. + // preferences.git.isolation is undefined, and getIsolationMode returns "none". + // Default changed from "worktree" to "none" so GSD works out of the box + // without preferences.md (#2480). const { preferences } = validatePreferences({}); assert.equal(preferences.git?.isolation, undefined, "no isolation in empty prefs"); - // The function returns "worktree" when prefs?.git?.isolation is not "none" or "branch" - // This is a compile-time-verifiable truth from the function body — test it directly - // by constructing the same conditions getIsolationMode checks. const isolation = preferences.git?.isolation; - const expected = isolation === "none" ? "none" : isolation === "branch" ? "branch" : "worktree"; - assert.equal(expected, "worktree", "default isolation mode is worktree"); + const expected = isolation === "worktree" ? "worktree" : isolation === "branch" ? "branch" : "none"; + assert.equal(expected, "none", "default isolation mode is none"); }); // ── Mode defaults ──────────────────────────────────────────────────────────── @@ -63,7 +61,7 @@ test("solo mode applies correct defaults", () => { assert.equal(result.git?.push_branches, false); assert.equal(result.git?.pre_merge_check, false); assert.equal(result.git?.merge_strategy, "squash"); - assert.equal(result.git?.isolation, "worktree"); + assert.equal(result.git?.isolation, "none"); assert.equal(result.unique_milestone_ids, false); });