fix(gsd): change default isolation mode from worktree to none (#2481)
When no preferences.md exists, getIsolationMode() and shouldUseWorktreeIsolation() defaulted to "worktree", which requires git branch infrastructure (milestone/<MID> branches) that isn't automatically set up. This caused milestone-complete to fail with "branch doesn't exist" when users worked directly on main without configuring preferences. Change the default to "none" (work on current branch) across all five locations: getIsolationMode(), shouldUseWorktreeIsolation(), MODE_DEFAULTS for solo/team, doctor.ts, and doctor-checks.ts. Worktree isolation is now explicit opt-in via preferences.md. Closes #2480
This commit is contained in:
parent
bf54012d1f
commit
7b162fe4ce
7 changed files with 64 additions and 23 deletions
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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<void> {
|
||||
// Degrade gracefully if not a git repo
|
||||
if (!nativeIsRepo(basePath)) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ export const MODE_DEFAULTS: Record<WorkflowMode, Partial<GSDPreferences>> = {
|
|||
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<WorkflowMode, Partial<GSDPreferences>> = {
|
|||
push_branches: true,
|
||||
pre_merge_check: true,
|
||||
merge_strategy: "squash",
|
||||
isolation: "worktree",
|
||||
isolation: "none",
|
||||
},
|
||||
unique_milestone_ids: true,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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()");
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue