From fa2bb7e8a8fa2f6a924316be4f78b57daf6394a9 Mon Sep 17 00:00:00 2001 From: mastertyko <11311479+mastertyko@users.noreply.github.com> Date: Thu, 26 Mar 2026 17:23:19 +0100 Subject: [PATCH 1/2] fix: include preferences.md in worktree sync and initial seed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit preferences.md was missing from both ROOT_STATE_FILES (used by syncGsdStateToWorktree and syncWorktreeStateBack) and the copyPlanningArtifacts file list (initial worktree seed). Because .gsd/ is gitignored, worktrees start with an empty .gsd/ directory — the bootstrap is the only opportunity to carry config over. Without preferences.md, post_unit_hooks, skill rules, custom instructions, and all other preference-driven config silently stop working once auto-mode enters a worktree. Closes #2684 --- src/resources/extensions/gsd/auto-worktree.ts | 4 + .../tests/preferences-worktree-sync.test.ts | 83 +++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 src/resources/extensions/gsd/tests/preferences-worktree-sync.test.ts diff --git a/src/resources/extensions/gsd/auto-worktree.ts b/src/resources/extensions/gsd/auto-worktree.ts index 70ee34d8b..9eaa31f47 100644 --- a/src/resources/extensions/gsd/auto-worktree.ts +++ b/src/resources/extensions/gsd/auto-worktree.ts @@ -82,6 +82,8 @@ const ROOT_STATE_FILES = [ "QUEUE.md", "completed-units.json", "metrics.json", + "preferences.md", // #2684: without this, post_unit_hooks and all preference-driven + // config silently stop working inside worktrees. ] as const; /** @@ -946,6 +948,8 @@ function copyPlanningArtifacts(srcBase: string, wtPath: string): void { "STATE.md", "KNOWLEDGE.md", "OVERRIDES.md", + "preferences.md", // #2684: must be seeded so post_unit_hooks and + // preference-driven config work inside worktrees. ]) { safeCopy(join(srcGsd, file), join(dstGsd, file), { force: true }); } diff --git a/src/resources/extensions/gsd/tests/preferences-worktree-sync.test.ts b/src/resources/extensions/gsd/tests/preferences-worktree-sync.test.ts new file mode 100644 index 000000000..776c28e70 --- /dev/null +++ b/src/resources/extensions/gsd/tests/preferences-worktree-sync.test.ts @@ -0,0 +1,83 @@ +/** + * Regression tests for #2684: preferences.md must be included in both + * ROOT_STATE_FILES (sync) and copyPlanningArtifacts (initial seed). + * + * Without this, post_unit_hooks and all preference-driven config silently + * stop working inside auto-mode worktrees. + */ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { readFileSync, mkdtempSync, mkdirSync, writeFileSync, existsSync, rmSync } from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; + +test("#2684: ROOT_STATE_FILES includes preferences.md", () => { + const srcPath = join(import.meta.dirname, "..", "auto-worktree.ts"); + const src = readFileSync(srcPath, "utf-8"); + + const constIdx = src.indexOf("ROOT_STATE_FILES"); + assert.ok(constIdx !== -1, "ROOT_STATE_FILES constant exists"); + + const arrayStart = src.indexOf("[", constIdx); + const arrayEnd = src.indexOf("]", arrayStart); + const block = src.slice(arrayStart, arrayEnd); + + assert.ok( + block.includes("preferences.md"), + "preferences.md should be in ROOT_STATE_FILES list", + ); +}); + +test("#2684: copyPlanningArtifacts file list includes preferences.md", () => { + const srcPath = join(import.meta.dirname, "..", "auto-worktree.ts"); + const src = readFileSync(srcPath, "utf-8"); + + // Find the copyPlanningArtifacts function body + const fnIdx = src.indexOf("function copyPlanningArtifacts"); + assert.ok(fnIdx !== -1, "copyPlanningArtifacts function exists"); + + // Extract function body (up to the next top-level function) + const fnBody = src.slice(fnIdx, fnIdx + 1500); + + assert.ok( + fnBody.includes('"preferences.md"'), + "preferences.md should be in copyPlanningArtifacts file list", + ); +}); + +test("#2684: syncGsdStateToWorktree copies preferences.md", async () => { + // Functional test: create a mock source and destination, call the sync + const srcBase = mkdtempSync(join(tmpdir(), "gsd-wt-prefs-src-")); + const dstBase = mkdtempSync(join(tmpdir(), "gsd-wt-prefs-dst-")); + const srcGsd = join(srcBase, ".gsd"); + const dstGsd = join(dstBase, ".gsd"); + mkdirSync(srcGsd, { recursive: true }); + mkdirSync(dstGsd, { recursive: true }); + + try { + // Write a preferences.md in source + writeFileSync( + join(srcGsd, "preferences.md"), + "---\nversion: 1\n---\n\npost_unit_hooks:\n - name: notify\n command: echo done\n", + ); + + // Import and call syncGsdStateToWorktree + const { syncGsdStateToWorktree } = await import("../auto-worktree.ts"); + syncGsdStateToWorktree(srcBase, dstBase); + + // Verify preferences.md was copied + assert.ok( + existsSync(join(dstGsd, "preferences.md")), + "preferences.md should be copied to worktree", + ); + + const content = readFileSync(join(dstGsd, "preferences.md"), "utf-8"); + assert.ok( + content.includes("post_unit_hooks"), + "copied preferences.md should contain the hooks config", + ); + } finally { + rmSync(srcBase, { recursive: true, force: true }); + rmSync(dstBase, { recursive: true, force: true }); + } +}); From 523e910f2148d578aa4610810ab1262f59dfb27c Mon Sep 17 00:00:00 2001 From: mastertyko <11311479+mastertyko@users.noreply.github.com> Date: Thu, 26 Mar 2026 23:57:17 +0100 Subject: [PATCH 2/2] fix: remove preferences.md from ROOT_STATE_FILES to prevent back-sync overwrite MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit preferences.md was in ROOT_STATE_FILES which caused syncWorktreeStateBack() to overwrite the project root's authoritative copy with the worktree's stale copy. The forward-sync (main → worktree) is already handled separately in syncGsdStateToWorktree() as additive-only. Fixes the failing CI test: worktree-preferences-sync.test.ts:107 '#2684: syncWorktreeStateBack does NOT overwrite project root preferences.md' Also updates preferences-worktree-sync.test.ts to assert preferences.md is NOT in ROOT_STATE_FILES (it must be handled separately). --- src/resources/extensions/gsd/auto-worktree.ts | 6 ++++-- .../gsd/tests/preferences-worktree-sync.test.ts | 16 ++++++++++++---- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/resources/extensions/gsd/auto-worktree.ts b/src/resources/extensions/gsd/auto-worktree.ts index 0b9d28bda..037bb516f 100644 --- a/src/resources/extensions/gsd/auto-worktree.ts +++ b/src/resources/extensions/gsd/auto-worktree.ts @@ -82,8 +82,10 @@ const ROOT_STATE_FILES = [ "QUEUE.md", "completed-units.json", "metrics.json", - "preferences.md", // #2684: without this, post_unit_hooks and all preference-driven - // config silently stop working inside worktrees. + // NOTE: preferences.md is intentionally NOT in ROOT_STATE_FILES. + // Forward-sync (main → worktree) is handled explicitly in syncGsdStateToWorktree(). + // Back-sync (worktree → main) must NEVER overwrite the project root's copy + // because the project root is authoritative for preferences (#2684). ] as const; /** diff --git a/src/resources/extensions/gsd/tests/preferences-worktree-sync.test.ts b/src/resources/extensions/gsd/tests/preferences-worktree-sync.test.ts index 776c28e70..04a0fbd0f 100644 --- a/src/resources/extensions/gsd/tests/preferences-worktree-sync.test.ts +++ b/src/resources/extensions/gsd/tests/preferences-worktree-sync.test.ts @@ -11,7 +11,7 @@ import { readFileSync, mkdtempSync, mkdirSync, writeFileSync, existsSync, rmSync import { join } from "node:path"; import { tmpdir } from "node:os"; -test("#2684: ROOT_STATE_FILES includes preferences.md", () => { +test("#2684: preferences.md is NOT in ROOT_STATE_FILES (forward-only sync)", () => { const srcPath = join(import.meta.dirname, "..", "auto-worktree.ts"); const src = readFileSync(srcPath, "utf-8"); @@ -19,12 +19,20 @@ test("#2684: ROOT_STATE_FILES includes preferences.md", () => { assert.ok(constIdx !== -1, "ROOT_STATE_FILES constant exists"); const arrayStart = src.indexOf("[", constIdx); - const arrayEnd = src.indexOf("]", arrayStart); + const arrayEnd = src.indexOf("] as const", arrayStart); const block = src.slice(arrayStart, arrayEnd); + // preferences.md must NOT be in ROOT_STATE_FILES — it is handled separately + // in syncGsdStateToWorktree() (forward-only, additive). Including it in + // ROOT_STATE_FILES would cause syncWorktreeStateBack() to overwrite the + // authoritative project root copy (#2684). + const entries = block.split("\n") + .map(l => l.trim()) + .filter(l => l.startsWith('"') && l.includes(".md")); + const hasPrefs = entries.some(l => l.includes("preferences.md")); assert.ok( - block.includes("preferences.md"), - "preferences.md should be in ROOT_STATE_FILES list", + !hasPrefs, + "preferences.md must NOT be in ROOT_STATE_FILES (back-sync would overwrite root)", ); });