gsd-pi 2.41.0-dev.9446b20 → 2.41.0-dev.97349b1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (218) hide show
  1. package/README.md +69 -29
  2. package/dist/cli-web-branch.d.ts +6 -0
  3. package/dist/cli-web-branch.js +17 -0
  4. package/dist/onboarding.js +2 -1
  5. package/dist/resources/extensions/gsd/auto/loop.js +9 -1
  6. package/dist/resources/extensions/gsd/auto/phases.js +26 -8
  7. package/dist/resources/extensions/gsd/auto-dashboard.js +6 -2
  8. package/dist/resources/extensions/gsd/auto-dispatch.js +19 -2
  9. package/dist/resources/extensions/gsd/auto-post-unit.js +7 -0
  10. package/dist/resources/extensions/gsd/auto-recovery.js +12 -4
  11. package/dist/resources/extensions/gsd/auto-start.js +8 -3
  12. package/dist/resources/extensions/gsd/auto-worktree.js +147 -13
  13. package/dist/resources/extensions/gsd/auto.js +36 -1
  14. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +199 -164
  15. package/dist/resources/extensions/gsd/bootstrap/journal-tools.js +62 -0
  16. package/dist/resources/extensions/gsd/bootstrap/register-extension.js +2 -0
  17. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +16 -0
  18. package/dist/resources/extensions/gsd/commands/catalog.js +8 -1
  19. package/dist/resources/extensions/gsd/commands/handlers/core.js +1 -0
  20. package/dist/resources/extensions/gsd/commands/handlers/ops.js +5 -0
  21. package/dist/resources/extensions/gsd/context-store.js +4 -3
  22. package/dist/resources/extensions/gsd/db-writer.js +5 -2
  23. package/dist/resources/extensions/gsd/detection.js +1 -1
  24. package/dist/resources/extensions/gsd/doctor.js +11 -1
  25. package/dist/resources/extensions/gsd/exit-command.js +12 -2
  26. package/dist/resources/extensions/gsd/export.js +9 -13
  27. package/dist/resources/extensions/gsd/extension-manifest.json +2 -2
  28. package/dist/resources/extensions/gsd/files.js +28 -11
  29. package/dist/resources/extensions/gsd/forensics.js +10 -3
  30. package/dist/resources/extensions/gsd/git-service.js +5 -1
  31. package/dist/resources/extensions/gsd/gsd-db.js +25 -8
  32. package/dist/resources/extensions/gsd/guided-flow-queue.js +1 -1
  33. package/dist/resources/extensions/gsd/guided-flow.js +7 -3
  34. package/dist/resources/extensions/gsd/journal.js +85 -0
  35. package/dist/resources/extensions/gsd/md-importer.js +5 -0
  36. package/dist/resources/extensions/gsd/milestone-ids.js +1 -1
  37. package/dist/resources/extensions/gsd/native-git-bridge.js +2 -2
  38. package/dist/resources/extensions/gsd/post-unit-hooks.js +24 -412
  39. package/dist/resources/extensions/gsd/preferences-types.js +1 -0
  40. package/dist/resources/extensions/gsd/preferences.js +1 -0
  41. package/dist/resources/extensions/gsd/prompt-loader.js +34 -4
  42. package/dist/resources/extensions/gsd/prompts/complete-milestone.md +11 -10
  43. package/dist/resources/extensions/gsd/prompts/discuss-headless.md +2 -2
  44. package/dist/resources/extensions/gsd/prompts/discuss.md +1 -1
  45. package/dist/resources/extensions/gsd/prompts/queue.md +1 -1
  46. package/dist/resources/extensions/gsd/repo-identity.js +46 -2
  47. package/dist/resources/extensions/gsd/rule-registry.js +489 -0
  48. package/dist/resources/extensions/gsd/rule-types.js +6 -0
  49. package/dist/resources/extensions/gsd/service-tier.js +138 -0
  50. package/dist/resources/extensions/gsd/structured-data-formatter.js +2 -1
  51. package/dist/resources/extensions/gsd/templates/decisions.md +2 -2
  52. package/dist/resources/extensions/gsd/workflow-templates.js +13 -1
  53. package/dist/resources/extensions/gsd/worktree-manager.js +20 -6
  54. package/dist/resources/extensions/gsd/worktree-resolver.js +19 -2
  55. package/dist/resources/extensions/subagent/index.js +7 -3
  56. package/dist/resources/extensions/voice/index.js +4 -4
  57. package/dist/web/standalone/.next/BUILD_ID +1 -1
  58. package/dist/web/standalone/.next/app-path-routes-manifest.json +16 -16
  59. package/dist/web/standalone/.next/build-manifest.json +3 -3
  60. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  61. package/dist/web/standalone/.next/react-loadable-manifest.json +1 -1
  62. package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
  63. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  64. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  65. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  66. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  67. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  68. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  69. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  70. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  71. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  72. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  73. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  74. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  75. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  76. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  77. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  78. package/dist/web/standalone/.next/server/app/api/boot/route.js +1 -1
  79. package/dist/web/standalone/.next/server/app/api/captures/route.js +1 -1
  80. package/dist/web/standalone/.next/server/app/api/cleanup/route.js +1 -1
  81. package/dist/web/standalone/.next/server/app/api/doctor/route.js +1 -1
  82. package/dist/web/standalone/.next/server/app/api/export-data/route.js +1 -1
  83. package/dist/web/standalone/.next/server/app/api/forensics/route.js +1 -1
  84. package/dist/web/standalone/.next/server/app/api/history/route.js +1 -1
  85. package/dist/web/standalone/.next/server/app/api/hooks/route.js +1 -1
  86. package/dist/web/standalone/.next/server/app/api/recovery/route.js +1 -1
  87. package/dist/web/standalone/.next/server/app/api/settings-data/route.js +1 -1
  88. package/dist/web/standalone/.next/server/app/api/skill-health/route.js +1 -1
  89. package/dist/web/standalone/.next/server/app/api/terminal/input/route.js +1 -1
  90. package/dist/web/standalone/.next/server/app/api/terminal/resize/route.js +1 -1
  91. package/dist/web/standalone/.next/server/app/api/undo/route.js +1 -1
  92. package/dist/web/standalone/.next/server/app/api/visualizer/route.js +1 -1
  93. package/dist/web/standalone/.next/server/app/index.html +1 -1
  94. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  95. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  96. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  97. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  98. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  99. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  100. package/dist/web/standalone/.next/server/app-paths-manifest.json +16 -16
  101. package/dist/web/standalone/.next/server/chunks/229.js +3 -3
  102. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  103. package/dist/web/standalone/.next/server/middleware-react-loadable-manifest.js +1 -1
  104. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  105. package/dist/web/standalone/.next/server/pages/500.html +2 -2
  106. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  107. package/dist/web/standalone/.next/static/chunks/4024.c195dc1fdd2adbea.js +9 -0
  108. package/dist/web/standalone/.next/static/chunks/{webpack-9afaaebf6042a1d7.js → webpack-fa307370fcf9fb2c.js} +1 -1
  109. package/dist/web-mode.d.ts +2 -0
  110. package/dist/web-mode.js +29 -7
  111. package/package.json +1 -1
  112. package/packages/native/src/__tests__/text.test.mjs +33 -0
  113. package/packages/pi-coding-agent/dist/core/discovery-cache.test.js +3 -1
  114. package/packages/pi-coding-agent/dist/core/discovery-cache.test.js.map +1 -1
  115. package/packages/pi-coding-agent/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
  116. package/packages/pi-coding-agent/dist/modes/interactive/components/login-dialog.js +10 -7
  117. package/packages/pi-coding-agent/dist/modes/interactive/components/login-dialog.js.map +1 -1
  118. package/packages/pi-coding-agent/src/core/discovery-cache.test.ts +4 -2
  119. package/packages/pi-coding-agent/src/modes/interactive/components/login-dialog.ts +11 -7
  120. package/src/resources/extensions/gsd/auto/loop-deps.ts +5 -1
  121. package/src/resources/extensions/gsd/auto/loop.ts +10 -1
  122. package/src/resources/extensions/gsd/auto/phases.ts +28 -8
  123. package/src/resources/extensions/gsd/auto/types.ts +4 -0
  124. package/src/resources/extensions/gsd/auto-dashboard.ts +7 -2
  125. package/src/resources/extensions/gsd/auto-dispatch.ts +25 -5
  126. package/src/resources/extensions/gsd/auto-post-unit.ts +8 -0
  127. package/src/resources/extensions/gsd/auto-recovery.ts +12 -4
  128. package/src/resources/extensions/gsd/auto-start.ts +8 -3
  129. package/src/resources/extensions/gsd/auto-worktree.ts +162 -18
  130. package/src/resources/extensions/gsd/auto.ts +40 -1
  131. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +209 -162
  132. package/src/resources/extensions/gsd/bootstrap/journal-tools.ts +62 -0
  133. package/src/resources/extensions/gsd/bootstrap/register-extension.ts +2 -0
  134. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +13 -0
  135. package/src/resources/extensions/gsd/commands/catalog.ts +8 -1
  136. package/src/resources/extensions/gsd/commands/handlers/core.ts +1 -0
  137. package/src/resources/extensions/gsd/commands/handlers/ops.ts +5 -0
  138. package/src/resources/extensions/gsd/context-store.ts +4 -3
  139. package/src/resources/extensions/gsd/db-writer.ts +6 -2
  140. package/src/resources/extensions/gsd/detection.ts +1 -1
  141. package/src/resources/extensions/gsd/doctor.ts +12 -1
  142. package/src/resources/extensions/gsd/exit-command.ts +14 -2
  143. package/src/resources/extensions/gsd/export.ts +8 -15
  144. package/src/resources/extensions/gsd/extension-manifest.json +2 -2
  145. package/src/resources/extensions/gsd/files.ts +29 -12
  146. package/src/resources/extensions/gsd/forensics.ts +9 -3
  147. package/src/resources/extensions/gsd/git-service.ts +5 -4
  148. package/src/resources/extensions/gsd/gsd-db.ts +37 -8
  149. package/src/resources/extensions/gsd/guided-flow-queue.ts +1 -1
  150. package/src/resources/extensions/gsd/guided-flow.ts +7 -3
  151. package/src/resources/extensions/gsd/journal.ts +134 -0
  152. package/src/resources/extensions/gsd/md-importer.ts +6 -0
  153. package/src/resources/extensions/gsd/milestone-ids.ts +1 -1
  154. package/src/resources/extensions/gsd/native-git-bridge.ts +2 -2
  155. package/src/resources/extensions/gsd/post-unit-hooks.ts +24 -462
  156. package/src/resources/extensions/gsd/preferences-types.ts +3 -0
  157. package/src/resources/extensions/gsd/preferences.ts +1 -0
  158. package/src/resources/extensions/gsd/prompt-loader.ts +35 -4
  159. package/src/resources/extensions/gsd/prompts/complete-milestone.md +11 -10
  160. package/src/resources/extensions/gsd/prompts/discuss-headless.md +2 -2
  161. package/src/resources/extensions/gsd/prompts/discuss.md +1 -1
  162. package/src/resources/extensions/gsd/prompts/queue.md +1 -1
  163. package/src/resources/extensions/gsd/repo-identity.ts +47 -2
  164. package/src/resources/extensions/gsd/rule-registry.ts +599 -0
  165. package/src/resources/extensions/gsd/rule-types.ts +68 -0
  166. package/src/resources/extensions/gsd/service-tier.ts +171 -0
  167. package/src/resources/extensions/gsd/structured-data-formatter.ts +3 -1
  168. package/src/resources/extensions/gsd/templates/decisions.md +2 -2
  169. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +103 -120
  170. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +85 -0
  171. package/src/resources/extensions/gsd/tests/auto-secrets-gate.test.ts +2 -2
  172. package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +202 -0
  173. package/src/resources/extensions/gsd/tests/captures.test.ts +12 -1
  174. package/src/resources/extensions/gsd/tests/context-store.test.ts +10 -5
  175. package/src/resources/extensions/gsd/tests/continue-here.test.ts +20 -20
  176. package/src/resources/extensions/gsd/tests/db-writer.test.ts +10 -0
  177. package/src/resources/extensions/gsd/tests/doctor-completion-deferral.test.ts +15 -10
  178. package/src/resources/extensions/gsd/tests/doctor-fixlevel.test.ts +5 -4
  179. package/src/resources/extensions/gsd/tests/doctor-roadmap-summary-atomicity.test.ts +167 -0
  180. package/src/resources/extensions/gsd/tests/doctor-task-done-missing-summary-slice-loop.test.ts +174 -0
  181. package/src/resources/extensions/gsd/tests/exit-command.test.ts +55 -0
  182. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +8 -1
  183. package/src/resources/extensions/gsd/tests/gsd-tools.test.ts +7 -7
  184. package/src/resources/extensions/gsd/tests/journal-integration.test.ts +513 -0
  185. package/src/resources/extensions/gsd/tests/journal-query-tool.test.ts +147 -0
  186. package/src/resources/extensions/gsd/tests/journal.test.ts +386 -0
  187. package/src/resources/extensions/gsd/tests/md-importer.test.ts +31 -1
  188. package/src/resources/extensions/gsd/tests/memory-store.test.ts +2 -2
  189. package/src/resources/extensions/gsd/tests/milestone-id-reservation.test.ts +1 -1
  190. package/src/resources/extensions/gsd/tests/parsers.test.ts +110 -0
  191. package/src/resources/extensions/gsd/tests/preferences.test.ts +47 -25
  192. package/src/resources/extensions/gsd/tests/prompt-db.test.ts +3 -1
  193. package/src/resources/extensions/gsd/tests/repo-identity-worktree.test.ts +61 -1
  194. package/src/resources/extensions/gsd/tests/routing-history.test.ts +11 -22
  195. package/src/resources/extensions/gsd/tests/rule-registry.test.ts +413 -0
  196. package/src/resources/extensions/gsd/tests/service-tier.test.ts +98 -0
  197. package/src/resources/extensions/gsd/tests/skill-lifecycle.test.ts +2 -2
  198. package/src/resources/extensions/gsd/tests/stalled-tool-recovery.test.ts +102 -0
  199. package/src/resources/extensions/gsd/tests/structured-data-formatter.test.ts +4 -3
  200. package/src/resources/extensions/gsd/tests/tool-naming.test.ts +117 -0
  201. package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +6 -1
  202. package/src/resources/extensions/gsd/tests/windows-path-normalization.test.ts +99 -0
  203. package/src/resources/extensions/gsd/tests/worktree-db-integration.test.ts +1 -0
  204. package/src/resources/extensions/gsd/tests/worktree-db.test.ts +4 -0
  205. package/src/resources/extensions/gsd/tests/worktree-health-dispatch.test.ts +178 -0
  206. package/src/resources/extensions/gsd/tests/worktree-manager.test.ts +195 -105
  207. package/src/resources/extensions/gsd/tests/worktree-resolver.test.ts +78 -3
  208. package/src/resources/extensions/gsd/tests/worktree-symlink-removal.test.ts +140 -0
  209. package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +74 -0
  210. package/src/resources/extensions/gsd/types.ts +3 -0
  211. package/src/resources/extensions/gsd/workflow-templates.ts +12 -1
  212. package/src/resources/extensions/gsd/worktree-manager.ts +21 -6
  213. package/src/resources/extensions/gsd/worktree-resolver.ts +30 -9
  214. package/src/resources/extensions/subagent/index.ts +7 -3
  215. package/src/resources/extensions/voice/index.ts +4 -4
  216. package/dist/web/standalone/.next/static/chunks/4024.279c423e4661ece1.js +0 -9
  217. /package/dist/web/standalone/.next/static/{02cti5IXH7FycOqkbAkWL → ZrI3HOoXD7Fh84fAHZVxb}/_buildManifest.js +0 -0
  218. /package/dist/web/standalone/.next/static/{02cti5IXH7FycOqkbAkWL → ZrI3HOoXD7Fh84fAHZVxb}/_ssgManifest.js +0 -0
@@ -1,4 +1,6 @@
1
- import { mkdtempSync, mkdirSync, rmSync, writeFileSync, existsSync, readFileSync } from "node:fs";
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync, existsSync } from "node:fs";
2
4
  import { join } from "node:path";
3
5
  import { tmpdir } from "node:os";
4
6
  import { execSync } from "node:child_process";
@@ -13,82 +15,42 @@ import {
13
15
  worktreeBranchName,
14
16
  worktreePath,
15
17
  } from "../worktree-manager.ts";
16
- import { createTestContext } from './test-helpers.ts';
17
18
 
18
- const { assertEq, assertTrue, report } = createTestContext();
19
19
  function run(command: string, cwd: string): string {
20
20
  return execSync(command, { cwd, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8" }).trim();
21
21
  }
22
22
 
23
- // Set up a test repo
24
- const base = mkdtempSync(join(tmpdir(), "gsd-worktree-mgr-test-"));
25
- run("git init -b main", base);
26
- run('git config user.name "Pi Test"', base);
27
- run('git config user.email "pi@example.com"', base);
28
-
29
- // Create initial project structure
30
- mkdirSync(join(base, ".gsd", "milestones", "M001"), { recursive: true });
31
- writeFileSync(join(base, "README.md"), "# Test Project\n", "utf-8");
32
- writeFileSync(
33
- join(base, ".gsd", "milestones", "M001", "M001-ROADMAP.md"),
34
- "# M001: Demo\n\n## Slices\n- [ ] **S01: First** `risk:low` `depends:[]`\n > After this: it works\n",
35
- "utf-8",
36
- );
37
- run("git add .", base);
38
- run('git commit -m "chore: init"', base);
39
-
40
- async function main(): Promise<void> {
41
- console.log("\n=== worktreeBranchName ===");
42
- assertEq(worktreeBranchName("feature-x"), "worktree/feature-x", "branch name format");
43
-
44
- console.log("\n=== createWorktree ===");
45
- const info = createWorktree(base, "feature-x");
46
- assertTrue(info.name === "feature-x", "name matches");
47
- assertTrue(info.branch === "worktree/feature-x", "branch matches");
48
- assertTrue(info.exists, "worktree exists");
49
- assertTrue(existsSync(info.path), "worktree path exists on disk");
50
- assertTrue(existsSync(join(info.path, "README.md")), "README.md copied to worktree");
51
- assertTrue(existsSync(join(info.path, ".gsd", "milestones", "M001", "M001-ROADMAP.md")), ".gsd files copied");
52
-
53
- // Branch was created
54
- const branches = run("git branch", base);
55
- assertTrue(branches.includes("worktree/feature-x"), "branch was created");
56
-
57
- console.log("\n=== createWorktree — duplicate ===");
58
- let duplicateError = "";
59
- try {
60
- createWorktree(base, "feature-x");
61
- } catch (e) {
62
- duplicateError = (e as Error).message;
63
- }
64
- assertTrue(duplicateError.includes("already exists"), "duplicate creation fails");
23
+ function makeBaseRepo(): string {
24
+ const base = mkdtempSync(join(tmpdir(), "gsd-wt-test-"));
25
+ run("git init -b main", base);
26
+ run('git config user.name "Test User"', base);
27
+ run('git config user.email "test@example.com"', base);
28
+ mkdirSync(join(base, ".gsd", "milestones", "M001"), { recursive: true });
29
+ writeFileSync(join(base, "README.md"), "# Test Project\n", "utf-8");
30
+ writeFileSync(
31
+ join(base, ".gsd", "milestones", "M001", "M001-ROADMAP.md"),
32
+ "# M001: Demo\n\n## Slices\n- [ ] **S01: First** `risk:low` `depends:[]`\n > After this: it works\n",
33
+ "utf-8",
34
+ );
35
+ run("git add .", base);
36
+ run('git commit -m "chore: init"', base);
37
+ return base;
38
+ }
65
39
 
66
- console.log("\n=== createWorktree invalid name ===");
67
- let invalidError = "";
68
- try {
69
- createWorktree(base, "bad name!");
70
- } catch (e) {
71
- invalidError = (e as Error).message;
72
- }
73
- assertTrue(invalidError.includes("Invalid worktree name"), "invalid name rejected");
74
-
75
- console.log("\n=== listWorktrees ===");
76
- const list = listWorktrees(base);
77
- assertEq(list.length, 1, "one worktree listed");
78
- assertEq(list[0]!.name, "feature-x", "correct name");
79
- assertEq(list[0]!.branch, "worktree/feature-x", "correct branch");
80
- assertTrue(list[0]!.exists, "exists flag is true");
81
-
82
- console.log("\n=== make changes in worktree ===");
83
- const wtPath = worktreePath(base, "feature-x");
84
- // Add a new GSD artifact in the worktree
40
+ function makeRepoWithWorktree(worktreeName: string): { base: string; wtPath: string } {
41
+ const base = makeBaseRepo();
42
+ createWorktree(base, worktreeName);
43
+ return { base, wtPath: worktreePath(base, worktreeName) };
44
+ }
45
+
46
+ function makeRepoWithChanges(worktreeName: string): { base: string; wtPath: string } {
47
+ const { base, wtPath } = makeRepoWithWorktree(worktreeName);
85
48
  mkdirSync(join(wtPath, ".gsd", "milestones", "M002"), { recursive: true });
86
49
  writeFileSync(
87
50
  join(wtPath, ".gsd", "milestones", "M002", "M002-ROADMAP.md"),
88
51
  "# M002: New Feature\n\n## Slices\n- [ ] **S01: Setup** `risk:low` `depends:[]`\n > After this: new feature ready\n",
89
52
  "utf-8",
90
53
  );
91
- // Modify an existing artifact
92
54
  writeFileSync(
93
55
  join(wtPath, ".gsd", "milestones", "M001", "M001-ROADMAP.md"),
94
56
  "# M001: Demo (updated)\n\n## Slices\n- [x] **S01: First** `risk:low` `depends:[]`\n > Done\n",
@@ -96,46 +58,174 @@ async function main(): Promise<void> {
96
58
  );
97
59
  run("git add .", wtPath);
98
60
  run('git commit -m "feat: add M002 and update M001"', wtPath);
99
-
100
- console.log("\n=== diffWorktreeGSD ===");
101
- const diff = diffWorktreeGSD(base, "feature-x");
102
- assertTrue(diff.added.length > 0, "has added files");
103
- assertTrue(diff.added.some(f => f.includes("M002")), "M002 roadmap is in added");
104
- assertTrue(diff.modified.length > 0, "has modified files");
105
- assertTrue(diff.modified.some(f => f.includes("M001")), "M001 roadmap is in modified");
106
- assertEq(diff.removed.length, 0, "no removed files");
107
-
108
- console.log("\n=== getWorktreeGSDDiff ===");
109
- const fullDiff = getWorktreeGSDDiff(base, "feature-x");
110
- assertTrue(fullDiff.includes("M002"), "full diff mentions M002");
111
- assertTrue(fullDiff.includes("updated"), "full diff mentions update");
112
-
113
- console.log("\n=== getWorktreeLog ===");
114
- const log = getWorktreeLog(base, "feature-x");
115
- assertTrue(log.includes("add M002"), "log shows commit message");
116
-
117
- console.log("\n=== removeWorktree ===");
118
- removeWorktree(base, "feature-x", { deleteBranch: true });
119
- assertTrue(!existsSync(wtPath), "worktree directory removed");
120
- const branchesAfter = run("git branch", base);
121
- assertTrue(!branchesAfter.includes("worktree/feature-x"), "branch deleted");
122
-
123
- console.log("\n=== listWorktrees after removal ===");
124
- const listAfter = listWorktrees(base);
125
- assertEq(listAfter.length, 0, "no worktrees after removal");
126
-
127
- console.log("\n=== removeWorktree — already gone ===");
128
- // Should not throw
129
- removeWorktree(base, "feature-x", { deleteBranch: true });
130
- assertTrue(true, "removeWorktree on missing worktree does not throw");
131
-
132
- // Cleanup
133
- rmSync(base, { recursive: true, force: true });
134
-
135
- report();
61
+ return { base, wtPath };
136
62
  }
137
63
 
138
- main().catch((error) => {
139
- console.error(error);
140
- process.exit(1);
64
+ // ─── worktreeBranchName ───────────────────────────────────────────────────────
65
+
66
+ test("worktreeBranchName formats branch name", () => {
67
+ assert.strictEqual(
68
+ worktreeBranchName("feature-x"),
69
+ "worktree/feature-x",
70
+ "should prefix with worktree/",
71
+ );
72
+ });
73
+
74
+ // ─── createWorktree ───────────────────────────────────────────────────────────
75
+
76
+ test("createWorktree creates worktree with correct metadata", () => {
77
+ const base = makeBaseRepo();
78
+ try {
79
+ const info = createWorktree(base, "feature-x");
80
+ assert.strictEqual(info.name, "feature-x", "name should match");
81
+ assert.strictEqual(info.branch, "worktree/feature-x", "branch should be prefixed");
82
+ assert.ok(info.exists, "exists flag should be true");
83
+ assert.ok(existsSync(info.path), "worktree path should exist on disk");
84
+ assert.ok(existsSync(join(info.path, "README.md")), "README.md should be in worktree");
85
+ assert.ok(
86
+ existsSync(join(info.path, ".gsd", "milestones", "M001", "M001-ROADMAP.md")),
87
+ ".gsd files should be in worktree",
88
+ );
89
+ const branches = run("git branch", base);
90
+ assert.ok(branches.includes("worktree/feature-x"), "branch should be created in base repo");
91
+ } finally {
92
+ rmSync(base, { recursive: true, force: true });
93
+ }
94
+ });
95
+
96
+ test("createWorktree rejects duplicate name", () => {
97
+ const { base } = makeRepoWithWorktree("feature-x");
98
+ try {
99
+ assert.throws(
100
+ () => createWorktree(base, "feature-x"),
101
+ (err: Error) => {
102
+ assert.ok(
103
+ err.message.includes("already exists"),
104
+ `expected "already exists" in error, got: ${err.message}`,
105
+ );
106
+ return true;
107
+ },
108
+ "should throw on duplicate worktree name",
109
+ );
110
+ } finally {
111
+ rmSync(base, { recursive: true, force: true });
112
+ }
113
+ });
114
+
115
+ test("createWorktree rejects invalid name", () => {
116
+ const base = makeBaseRepo();
117
+ try {
118
+ assert.throws(
119
+ () => createWorktree(base, "bad name!"),
120
+ (err: Error) => {
121
+ assert.ok(
122
+ err.message.includes("Invalid worktree name"),
123
+ `expected "Invalid worktree name" in error, got: ${err.message}`,
124
+ );
125
+ return true;
126
+ },
127
+ "should throw on invalid worktree name",
128
+ );
129
+ } finally {
130
+ rmSync(base, { recursive: true, force: true });
131
+ }
132
+ });
133
+
134
+ // ─── listWorktrees ────────────────────────────────────────────────────────────
135
+
136
+ test("listWorktrees returns active worktrees", () => {
137
+ const { base } = makeRepoWithWorktree("feature-x");
138
+ try {
139
+ const list = listWorktrees(base);
140
+ assert.strictEqual(list.length, 1, "should list exactly one worktree");
141
+ assert.strictEqual(list[0]!.name, "feature-x", "name should match");
142
+ assert.strictEqual(list[0]!.branch, "worktree/feature-x", "branch should match");
143
+ assert.ok(list[0]!.exists, "exists flag should be true");
144
+ } finally {
145
+ rmSync(base, { recursive: true, force: true });
146
+ }
147
+ });
148
+
149
+ test("listWorktrees returns empty after removal", () => {
150
+ const { base } = makeRepoWithWorktree("feature-x");
151
+ try {
152
+ removeWorktree(base, "feature-x");
153
+ const list = listWorktrees(base);
154
+ assert.strictEqual(list.length, 0, "should have no worktrees after removal");
155
+ } finally {
156
+ rmSync(base, { recursive: true, force: true });
157
+ }
158
+ });
159
+
160
+ // ─── diffWorktreeGSD ─────────────────────────────────────────────────────────
161
+
162
+ test("diffWorktreeGSD detects added and modified GSD files", () => {
163
+ const { base } = makeRepoWithChanges("feature-x");
164
+ try {
165
+ const diff = diffWorktreeGSD(base, "feature-x");
166
+ assert.ok(diff.added.length > 0, "should have added files");
167
+ assert.ok(
168
+ diff.added.some((f) => f.includes("M002")),
169
+ "M002 roadmap should be in added files",
170
+ );
171
+ assert.ok(diff.modified.length > 0, "should have modified files");
172
+ assert.ok(
173
+ diff.modified.some((f) => f.includes("M001")),
174
+ "M001 roadmap should be in modified files",
175
+ );
176
+ assert.strictEqual(diff.removed.length, 0, "should have no removed files");
177
+ } finally {
178
+ rmSync(base, { recursive: true, force: true });
179
+ }
180
+ });
181
+
182
+ // ─── getWorktreeGSDDiff ───────────────────────────────────────────────────────
183
+
184
+ test("getWorktreeGSDDiff returns patch content", () => {
185
+ const { base } = makeRepoWithChanges("feature-x");
186
+ try {
187
+ const fullDiff = getWorktreeGSDDiff(base, "feature-x");
188
+ assert.ok(fullDiff.includes("M002"), "diff should mention M002");
189
+ assert.ok(fullDiff.includes("updated"), "diff should mention the update");
190
+ } finally {
191
+ rmSync(base, { recursive: true, force: true });
192
+ }
193
+ });
194
+
195
+ // ─── getWorktreeLog ───────────────────────────────────────────────────────────
196
+
197
+ test("getWorktreeLog shows commits", () => {
198
+ const { base } = makeRepoWithChanges("feature-x");
199
+ try {
200
+ const log = getWorktreeLog(base, "feature-x");
201
+ assert.ok(log.includes("add M002"), "log should include the commit message");
202
+ } finally {
203
+ rmSync(base, { recursive: true, force: true });
204
+ }
205
+ });
206
+
207
+ // ─── removeWorktree ───────────────────────────────────────────────────────────
208
+
209
+ test("removeWorktree removes directory and branch", () => {
210
+ const { base, wtPath } = makeRepoWithWorktree("feature-x");
211
+ try {
212
+ removeWorktree(base, "feature-x", { deleteBranch: true });
213
+ assert.ok(!existsSync(wtPath), "worktree directory should be gone");
214
+ const branches = run("git branch", base);
215
+ assert.ok(!branches.includes("worktree/feature-x"), "branch should be deleted");
216
+ } finally {
217
+ rmSync(base, { recursive: true, force: true });
218
+ }
219
+ });
220
+
221
+ test("removeWorktree on missing worktree does not throw", () => {
222
+ const base = makeBaseRepo();
223
+ try {
224
+ assert.doesNotThrow(
225
+ () => removeWorktree(base, "nonexistent"),
226
+ "should not throw when worktree does not exist",
227
+ );
228
+ } finally {
229
+ rmSync(base, { recursive: true, force: true });
230
+ }
141
231
  });
@@ -52,7 +52,7 @@ function makeDeps(
52
52
  fn: "mergeMilestoneToMain",
53
53
  args: [basePath, milestoneId, roadmapContent],
54
54
  });
55
- return { pushed: false };
55
+ return { pushed: false, codeFilesChanged: true };
56
56
  },
57
57
  syncWorktreeStateBack: (
58
58
  mainBasePath: string,
@@ -424,7 +424,7 @@ test("mergeAndExit in worktree mode shows pushed status", () => {
424
424
  const deps = makeDeps({
425
425
  isInAutoWorktree: () => true,
426
426
  getIsolationMode: () => "worktree",
427
- mergeMilestoneToMain: () => ({ pushed: true }),
427
+ mergeMilestoneToMain: () => ({ pushed: true, codeFilesChanged: true }),
428
428
  });
429
429
  const ctx = makeNotifyCtx();
430
430
  const resolver = new WorktreeResolver(s, deps);
@@ -659,6 +659,81 @@ test("mergeAndExit in none mode is a no-op", () => {
659
659
  assert.equal(ctx.messages.length, 0);
660
660
  });
661
661
 
662
+ // ─── #1906 — metadata-only merge warning ────────────────────────────────────
663
+
664
+ test("mergeAndExit warns when merge contains no code changes (#1906)", () => {
665
+ const s = makeSession({
666
+ basePath: "/project/.gsd/worktrees/M001",
667
+ originalBasePath: "/project",
668
+ });
669
+ const deps = makeDeps({
670
+ isInAutoWorktree: () => true,
671
+ getIsolationMode: () => "worktree",
672
+ mergeMilestoneToMain: () => ({ pushed: false, codeFilesChanged: false }),
673
+ });
674
+ const ctx = makeNotifyCtx();
675
+ const resolver = new WorktreeResolver(s, deps);
676
+
677
+ resolver.mergeAndExit("M001", ctx);
678
+
679
+ assert.ok(
680
+ ctx.messages.some((m) => m.msg.includes("NO code changes") && m.level === "warning"),
681
+ "must emit warning when only .gsd/ metadata was merged",
682
+ );
683
+ assert.ok(
684
+ !ctx.messages.some((m) => m.msg.includes("merged to main") && m.level === "info"),
685
+ "must NOT emit success-style info notification for metadata-only merge",
686
+ );
687
+ });
688
+
689
+ test("mergeAndExit emits info when merge contains code changes (#1906)", () => {
690
+ const s = makeSession({
691
+ basePath: "/project/.gsd/worktrees/M001",
692
+ originalBasePath: "/project",
693
+ });
694
+ const deps = makeDeps({
695
+ isInAutoWorktree: () => true,
696
+ getIsolationMode: () => "worktree",
697
+ mergeMilestoneToMain: () => ({ pushed: false, codeFilesChanged: true }),
698
+ });
699
+ const ctx = makeNotifyCtx();
700
+ const resolver = new WorktreeResolver(s, deps);
701
+
702
+ resolver.mergeAndExit("M001", ctx);
703
+
704
+ assert.ok(
705
+ ctx.messages.some((m) => m.msg.includes("merged to main") && m.level === "info"),
706
+ "must emit info notification when code files were merged",
707
+ );
708
+ assert.ok(
709
+ !ctx.messages.some((m) => m.msg.includes("NO code changes")),
710
+ "must NOT emit metadata-only warning when code files were merged",
711
+ );
712
+ });
713
+
714
+ test("mergeAndExit branch mode warns when merge contains no code changes (#1906)", () => {
715
+ const s = makeSession({
716
+ basePath: "/project",
717
+ originalBasePath: "/project",
718
+ });
719
+ const deps = makeDeps({
720
+ isInAutoWorktree: () => false,
721
+ getIsolationMode: () => "branch",
722
+ getCurrentBranch: () => "milestone/M001",
723
+ autoWorktreeBranch: () => "milestone/M001",
724
+ mergeMilestoneToMain: () => ({ pushed: false, codeFilesChanged: false }),
725
+ });
726
+ const ctx = makeNotifyCtx();
727
+ const resolver = new WorktreeResolver(s, deps);
728
+
729
+ resolver.mergeAndExit("M001", ctx);
730
+
731
+ assert.ok(
732
+ ctx.messages.some((m) => m.msg.includes("NO code changes") && m.level === "warning"),
733
+ "branch mode must emit warning when only .gsd/ metadata was merged",
734
+ );
735
+ });
736
+
662
737
  // ─── mergeAndEnterNext Tests ─────────────────────────────────────────────────
663
738
 
664
739
  test("mergeAndEnterNext calls mergeAndExit then enterMilestone", () => {
@@ -677,7 +752,7 @@ test("mergeAndEnterNext calls mergeAndExit then enterMilestone", () => {
677
752
  _roadmap: string,
678
753
  ) => {
679
754
  callOrder.push(`merge:${milestoneId}`);
680
- return { pushed: false };
755
+ return { pushed: false, codeFilesChanged: true };
681
756
  },
682
757
  getAutoWorktreePath: () => null,
683
758
  createAutoWorktree: (basePath: string, milestoneId: string) => {
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Regression test for #1852: removeWorktree targets wrong path when .gsd/ is a symlink.
3
+ *
4
+ * When .gsd/ is a symlink to an external state directory, git registers
5
+ * the worktree at the resolved (real) path. But removeWorktree recomputes
6
+ * the path via worktreePath() which uses the unresolved symlink, causing
7
+ * a mismatch — the removal silently fails.
8
+ *
9
+ * Fix: removeWorktree should query `git worktree list` to find the actual
10
+ * registered path when the computed path doesn't match.
11
+ */
12
+ import { mkdtempSync, mkdirSync, rmSync, symlinkSync, unlinkSync, writeFileSync, existsSync, realpathSync } from "node:fs";
13
+ import { join } from "node:path";
14
+ import { tmpdir } from "node:os";
15
+ import { execSync } from "node:child_process";
16
+
17
+ import {
18
+ createWorktree,
19
+ removeWorktree,
20
+ listWorktrees,
21
+ worktreePath,
22
+ } from "../worktree-manager.ts";
23
+ import { createTestContext } from './test-helpers.ts';
24
+
25
+ const { assertEq, assertTrue, report } = createTestContext();
26
+
27
+ function run(command: string, cwd: string): string {
28
+ return execSync(command, { cwd, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8" }).trim();
29
+ }
30
+
31
+ // Set up a test repo with .gsd/ as a symlink to an external directory,
32
+ // mimicking the external state directory layout (~/.gsd/projects/<hash>/).
33
+ // Resolve tmpdir to handle macOS /tmp -> /private/var/... symlink.
34
+ const realTmp = realpathSync(tmpdir());
35
+ const base = mkdtempSync(join(realTmp, "gsd-wt-symlink-test-"));
36
+ const externalState = mkdtempSync(join(realTmp, "gsd-wt-symlink-ext-"));
37
+
38
+ run("git init -b main", base);
39
+ run('git config user.name "Test"', base);
40
+ run('git config user.email "test@example.com"', base);
41
+
42
+ // Create external state directory structure
43
+ mkdirSync(join(externalState, "worktrees"), { recursive: true });
44
+
45
+ // Create .gsd as a symlink to the external state directory
46
+ symlinkSync(externalState, join(base, ".gsd"));
47
+
48
+ // Verify the symlink is in place
49
+ assertTrue(existsSync(join(base, ".gsd")), ".gsd symlink exists");
50
+ assertTrue(
51
+ realpathSync(join(base, ".gsd")) === externalState,
52
+ ".gsd resolves to external state dir",
53
+ );
54
+
55
+ // Create initial commit so we have a valid repo
56
+ writeFileSync(join(base, "README.md"), "# Test\n", "utf-8");
57
+ run("git add .", base);
58
+ run('git commit -m "init"', base);
59
+
60
+ async function main(): Promise<void> {
61
+ console.log("\n=== #1852: removeWorktree with symlinked .gsd/ ===");
62
+
63
+ // Create a worktree — git will resolve the symlink and register
64
+ // the worktree at the external path
65
+ const info = createWorktree(base, "M002", { branch: "milestone/M002" });
66
+ assertTrue(info.exists, "worktree created");
67
+
68
+ // Verify worktree was created at the resolved (external) path
69
+ const realWtPath = realpathSync(info.path);
70
+ assertTrue(
71
+ realWtPath.startsWith(externalState),
72
+ `worktree real path (${realWtPath}) is under external state dir`,
73
+ );
74
+
75
+ // Verify git registered the worktree
76
+ const gitList = run("git worktree list", base);
77
+ assertTrue(gitList.includes("M002"), "git worktree list shows M002");
78
+
79
+ // The computed path via worktreePath uses the symlink path
80
+ const computedPath = worktreePath(base, "M002");
81
+ assertTrue(existsSync(computedPath), "computed path exists (via symlink)");
82
+
83
+ // Simulate what syncStateToProjectRoot does: replace the .gsd symlink with
84
+ // a real directory containing stale worktree data. This causes worktreePath()
85
+ // to compute a LOCAL path that differs from git's REGISTERED path (the
86
+ // resolved external path). The stale local dir passes existsSync but is not
87
+ // a real git worktree, so nativeWorktreeRemove fails silently.
88
+ unlinkSync(join(base, ".gsd")); // remove the symlink
89
+ mkdirSync(join(base, ".gsd", "worktrees", "M002"), { recursive: true });
90
+ // Write a dummy file so the stale directory is non-empty
91
+ writeFileSync(join(base, ".gsd", "worktrees", "M002", "stale.txt"), "stale sync artifact", "utf-8");
92
+
93
+ // Now worktreePath(base, "M002") points to the LOCAL stale dir, not the
94
+ // external path where git actually registered the worktree.
95
+ const stalePath = worktreePath(base, "M002");
96
+ assertTrue(existsSync(stalePath), "stale local worktree dir exists");
97
+ assertTrue(
98
+ stalePath !== realWtPath,
99
+ `computed path (${stalePath}) differs from git-registered path (${realWtPath})`,
100
+ );
101
+
102
+ // THE ACTUAL TEST: removeWorktree must find the git-registered path and
103
+ // remove the real worktree, not just operate on the stale local directory.
104
+ removeWorktree(base, "M002", { branch: "milestone/M002", deleteBranch: true });
105
+
106
+ // After removal, the worktree should be gone from git's list
107
+ const gitListAfter = run("git worktree list", base);
108
+ assertTrue(
109
+ !gitListAfter.includes("M002"),
110
+ "worktree removed from git worktree list after removeWorktree",
111
+ );
112
+
113
+ // The branch should be deleted
114
+ const branches = run("git branch", base);
115
+ assertTrue(
116
+ !branches.includes("milestone/M002"),
117
+ "milestone/M002 branch deleted after removeWorktree",
118
+ );
119
+
120
+ // The worktree directory should be gone
121
+ assertTrue(
122
+ !existsSync(realWtPath),
123
+ "worktree directory removed from disk",
124
+ );
125
+
126
+ // List should be empty
127
+ const listed = listWorktrees(base);
128
+ assertEq(listed.length, 0, "no worktrees listed after removal");
129
+
130
+ // Cleanup
131
+ rmSync(base, { recursive: true, force: true });
132
+ rmSync(externalState, { recursive: true, force: true });
133
+
134
+ report();
135
+ }
136
+
137
+ main().catch((error) => {
138
+ console.error(error);
139
+ process.exit(1);
140
+ });
@@ -19,6 +19,8 @@
19
19
  * - syncWorktreeStateBack syncs root-level .gsd/ files (REQUIREMENTS, PROJECT, etc.)
20
20
  * - syncWorktreeStateBack syncs ALL milestone directories, not just the current one
21
21
  * - syncWorktreeStateBack handles next-milestone artifacts created during completion
22
+ * - syncGsdStateToWorktree syncs non-standard milestone dir names (#1547)
23
+ * - syncWorktreeStateBack syncs non-standard milestone dir names (#1547)
22
24
  */
23
25
 
24
26
  import { mkdtempSync, mkdirSync, writeFileSync, rmSync, existsSync, readFileSync } from 'node:fs';
@@ -517,6 +519,78 @@ async function main(): Promise<void> {
517
519
  }
518
520
  }
519
521
 
522
+ // ─── 14. syncGsdStateToWorktree syncs non-standard milestone dir names (#1547) ──
523
+ console.log('\n=== 14. syncGsdStateToWorktree syncs non-standard milestone dir names (#1547) ===');
524
+ {
525
+ const mainBase = createBase('main');
526
+ const wtBase = createBase('wt');
527
+
528
+ try {
529
+ // Main has milestone dirs with non-standard names
530
+ const customDir = join(mainBase, '.gsd', 'milestones', 'sprint-alpha');
531
+ mkdirSync(customDir, { recursive: true });
532
+ writeFileSync(join(customDir, 'CONTEXT.md'), '# Sprint Alpha Context');
533
+
534
+ const suffixDir = join(mainBase, '.gsd', 'milestones', 'M001-abc123');
535
+ mkdirSync(suffixDir, { recursive: true });
536
+ writeFileSync(join(suffixDir, 'M001-abc123-CONTEXT.md'), '# M001 Context');
537
+
538
+ assertTrue(!existsSync(join(wtBase, '.gsd', 'milestones', 'sprint-alpha')), 'sprint-alpha missing before sync');
539
+ assertTrue(!existsSync(join(wtBase, '.gsd', 'milestones', 'M001-abc123')), 'M001-abc123 missing before sync');
540
+
541
+ const result = syncGsdStateToWorktree(mainBase, wtBase);
542
+
543
+ assertTrue(
544
+ existsSync(join(wtBase, '.gsd', 'milestones', 'sprint-alpha', 'CONTEXT.md')),
545
+ '#1547: non-standard milestone dir "sprint-alpha" synced to worktree',
546
+ );
547
+ assertTrue(
548
+ existsSync(join(wtBase, '.gsd', 'milestones', 'M001-abc123', 'M001-abc123-CONTEXT.md')),
549
+ '#1547: suffixed milestone dir "M001-abc123" synced to worktree',
550
+ );
551
+ assertTrue(result.synced.length > 0, 'sync reported files');
552
+ } finally {
553
+ cleanup(mainBase);
554
+ cleanup(wtBase);
555
+ }
556
+ }
557
+
558
+ // ─── 15. syncWorktreeStateBack syncs non-standard milestone dir names (#1547) ──
559
+ console.log('\n=== 15. syncWorktreeStateBack syncs non-standard milestone dir names (#1547) ===');
560
+ {
561
+ const mainBase = mkdtempSync(join(tmpdir(), 'gsd-wt-back-custom-main-'));
562
+ const wtBase = mkdtempSync(join(tmpdir(), 'gsd-wt-back-custom-wt-'));
563
+
564
+ try {
565
+ mkdirSync(join(mainBase, '.gsd', 'milestones'), { recursive: true });
566
+ mkdirSync(join(wtBase, '.gsd', 'milestones'), { recursive: true });
567
+
568
+ // Worktree has a non-standard milestone dir
569
+ const wtCustomDir = join(wtBase, '.gsd', 'milestones', 'sprint-beta');
570
+ mkdirSync(wtCustomDir, { recursive: true });
571
+ writeFileSync(join(wtCustomDir, 'SUMMARY.md'), '# Sprint Beta Summary');
572
+
573
+ assertTrue(
574
+ !existsSync(join(mainBase, '.gsd', 'milestones', 'sprint-beta')),
575
+ 'sprint-beta missing in main before sync',
576
+ );
577
+
578
+ const { synced } = syncWorktreeStateBack(mainBase, wtBase, 'M001');
579
+
580
+ assertTrue(
581
+ existsSync(join(mainBase, '.gsd', 'milestones', 'sprint-beta', 'SUMMARY.md')),
582
+ '#1547: non-standard milestone dir "sprint-beta" synced back to main',
583
+ );
584
+ assertTrue(
585
+ synced.some((p) => p.includes('sprint-beta')),
586
+ '#1547: sprint-beta appears in synced list',
587
+ );
588
+ } finally {
589
+ rmSync(mainBase, { recursive: true, force: true });
590
+ rmSync(wtBase, { recursive: true, force: true });
591
+ }
592
+ }
593
+
520
594
  report();
521
595
  }
522
596