diff --git a/src/resources/extensions/gsd/auto-start.ts b/src/resources/extensions/gsd/auto-start.ts index 8751d51c7..a3d0232d5 100644 --- a/src/resources/extensions/gsd/auto-start.ts +++ b/src/resources/extensions/gsd/auto-start.ts @@ -44,6 +44,9 @@ import { nativeInit, nativeAddAll, nativeCommit, + nativeGetCurrentBranch, + nativeDetectMainBranch, + nativeCheckoutBranch, } from "./native-git-bridge.js"; import { GitServiceImpl } from "./git-service.js"; import { @@ -528,6 +531,22 @@ export async function bootstrapAutoSession( setActiveMilestoneId(base, s.currentMilestoneId); } + // Guard against stale milestone branch when isolation:none (#3613). + // A prior session with isolation:branch/worktree may have left HEAD on + // milestone/. Auto-checkout back to the integration branch. + if (getIsolationMode() === "none" && nativeIsRepo(base)) { + try { + const currentBranch = nativeGetCurrentBranch(base); + if (currentBranch.startsWith("milestone/")) { + const integrationBranch = nativeDetectMainBranch(base); + nativeCheckoutBranch(base, integrationBranch); + logWarning("bootstrap", `Returned to "${integrationBranch}" — HEAD was on stale milestone branch "${currentBranch}" (isolation: none does not use milestone branches).`); + } + } catch (err) { + logWarning("bootstrap", `Could not auto-checkout from stale milestone branch: ${err instanceof Error ? err.message : String(err)}`); + } + } + // ── Auto-worktree setup ── s.originalBasePath = base; diff --git a/src/resources/extensions/gsd/tests/isolation-none-branch-guard.test.ts b/src/resources/extensions/gsd/tests/isolation-none-branch-guard.test.ts new file mode 100644 index 000000000..5acf71583 --- /dev/null +++ b/src/resources/extensions/gsd/tests/isolation-none-branch-guard.test.ts @@ -0,0 +1,62 @@ +/** + * Regression test for #3675 — isolation:none stale branch guard + * + * When switching from isolation:branch/worktree to isolation:none, HEAD + * could remain on a milestone/ branch. The fix in auto-start.ts + * detects this and auto-checks out to the integration branch. + * + * This structural test verifies the milestone/ branch check exists + * in auto-start.ts. + */ + +import { describe, test } from 'node:test'; +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'node:path'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const source = readFileSync(join(__dirname, '..', 'auto-start.ts'), 'utf-8'); + +describe('isolation:none stale branch guard (#3675)', () => { + test('checks for milestone/ branch prefix', () => { + assert.match(source, /startsWith\(["']milestone\//, + 'auto-start should check for milestone/ branch prefix'); + }); + + test('imports nativeGetCurrentBranch', () => { + assert.match(source, /nativeGetCurrentBranch/, + 'auto-start should import nativeGetCurrentBranch'); + }); + + test('imports nativeDetectMainBranch', () => { + assert.match(source, /nativeDetectMainBranch/, + 'auto-start should import nativeDetectMainBranch'); + }); + + test('imports nativeCheckoutBranch', () => { + assert.match(source, /nativeCheckoutBranch/, + 'auto-start should import nativeCheckoutBranch'); + }); + + test('guard is conditional on isolation mode "none"', () => { + assert.match(source, /getIsolationMode\(\)\s*===\s*["']none["']/, + 'guard should only activate when isolation mode is "none"'); + }); + + test('calls nativeCheckoutBranch to return to integration branch', () => { + assert.match(source, /nativeCheckoutBranch\(base,\s*integrationBranch\)/, + 'should checkout to the integration branch'); + }); + + test('guard is wrapped in try-catch (non-fatal)', () => { + // Find the milestone/ check and verify it is inside a try block + const milestoneIdx = source.indexOf('startsWith("milestone/")'); + assert.ok(milestoneIdx > 0, 'milestone/ check should exist'); + const before = source.slice(Math.max(0, milestoneIdx - 500), milestoneIdx); + assert.match(before, /try\s*\{/, + 'milestone branch guard should be inside a try block'); + }); +});