From c16b874aaaa389343145d7f8fa7db66d673bff3a Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Tue, 24 Mar 2026 15:59:42 -0400 Subject: [PATCH] fix: findProjectRoot returns startDir when it already has .planning/ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When cwd is a git repo inside a GSD workspace, findProjectRoot() walked up and returned the workspace parent (which also has .planning/) instead of the cwd itself. This caused all init commands to resolve project_root to the workspace root, making phase/roadmap lookups fail with "Phase not found" errors. The fix adds an early return: if startDir already contains a .planning/ directory, it is the project root — no need to walk up to a parent. Fixes #1362 Co-Authored-By: Claude Opus 4.6 (1M context) --- get-shit-done/bin/lib/core.cjs | 7 +++++++ tests/core.test.cjs | 27 +++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/get-shit-done/bin/lib/core.cjs b/get-shit-done/bin/lib/core.cjs index b6d9edc3b..b40db3d86 100644 --- a/get-shit-done/bin/lib/core.cjs +++ b/get-shit-done/bin/lib/core.cjs @@ -58,6 +58,13 @@ function findProjectRoot(startDir) { const root = path.parse(resolved).root; const homedir = require('os').homedir(); + // If startDir already contains .planning/, it IS the project root. + // Do not walk up to a parent workspace that also has .planning/ (#1362). + const ownPlanning = path.join(resolved, '.planning'); + if (fs.existsSync(ownPlanning) && fs.statSync(ownPlanning).isDirectory()) { + return startDir; + } + // Check if startDir or any of its ancestors (up to AND including the // candidate project root) contains a .git directory. This handles both // `backend/` (direct sub-repo) and `backend/src/modules/` (nested inside), diff --git a/tests/core.test.cjs b/tests/core.test.cjs index c4a234a9b..92e3eefcd 100644 --- a/tests/core.test.cjs +++ b/tests/core.test.cjs @@ -1509,6 +1509,33 @@ describe('findProjectRoot', () => { assert.strictEqual(findProjectRoot(projectRoot), projectRoot); }); + + test('does not walk past child with own .planning/ to workspace parent (#1362)', () => { + // Workspace layout: parent has .planning/, child git repo also has .planning/ + // findProjectRoot should return the child (startDir), not the parent + fs.mkdirSync(path.join(projectRoot, '.planning'), { recursive: true }); + + const childRepo = path.join(projectRoot, 'authenticator'); + fs.mkdirSync(path.join(childRepo, '.planning'), { recursive: true }); + fs.mkdirSync(path.join(childRepo, '.git'), { recursive: true }); + + assert.strictEqual(findProjectRoot(childRepo), childRepo); + }); + + test('does not walk past nested dir whose git root has .planning/ (#1362)', () => { + // Workspace layout: parent has .planning/, child git repo also has .planning/ + // cwd is deep inside child — should resolve to child root, not workspace root + fs.mkdirSync(path.join(projectRoot, '.planning'), { recursive: true }); + + const childRepo = path.join(projectRoot, 'authenticator'); + fs.mkdirSync(path.join(childRepo, '.planning'), { recursive: true }); + fs.mkdirSync(path.join(childRepo, '.git'), { recursive: true }); + + const deepDir = path.join(childRepo, 'src', 'lib'); + fs.mkdirSync(deepDir, { recursive: true }); + + assert.strictEqual(findProjectRoot(deepDir), childRepo); + }); }); // ─── reapStaleTempFiles ─────────────────────────────────────────────────────