From de11114d59ab879389666fe98c7ce4c722a670c7 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Sat, 21 Mar 2026 09:20:14 -0400 Subject: [PATCH] fix: preserve subdirectory CWD in monorepo worktrees (#1283) When running GSD from a monorepo subdirectory inside a git worktree, resolveWorktreeRoot() resolved to the worktree root, discarding the subdirectory where .planning/ lives. All commands failed with "No ROADMAP.md found" even though the planning structure existed. Now check if CWD already contains .planning/ before worktree resolution. If it does, the CWD is already the correct project root and worktree resolution is skipped. Co-Authored-By: Claude Opus 4.6 (1M context) --- get-shit-done/bin/gsd-tools.cjs | 12 +++++++---- tests/core.test.cjs | 38 +++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/get-shit-done/bin/gsd-tools.cjs b/get-shit-done/bin/gsd-tools.cjs index 18fc219df..5cb1fda53 100755 --- a/get-shit-done/bin/gsd-tools.cjs +++ b/get-shit-done/bin/gsd-tools.cjs @@ -174,11 +174,15 @@ async function main() { error(`Invalid --cwd: ${cwd}`); } - // Resolve worktree root: in a linked worktree, .planning/ lives in the main worktree + // Resolve worktree root: in a linked worktree, .planning/ lives in the main worktree. + // However, in monorepo worktrees where the subdirectory itself owns .planning/, + // skip worktree resolution — the CWD is already the correct project root. const { resolveWorktreeRoot } = require('./lib/core.cjs'); - const worktreeRoot = resolveWorktreeRoot(cwd); - if (worktreeRoot !== cwd) { - cwd = worktreeRoot; + if (!fs.existsSync(path.join(cwd, '.planning'))) { + const worktreeRoot = resolveWorktreeRoot(cwd); + if (worktreeRoot !== cwd) { + cwd = worktreeRoot; + } } const rawIndex = args.indexOf('--raw'); diff --git a/tests/core.test.cjs b/tests/core.test.cjs index a1b31074e..b6daaf8fb 100644 --- a/tests/core.test.cjs +++ b/tests/core.test.cjs @@ -1039,6 +1039,44 @@ describe('resolveWorktreeRoot', () => { }); }); +// ─── monorepo worktree CWD preservation (#1283) ───────────────────────────── + +describe('monorepo worktree CWD preservation', () => { + const { resolveWorktreeRoot } = require('../get-shit-done/bin/lib/core.cjs'); + + test('CWD with .planning/ skips worktree resolution (monorepo subdirectory)', () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-monorepo-wt-')); + const subDir = path.join(tmpDir, 'service-alpha'); + fs.mkdirSync(path.join(subDir, '.planning'), { recursive: true }); + try { + let cwd = subDir; + if (!fs.existsSync(path.join(cwd, '.planning'))) { + const worktreeRoot = resolveWorktreeRoot(cwd); + if (worktreeRoot !== cwd) cwd = worktreeRoot; + } + assert.strictEqual(cwd, subDir, 'CWD with .planning/ must not be overridden by worktree resolution'); + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + }); + + test('CWD without .planning/ still goes through worktree resolution', () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-monorepo-wt-')); + try { + let cwd = tmpDir; + let worktreeResolutionCalled = false; + if (!fs.existsSync(path.join(cwd, '.planning'))) { + worktreeResolutionCalled = true; + const worktreeRoot = resolveWorktreeRoot(cwd); + if (worktreeRoot !== cwd) cwd = worktreeRoot; + } + assert.ok(worktreeResolutionCalled, 'worktree resolution must be called when .planning/ is absent'); + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + }); +}); + // ─── withPlanningLock ──────────────────────────────────────────────────────── describe('withPlanningLock', () => {