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) <noreply@anthropic.com>
This commit is contained in:
Tom Boucher 2026-03-21 14:55:25 -04:00 committed by GitHub
parent f29d54b7e0
commit 72f39b6e23
2 changed files with 71 additions and 1 deletions

View file

@ -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);

View file

@ -453,6 +453,70 @@ async function main(): Promise<void> {
}
}
// ─── 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();
}