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(); }