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
@@ -21,6 +21,8 @@ import {
21
21
  maybeHandleEmptyIntentTurn,
22
22
  resetEmptyTurnCounter,
23
23
  } from "../guided-flow.ts";
24
+ import { drainLogs } from "../workflow-logger.ts";
25
+ import { resolveMilestoneFile, clearPathCache } from "../paths.ts";
24
26
 
25
27
  // ─── Test harness ──────────────────────────────────────────────────────────
26
28
 
@@ -177,6 +179,79 @@ describe("#4573 maybeHandleReadyPhraseWithoutFiles", () => {
177
179
  }
178
180
  });
179
181
 
182
+ test("stale path cache from a prior listing → fresh writes are detected (regression)", () => {
183
+ // Repro the live binary failure where:
184
+ // 1. paths.ts cached dir listings were populated when M001/ was empty
185
+ // (or the milestone dir didn't yet exist).
186
+ // 2. The LLM then wrote M001-CONTEXT.md and M001-ROADMAP.md via the
187
+ // standard Write tool — which has no awareness of paths.ts caches.
188
+ // 3. maybeHandleReadyPhraseWithoutFiles called resolveMilestoneFile,
189
+ // which read the stale cache and reported the artifacts missing,
190
+ // firing a false rejection nudge until MAX_READY_REJECTS aborted
191
+ // the auto-start with `LLM signaled "ready" 3 times without
192
+ // writing files`.
193
+ //
194
+ // The fix busts the path cache at the top of the validator before
195
+ // re-resolving. This test fails pre-fix (handled === true) because the
196
+ // cache returns the empty listing it captured in step (a).
197
+ const base = mkBase();
198
+ try {
199
+ const mDir = join(base, ".gsd", "milestones", "M001");
200
+
201
+ // (a) Prime the cache with a listing that DOES NOT include M001's
202
+ // CONTEXT/ROADMAP files. mkBase() has already created the M001
203
+ // directory but nothing inside it yet — so this readdir caches an
204
+ // empty entry list keyed by the M001 dir path.
205
+ clearPathCache();
206
+ assert.equal(
207
+ resolveMilestoneFile(base, "M001", "CONTEXT"),
208
+ null,
209
+ "precondition: resolver must report missing before files are written",
210
+ );
211
+
212
+ // (b) Write the artifacts directly to disk (simulates the LLM Write
213
+ // tool — no clearPathCache() call between the write and the
214
+ // validator).
215
+ writeFileSync(join(mDir, "M001-CONTEXT.md"), "# ctx");
216
+ writeFileSync(join(mDir, "M001-ROADMAP.md"), "# roadmap");
217
+
218
+ // (c) Sanity: the cache is still stale. Without the fix, the
219
+ // validator would still see the empty cached listing.
220
+ assert.equal(
221
+ resolveMilestoneFile(base, "M001", "CONTEXT"),
222
+ null,
223
+ "stale cache still reports missing pre-clearPathCache",
224
+ );
225
+
226
+ // (d) Run the validator. With the fix it busts the cache before
227
+ // resolving and returns false (no nudge). Without the fix it
228
+ // fires the nudge.
229
+ const cap = mkCapture();
230
+ setPendingAutoStart(base, {
231
+ basePath: base,
232
+ milestoneId: "M001",
233
+ ctx: mkCtx(cap),
234
+ pi: mkPi(cap),
235
+ });
236
+ const handled = maybeHandleReadyPhraseWithoutFiles({
237
+ messages: [assistantMsg("Milestone M001 ready.")],
238
+ });
239
+ assert.equal(
240
+ handled,
241
+ false,
242
+ "fresh writes must not trigger the rejection nudge — cache must be busted before resolution",
243
+ );
244
+ assert.equal(cap.messages.length, 0, "no nudge sent");
245
+ assert.equal(
246
+ cap.notifies.length,
247
+ 0,
248
+ "no rejection notify when files exist on disk",
249
+ );
250
+ } finally {
251
+ clearPendingAutoStart();
252
+ }
253
+ });
254
+
180
255
  test("legacy unprefixed files present → no nudge", () => {
181
256
  const base = mkBase();
182
257
  try {
@@ -219,6 +294,81 @@ describe("#4573 maybeHandleReadyPhraseWithoutFiles", () => {
219
294
  }
220
295
  });
221
296
 
297
+ test("nudge fires → diagnostic warning logged with basePath, mDir, canonical-path existsSync results", () => {
298
+ // Diagnostic logging added so we can tell, in real failures, whether
299
+ // resolveMilestoneFile is reporting files missing that actually exist on
300
+ // disk (basePath/symlink mismatch, stale cache despite the
301
+ // agent-end-recovery flush, legacy descriptor dir, etc.).
302
+ const base = mkBase();
303
+ try {
304
+ drainLogs(); // discard prior test noise
305
+ const cap = mkCapture();
306
+ setPendingAutoStart(base, {
307
+ basePath: base,
308
+ milestoneId: "M001",
309
+ ctx: mkCtx(cap),
310
+ pi: mkPi(cap),
311
+ });
312
+ const handled = maybeHandleReadyPhraseWithoutFiles({
313
+ messages: [assistantMsg("Milestone M001 ready.")],
314
+ });
315
+ assert.equal(handled, true);
316
+
317
+ const logs = drainLogs();
318
+ const diag = logs.find(
319
+ (e) => e.component === "guided" && /ready-phrase-reject diagnostic/.test(e.message),
320
+ );
321
+ assert.ok(diag, "expected diagnostic warning to be logged when nudge fires");
322
+ assert.match(diag!.message, /mid=M001/);
323
+ assert.match(diag!.message, new RegExp(`basePath=${base.replace(/[/\\]/g, "[/\\\\]")}`));
324
+ assert.match(diag!.message, /mDir=/);
325
+ assert.match(diag!.message, /ctx-exists=false/);
326
+ assert.match(diag!.message, /roadmap-exists=false/);
327
+ } finally {
328
+ clearPendingAutoStart();
329
+ }
330
+ });
331
+
332
+ test("diagnostic logs ctx-exists=true when file is on disk but cached resolver missed it", () => {
333
+ // Simulates the test123 #5xxx scenario: file exists on disk, cached
334
+ // resolver claims it doesn't. We drop a file with a non-canonical path
335
+ // (forces the legacy-descriptor pattern miss) so resolveMilestoneFile
336
+ // returns null but existsSync on the canonical path returns true.
337
+ //
338
+ // Note: the canonical path probe in the diagnostic uses the literal
339
+ // `${milestoneId}-CONTEXT.md` filename. If a file is at that path,
340
+ // existsSync will see it regardless of resolver behavior.
341
+ const base = mkBase();
342
+ try {
343
+ drainLogs();
344
+ // Write the canonical file directly — both resolver AND existsSync
345
+ // would normally see it. To prove the diagnostic captures the
346
+ // existsSync result independently, we cover the basic case here.
347
+ const cap = mkCapture();
348
+ setPendingAutoStart(base, {
349
+ basePath: base,
350
+ milestoneId: "M001",
351
+ ctx: mkCtx(cap),
352
+ pi: mkPi(cap),
353
+ });
354
+ // No files written — both probes should report false.
355
+ maybeHandleReadyPhraseWithoutFiles({
356
+ messages: [assistantMsg("Milestone M001 ready.")],
357
+ });
358
+ const logs = drainLogs();
359
+ const diag = logs.find(
360
+ (e) => e.component === "guided" && /ready-phrase-reject diagnostic/.test(e.message),
361
+ );
362
+ assert.ok(diag, "diagnostic logged");
363
+ // mDir resolves because mkBase creates the directory
364
+ assert.match(diag!.message, /mDir=.+M001/);
365
+ assert.match(diag!.message, /canonical-ctx=.+M001-CONTEXT\.md/);
366
+ assert.match(diag!.message, /canonical-roadmap=.+M001-ROADMAP\.md/);
367
+ } finally {
368
+ clearPendingAutoStart();
369
+ }
370
+ });
371
+
222
372
  test("fresh entry after give-up resets counter", () => {
223
373
  const base = mkBase();
224
374
  try {
@@ -344,6 +494,40 @@ describe("#4573 maybeHandleEmptyIntentTurn", () => {
344
494
  }
345
495
  });
346
496
 
497
+ test("single-line approval prompt with mid-line `?` and conditional intent → treated as user-handoff (regression: #5187 follow-up)", () => {
498
+ // Regression for the discuss-milestone case where the LLM presented a
499
+ // depth summary and ended with: "Did I capture that correctly? If so,
500
+ // say yes and I'll write requirements and the roadmap preview."
501
+ // The previous heuristic only checked for lines *ending* in `?`, so
502
+ // this single-line paragraph (terminating in `.`) bypassed the
503
+ // user-handoff guard, then COMMIT_INTENT_RE matched "I'll write" and
504
+ // the nudge auto-replied while the user was meant to approve.
505
+ const base = mkBase();
506
+ try {
507
+ const cap = mkCapture();
508
+ setPendingAutoStart(base, {
509
+ basePath: base,
510
+ milestoneId: "M001",
511
+ ctx: mkCtx(cap),
512
+ pi: mkPi(cap),
513
+ });
514
+ const handled = maybeHandleEmptyIntentTurn(
515
+ {
516
+ messages: [
517
+ assistantMsg(
518
+ "Did I capture that correctly? If so, say yes and I'll write requirements and the roadmap preview.",
519
+ ),
520
+ ],
521
+ },
522
+ false,
523
+ );
524
+ assert.equal(handled, false, "any sentence-terminating ? must defer to the user");
525
+ assert.equal(cap.messages.length, 0);
526
+ } finally {
527
+ clearPendingAutoStart();
528
+ }
529
+ });
530
+
347
531
  test('"Let me make sure" meta phrase → not flagged as commit intent (regression)', () => {
348
532
  const base = mkBase();
349
533
  try {
@@ -6,7 +6,7 @@ import { tmpdir } from "node:os";
6
6
 
7
7
  import { registerHooks } from "../bootstrap/register-hooks.ts";
8
8
  import { parseContinue } from "../files.ts";
9
- import { closeDatabase } from "../gsd-db.ts";
9
+ import { closeDatabase, insertMilestone, insertSlice, openDatabase } from "../gsd-db.ts";
10
10
  import { deriveState, invalidateStateCache } from "../state.ts";
11
11
 
12
12
  function createPlanningFixtureBase(): string {
@@ -39,6 +39,11 @@ function createPlanningFixtureBase(): string {
39
39
  `,
40
40
  );
41
41
 
42
+ openDatabase(join(base, ".gsd", "gsd.db"));
43
+ insertMilestone({ id: "M001", title: "Test Milestone", status: "active" });
44
+ insertSlice({ id: "S01", milestoneId: "M001", title: "Test Slice", status: "active", risk: "low", depends: [] });
45
+ closeDatabase();
46
+
42
47
  return base;
43
48
  }
44
49
 
@@ -8,6 +8,9 @@ import { fileURLToPath } from 'node:url';
8
8
  import { parseSummary } from '../files.ts';
9
9
  import { deriveState } from '../state.ts';
10
10
 
11
+ // This suite exercises the explicit legacy markdown derivation path.
12
+ process.env.GSD_ALLOW_MARKDOWN_DERIVE_FALLBACK = '1';
13
+
11
14
  const __dirname = dirname(fileURLToPath(import.meta.url));
12
15
  const worktreePromptsDir = join(__dirname, '..', 'prompts');
13
16
 
@@ -1,5 +1,9 @@
1
1
  import { register } from 'node:module';
2
2
  import { pathToFileURL } from 'node:url';
3
3
 
4
+ // Source legacy state tests exercise markdown derivation through deriveState().
5
+ // Production/runtime keeps this fallback disabled unless explicitly requested.
6
+ process.env.GSD_ALLOW_MARKDOWN_DERIVE_FALLBACK ??= '1';
7
+
4
8
  // Register hook to redirect imports to the dist directory
5
9
  register(new URL('./dist-redirect.mjs', import.meta.url), pathToFileURL('./'));
@@ -149,7 +149,7 @@ test("rogue detection: DB not available → returns empty array (graceful degrad
149
149
  }
150
150
  });
151
151
 
152
- test("rogue detection: slice summary on disk, no DB rowauto-remediated (not rogue)", () => {
152
+ test("rogue detection: slice summary on disk, no DB completiondetected as rogue without DB import", () => {
153
153
  const basePath = createTmpBase();
154
154
  const dbPath = join(basePath, ".gsd", "gsd.db");
155
155
  mkdirSync(join(basePath, ".gsd"), { recursive: true });
@@ -160,10 +160,9 @@ test("rogue detection: slice summary on disk, no DB row → auto-remediated (not
160
160
  const summaryPath = createSliceSummaryOnDisk(basePath, "M001", "S01");
161
161
  assert.ok(existsSync(summaryPath), "Slice summary file should exist on disk");
162
162
 
163
- // Fix #3633: stale slice DB status is auto-remediated via updateSliceStatus()
164
- // instead of being reported as rogue, so rogues array should be empty.
165
163
  const rogues = detectRogueFileWrites("complete-slice", "M001/S01", basePath);
166
- assert.equal(rogues.length, 0, "Should auto-remediate stale slice, not report as rogue");
164
+ assert.equal(rogues.length, 1, "Should report stale disk summary instead of mutating DB");
165
+ assert.equal(rogues[0]?.path, summaryPath);
167
166
  } finally {
168
167
  closeDatabase();
169
168
  rmSync(basePath, { recursive: true, force: true });
@@ -1,10 +1,8 @@
1
1
  /**
2
2
  * slice-disk-reconcile.test.ts — #2533
3
3
  *
4
- * Slices that exist on disk (in ROADMAP.md) but are missing from the SQLite
5
- * database cause permanent "No slice eligible check dependency ordering"
6
- * blocks. deriveStateFromDb must reconcile disk slices into the DB, just as
7
- * it already does for milestones (#2416).
4
+ * DB-authoritative state: slices that exist only in ROADMAP.md are projections
5
+ * and must not be imported into the SQLite database by deriveStateFromDb().
8
6
  *
9
7
  * Scenario: M001 has a ROADMAP with S01-S04. S01 and S02 have SUMMARY files
10
8
  * (complete on disk). S03 depends on S01. Only S04 is in the DB (depends on
@@ -70,7 +68,7 @@ const ROADMAP_CONTENT = `# M001: Test Milestone
70
68
  `;
71
69
 
72
70
  async function testMissingSlicesCauseBlock(): Promise<void> {
73
- console.log("\n--- Test: missing DB slices cause permanent block (pre-fix) ---");
71
+ console.log("\n--- Test: missing DB slices are not imported from ROADMAP.md ---");
74
72
 
75
73
  const base = createFixtureBase();
76
74
  const dbPath = join(base, ".gsd", "gsd.db");
@@ -96,51 +94,11 @@ async function testMissingSlicesCauseBlock(): Promise<void> {
96
94
  invalidateStateCache();
97
95
  const state = await deriveStateFromDb(base);
98
96
 
99
- // After the fix, slices S01-S03 should be reconciled into DB
100
97
  const dbSlices = getMilestoneSlices("M001");
101
- assertTrue(
102
- dbSlices.length === 4,
103
- `All 4 roadmap slices should be in DB after reconciliation, got ${dbSlices.length}`,
104
- );
105
-
106
- // S01 and S02 should be marked complete (have SUMMARY files)
107
- const s01 = dbSlices.find(s => s.id === "S01");
108
- assertTrue(s01 !== undefined, "S01 should exist in DB after reconciliation");
109
- if (s01) {
110
- assertEq(s01.status, "complete", "S01 should be 'complete' (has SUMMARY on disk)");
111
- }
112
-
113
- const s02 = dbSlices.find(s => s.id === "S02");
114
- assertTrue(s02 !== undefined, "S02 should exist in DB after reconciliation");
115
- if (s02) {
116
- assertEq(s02.status, "complete", "S02 should be 'complete' (has SUMMARY on disk)");
117
- }
118
-
119
- // S03 should be pending (no SUMMARY)
120
- const s03 = dbSlices.find(s => s.id === "S03");
121
- assertTrue(s03 !== undefined, "S03 should exist in DB after reconciliation");
122
- if (s03) {
123
- assertEq(s03.status, "pending", "S03 should be 'pending' (no SUMMARY on disk)");
124
- }
125
-
126
- // The state should NOT be blocked — S03 should be eligible (S01 dep satisfied)
127
- assertTrue(
128
- state.phase !== "blocked",
129
- `Phase should not be 'blocked' after reconciliation, got '${state.phase}'`,
130
- );
131
-
132
- // Active slice should be S03 (S01 dep met, S03 is first incomplete with satisfied deps)
133
- assertTrue(
134
- state.activeSlice !== null,
135
- "There should be an active slice after reconciliation",
136
- );
137
- if (state.activeSlice) {
138
- assertEq(
139
- state.activeSlice.id,
140
- "S03",
141
- "Active slice should be S03 (its dependency S01 is complete) (#2533)",
142
- );
143
- }
98
+ assertEq(dbSlices.length, 1, `Only DB slice S04 should remain, got ${dbSlices.length}`);
99
+ assertEq(dbSlices[0]?.id, "S04", "Disk-only slices are not inserted");
100
+ assertEq(state.phase, "blocked", "DB-only S04 remains blocked on missing DB dependency S03");
101
+ assertEq(state.activeSlice, null, "No active slice is inferred from roadmap-only rows");
144
102
  } finally {
145
103
  closeDatabase();
146
104
  cleanup(base);
@@ -148,7 +106,7 @@ async function testMissingSlicesCauseBlock(): Promise<void> {
148
106
  }
149
107
 
150
108
  async function testSliceReconciliationIdempotent(): Promise<void> {
151
- console.log("\n--- Test: slice reconciliation is idempotent ---");
109
+ console.log("\n--- Test: disk-only slices remain absent on repeated derives ---");
152
110
 
153
111
  const base = createFixtureBase();
154
112
  const dbPath = join(base, ".gsd", "gsd.db");
@@ -178,11 +136,7 @@ async function testSliceReconciliationIdempotent(): Promise<void> {
178
136
  assertEq(s01.status, "complete", "S01 status should remain 'complete' (not overwritten)");
179
137
  }
180
138
 
181
- // S02-S04 should have been added
182
- assertTrue(
183
- dbSlices.length === 4,
184
- `Should have 4 slices after reconciliation (existing + new), got ${dbSlices.length}`,
185
- );
139
+ assertEq(dbSlices.length, 1, `Only existing DB slice should remain, got ${dbSlices.length}`);
186
140
  } finally {
187
141
  closeDatabase();
188
142
  cleanup(base);
@@ -218,7 +172,7 @@ async function testNoRoadmapSkipsReconciliation(): Promise<void> {
218
172
  }
219
173
 
220
174
  async function main(): Promise<void> {
221
- console.log("\n=== #2533: deriveStateFromDb reconciles disk slices ===");
175
+ console.log("\n=== deriveStateFromDb does not reconcile disk slices ===");
222
176
 
223
177
  await testMissingSlicesCauseBlock();
224
178
  await testSliceReconciliationIdempotent();
@@ -1,10 +1,9 @@
1
1
  /**
2
- * stale-slice-rows.test.ts — #3658
2
+ * stale-slice-rows.test.ts
3
3
  *
4
- * Verify that state.ts contains slice-level status reconciliation that
5
- * updates stale DB rows (status "pending") when disk artifacts (SUMMARY)
6
- * prove the slice is complete. Without this, the dependency resolver builds
7
- * doneSliceIds from stale DB rows and downstream slices stay blocked.
4
+ * Verify that state.ts no longer treats slice SUMMARY.md projections as
5
+ * authority for DB slice status. Slice rows must be updated through DB-backed
6
+ * completion/import APIs.
8
7
  */
9
8
 
10
9
  import { describe, test } from "node:test";
@@ -16,26 +15,26 @@ import { fileURLToPath } from "node:url";
16
15
  const __dirname = dirname(fileURLToPath(import.meta.url));
17
16
  const sourceFile = join(__dirname, "..", "state.ts");
18
17
 
19
- describe("stale slice row reconciliation (#3658)", () => {
18
+ describe("stale slice row DB-authoritative boundary", () => {
20
19
  const source = readFileSync(sourceFile, "utf-8");
21
20
 
22
- test("imports updateSliceStatus from gsd-db", () => {
23
- assert.match(source, /import\s*\{[^}]*updateSliceStatus[^}]*\}\s*from/);
21
+ test("does not import updateSliceStatus into state derivation", () => {
22
+ assert.doesNotMatch(source, /import\s*\{[^}]*updateSliceStatus[^}]*\}\s*from/);
24
23
  });
25
24
 
26
- test("checks isStatusDone before reconciling slice rows", () => {
27
- assert.match(source, /isStatusDone\(dbSlice\.status\)/);
25
+ test("does not scan DB slice rows for disk SUMMARY reconciliation", () => {
26
+ assert.doesNotMatch(source, /dbSlice/);
28
27
  });
29
28
 
30
- test("resolves SUMMARY file to detect completed slices on disk", () => {
31
- assert.match(source, /resolveSliceFile\(basePath,\s*mid,\s*dbSlice\.id,\s*["']SUMMARY["']\)/);
29
+ test("does not resolve slice SUMMARY to mutate DB state", () => {
30
+ assert.doesNotMatch(source, /resolveSliceFile\(basePath,\s*mid,\s*dbSlice\.id,\s*["']SUMMARY["']\)/);
32
31
  });
33
32
 
34
- test("calls updateSliceStatus to reconcile stale rows", () => {
35
- assert.match(source, /updateSliceStatus\(mid,\s*dbSlice\.id,\s*["']complete["']\)/);
33
+ test("does not call updateSliceStatus from state derivation", () => {
34
+ assert.doesNotMatch(source, /updateSliceStatus\(/);
36
35
  });
37
36
 
38
- test("references issue #3599 in reconciliation comment", () => {
39
- assert.match(source, /#3599/);
37
+ test("documents markdown projections as non-authoritative", () => {
38
+ assert.match(source, /Markdown files are projections only/);
40
39
  });
41
40
  });
@@ -69,6 +69,7 @@ function makeMilestoneRow(overrides: Partial<MilestoneRow> = {}): MilestoneRow {
69
69
  definition_of_done: [],
70
70
  requirement_coverage: "",
71
71
  boundary_map_markdown: "",
72
+ sequence: 0,
72
73
  ...overrides,
73
74
  };
74
75
  }
@@ -858,11 +858,11 @@ describe("state-machine-full-walkthrough", () => {
858
858
  });
859
859
 
860
860
  // ═══════════════════════════════════════════════════════════════════════════
861
- // RECONCILIATION
861
+ // DB-AUTHORITATIVE DERIVATION
862
862
  // ═══════════════════════════════════════════════════════════════════════════
863
863
 
864
- describe("Reconciliation", () => {
865
- test("DB: task with SUMMARY on disk but DB says pending → reconciliation fixes status (#2514)", async () => {
864
+ describe("DB-authoritative derivation", () => {
865
+ test("DB: task with SUMMARY on disk but DB says pending → DB remains authoritative", async () => {
866
866
  const base = createFixtureBase();
867
867
  const dbPath = join(base, ".gsd", "gsd.db");
868
868
  openDatabase(dbPath);
@@ -875,19 +875,19 @@ describe("state-machine-full-walkthrough", () => {
875
875
  writeRoadmap(base, "M001", standardRoadmap());
876
876
  writePlan(base, "M001", "S01", standardPlan());
877
877
 
878
- // Write SUMMARY files on disk for both tasks (simulating session disconnect)
878
+ // Write SUMMARY files on disk for both tasks. These are projections and
879
+ // must not complete pending DB tasks during runtime derivation.
879
880
  writeTaskSummary(base, "M001", "S01", "T01");
880
881
  writeTaskSummary(base, "M001", "S01", "T02");
881
882
 
882
883
  invalidateStateCache();
883
884
  const state = await deriveStateFromDb(base);
884
885
 
885
- // Reconciliation should detect SUMMARY→DB mismatch and update
886
- // All tasks done summarizing (not executing)
887
- assert.equal(state.phase, "summarizing", "reconciliation should advance past pending tasks");
886
+ assert.equal(state.phase, "executing", "disk SUMMARY projections must not complete DB tasks");
887
+ assert.equal(state.activeTask?.id, "T01", "first pending DB task remains active");
888
888
  });
889
889
 
890
- test("empty DB with disk milestones → disk-to-DB sync (#2631)", async () => {
890
+ test("empty DB with disk milestones → no runtime disk-to-DB sync", async () => {
891
891
  const base = createFixtureBase();
892
892
  writeContext(base, "M001", "# M001: Test\n\nContext.");
893
893
 
@@ -899,11 +899,10 @@ describe("state-machine-full-walkthrough", () => {
899
899
  invalidateStateCache();
900
900
  const state = await deriveState(base);
901
901
 
902
- // After deriveState, DB should have the disk milestone
902
+ // Runtime derivation must not import disk milestones into the DB.
903
903
  const after = getAllMilestones();
904
- assert.ok(after.length > 0, "DB should have milestones after reconciliation");
905
- assert.equal(after[0]!.id, "M001");
906
- assert.ok(state.activeMilestone !== null);
904
+ assert.equal(after.length, 0, "DB should remain empty without explicit migration");
905
+ assert.equal(state.activeMilestone, null, "disk milestone is ignored while DB is authoritative");
907
906
  });
908
907
 
909
908
  test("ghost milestone (empty dir) → NOT in registry", async () => {
@@ -1063,7 +1062,7 @@ describe("state-machine-full-walkthrough", () => {
1063
1062
  // ═══════════════════════════════════════════════════════════════════════════
1064
1063
 
1065
1064
  describe("Recovery: DB has slice but no task rows (partial migration)", () => {
1066
- test("DB tasks empty but PLAN on disk has tasks → reconciles to executing", async () => {
1065
+ test("DB tasks empty but PLAN on disk has tasks → stays planning", async () => {
1067
1066
  const base = createFixtureBase();
1068
1067
  const dbPath = join(base, ".gsd", "gsd.db");
1069
1068
  openDatabase(dbPath);
@@ -1078,15 +1077,13 @@ describe("state-machine-full-walkthrough", () => {
1078
1077
  invalidateStateCache();
1079
1078
  const state = await deriveStateFromDb(base);
1080
1079
 
1081
- // FIX (#3600): plan-file tasks are now reconciled into the DB,
1082
- // so the phase correctly advances to executing instead of planning.
1083
- assert.equal(state.phase, "executing",
1084
- "reconciled plan-file tasks → executing (not stuck in planning)");
1080
+ assert.equal(state.phase, "planning",
1081
+ "PLAN.md projection must not import DB tasks during runtime derivation");
1085
1082
  });
1086
1083
  });
1087
1084
 
1088
1085
  describe("Failure: partial SUMMARY reconciliation", () => {
1089
- test("only one task has SUMMARY, other still pending → executing next task", async () => {
1086
+ test("only one task has SUMMARY, other still pending → executing first DB-pending task", async () => {
1090
1087
  const base = createFixtureBase();
1091
1088
  const dbPath = join(base, ".gsd", "gsd.db");
1092
1089
  openDatabase(dbPath);
@@ -1104,9 +1101,8 @@ describe("state-machine-full-walkthrough", () => {
1104
1101
  invalidateStateCache();
1105
1102
  const state = await deriveStateFromDb(base);
1106
1103
 
1107
- // T01 reconciled to complete, T02 still pending → executing T02
1108
1104
  assert.equal(state.phase, "executing");
1109
- assert.equal(state.activeTask?.id, "T02", "should advance to next pending task");
1105
+ assert.equal(state.activeTask?.id, "T01", "disk SUMMARY must not advance past pending DB task");
1110
1106
  });
1111
1107
  });
1112
1108
 
@@ -1255,7 +1251,7 @@ describe("state-machine-full-walkthrough", () => {
1255
1251
  });
1256
1252
 
1257
1253
  describe("Failure: missing task plan files in DB path", () => {
1258
- test("DB has tasks but no T##-PLAN.md files → planning phase", async () => {
1254
+ test("DB has tasks but no T##-PLAN.md files → executing phase", async () => {
1259
1255
  const base = createFixtureBase();
1260
1256
  const dbPath = join(base, ".gsd", "gsd.db");
1261
1257
  openDatabase(dbPath);
@@ -1273,8 +1269,8 @@ describe("state-machine-full-walkthrough", () => {
1273
1269
  invalidateStateCache();
1274
1270
  const state = await deriveStateFromDb(base);
1275
1271
 
1276
- assert.equal(state.phase, "planning",
1277
- "missing T##-PLAN.md files should keep state in planning");
1272
+ assert.equal(state.phase, "executing",
1273
+ "DB tasks are authoritative even when task plan projections are missing");
1278
1274
  });
1279
1275
  });
1280
1276
 
@@ -1591,7 +1587,7 @@ describe("state-machine-full-walkthrough", () => {
1591
1587
  });
1592
1588
 
1593
1589
  describe("Failure: multiple reconciliation in single derivation", () => {
1594
- test("DB has 3 stale tasks, all with SUMMARY on disk → all reconciled in one pass", async () => {
1590
+ test("DB has 3 stale tasks, all with SUMMARY on disk → first DB-pending task remains active", async () => {
1595
1591
  const base = createFixtureBase();
1596
1592
  const dbPath = join(base, ".gsd", "gsd.db");
1597
1593
  openDatabase(dbPath);
@@ -1641,9 +1637,9 @@ describe("state-machine-full-walkthrough", () => {
1641
1637
  invalidateStateCache();
1642
1638
  const state = await deriveStateFromDb(base);
1643
1639
 
1644
- // All 3 should be reconciled in one pass → summarizing
1645
- assert.equal(state.phase, "summarizing",
1646
- "all 3 stale tasks should be reconciled to complete in one derivation");
1640
+ assert.equal(state.phase, "executing",
1641
+ "disk SUMMARY projections must not reconcile DB task state");
1642
+ assert.equal(state.activeTask?.id, "T01", "first non-closed DB task remains active");
1647
1643
  });
1648
1644
  });
1649
1645
  });
@@ -1,6 +1,6 @@
1
1
  // GSD Extension - Steer Worktree Path Resolution Test
2
- // Regression test for #3476: /gsd steer must write overrides to the worktree .gsd/,
3
- // not the project root .gsd/, when a worktree is active.
2
+ // Worktrees share the canonical project .gsd state root. /gsd steer writes
3
+ // overrides to that canonical root even when invoked with a worktree path.
4
4
 
5
5
  import { describe, test, beforeEach, afterEach } from "node:test";
6
6
  import assert from "node:assert/strict";
@@ -27,32 +27,31 @@ describe("steer worktree path resolution (#3476)", () => {
27
27
  rmSync(projectRoot, { recursive: true, force: true });
28
28
  });
29
29
 
30
- test("appendOverride writes to worktree .gsd/ when worktree path is used", async () => {
30
+ test("appendOverride writes to canonical project .gsd/ when worktree path is used", async () => {
31
31
  await appendOverride(worktreePath, "Use Postgres instead of SQLite", "M001/S01/T01");
32
32
 
33
- // Override should be in the worktree .gsd/
33
+ // Override should be in the canonical project .gsd/
34
34
  const wtOverrides = join(worktreePath, ".gsd", "OVERRIDES.md");
35
- assert.ok(existsSync(wtOverrides), "override file exists in worktree .gsd/");
35
+ const rootOverrides = join(projectRoot, ".gsd", "OVERRIDES.md");
36
+ assert.ok(!existsSync(wtOverrides), "no override file in worktree-local .gsd/");
37
+ assert.ok(existsSync(rootOverrides), "override file exists in project root .gsd/");
36
38
 
37
- const content = readFileSync(wtOverrides, "utf-8");
39
+ const content = readFileSync(rootOverrides, "utf-8");
38
40
  assert.ok(content.includes("Use Postgres instead of SQLite"), "override content is correct");
39
-
40
- // Override should NOT be in the project root .gsd/
41
- const rootOverrides = join(projectRoot, ".gsd", "OVERRIDES.md");
42
- assert.ok(!existsSync(rootOverrides), "no override file in project root .gsd/");
43
41
  });
44
42
 
45
- test("loadActiveOverrides reads from worktree .gsd/ when worktree path is used", async () => {
43
+ test("loadActiveOverrides reads canonical project .gsd/ when worktree path is used", async () => {
46
44
  await appendOverride(worktreePath, "Switch to JWT auth", "M001/S02/T01");
47
45
 
48
- // Loading from worktree should find the override
46
+ // Loading from worktree resolves to the canonical project state root.
49
47
  const wtOverrides = await loadActiveOverrides(worktreePath);
50
48
  assert.equal(wtOverrides.length, 1, "one active override in worktree");
51
49
  assert.equal(wtOverrides[0].change, "Switch to JWT auth");
52
50
 
53
- // Loading from project root should find nothing
51
+ // Loading from project root sees the same canonical override.
54
52
  const rootOverrides = await loadActiveOverrides(projectRoot);
55
- assert.equal(rootOverrides.length, 0, "no overrides in project root");
53
+ assert.equal(rootOverrides.length, 1, "same override visible from project root");
54
+ assert.equal(rootOverrides[0].change, "Switch to JWT auth");
56
55
  });
57
56
 
58
57
  test("appendOverride falls back to project root when no worktree exists", async () => {