From 5cc2e24800329378c38c3ea66892c063d01bd226 Mon Sep 17 00:00:00 2001 From: Tibsfox Date: Mon, 6 Apr 2026 19:12:48 -0700 Subject: [PATCH 1/5] fix(gsd): handle deleted cwd in projectRoot to prevent ENOENT crash process.cwd() throws ENOENT when the worktree directory was deleted during a failed merge. Every /gsd command calls projectRoot(), causing the entire extension to crash. Now catches the ENOENT and falls back to HOME directory. Closes #3598 Co-Authored-By: Claude Opus 4.6 (1M context) --- src/resources/extensions/gsd/commands/context.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/resources/extensions/gsd/commands/context.ts b/src/resources/extensions/gsd/commands/context.ts index 7bbaa5790..f4a5aa423 100644 --- a/src/resources/extensions/gsd/commands/context.ts +++ b/src/resources/extensions/gsd/commands/context.ts @@ -13,7 +13,13 @@ export interface GsdDispatchContext { } export function projectRoot(): string { - const cwd = process.cwd(); + let cwd: string; + try { + cwd = process.cwd(); + } catch { + // cwd directory was deleted (e.g. worktree teardown) — fall back to HOME (#3598) + cwd = process.env.HOME ?? "/"; + } const root = resolveProjectRoot(cwd); if (root !== cwd) { assertSafeDirectory(cwd); From 902e3be6d8336636e8d8c128b9b32ab84be3679d Mon Sep 17 00:00:00 2001 From: Tibsfox Date: Mon, 6 Apr 2026 19:12:54 -0700 Subject: [PATCH 2/5] fix(gsd): validate main_branch preference exists before using in merge mergeMilestoneToMain used prefs.main_branch without checking if the branch exists in the repo. A stale preference (e.g. "master" when repo uses "main") caused merge failure. Now validates with nativeBranchExists before using the preference, falling through to nativeDetectMainBranch if invalid. Closes #3589 Co-Authored-By: Claude Opus 4.6 (1M context) --- src/resources/extensions/gsd/auto-worktree.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/resources/extensions/gsd/auto-worktree.ts b/src/resources/extensions/gsd/auto-worktree.ts index a0275619a..d2420257f 100644 --- a/src/resources/extensions/gsd/auto-worktree.ts +++ b/src/resources/extensions/gsd/auto-worktree.ts @@ -1453,8 +1453,13 @@ export function mergeMilestoneToMain( originalBasePath_, milestoneId, ); + // Validate prefs.main_branch exists before using it — a stale preference + // (e.g. "master" when repo uses "main") causes merge failure (#3589). + const validatedPrefBranch = prefs.main_branch && nativeBranchExists(originalBasePath_, prefs.main_branch) + ? prefs.main_branch + : undefined; const mainBranch = - integrationBranch ?? prefs.main_branch ?? nativeDetectMainBranch(originalBasePath_); + integrationBranch ?? validatedPrefBranch ?? nativeDetectMainBranch(originalBasePath_); // Remove transient project-root state files before any branch or merge // operation. Untracked milestone metadata can otherwise block squash merges. From ba8f6a0be293627c12baf6d5b91cc136dc951d6b Mon Sep 17 00:00:00 2001 From: Tibsfox Date: Mon, 6 Apr 2026 22:19:52 -0700 Subject: [PATCH 3/5] test: add regression test for findMissingSummaries closed-status exclusion Co-Authored-By: Claude Opus 4.6 (1M context) --- .../find-missing-summaries-closed.test.ts | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 src/resources/extensions/gsd/tests/find-missing-summaries-closed.test.ts diff --git a/src/resources/extensions/gsd/tests/find-missing-summaries-closed.test.ts b/src/resources/extensions/gsd/tests/find-missing-summaries-closed.test.ts new file mode 100644 index 000000000..a0d0d70b0 --- /dev/null +++ b/src/resources/extensions/gsd/tests/find-missing-summaries-closed.test.ts @@ -0,0 +1,48 @@ +/** + * Regression test for #3669 — findMissingSummaries skips closed slices + * + * When a slice has status "skipped", "complete", or "done", it should be + * excluded from the missing-summary check because closed slices intentionally + * lack SUMMARY files (or their DB status is authoritative). + * + * This is a structural verification test — it reads the source to confirm the + * CLOSED_STATUSES guard exists at the filter site. + */ + +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-dispatch.ts'), 'utf-8'); + +describe('findMissingSummaries closed-status exclusion (#3669)', () => { + test('CLOSED_STATUSES set includes skipped, complete, and done', () => { + // The source must define a CLOSED_STATUSES set with all three statuses + assert.match(source, /CLOSED_STATUSES.*=.*new Set\(/, + 'CLOSED_STATUSES set should be defined'); + assert.match(source, /"skipped"/, 'CLOSED_STATUSES should include "skipped"'); + assert.match(source, /"complete"/, 'CLOSED_STATUSES should include "complete"'); + assert.match(source, /"done"/, 'CLOSED_STATUSES should include "done"'); + }); + + test('filter uses CLOSED_STATUSES.has() to exclude closed slices', () => { + assert.match(source, /CLOSED_STATUSES\.has\(s\.status\)/, + 'filter should call CLOSED_STATUSES.has(s.status)'); + }); + + test('findMissingSummaries function exists', () => { + assert.match(source, /function findMissingSummaries\(/, + 'findMissingSummaries function should be defined'); + }); + + test('filter is negated (excludes closed, keeps open)', () => { + // The filter should use !CLOSED_STATUSES.has() to exclude closed slices + assert.match(source, /!CLOSED_STATUSES\.has\(s\.status\)/, + 'filter should negate CLOSED_STATUSES.has() to exclude closed slices'); + }); +}); From 554c7a5ff35c9515cdb9ca5f35bdded560405b92 Mon Sep 17 00:00:00 2001 From: Tibsfox Date: Mon, 6 Apr 2026 22:24:26 -0700 Subject: [PATCH 4/5] test: add regression test for project-root cwd crash and main_branch validation Structural verification that process.cwd() is wrapped in try/catch (#3598) and nativeBranchExists validates prefs.main_branch (#3589). Co-Authored-By: Claude Opus 4.6 (1M context) --- .../gsd/tests/project-root-cwd-crash.test.ts | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 src/resources/extensions/gsd/tests/project-root-cwd-crash.test.ts diff --git a/src/resources/extensions/gsd/tests/project-root-cwd-crash.test.ts b/src/resources/extensions/gsd/tests/project-root-cwd-crash.test.ts new file mode 100644 index 000000000..a75d3f13f --- /dev/null +++ b/src/resources/extensions/gsd/tests/project-root-cwd-crash.test.ts @@ -0,0 +1,53 @@ +/** + * Regression test for #3598 — projectRoot ENOENT crash on deleted cwd + * + * When the working directory is deleted (e.g. worktree teardown), process.cwd() + * throws ENOENT. The fix wraps process.cwd() in a try/catch and falls back to + * process.env.HOME. + * + * Also verifies #3589 — nativeBranchExists validation for prefs.main_branch + * in auto-worktree.ts to prevent merge failures with stale preferences. + * + * Structural verification test — reads source to confirm the guards exist. + */ + +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 contextSource = readFileSync(join(__dirname, '..', 'commands', 'context.ts'), 'utf-8'); +const worktreeSource = readFileSync(join(__dirname, '..', 'auto-worktree.ts'), 'utf-8'); + +describe('projectRoot cwd crash guard (#3598)', () => { + test('projectRoot wraps process.cwd() in try/catch', () => { + assert.match(contextSource, /try\s*\{[\s\S]*?process\.cwd\(\)/, + 'process.cwd() should be inside a try block'); + }); + + test('catch block falls back to process.env.HOME', () => { + assert.match(contextSource, /catch[\s\S]*?process\.env\.HOME/, + 'catch block should fall back to process.env.HOME'); + }); + + test('projectRoot function is exported', () => { + assert.match(contextSource, /export function projectRoot\(\)/, + 'projectRoot should be an exported function'); + }); +}); + +describe('main_branch nativeBranchExists validation (#3589)', () => { + test('prefs.main_branch is validated with nativeBranchExists', () => { + assert.match(worktreeSource, /nativeBranchExists\(.*prefs\.main_branch\)/, + 'nativeBranchExists should validate prefs.main_branch'); + }); + + test('validatedPrefBranch falls back to undefined when branch missing', () => { + assert.match(worktreeSource, /validatedPrefBranch[\s\S]*?:\s*undefined/, + 'validatedPrefBranch should fall back to undefined'); + }); +}); From e8903fa716ea082f11c75db2b96132232702828d Mon Sep 17 00:00:00 2001 From: Tibsfox Date: Mon, 6 Apr 2026 22:49:46 -0700 Subject: [PATCH 5/5] fix(test): remove extraneous test file from wrong branch The find-missing-summaries-closed.test.ts was accidentally committed to this branch but belongs to fix/find-missing-summaries-skip-closed. Remove it to fix CI failures. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../find-missing-summaries-closed.test.ts | 48 ------------------- 1 file changed, 48 deletions(-) delete mode 100644 src/resources/extensions/gsd/tests/find-missing-summaries-closed.test.ts diff --git a/src/resources/extensions/gsd/tests/find-missing-summaries-closed.test.ts b/src/resources/extensions/gsd/tests/find-missing-summaries-closed.test.ts deleted file mode 100644 index a0d0d70b0..000000000 --- a/src/resources/extensions/gsd/tests/find-missing-summaries-closed.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -/** - * Regression test for #3669 — findMissingSummaries skips closed slices - * - * When a slice has status "skipped", "complete", or "done", it should be - * excluded from the missing-summary check because closed slices intentionally - * lack SUMMARY files (or their DB status is authoritative). - * - * This is a structural verification test — it reads the source to confirm the - * CLOSED_STATUSES guard exists at the filter site. - */ - -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-dispatch.ts'), 'utf-8'); - -describe('findMissingSummaries closed-status exclusion (#3669)', () => { - test('CLOSED_STATUSES set includes skipped, complete, and done', () => { - // The source must define a CLOSED_STATUSES set with all three statuses - assert.match(source, /CLOSED_STATUSES.*=.*new Set\(/, - 'CLOSED_STATUSES set should be defined'); - assert.match(source, /"skipped"/, 'CLOSED_STATUSES should include "skipped"'); - assert.match(source, /"complete"/, 'CLOSED_STATUSES should include "complete"'); - assert.match(source, /"done"/, 'CLOSED_STATUSES should include "done"'); - }); - - test('filter uses CLOSED_STATUSES.has() to exclude closed slices', () => { - assert.match(source, /CLOSED_STATUSES\.has\(s\.status\)/, - 'filter should call CLOSED_STATUSES.has(s.status)'); - }); - - test('findMissingSummaries function exists', () => { - assert.match(source, /function findMissingSummaries\(/, - 'findMissingSummaries function should be defined'); - }); - - test('filter is negated (excludes closed, keeps open)', () => { - // The filter should use !CLOSED_STATUSES.has() to exclude closed slices - assert.match(source, /!CLOSED_STATUSES\.has\(s\.status\)/, - 'filter should negate CLOSED_STATUSES.has() to exclude closed slices'); - }); -});