From bd863e3e211f22a94e4737ee51ba87372c6d55fb Mon Sep 17 00:00:00 2001 From: Jeremy Date: Sat, 4 Apr 2026 15:25:26 -0500 Subject: [PATCH] fix(gsd): resolve steer overrides to worktree path when worktree is active MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit handleSteer used process.cwd() as the base path for appendOverride, which writes to project/.gsd/OVERRIDES.md. When auto-mode runs in a worktree, it reads from worktree/.gsd/ — so overrides written from a second terminal were never seen by the agent. Now checks for an active worktree via getAutoWorktreePath and writes the override there when one exists, falling back to the project root when no worktree is active. Closes #3476 --- .../extensions/gsd/commands-handlers.ts | 10 ++- .../gsd/tests/steer-worktree-path.test.ts | 66 +++++++++++++++++++ 2 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 src/resources/extensions/gsd/tests/steer-worktree-path.test.ts diff --git a/src/resources/extensions/gsd/commands-handlers.ts b/src/resources/extensions/gsd/commands-handlers.ts index e87e89bbc..57a701e5c 100644 --- a/src/resources/extensions/gsd/commands-handlers.ts +++ b/src/resources/extensions/gsd/commands-handlers.ts @@ -21,6 +21,7 @@ import { filterDoctorIssues, } from "./doctor.js"; import { isAutoActive } from "./auto.js"; +import { getAutoWorktreePath } from "./auto-worktree.js"; import { projectRoot } from "./commands/context.js"; import { loadPrompt } from "./prompt-loader.js"; @@ -222,7 +223,14 @@ export async function handleSteer(change: string, ctx: ExtensionCommandContext, const sid = state.activeSlice?.id ?? "none"; const tid = state.activeTask?.id ?? "none"; const appliedAt = `${mid}/${sid}/${tid}`; - await appendOverride(basePath, change, appliedAt); + + // Resolve the correct target path: if a worktree is active for the current + // milestone, write the override there so the auto-mode agent sees it. + // Without this, steering from a second terminal writes to the project root + // .gsd/ while the agent reads from the worktree .gsd/ — the override is lost. + const wtPath = mid !== "none" ? getAutoWorktreePath(basePath, mid) : null; + const targetPath = wtPath ?? basePath; + await appendOverride(targetPath, change, appliedAt); if (isAutoActive()) { pi.sendMessage({ diff --git a/src/resources/extensions/gsd/tests/steer-worktree-path.test.ts b/src/resources/extensions/gsd/tests/steer-worktree-path.test.ts new file mode 100644 index 000000000..45ce5fa17 --- /dev/null +++ b/src/resources/extensions/gsd/tests/steer-worktree-path.test.ts @@ -0,0 +1,66 @@ +// GSD Extension - Steer Worktree Path Resolution Test +// Regression test for #3476: /gsd steer must write overrides to the worktree .gsd/, +// not the project root .gsd/, when a worktree is active. + +import { describe, test, beforeEach, afterEach } from "node:test"; +import assert from "node:assert/strict"; +import { mkdtempSync, mkdirSync, existsSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; +import { appendOverride, loadActiveOverrides } from "../files.ts"; + +describe("steer worktree path resolution (#3476)", () => { + let projectRoot: string; + let worktreePath: string; + + beforeEach(() => { + projectRoot = mkdtempSync(join(tmpdir(), "gsd-steer-wt-")); + mkdirSync(join(projectRoot, ".gsd"), { recursive: true }); + + // Simulate a worktree with its own .gsd directory + worktreePath = join(projectRoot, ".gsd", "worktrees", "M001"); + mkdirSync(join(worktreePath, ".gsd"), { recursive: true }); + }); + + afterEach(() => { + rmSync(projectRoot, { recursive: true, force: true }); + }); + + test("appendOverride writes to worktree .gsd/ when worktree path is used", async () => { + await appendOverride(worktreePath, "Use Postgres instead of SQLite", "M001/S01/T01"); + + // Override should be in the worktree .gsd/ + const wtOverrides = join(worktreePath, ".gsd", "OVERRIDES.md"); + assert.ok(existsSync(wtOverrides), "override file exists in worktree .gsd/"); + + const content = readFileSync(wtOverrides, "utf-8"); + assert.ok(content.includes("Use Postgres instead of SQLite"), "override content is correct"); + + // Override should NOT be in the project root .gsd/ + const rootOverrides = join(projectRoot, ".gsd", "OVERRIDES.md"); + assert.ok(!existsSync(rootOverrides), "no override file in project root .gsd/"); + }); + + test("loadActiveOverrides reads from worktree .gsd/ when worktree path is used", async () => { + await appendOverride(worktreePath, "Switch to JWT auth", "M001/S02/T01"); + + // Loading from worktree should find the override + const wtOverrides = await loadActiveOverrides(worktreePath); + assert.equal(wtOverrides.length, 1, "one active override in worktree"); + assert.equal(wtOverrides[0].change, "Switch to JWT auth"); + + // Loading from project root should find nothing + const rootOverrides = await loadActiveOverrides(projectRoot); + assert.equal(rootOverrides.length, 0, "no overrides in project root"); + }); + + test("appendOverride falls back to project root when no worktree exists", async () => { + await appendOverride(projectRoot, "Use Redis cache", "M001/S01/T01"); + + const rootOverrides = join(projectRoot, ".gsd", "OVERRIDES.md"); + assert.ok(existsSync(rootOverrides), "override file exists in project root .gsd/"); + + const content = readFileSync(rootOverrides, "utf-8"); + assert.ok(content.includes("Use Redis cache"), "override content is correct"); + }); +});