From 2488a686a454c72a2eeadec70e82110c11751223 Mon Sep 17 00:00:00 2001 From: Jeremy McSpadden Date: Wed, 25 Mar 2026 07:45:31 -0500 Subject: [PATCH] fix(gsd): downgrade isolation mode when worktree creation fails --- src/resources/extensions/gsd/auto/session.ts | 5 ++ .../gsd/tests/worktree-resolver.test.ts | 67 +++++++++++++++++++ .../extensions/gsd/worktree-resolver.ts | 31 +++++++++ 3 files changed, 103 insertions(+) diff --git a/src/resources/extensions/gsd/auto/session.ts b/src/resources/extensions/gsd/auto/session.ts index 16b94f2e1..e61298d3e 100644 --- a/src/resources/extensions/gsd/auto/session.ts +++ b/src/resources/extensions/gsd/auto/session.ts @@ -126,6 +126,10 @@ export class AutoSession { // ── Sidecar queue ───────────────────────────────────────────────────── sidecarQueue: SidecarItem[] = []; + // ── Isolation degradation ──────────────────────────────────────────── + /** Set to true when worktree creation fails; prevents merge of nonexistent branch. */ + isolationDegraded = false; + // ── Dispatch circuit breakers ────────────────────────────────────── rewriteAttemptCount = 0; @@ -217,6 +221,7 @@ export class AutoSession { this.pendingQuickTasks = []; this.sidecarQueue = []; this.rewriteAttemptCount = 0; + this.isolationDegraded = false; // Signal handler this.sigtermHandler = null; diff --git a/src/resources/extensions/gsd/tests/worktree-resolver.test.ts b/src/resources/extensions/gsd/tests/worktree-resolver.test.ts index 11718a263..c3a7f7aba 100644 --- a/src/resources/extensions/gsd/tests/worktree-resolver.test.ts +++ b/src/resources/extensions/gsd/tests/worktree-resolver.test.ts @@ -846,3 +846,70 @@ test("GitService is rebuilt with originalBasePath after exitMilestone", () => { assert.equal(gitServiceBasePath, "/project"); // project root, not worktree }); + +// ─── Isolation Degradation Tests (#2483) ────────────────────────────────── + +test("enterMilestone sets isolationDegraded when worktree creation throws (#2483)", () => { + const s = makeSession(); + const deps = makeDeps({ + getAutoWorktreePath: () => null, + createAutoWorktree: () => { + throw new Error("empty repo"); + }, + }); + const ctx = makeNotifyCtx(); + const resolver = new WorktreeResolver(s, deps); + + resolver.enterMilestone("M001", ctx); + + assert.equal(s.isolationDegraded, true); + assert.equal(s.basePath, "/project"); // unchanged — error recovery +}); + +test("enterMilestone is no-op when isolationDegraded is true (#2483)", () => { + const s = makeSession(); + s.isolationDegraded = true; + const deps = makeDeps(); + const ctx = makeNotifyCtx(); + const resolver = new WorktreeResolver(s, deps); + + resolver.enterMilestone("M001", ctx); + + assert.equal(s.basePath, "/project"); // unchanged + assert.equal(findCalls(deps.calls, "createAutoWorktree").length, 0); + assert.equal(findCalls(deps.calls, "enterAutoWorktree").length, 0); + assert.equal(findCalls(deps.calls, "shouldUseWorktreeIsolation").length, 0); +}); + +test("mergeAndExit is no-op when isolationDegraded is true (#2483)", () => { + const s = makeSession({ + basePath: "/project", + originalBasePath: "/project", + }); + s.isolationDegraded = true; + const deps = makeDeps({ + getIsolationMode: () => "worktree", + }); + const ctx = makeNotifyCtx(); + const resolver = new WorktreeResolver(s, deps); + + resolver.mergeAndExit("M001", ctx); + + assert.equal(findCalls(deps.calls, "mergeMilestoneToMain").length, 0); + assert.equal(findCalls(deps.calls, "teardownAutoWorktree").length, 0); + assert.equal(findCalls(deps.calls, "getIsolationMode").length, 0); + assert.ok( + ctx.messages.some( + (m) => m.level === "info" && m.msg.includes("isolation was degraded"), + ), + ); +}); + +test("isolationDegraded is reset by session.reset() (#2483)", () => { + const s = new AutoSession(); + s.isolationDegraded = true; + + s.reset(); + + assert.equal(s.isolationDegraded, false); +}); diff --git a/src/resources/extensions/gsd/worktree-resolver.ts b/src/resources/extensions/gsd/worktree-resolver.ts index 093899297..c245c4f95 100644 --- a/src/resources/extensions/gsd/worktree-resolver.ts +++ b/src/resources/extensions/gsd/worktree-resolver.ts @@ -148,6 +148,18 @@ export class WorktreeResolver { */ enterMilestone(milestoneId: string, ctx: NotifyCtx): void { this.validateMilestoneId(milestoneId); + + // If worktree creation failed earlier this session, skip all future attempts + if (this.s.isolationDegraded) { + debugLog("WorktreeResolver", { + action: "enterMilestone", + milestoneId, + skipped: true, + reason: "isolation-degraded", + }); + return; + } + if (!this.deps.shouldUseWorktreeIsolation()) { debugLog("WorktreeResolver", { action: "enterMilestone", @@ -197,6 +209,9 @@ export class WorktreeResolver { `Auto-worktree creation for ${milestoneId} failed: ${msg}. Continuing in project root.`, "warning", ); + // Degrade isolation for the rest of this session so mergeAndExit + // doesn't try to merge a nonexistent worktree branch (#2483) + this.s.isolationDegraded = true; // Do NOT update s.basePath — stay in project root } } @@ -281,6 +296,22 @@ export class WorktreeResolver { */ mergeAndExit(milestoneId: string, ctx: NotifyCtx): void { this.validateMilestoneId(milestoneId); + + // If worktree creation failed earlier, skip merge — work is on current branch (#2483) + if (this.s.isolationDegraded) { + debugLog("WorktreeResolver", { + action: "mergeAndExit", + milestoneId, + skipped: true, + reason: "isolation-degraded", + }); + ctx.notify( + `Skipping worktree merge for ${milestoneId} — isolation was degraded (worktree creation failed earlier). Work is on the current branch.`, + "info", + ); + return; + } + const mode = this.deps.getIsolationMode(); debugLog("WorktreeResolver", { action: "mergeAndExit",