gsd-pi 2.78.1-dev.b0759e59b → 2.78.1-dev.e9d88a536

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 (203) hide show
  1. package/README.md +8 -5
  2. package/dist/headless-recover.d.ts +23 -0
  3. package/dist/headless-recover.js +93 -0
  4. package/dist/headless.js +9 -0
  5. package/dist/help-text.js +1 -0
  6. package/dist/resources/.managed-resources-content-hash +1 -1
  7. package/dist/resources/extensions/browser-tools/tools/intent.js +8 -1
  8. package/dist/resources/extensions/gsd/auto-dispatch.js +4 -56
  9. package/dist/resources/extensions/gsd/auto-post-unit.js +7 -27
  10. package/dist/resources/extensions/gsd/auto-start.js +1 -8
  11. package/dist/resources/extensions/gsd/auto-worktree.js +59 -176
  12. package/dist/resources/extensions/gsd/auto.js +24 -6
  13. package/dist/resources/extensions/gsd/bootstrap/dynamic-tools.js +9 -77
  14. package/dist/resources/extensions/gsd/commands-codebase.js +2 -2
  15. package/dist/resources/extensions/gsd/commands-handlers.js +5 -5
  16. package/dist/resources/extensions/gsd/commands-logs.js +2 -2
  17. package/dist/resources/extensions/gsd/commands-scan.js +2 -2
  18. package/dist/resources/extensions/gsd/commands-ship.js +2 -2
  19. package/dist/resources/extensions/gsd/commands-workflow-templates.js +5 -5
  20. package/dist/resources/extensions/gsd/db-writer.js +16 -85
  21. package/dist/resources/extensions/gsd/dispatch-guard.js +6 -10
  22. package/dist/resources/extensions/gsd/doctor-engine-checks.js +2 -2
  23. package/dist/resources/extensions/gsd/gsd-db.js +74 -8
  24. package/dist/resources/extensions/gsd/guided-flow.js +31 -8
  25. package/dist/resources/extensions/gsd/markdown-renderer.js +14 -51
  26. package/dist/resources/extensions/gsd/parallel-merge.js +14 -13
  27. package/dist/resources/extensions/gsd/parallel-monitor-overlay.js +5 -2
  28. package/dist/resources/extensions/gsd/paths.js +35 -1
  29. package/dist/resources/extensions/gsd/prompts/plan-milestone.md +6 -0
  30. package/dist/resources/extensions/gsd/queue-order.js +6 -1
  31. package/dist/resources/extensions/gsd/rethink.js +2 -2
  32. package/dist/resources/extensions/gsd/state.js +91 -372
  33. package/dist/resources/extensions/gsd/tools/complete-milestone.js +6 -5
  34. package/dist/resources/extensions/gsd/tools/complete-slice.js +7 -12
  35. package/dist/resources/extensions/gsd/tools/complete-task.js +19 -31
  36. package/dist/resources/extensions/gsd/tools/validate-milestone.js +7 -5
  37. package/dist/resources/extensions/gsd/workflow-manifest.js +2 -1
  38. package/dist/resources/extensions/gsd/workflow-mcp-auto-prep.js +3 -21
  39. package/dist/resources/extensions/gsd/workflow-reconcile.js +3 -3
  40. package/dist/resources/extensions/gsd/worktree-command.js +4 -3
  41. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  42. package/dist/web/standalone/.next/BUILD_ID +1 -1
  43. package/dist/web/standalone/.next/app-path-routes-manifest.json +12 -12
  44. package/dist/web/standalone/.next/build-manifest.json +2 -2
  45. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  46. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  47. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  55. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/api/boot/route.js.nft.json +1 -1
  63. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js.nft.json +1 -1
  64. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js.nft.json +1 -1
  65. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js.nft.json +1 -1
  66. package/dist/web/standalone/.next/server/app/api/captures/route.js.nft.json +1 -1
  67. package/dist/web/standalone/.next/server/app/api/cleanup/route.js.nft.json +1 -1
  68. package/dist/web/standalone/.next/server/app/api/doctor/route.js.nft.json +1 -1
  69. package/dist/web/standalone/.next/server/app/api/export-data/route.js.nft.json +1 -1
  70. package/dist/web/standalone/.next/server/app/api/files/route.js.nft.json +1 -1
  71. package/dist/web/standalone/.next/server/app/api/forensics/route.js.nft.json +1 -1
  72. package/dist/web/standalone/.next/server/app/api/git/route.js.nft.json +1 -1
  73. package/dist/web/standalone/.next/server/app/api/history/route.js.nft.json +1 -1
  74. package/dist/web/standalone/.next/server/app/api/hooks/route.js.nft.json +1 -1
  75. package/dist/web/standalone/.next/server/app/api/inspect/route.js.nft.json +1 -1
  76. package/dist/web/standalone/.next/server/app/api/knowledge/route.js.nft.json +1 -1
  77. package/dist/web/standalone/.next/server/app/api/live-state/route.js.nft.json +1 -1
  78. package/dist/web/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
  79. package/dist/web/standalone/.next/server/app/api/onboarding/route.js.nft.json +1 -1
  80. package/dist/web/standalone/.next/server/app/api/projects/route.js.nft.json +1 -1
  81. package/dist/web/standalone/.next/server/app/api/recovery/route.js.nft.json +1 -1
  82. package/dist/web/standalone/.next/server/app/api/session/browser/route.js.nft.json +1 -1
  83. package/dist/web/standalone/.next/server/app/api/session/command/route.js.nft.json +1 -1
  84. package/dist/web/standalone/.next/server/app/api/session/events/route.js.nft.json +1 -1
  85. package/dist/web/standalone/.next/server/app/api/session/manage/route.js.nft.json +1 -1
  86. package/dist/web/standalone/.next/server/app/api/settings-data/route.js.nft.json +1 -1
  87. package/dist/web/standalone/.next/server/app/api/skill-health/route.js.nft.json +1 -1
  88. package/dist/web/standalone/.next/server/app/api/steer/route.js.nft.json +1 -1
  89. package/dist/web/standalone/.next/server/app/api/switch-root/route.js.nft.json +1 -1
  90. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js.nft.json +1 -1
  91. package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js.nft.json +1 -1
  92. package/dist/web/standalone/.next/server/app/api/undo/route.js.nft.json +1 -1
  93. package/dist/web/standalone/.next/server/app/api/visualizer/route.js.nft.json +1 -1
  94. package/dist/web/standalone/.next/server/app/index.html +1 -1
  95. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  96. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  97. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  98. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  99. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  100. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  101. package/dist/web/standalone/.next/server/app-paths-manifest.json +12 -12
  102. package/dist/web/standalone/.next/server/chunks/6336.js +1 -0
  103. package/dist/web/standalone/.next/server/chunks/6897.js +1 -1
  104. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  105. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  106. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  107. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  108. package/package.json +1 -1
  109. package/packages/mcp-server/dist/workflow-tools.d.ts +6 -0
  110. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  111. package/packages/mcp-server/dist/workflow-tools.js +56 -2
  112. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  113. package/packages/mcp-server/src/parse-workflow-args.test.ts +80 -0
  114. package/packages/mcp-server/src/workflow-tools.ts +61 -2
  115. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
  116. package/src/resources/extensions/browser-tools/tools/intent.ts +13 -2
  117. package/src/resources/extensions/gsd/auto-dispatch.ts +4 -60
  118. package/src/resources/extensions/gsd/auto-post-unit.ts +7 -26
  119. package/src/resources/extensions/gsd/auto-start.ts +1 -8
  120. package/src/resources/extensions/gsd/auto-worktree.ts +61 -204
  121. package/src/resources/extensions/gsd/auto.ts +23 -6
  122. package/src/resources/extensions/gsd/bootstrap/dynamic-tools.ts +9 -84
  123. package/src/resources/extensions/gsd/commands-codebase.ts +2 -2
  124. package/src/resources/extensions/gsd/commands-handlers.ts +5 -5
  125. package/src/resources/extensions/gsd/commands-logs.ts +2 -2
  126. package/src/resources/extensions/gsd/commands-scan.ts +2 -2
  127. package/src/resources/extensions/gsd/commands-ship.ts +2 -2
  128. package/src/resources/extensions/gsd/commands-workflow-templates.ts +5 -5
  129. package/src/resources/extensions/gsd/db-writer.ts +16 -83
  130. package/src/resources/extensions/gsd/dispatch-guard.ts +6 -11
  131. package/src/resources/extensions/gsd/doctor-engine-checks.ts +2 -2
  132. package/src/resources/extensions/gsd/gsd-db.ts +85 -8
  133. package/src/resources/extensions/gsd/guided-flow.ts +35 -8
  134. package/src/resources/extensions/gsd/markdown-renderer.ts +13 -64
  135. package/src/resources/extensions/gsd/parallel-merge.ts +14 -13
  136. package/src/resources/extensions/gsd/parallel-monitor-overlay.ts +5 -2
  137. package/src/resources/extensions/gsd/paths.ts +55 -1
  138. package/src/resources/extensions/gsd/prompts/plan-milestone.md +6 -0
  139. package/src/resources/extensions/gsd/queue-order.ts +6 -1
  140. package/src/resources/extensions/gsd/rethink.ts +2 -2
  141. package/src/resources/extensions/gsd/state.ts +91 -389
  142. package/src/resources/extensions/gsd/tests/artifact-corruption-2630.test.ts +1 -0
  143. package/src/resources/extensions/gsd/tests/auto-paused-session-validation.test.ts +6 -0
  144. package/src/resources/extensions/gsd/tests/auto-remediate-slice-status.test.ts +21 -34
  145. package/src/resources/extensions/gsd/tests/complete-task-rollback-evidence.test.ts +6 -7
  146. package/src/resources/extensions/gsd/tests/complete-task.test.ts +8 -6
  147. package/src/resources/extensions/gsd/tests/completed-at-reconcile.test.ts +12 -27
  148. package/src/resources/extensions/gsd/tests/completed-units-metrics-sync.test.ts +18 -5
  149. package/src/resources/extensions/gsd/tests/db-path-worktree-symlink.test.ts +4 -4
  150. package/src/resources/extensions/gsd/tests/db-writer.test.ts +14 -16
  151. package/src/resources/extensions/gsd/tests/derive-state-crossval.test.ts +6 -5
  152. package/src/resources/extensions/gsd/tests/derive-state-db-disk-reconcile.test.ts +10 -38
  153. package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +136 -56
  154. package/src/resources/extensions/gsd/tests/derive-state-draft.test.ts +3 -0
  155. package/src/resources/extensions/gsd/tests/derive-state-helpers.test.ts +119 -61
  156. package/src/resources/extensions/gsd/tests/derive-state.test.ts +4 -0
  157. package/src/resources/extensions/gsd/tests/dispatch-complete-milestone-guard.test.ts +6 -20
  158. package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +4 -5
  159. package/src/resources/extensions/gsd/tests/dispatcher-stuck-planning.test.ts +14 -15
  160. package/src/resources/extensions/gsd/tests/ensure-db-open.test.ts +11 -16
  161. package/src/resources/extensions/gsd/tests/escalation.test.ts +2 -1
  162. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +2 -1
  163. package/src/resources/extensions/gsd/tests/gsdroot-worktree-detection.test.ts +15 -36
  164. package/src/resources/extensions/gsd/tests/handler-worktree-write-isolation.test.ts +57 -0
  165. package/src/resources/extensions/gsd/tests/integration/parallel-merge.test.ts +15 -15
  166. package/src/resources/extensions/gsd/tests/integration/state-machine-edge-cases.test.ts +15 -5
  167. package/src/resources/extensions/gsd/tests/markdown-renderer.test.ts +14 -8
  168. package/src/resources/extensions/gsd/tests/md-importer.test.ts +2 -1
  169. package/src/resources/extensions/gsd/tests/memory-store.test.ts +3 -2
  170. package/src/resources/extensions/gsd/tests/park-milestone.test.ts +2 -0
  171. package/src/resources/extensions/gsd/tests/progressive-planning.test.ts +25 -16
  172. package/src/resources/extensions/gsd/tests/projection-regression.test.ts +1 -0
  173. package/src/resources/extensions/gsd/tests/ready-phrase-no-files-4573.test.ts +184 -0
  174. package/src/resources/extensions/gsd/tests/register-hooks-compaction-checkpoint.test.ts +6 -1
  175. package/src/resources/extensions/gsd/tests/replan-slice.test.ts +3 -0
  176. package/src/resources/extensions/gsd/tests/resolve-ts.mjs +4 -0
  177. package/src/resources/extensions/gsd/tests/rogue-file-detection.test.ts +3 -4
  178. package/src/resources/extensions/gsd/tests/slice-disk-reconcile.test.ts +10 -56
  179. package/src/resources/extensions/gsd/tests/stale-slice-rows.test.ts +15 -16
  180. package/src/resources/extensions/gsd/tests/state-corruption-2945.test.ts +1 -0
  181. package/src/resources/extensions/gsd/tests/state-machine-full-walkthrough.test.ts +23 -27
  182. package/src/resources/extensions/gsd/tests/steer-worktree-path.test.ts +13 -14
  183. package/src/resources/extensions/gsd/tests/stop-auto-remote.test.ts +4 -3
  184. package/src/resources/extensions/gsd/tests/sync-worktree-skip-current.test.ts +10 -33
  185. package/src/resources/extensions/gsd/tests/validate-milestone-write-order.test.ts +7 -8
  186. package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +9 -15
  187. package/src/resources/extensions/gsd/tests/workflow-logger-wiring.test.ts +12 -7
  188. package/src/resources/extensions/gsd/tests/workflow-mcp-auto-prep.test.ts +4 -4
  189. package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +24 -1
  190. package/src/resources/extensions/gsd/tests/worktree-db-same-file.test.ts +13 -0
  191. package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +65 -71
  192. package/src/resources/extensions/gsd/tests/worktree-sync-tasks.test.ts +26 -151
  193. package/src/resources/extensions/gsd/tools/complete-milestone.ts +7 -5
  194. package/src/resources/extensions/gsd/tools/complete-slice.ts +7 -14
  195. package/src/resources/extensions/gsd/tools/complete-task.ts +19 -34
  196. package/src/resources/extensions/gsd/tools/validate-milestone.ts +7 -5
  197. package/src/resources/extensions/gsd/workflow-manifest.ts +4 -1
  198. package/src/resources/extensions/gsd/workflow-mcp-auto-prep.ts +2 -18
  199. package/src/resources/extensions/gsd/workflow-reconcile.ts +3 -3
  200. package/src/resources/extensions/gsd/worktree-command.ts +4 -3
  201. package/dist/web/standalone/.next/server/chunks/8527.js +0 -1
  202. /package/dist/web/standalone/.next/static/{rk1EN3FQTE6Z1yalkW_GE → oZGTPvJBQX_IDKKnuV8Bt}/_buildManifest.js +0 -0
  203. /package/dist/web/standalone/.next/static/{rk1EN3FQTE6Z1yalkW_GE → oZGTPvJBQX_IDKKnuV8Bt}/_ssgManifest.js +0 -0
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * gsdroot-worktree-detection.test.ts — Regression test for #2594.
3
3
  *
4
- * gsdRoot() must return the worktree's own .gsd directory when the basePath
5
- * is inside a .gsd/worktrees/<name>/ structure, not walk up to the project
6
- * root's .gsd via the git-root probe.
4
+ * gsdRoot() must return the canonical project .gsd directory when basePath
5
+ * is inside a .gsd/worktrees/<name>/ structure. Worktree-local .gsd folders
6
+ * are legacy projection roots only; runtime state is DB-authoritative at the
7
+ * project .gsd.
7
8
  *
8
9
  * The bug: when a git worktree lives at /project/.gsd/worktrees/M008/,
9
10
  * probeGsdRoot() runs `git rev-parse --show-toplevel` which can return the
@@ -20,7 +21,7 @@ import { mkdtempSync, realpathSync } from "node:fs";
20
21
  import { tmpdir } from "node:os";
21
22
  import { spawnSync } from "node:child_process";
22
23
 
23
- import { gsdRoot, _clearGsdRootCache } from "../paths.ts";
24
+ import { gsdRoot, resolveGsdPathContract, _clearGsdRootCache } from "../paths.ts";
24
25
 
25
26
  describe("gsdRoot() worktree detection (#2594)", () => {
26
27
  let projectRoot: string;
@@ -61,7 +62,7 @@ describe("gsdRoot() worktree detection (#2594)", () => {
61
62
  rmSync(projectRoot, { recursive: true, force: true });
62
63
  });
63
64
 
64
- test("returns worktree .gsd when basePath is a worktree with its own .gsd (fast path)", () => {
65
+ test("returns project .gsd when basePath is a worktree with its own .gsd", () => {
65
66
  // Simulates a worktree that already had copyPlanningArtifacts() run,
66
67
  // so it has its own .gsd/ directory.
67
68
  const worktreeBase = join(projectGsd, "worktrees", "M008");
@@ -71,41 +72,26 @@ describe("gsdRoot() worktree detection (#2594)", () => {
71
72
  const result = gsdRoot(worktreeBase);
72
73
  assert.equal(
73
74
  result,
74
- worktreeGsd,
75
- `Expected worktree .gsd (${worktreeGsd}), got ${result}. ` +
76
- "gsdRoot() should use the fast path for an existing worktree .gsd.",
75
+ projectGsd,
76
+ `Expected canonical project .gsd (${projectGsd}), got ${result}.`,
77
77
  );
78
+ assert.equal(resolveGsdPathContract(worktreeBase).worktreeGsd, worktreeGsd);
78
79
  });
79
80
 
80
- test("returns worktree .gsd path (not project root .gsd) when worktree .gsd does not exist yet", () => {
81
- // This is the core #2594 bug: the worktree directory exists but its .gsd
82
- // subdirectory hasn't been created yet. Without the fix, probeGsdRoot()
83
- // walks up from the worktree path, finds /project/.gsd, and returns it.
84
- // With the fix, it detects the .gsd/worktrees/<name>/ pattern and returns
85
- // the worktree-local .gsd path as the creation fallback.
81
+ test("returns project .gsd when worktree .gsd does not exist yet", () => {
86
82
  const worktreeBase = join(projectGsd, "worktrees", "M008");
87
83
  mkdirSync(worktreeBase, { recursive: true });
88
84
  // NOTE: no .gsd/ inside worktreeBase
89
85
 
90
86
  const result = gsdRoot(worktreeBase);
91
- const expected = join(worktreeBase, ".gsd");
92
-
93
- // Without the fix, this returns projectGsd (/project/.gsd) because the
94
- // walk-up from worktreeBase finds it. With the fix, it returns the
95
- // worktree-local path.
96
- assert.notEqual(
97
- result,
98
- projectGsd,
99
- "gsdRoot() must NOT return the project root .gsd when basePath is inside .gsd/worktrees/",
100
- );
101
87
  assert.equal(
102
88
  result,
103
- expected,
104
- `Expected worktree-local .gsd (${expected}), got ${result}.`,
89
+ projectGsd,
90
+ `Expected canonical project .gsd (${projectGsd}), got ${result}.`,
105
91
  );
106
92
  });
107
93
 
108
- test("returns worktree .gsd when basePath is a real git worktree inside .gsd/worktrees/", () => {
94
+ test("returns project .gsd when basePath is a real git worktree inside .gsd/worktrees/", () => {
109
95
  // Create a real git worktree at .gsd/worktrees/M010
110
96
  const worktreeName = "M010";
111
97
  const worktreeBase = join(projectGsd, "worktrees", worktreeName);
@@ -125,17 +111,10 @@ describe("gsdRoot() worktree detection (#2594)", () => {
125
111
 
126
112
  // The real git worktree exists at worktreeBase but has NO .gsd/ subdir yet
127
113
  const gsdResult = gsdRoot(worktreeBase);
128
- const expected = join(worktreeBase, ".gsd");
129
-
130
- assert.notEqual(
131
- gsdResult,
132
- projectGsd,
133
- "gsdRoot() must NOT escape to project root .gsd from inside a git worktree",
134
- );
135
114
  assert.equal(
136
115
  gsdResult,
137
- expected,
138
- `Expected worktree-local .gsd (${expected}), got ${gsdResult}`,
116
+ projectGsd,
117
+ `Expected canonical project .gsd (${projectGsd}), got ${gsdResult}`,
139
118
  );
140
119
 
141
120
  // Cleanup worktree
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Regression: when /gsd handlers run with cwd inside a worktree, writes must
3
+ * land in the worktree's .gsd/, not the parent project's .gsd/.
4
+ *
5
+ * The fix in 01464a97 replaced `process.cwd()` with `projectRoot()` to block
6
+ * $HOME pollution, but `projectRoot()` walks UP from a worktree path to the
7
+ * outer project root — breaking the worktree isolation invariant agents rely
8
+ * on. The corrected pattern: handlers use `currentDirectoryRoot()`, which
9
+ * preserves the active cwd (worktree or project) and still throws when cwd
10
+ * is $HOME.
11
+ */
12
+ import { describe, test, beforeEach, afterEach } from "node:test";
13
+ import assert from "node:assert/strict";
14
+ import { mkdtempSync, mkdirSync, rmSync, realpathSync } from "node:fs";
15
+ import { tmpdir, homedir } from "node:os";
16
+ import { join } from "node:path";
17
+
18
+ import { currentDirectoryRoot, projectRoot, withCommandCwd, GSDNoProjectError } from "../commands/context.ts";
19
+
20
+ describe("handlers preserve worktree cwd via currentDirectoryRoot()", () => {
21
+ let project: string;
22
+ let worktree: string;
23
+
24
+ beforeEach(() => {
25
+ project = realpathSync(mkdtempSync(join(tmpdir(), "gsd-wt-iso-")));
26
+ mkdirSync(join(project, ".gsd"), { recursive: true });
27
+ mkdirSync(join(project, ".git"), { recursive: true });
28
+ worktree = join(project, ".gsd", "worktrees", "M001");
29
+ mkdirSync(join(worktree, ".gsd"), { recursive: true });
30
+ });
31
+
32
+ afterEach(() => {
33
+ rmSync(project, { recursive: true, force: true });
34
+ });
35
+
36
+ test("currentDirectoryRoot() returns the worktree path when cwd is the worktree", async () => {
37
+ const resolved = await withCommandCwd(worktree, async () => currentDirectoryRoot());
38
+ assert.equal(resolved, worktree, "must keep worktree path so writes isolate");
39
+ });
40
+
41
+ test("projectRoot() walks UP from worktree to the project root (legacy semantics)", async () => {
42
+ const resolved = await withCommandCwd(worktree, async () => projectRoot());
43
+ assert.equal(resolved, project, "projectRoot intentionally returns project, not worktree");
44
+ });
45
+
46
+ test("currentDirectoryRoot() throws GSDNoProjectError when cwd is $HOME", async () => {
47
+ await assert.rejects(
48
+ withCommandCwd(homedir(), async () => currentDirectoryRoot()),
49
+ (err: unknown) => err instanceof GSDNoProjectError,
50
+ );
51
+ });
52
+
53
+ test("currentDirectoryRoot() returns project root when cwd is project root", async () => {
54
+ const resolved = await withCommandCwd(project, async () => currentDirectoryRoot());
55
+ assert.equal(resolved, project);
56
+ });
57
+ });
@@ -473,26 +473,26 @@ test("mergeAllCompleted — by-completion order respects startedAt", async () =>
473
473
  });
474
474
 
475
475
  // ═══════════════════════════════════════════════════════════════════════════════
476
- // Bug #2812 — determineMergeOrder should use worktree DB as source of truth
476
+ // Bug #2812 — determineMergeOrder should use DB state as source of truth
477
477
  // ═══════════════════════════════════════════════════════════════════════════════
478
478
 
479
- /** Set up a worktree DB with a milestone marked complete */
480
- function setupWorktreeDb(basePath: string, mid: string): void {
481
- const wtGsdDir = join(basePath, ".gsd", "worktrees", mid, ".gsd");
482
- mkdirSync(wtGsdDir, { recursive: true });
483
- const dbPath = join(wtGsdDir, "gsd.db");
479
+ /** Set up canonical DB with a milestone marked complete and a worktree marker dir */
480
+ function setupCanonicalDbWithWorktree(basePath: string, mid: string): void {
481
+ mkdirSync(join(basePath, ".gsd", "worktrees", mid), { recursive: true });
482
+ mkdirSync(join(basePath, ".gsd"), { recursive: true });
483
+ const dbPath = join(basePath, ".gsd", "gsd.db");
484
484
  openDatabase(dbPath);
485
485
  insertMilestone({ id: mid, title: `Milestone ${mid}`, status: "complete" });
486
486
  updateMilestoneStatus(mid, "complete", new Date().toISOString());
487
487
  closeDatabase();
488
488
  }
489
489
 
490
- test("determineMergeOrder — finds milestones completed in worktree DB even when worker state is 'error' (#2812)", () => {
490
+ test("determineMergeOrder — finds milestones completed in canonical DB even when worker state is 'error' (#2812)", () => {
491
491
  const base = realpathSync(mkdtempSync(join(tmpdir(), "merge-db-bug-")));
492
492
  try {
493
493
  // Simulate the bug scenario: orchestrator has stale "error" state
494
- // but the worktree DB shows milestone is actually complete.
495
- setupWorktreeDb(base, "M011");
494
+ // but the canonical DB shows milestone is actually complete.
495
+ setupCanonicalDbWithWorktree(base, "M011");
496
496
 
497
497
  const workers = [
498
498
  makeWorker({ milestoneId: "M010", state: "error" }),
@@ -502,12 +502,12 @@ test("determineMergeOrder — finds milestones completed in worktree DB even whe
502
502
 
503
503
  const order = determineMergeOrder(workers, "sequential", base);
504
504
 
505
- // M011 should be included because its worktree DB says status='complete'
505
+ // M011 should be included because the canonical DB says status='complete'
506
506
  assert.ok(
507
507
  order.includes("M011"),
508
- `Expected M011 in merge order (worktree DB says complete), got: [${order}]`,
508
+ `Expected M011 in merge order (canonical DB says complete), got: [${order}]`,
509
509
  );
510
- // M010 and M012 should NOT be included (no worktree DB with complete status)
510
+ // M010 and M012 should NOT be included (no canonical complete status)
511
511
  assert.ok(!order.includes("M010"), "M010 should not be in merge order (error, no DB)");
512
512
  assert.ok(!order.includes("M012"), "M012 should not be in merge order (running, no DB)");
513
513
  } finally {
@@ -529,7 +529,7 @@ test("determineMergeOrder — combines stopped workers and DB-complete milestone
529
529
  const base = realpathSync(mkdtempSync(join(tmpdir(), "merge-dedup-")));
530
530
  try {
531
531
  // M001 is stopped in orchestrator AND complete in worktree DB
532
- setupWorktreeDb(base, "M001");
532
+ setupCanonicalDbWithWorktree(base, "M001");
533
533
 
534
534
  const workers = [
535
535
  makeWorker({ milestoneId: "M001", state: "stopped" }),
@@ -555,8 +555,8 @@ test("mergeAllCompleted — discovers DB-complete milestones when workers show e
555
555
  ]);
556
556
  setupRoadmap(repo, "M011", "Feature System", ["S01: Feature module"]);
557
557
 
558
- // Set up worktree DB showing M011 is complete
559
- setupWorktreeDb(repo, "M011");
558
+ // Set up canonical DB showing M011 is complete
559
+ setupCanonicalDbWithWorktree(repo, "M011");
560
560
 
561
561
  // Orchestrator thinks M011 is in error (stale state)
562
562
  const workers = [
@@ -46,6 +46,7 @@ import {
46
46
  updateTaskStatus,
47
47
  updateSliceStatus,
48
48
  updateMilestoneStatus,
49
+ insertAssessment,
49
50
  insertReplanHistory,
50
51
  getReplanHistory,
51
52
  insertGateRow,
@@ -363,13 +364,13 @@ describe("state derivation failures", () => {
363
364
  const state2 = await deriveState(base);
364
365
  assert.equal(state2.phase, "executing", "cached result should still show executing");
365
366
 
366
- // After explicit invalidation, should reflect the DB mutation and reconcile
367
- // missing plan tasks instead of prematurely summarizing a partial DB row set.
367
+ // After explicit invalidation, DB rows remain authoritative; PLAN.md is a
368
+ // projection and must not import missing task rows.
368
369
  invalidateStateCache();
369
370
  const state3 = await deriveState(base);
370
- assert.equal(state3.phase, "executing", "after cache invalidation should continue with the missing plan task");
371
- assert.equal(state3.activeTask?.id, "T02", "missing plan task T02 should be imported and selected");
372
- assert.deepEqual(state3.progress?.tasks, { done: 1, total: 2 });
371
+ assert.equal(state3.phase, "summarizing", "after cache invalidation should follow DB tasks only");
372
+ assert.equal(state3.activeTask, null, "disk-only plan task T02 should not be imported");
373
+ assert.deepEqual(state3.progress?.tasks, { done: 1, total: 1 });
373
374
  });
374
375
 
375
376
  test("corrupt ROADMAP: binary content does not crash deriveState", async () => {
@@ -486,12 +487,14 @@ describe("transition boundary failures", () => {
486
487
  writeFileSync(join(mDir, "M001-CONTEXT-DRAFT.md"), "# Draft\nSome draft.\n");
487
488
 
488
489
  openDatabase(join(base, ".gsd", "gsd.db"));
490
+ insertMilestone({ id: "M001", title: "Draft", status: "needs-discussion" });
489
491
  invalidateAllCaches();
490
492
  const state1 = await deriveState(base);
491
493
  assert.equal(state1.phase, "needs-discussion");
492
494
 
493
495
  // Now write the full CONTEXT (simulates discussion completion)
494
496
  writeFileSync(join(mDir, "M001-CONTEXT.md"), "# M001: Resolved\n\n## Purpose\nDone.\n");
497
+ updateMilestoneStatus("M001", "active");
495
498
 
496
499
  invalidateAllCaches();
497
500
  const state2 = await deriveState(base);
@@ -1148,6 +1151,13 @@ describe("completion and verification failures", () => {
1148
1151
  insertTask({ id: "T01", sliceId: "S01", milestoneId: "M001", status: "complete" });
1149
1152
  insertTask({ id: "T02", sliceId: "S01", milestoneId: "M001", status: "complete" });
1150
1153
  insertTask({ id: "T01", sliceId: "S02", milestoneId: "M001", status: "complete" });
1154
+ insertAssessment({
1155
+ path: "milestones/M001/M001-VALIDATION.md",
1156
+ milestoneId: "M001",
1157
+ status: "pass",
1158
+ scope: "milestone-validation",
1159
+ fullContent: "verdict: pass",
1160
+ });
1151
1161
 
1152
1162
  invalidateAllCaches();
1153
1163
  const state = await deriveStateFromDb(base);
@@ -856,10 +856,10 @@ test('── markdown-renderer: renderAllFromDb produces all files ──', asyn
856
856
  });
857
857
 
858
858
  // ═══════════════════════════════════════════════════════════════════════════
859
- // Graceful Degradation (Disk Fallback)
859
+ // DB-authoritative regeneration
860
860
  // ═══════════════════════════════════════════════════════════════════════════
861
861
 
862
- test('── markdown-renderer: graceful fallback reads from disk when artifact not in DB ──', async () => {
862
+ test('── markdown-renderer: missing artifact regenerates from DB without importing disk projection ──', async () => {
863
863
  const tmpDir = makeTmpDir();
864
864
  const dbPath = path.join(tmpDir, '.gsd', 'gsd.db');
865
865
  openDatabase(dbPath);
@@ -874,7 +874,7 @@ test('── markdown-renderer: graceful fallback reads from disk when artifact
874
874
  // Write roadmap to disk but NOT in artifacts DB
875
875
  const roadmapContent = makeRoadmapContent([
876
876
  { id: 'S01', title: 'Core', done: false },
877
- ]);
877
+ ]) + '\n\nDISK_ONLY_SENTINEL';
878
878
  const roadmapPath = path.join(tmpDir, '.gsd', 'milestones', 'M001', 'M001-ROADMAP.md');
879
879
  fs.writeFileSync(roadmapPath, roadmapContent);
880
880
  clearAllCaches();
@@ -883,14 +883,20 @@ test('── markdown-renderer: graceful fallback reads from disk when artifact
883
883
  const before = getArtifact('milestones/M001/M001-ROADMAP.md');
884
884
  assert.deepStrictEqual(before, null, 'artifact not in DB before render');
885
885
 
886
- // Render — should read from disk, store in DB
886
+ // Render — should regenerate from DB rows, not import/patch disk content.
887
887
  const ok = await renderRoadmapCheckboxes(tmpDir, 'M001');
888
- assert.ok(ok, 'render succeeds with disk fallback');
888
+ assert.ok(ok, 'render succeeds by regenerating from DB');
889
889
 
890
- // Verify artifact now in DB (stored after reading from disk)
890
+ // Verify artifact now exists in DB but does not contain disk-only content.
891
891
  const after = getArtifact('milestones/M001/M001-ROADMAP.md');
892
- assert.ok(after !== null, 'artifact stored in DB after disk fallback render');
893
- assert.ok(after!.full_content.includes('[x] **S01:'), 'DB artifact reflects rendered state');
892
+ assert.ok(after !== null, 'artifact regenerated in DB');
893
+ assert.ok(!after!.full_content.includes('DISK_ONLY_SENTINEL'), 'disk projection content was not imported');
894
+ assert.ok(after!.full_content.includes('S01'), 'DB artifact reflects DB slice state');
895
+
896
+ assert.ok(fs.existsSync(roadmapPath), 'roadmap projection regenerated on disk');
897
+ const diskAfter = fs.readFileSync(roadmapPath, 'utf-8');
898
+ assert.ok(!diskAfter.includes('DISK_ONLY_SENTINEL'), 'disk projection was rewritten from DB');
899
+ assert.ok(diskAfter.includes('S01'), 'disk projection reflects DB slice state');
894
900
  } finally {
895
901
  closeDatabase();
896
902
  cleanupDir(tmpDir);
@@ -9,6 +9,7 @@ import {
9
9
  getRequirementById,
10
10
  getActiveRequirements,
11
11
  insertArtifact,
12
+ SCHEMA_VERSION,
12
13
  _getAdapter,
13
14
  } from '../gsd-db.ts';
14
15
  import {
@@ -363,7 +364,7 @@ test('md-importer: schema v1→v2 migration', () => {
363
364
  openDatabase(':memory:');
364
365
  const adapter = _getAdapter();
365
366
  const version = adapter?.prepare('SELECT MAX(version) as v FROM schema_version').get();
366
- assert.deepStrictEqual(version?.v, 22, 'new DB should be at schema version 22');
367
+ assert.deepStrictEqual(version?.v, SCHEMA_VERSION, `new DB should be at schema version ${SCHEMA_VERSION}`);
367
368
 
368
369
  // Artifacts table should exist
369
370
  const tableCheck = adapter?.prepare("SELECT count(*) as c FROM sqlite_master WHERE type='table' AND name='artifacts'").get();
@@ -2,6 +2,7 @@ import {
2
2
  openDatabase,
3
3
  closeDatabase,
4
4
  isDbAvailable,
5
+ SCHEMA_VERSION,
5
6
  _getAdapter,
6
7
  } from '../gsd-db.ts';
7
8
  import {
@@ -328,9 +329,9 @@ test('memory-store: schema includes memories table', () => {
328
329
  const viewCount = adapter.prepare('SELECT count(*) as cnt FROM active_memories').get();
329
330
  assert.deepStrictEqual(viewCount?.['cnt'], 0, 'active_memories view should exist');
330
331
 
331
- // Verify schema version is 22 (v22 quality_gates DDL fix included)
332
+ // Verify schema version is current (includes quality_gates DDL fix and later migrations)
332
333
  const version = adapter.prepare('SELECT MAX(version) as v FROM schema_version').get();
333
- assert.deepStrictEqual(version?.["v"], 22, 'schema version should be 22');
334
+ assert.deepStrictEqual(version?.["v"], SCHEMA_VERSION, `schema version should be ${SCHEMA_VERSION}`);
334
335
 
335
336
  closeDatabase();
336
337
  });
@@ -20,6 +20,8 @@ import {
20
20
  } from "../gsd-db.ts";
21
21
  import { createWorktree } from "../worktree-manager.ts";
22
22
 
23
+ // This suite exercises the explicit legacy markdown derivation path.
24
+ process.env.GSD_ALLOW_MARKDOWN_DERIVE_FALLBACK = '1';
23
25
 
24
26
 
25
27
  // ─── Fixture Helpers ───────────────────────────────────────────────────────
@@ -6,6 +6,7 @@ import assert from "node:assert/strict";
6
6
  import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
7
7
  import { join } from "node:path";
8
8
  import { tmpdir } from "node:os";
9
+ import { performance } from "node:perf_hooks";
9
10
 
10
11
  import {
11
12
  openDatabase,
@@ -99,21 +100,21 @@ test("ADR-011: sketch slice + progressive_planning ON → phase='refining'", asy
99
100
  assert.equal(state.phase, "refining", "sketch slice with flag ON must yield refining phase");
100
101
  });
101
102
 
102
- test("ADR-011: sketch slice + progressive_planning OFF → phase='planning' (backwards compat)", async (t) => {
103
+ test("ADR-011: sketch slice + progressive_planning OFF → DB sketch metadata still yields refining", async (t) => {
103
104
  const originalCwd = process.cwd();
104
105
  const base = makeFixtureBase();
105
106
  t.after(() => cleanup(base, originalCwd));
106
107
 
107
108
  seedMilestoneWithSketchedS02(base);
108
109
  writeS01Artifacts(base);
109
- // Write a PREFERENCES.md without the flag so loadEffectiveGSDPreferences finds
110
- // a valid file but progressive_planning resolves to undefined.
110
+ // Write a PREFERENCES.md without the flag. DB slice metadata remains
111
+ // authoritative for whether this slice needs refinement.
111
112
  writePreferences(base, "phases:\n skip_research: false");
112
113
  process.chdir(base);
113
114
 
114
115
  const state = await deriveStateFromDb(base);
115
116
  assert.equal(state.activeSlice?.id, "S02");
116
- assert.equal(state.phase, "planning", "flag absent must fall through to planning");
117
+ assert.equal(state.phase, "refining", "flag absent must not override DB sketch metadata");
117
118
  });
118
119
 
119
120
  test("ADR-011: dispatch rule maps refining → refine-slice unit", async (t) => {
@@ -516,24 +517,32 @@ test("ADR-011 P3 #26: refine-slice dispatch latency is bounded vs plan-slice bas
516
517
  await buildPlanSlicePrompt("M001", "Test", "S02", "Feature", base);
517
518
  await buildRefineSlicePrompt("M001", "Test", "S02", "Feature", base);
518
519
 
519
- const planStart = Date.now();
520
- await buildPlanSlicePrompt("M001", "Test", "S02", "Feature", base);
521
- const planElapsed = Date.now() - planStart;
520
+ const measure = async (fn: () => Promise<string>): Promise<number> => {
521
+ const start = performance.now();
522
+ await fn();
523
+ return performance.now() - start;
524
+ };
522
525
 
523
- const refineStart = Date.now();
524
- await buildRefineSlicePrompt("M001", "Test", "S02", "Feature", base);
525
- const refineElapsed = Date.now() - refineStart;
526
+ const planSamples: number[] = [];
527
+ const refineSamples: number[] = [];
528
+ for (let i = 0; i < 5; i++) {
529
+ planSamples.push(await measure(() => buildPlanSlicePrompt("M001", "Test", "S02", "Feature", base)));
530
+ refineSamples.push(await measure(() => buildRefineSlicePrompt("M001", "Test", "S02", "Feature", base)));
531
+ }
532
+ const bestPlan = Math.min(...planSamples);
533
+ const bestRefine = Math.min(...refineSamples);
526
534
 
527
535
  assert.ok(
528
- refineElapsed < 500,
529
- `refine-slice prompt build must complete under 500ms (took ${refineElapsed}ms)`,
536
+ bestRefine < 500,
537
+ `refine-slice prompt build must complete under 500ms (best=${bestRefine.toFixed(1)}ms, samples=${refineSamples.map(n => n.toFixed(1)).join(",")})`,
530
538
  );
531
539
  // Guard the ratio only when the baseline is large enough to be meaningful —
532
- // if plan-slice measures 0-2ms the ratio is dominated by timer noise.
533
- if (planElapsed >= 5) {
540
+ // if plan-slice measures in single-digit milliseconds, the ratio is dominated
541
+ // by scheduler and filesystem noise under the concurrent test runner.
542
+ if (bestPlan >= 20) {
534
543
  assert.ok(
535
- refineElapsed < planElapsed * 3,
536
- `refine-slice must not exceed 3x plan-slice baseline (refine=${refineElapsed}ms, plan=${planElapsed}ms)`,
544
+ bestRefine < bestPlan * 3,
545
+ `refine-slice must not exceed 3x plan-slice baseline (refine=${bestRefine.toFixed(1)}ms, plan=${bestPlan.toFixed(1)}ms)`,
537
546
  );
538
547
  }
539
548
  });
@@ -91,6 +91,7 @@ function makeMilestoneRow() {
91
91
  definition_of_done: [],
92
92
  requirement_coverage: '',
93
93
  boundary_map_markdown: '',
94
+ sequence: 0,
94
95
  };
95
96
  }
96
97