From 72f39b6e235e4aef7cd7aaf4243cb79c8e710fe6 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Sat, 21 Mar 2026 14:55:25 -0400 Subject: [PATCH] fix(auto): reverse-sync root-level .gsd files on worktree teardown (#1831) Add QUEUE.md and completed-units.json to the durable file whitelist in both syncGsdStateToWorktree (forward sync) and syncWorktreeStateBack (reverse sync). These files are written during milestone closeout but were not being copied back to the project root, causing state loss on worktree teardown. Adds regression test verifying both files survive reverse sync. Fixes #1787 Co-authored-by: Claude Opus 4.6 (1M context) --- src/resources/extensions/gsd/auto-worktree.ts | 8 ++- .../tests/worktree-sync-milestones.test.ts | 64 +++++++++++++++++++ 2 files changed, 71 insertions(+), 1 deletion(-) diff --git a/src/resources/extensions/gsd/auto-worktree.ts b/src/resources/extensions/gsd/auto-worktree.ts index 35ef5dada..4f34c2aef 100644 --- a/src/resources/extensions/gsd/auto-worktree.ts +++ b/src/resources/extensions/gsd/auto-worktree.ts @@ -149,13 +149,15 @@ export function syncGsdStateToWorktree( if (!existsSync(mainGsd) || !existsSync(wtGsd)) return { synced }; - // Sync root-level .gsd/ files (DECISIONS, REQUIREMENTS, PROJECT, KNOWLEDGE) + // Sync root-level .gsd/ files (DECISIONS, REQUIREMENTS, PROJECT, KNOWLEDGE, etc.) const rootFiles = [ "DECISIONS.md", "REQUIREMENTS.md", "PROJECT.md", "KNOWLEDGE.md", "OVERRIDES.md", + "QUEUE.md", + "completed-units.json", ]; for (const f of rootFiles) { const src = join(mainGsd, f); @@ -303,12 +305,16 @@ export function syncWorktreeStateBack( // ── 1. Sync root-level .gsd/ files back ────────────────────────────── // The worktree is authoritative — complete-milestone updates REQUIREMENTS, // PROJECT, etc. These must overwrite main's copies so they survive teardown. + // Also includes QUEUE.md and completed-units.json which are written during + // milestone closeout and lost on teardown without explicit sync (#1787). const rootFiles = [ "DECISIONS.md", "REQUIREMENTS.md", "PROJECT.md", "KNOWLEDGE.md", "OVERRIDES.md", + "QUEUE.md", + "completed-units.json", ]; for (const f of rootFiles) { const src = join(wtGsd, f); diff --git a/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts b/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts index a693c3144..56fdb4f9b 100644 --- a/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +++ b/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts @@ -453,6 +453,70 @@ async function main(): Promise { } } + // ─── 13. syncWorktreeStateBack syncs QUEUE.md and completed-units.json (#1787) ── + console.log('\n=== 13. QUEUE.md and completed-units.json synced from worktree (#1787) ==='); + { + const mainBase = mkdtempSync(join(tmpdir(), 'gsd-wt-back-queue-main-')); + const wtBase = mkdtempSync(join(tmpdir(), 'gsd-wt-back-queue-wt-')); + + try { + mkdirSync(join(mainBase, '.gsd', 'milestones', 'M001'), { recursive: true }); + mkdirSync(join(wtBase, '.gsd', 'milestones', 'M001'), { recursive: true }); + + // Worktree has QUEUE.md and completed-units.json written during milestone closeout + writeFileSync(join(wtBase, '.gsd', 'QUEUE.md'), '# Queue\n- M002 next'); + writeFileSync( + join(wtBase, '.gsd', 'completed-units.json'), + JSON.stringify({ units: [{ id: 'M001-S01-T01', completed: true }] }), + ); + + // Main has neither + assertTrue( + !existsSync(join(mainBase, '.gsd', 'QUEUE.md')), + 'QUEUE.md missing in main before sync', + ); + assertTrue( + !existsSync(join(mainBase, '.gsd', 'completed-units.json')), + 'completed-units.json missing in main before sync', + ); + + const { synced } = syncWorktreeStateBack(mainBase, wtBase, 'M001'); + + // QUEUE.md should be synced + assertTrue( + existsSync(join(mainBase, '.gsd', 'QUEUE.md')), + '#1787: QUEUE.md synced from worktree to main', + ); + const queueContent = readFileSync(join(mainBase, '.gsd', 'QUEUE.md'), 'utf-8'); + assertTrue( + queueContent.includes('M002 next'), + '#1787: QUEUE.md has correct content', + ); + assertTrue( + synced.includes('QUEUE.md'), + '#1787: QUEUE.md appears in synced list', + ); + + // completed-units.json should be synced + assertTrue( + existsSync(join(mainBase, '.gsd', 'completed-units.json')), + '#1787: completed-units.json synced from worktree to main', + ); + const cuContent = readFileSync(join(mainBase, '.gsd', 'completed-units.json'), 'utf-8'); + assertTrue( + cuContent.includes('M001-S01-T01'), + '#1787: completed-units.json has correct content', + ); + assertTrue( + synced.includes('completed-units.json'), + '#1787: completed-units.json appears in synced list', + ); + } finally { + rmSync(mainBase, { recursive: true, force: true }); + rmSync(wtBase, { recursive: true, force: true }); + } + } + report(); }