@opengsd/gsd-pi 1.2.0-dev.4c756166 → 1.2.0-dev.955e4da0

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 (246) hide show
  1. package/dist/resources/.managed-resources-content-hash +1 -1
  2. package/dist/resources/extensions/bg-shell/utilities.js +2 -2
  3. package/dist/resources/extensions/claude-code-cli/models.js +9 -0
  4. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +8 -2
  5. package/dist/resources/extensions/gsd/auto/orchestrator.js +33 -4
  6. package/dist/resources/extensions/gsd/auto/phases.js +6 -1
  7. package/dist/resources/extensions/gsd/auto-post-unit.js +8 -6
  8. package/dist/resources/extensions/gsd/auto-start.js +8 -13
  9. package/dist/resources/extensions/gsd/auto-worktree-repair.js +10 -2
  10. package/dist/resources/extensions/gsd/auto-worktree.js +13 -270
  11. package/dist/resources/extensions/gsd/auto.js +4 -7
  12. package/dist/resources/extensions/gsd/bootstrap/dynamic-tools.js +9 -6
  13. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +32 -3
  14. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +26 -4
  15. package/dist/resources/extensions/gsd/captures.js +5 -13
  16. package/dist/resources/extensions/gsd/closeout-recovery.js +3 -2
  17. package/dist/resources/extensions/gsd/commands/catalog.js +6 -62
  18. package/dist/resources/extensions/gsd/db/engine.js +755 -0
  19. package/dist/resources/extensions/gsd/db/queries.js +372 -0
  20. package/dist/resources/extensions/gsd/db/sql-constants.js +11 -0
  21. package/dist/resources/extensions/gsd/db/writers/cascades.js +194 -0
  22. package/dist/resources/extensions/gsd/db/writers/import-restore.js +182 -0
  23. package/dist/resources/extensions/gsd/db/writers/memory.js +149 -0
  24. package/dist/resources/extensions/gsd/db/writers/reconcile.js +458 -0
  25. package/dist/resources/extensions/gsd/db/writers/status.js +70 -0
  26. package/dist/resources/extensions/gsd/doctor-environment.js +8 -10
  27. package/dist/resources/extensions/gsd/doctor-git-checks.js +4 -3
  28. package/dist/resources/extensions/gsd/doctor-runtime-checks.js +9 -2
  29. package/dist/resources/extensions/gsd/git-service.js +1 -0
  30. package/dist/resources/extensions/gsd/gitignore.js +3 -0
  31. package/dist/resources/extensions/gsd/gsd-db.js +171 -2048
  32. package/dist/resources/extensions/gsd/guided-flow.js +34 -3
  33. package/dist/resources/extensions/gsd/migrate/safety.js +17 -9
  34. package/dist/resources/extensions/gsd/migration-auto-check.js +24 -3
  35. package/dist/resources/extensions/gsd/model-cost-table.js +1 -0
  36. package/dist/resources/extensions/gsd/model-router.js +3 -0
  37. package/dist/resources/extensions/gsd/parallel-merge.js +14 -11
  38. package/dist/resources/extensions/gsd/parallel-monitor-overlay.js +7 -5
  39. package/dist/resources/extensions/gsd/paths.js +10 -24
  40. package/dist/resources/extensions/gsd/preferences.js +14 -0
  41. package/dist/resources/extensions/gsd/recovery-classification.js +12 -1
  42. package/dist/resources/extensions/gsd/safety/evidence-collector.js +37 -4
  43. package/dist/resources/extensions/gsd/safety/evidence-cross-ref.js +7 -2
  44. package/dist/resources/extensions/gsd/safety/file-change-validator.js +10 -0
  45. package/dist/resources/extensions/gsd/state-transition-matrix.js +38 -0
  46. package/dist/resources/extensions/gsd/status-guards.js +56 -8
  47. package/dist/resources/extensions/gsd/tools/complete-slice.js +24 -43
  48. package/dist/resources/extensions/gsd/tools/exec-tool.js +5 -5
  49. package/dist/resources/extensions/gsd/tools/reopen-milestone.js +11 -29
  50. package/dist/resources/extensions/gsd/tools/reopen-slice.js +14 -33
  51. package/dist/resources/extensions/gsd/tools/skip-slice.js +18 -36
  52. package/dist/resources/extensions/gsd/undo.js +8 -7
  53. package/dist/resources/extensions/gsd/worktree-git-recovery.js +287 -0
  54. package/dist/resources/extensions/gsd/worktree-lifecycle.js +9 -1
  55. package/dist/resources/extensions/gsd/worktree-manager.js +45 -28
  56. package/dist/resources/extensions/gsd/worktree-placement.js +59 -0
  57. package/dist/resources/extensions/gsd/worktree-reentry.js +12 -8
  58. package/dist/resources/extensions/gsd/worktree-root.js +17 -6
  59. package/dist/resources/extensions/gsd/worktree-safety.js +8 -5
  60. package/dist/resources/extensions/gsd/worktree-session-state.js +12 -10
  61. package/dist/resources/skills/gsd-browser/SKILL.md +1 -1
  62. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  63. package/dist/web/standalone/.next/BUILD_ID +1 -1
  64. package/dist/web/standalone/.next/app-path-routes-manifest.json +13 -13
  65. package/dist/web/standalone/.next/build-manifest.json +2 -2
  66. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  67. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  68. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  69. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  70. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  71. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  72. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  73. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  74. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  75. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  76. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  77. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  78. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  79. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  80. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  81. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  82. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  83. package/dist/web/standalone/.next/server/app/api/boot/route.js.nft.json +1 -1
  84. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js.nft.json +1 -1
  85. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js.nft.json +1 -1
  86. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js.nft.json +1 -1
  87. package/dist/web/standalone/.next/server/app/api/captures/route.js.nft.json +1 -1
  88. package/dist/web/standalone/.next/server/app/api/cleanup/route.js.nft.json +1 -1
  89. package/dist/web/standalone/.next/server/app/api/doctor/route.js.nft.json +1 -1
  90. package/dist/web/standalone/.next/server/app/api/export-data/route.js.nft.json +1 -1
  91. package/dist/web/standalone/.next/server/app/api/files/route.js.nft.json +1 -1
  92. package/dist/web/standalone/.next/server/app/api/forensics/route.js.nft.json +1 -1
  93. package/dist/web/standalone/.next/server/app/api/git/route.js.nft.json +1 -1
  94. package/dist/web/standalone/.next/server/app/api/history/route.js.nft.json +1 -1
  95. package/dist/web/standalone/.next/server/app/api/hooks/route.js.nft.json +1 -1
  96. package/dist/web/standalone/.next/server/app/api/inspect/route.js.nft.json +1 -1
  97. package/dist/web/standalone/.next/server/app/api/knowledge/route.js.nft.json +1 -1
  98. package/dist/web/standalone/.next/server/app/api/live-state/route.js.nft.json +1 -1
  99. package/dist/web/standalone/.next/server/app/api/mcp-connections/route.js.nft.json +1 -1
  100. package/dist/web/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
  101. package/dist/web/standalone/.next/server/app/api/onboarding/route.js.nft.json +1 -1
  102. package/dist/web/standalone/.next/server/app/api/projects/route.js.nft.json +1 -1
  103. package/dist/web/standalone/.next/server/app/api/recovery/route.js.nft.json +1 -1
  104. package/dist/web/standalone/.next/server/app/api/session/browser/route.js.nft.json +1 -1
  105. package/dist/web/standalone/.next/server/app/api/session/command/route.js.nft.json +1 -1
  106. package/dist/web/standalone/.next/server/app/api/session/events/route.js.nft.json +1 -1
  107. package/dist/web/standalone/.next/server/app/api/session/manage/route.js.nft.json +1 -1
  108. package/dist/web/standalone/.next/server/app/api/settings-data/route.js.nft.json +1 -1
  109. package/dist/web/standalone/.next/server/app/api/shutdown/route.js.nft.json +1 -1
  110. package/dist/web/standalone/.next/server/app/api/skill-health/route.js.nft.json +1 -1
  111. package/dist/web/standalone/.next/server/app/api/steer/route.js.nft.json +1 -1
  112. package/dist/web/standalone/.next/server/app/api/switch-root/route.js.nft.json +1 -1
  113. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js.nft.json +1 -1
  114. package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js.nft.json +1 -1
  115. package/dist/web/standalone/.next/server/app/api/undo/route.js.nft.json +1 -1
  116. package/dist/web/standalone/.next/server/app/api/visualizer/route.js.nft.json +1 -1
  117. package/dist/web/standalone/.next/server/app/index.html +1 -1
  118. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  119. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  120. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  121. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  122. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  123. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  124. package/dist/web/standalone/.next/server/app-paths-manifest.json +13 -13
  125. package/dist/web/standalone/.next/server/chunks/{5047.js → 5942.js} +2 -2
  126. package/dist/web/standalone/.next/server/chunks/8357.js +1 -1
  127. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  128. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  129. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  130. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  131. package/dist/worktree-status-banner.js +7 -3
  132. package/package.json +1 -1
  133. package/packages/cloud-mcp-gateway/package.json +2 -2
  134. package/packages/contracts/package.json +1 -1
  135. package/packages/daemon/package.json +4 -4
  136. package/packages/gsd-agent-core/package.json +5 -5
  137. package/packages/gsd-agent-modes/package.json +7 -7
  138. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  139. package/packages/mcp-server/dist/workflow-tools.js +30 -21
  140. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  141. package/packages/mcp-server/package.json +3 -3
  142. package/packages/native/package.json +1 -1
  143. package/packages/pi-agent-core/package.json +1 -1
  144. package/packages/pi-ai/dist/models.generated.d.ts +266 -35
  145. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  146. package/packages/pi-ai/dist/models.generated.js +235 -46
  147. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  148. package/packages/pi-ai/package.json +1 -1
  149. package/packages/pi-coding-agent/dist/core/capability-patches.d.ts.map +1 -1
  150. package/packages/pi-coding-agent/dist/core/capability-patches.js +3 -1
  151. package/packages/pi-coding-agent/dist/core/capability-patches.js.map +1 -1
  152. package/packages/pi-coding-agent/package.json +7 -7
  153. package/packages/pi-tui/package.json +2 -2
  154. package/packages/rpc-client/package.json +2 -2
  155. package/pkg/package.json +1 -1
  156. package/src/resources/extensions/bg-shell/utilities.ts +2 -2
  157. package/src/resources/extensions/claude-code-cli/models.ts +9 -0
  158. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +6 -0
  159. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +28 -0
  160. package/src/resources/extensions/gsd/auto/loop-deps.ts +1 -1
  161. package/src/resources/extensions/gsd/auto/orchestrator.ts +39 -5
  162. package/src/resources/extensions/gsd/auto/phases.ts +10 -1
  163. package/src/resources/extensions/gsd/auto-post-unit.ts +12 -5
  164. package/src/resources/extensions/gsd/auto-start.ts +8 -14
  165. package/src/resources/extensions/gsd/auto-worktree-repair.ts +13 -2
  166. package/src/resources/extensions/gsd/auto-worktree.ts +20 -280
  167. package/src/resources/extensions/gsd/auto.ts +12 -9
  168. package/src/resources/extensions/gsd/bootstrap/dynamic-tools.ts +10 -6
  169. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +32 -3
  170. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +25 -3
  171. package/src/resources/extensions/gsd/captures.ts +5 -14
  172. package/src/resources/extensions/gsd/closeout-recovery.ts +2 -1
  173. package/src/resources/extensions/gsd/commands/catalog.ts +6 -68
  174. package/src/resources/extensions/gsd/db/engine.ts +809 -0
  175. package/src/resources/extensions/gsd/db/queries.ts +453 -0
  176. package/src/resources/extensions/gsd/db/sql-constants.ts +12 -0
  177. package/src/resources/extensions/gsd/db/writers/cascades.ts +237 -0
  178. package/src/resources/extensions/gsd/db/writers/import-restore.ts +310 -0
  179. package/src/resources/extensions/gsd/db/writers/memory.ts +220 -0
  180. package/src/resources/extensions/gsd/db/writers/reconcile.ts +500 -0
  181. package/src/resources/extensions/gsd/db/writers/status.ts +88 -0
  182. package/src/resources/extensions/gsd/doctor-environment.ts +8 -11
  183. package/src/resources/extensions/gsd/doctor-git-checks.ts +3 -3
  184. package/src/resources/extensions/gsd/doctor-runtime-checks.ts +10 -3
  185. package/src/resources/extensions/gsd/git-service.ts +1 -0
  186. package/src/resources/extensions/gsd/gitignore.ts +3 -0
  187. package/src/resources/extensions/gsd/gsd-db.ts +173 -2373
  188. package/src/resources/extensions/gsd/guided-flow.ts +34 -3
  189. package/src/resources/extensions/gsd/migrate/safety.ts +15 -7
  190. package/src/resources/extensions/gsd/migration-auto-check.ts +28 -3
  191. package/src/resources/extensions/gsd/model-cost-table.ts +1 -0
  192. package/src/resources/extensions/gsd/model-router.ts +3 -0
  193. package/src/resources/extensions/gsd/parallel-merge.ts +12 -9
  194. package/src/resources/extensions/gsd/parallel-monitor-overlay.ts +6 -5
  195. package/src/resources/extensions/gsd/paths.ts +9 -22
  196. package/src/resources/extensions/gsd/preferences.ts +18 -0
  197. package/src/resources/extensions/gsd/recovery-classification.ts +14 -1
  198. package/src/resources/extensions/gsd/safety/evidence-collector.ts +36 -4
  199. package/src/resources/extensions/gsd/safety/evidence-cross-ref.ts +7 -2
  200. package/src/resources/extensions/gsd/safety/file-change-validator.ts +14 -0
  201. package/src/resources/extensions/gsd/state-transition-matrix.ts +42 -0
  202. package/src/resources/extensions/gsd/status-guards.ts +59 -8
  203. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +123 -0
  204. package/src/resources/extensions/gsd/tests/auto-paused-ui-cleanup.test.ts +3 -1
  205. package/src/resources/extensions/gsd/tests/auto-post-unit-evidence-crossref-4909.test.ts +46 -0
  206. package/src/resources/extensions/gsd/tests/auto-worktree-registry.test.ts +2 -2
  207. package/src/resources/extensions/gsd/tests/auto-worktree-repair.test.ts +4 -2
  208. package/src/resources/extensions/gsd/tests/clear-stale-autostart.test.ts +22 -0
  209. package/src/resources/extensions/gsd/tests/evidence-xref-gsd-exec.test.ts +157 -0
  210. package/src/resources/extensions/gsd/tests/file-change-validator.test.ts +33 -1
  211. package/src/resources/extensions/gsd/tests/integration/auto-worktree-milestone-merge.test.ts +5 -4
  212. package/src/resources/extensions/gsd/tests/integration/auto-worktree.test.ts +1 -1
  213. package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +3 -2
  214. package/src/resources/extensions/gsd/tests/migration-auto-check.test.ts +85 -1
  215. package/src/resources/extensions/gsd/tests/recovery-classification-illegal-transition.test.ts +30 -0
  216. package/src/resources/extensions/gsd/tests/register-hooks-depth-verification.test.ts +91 -1
  217. package/src/resources/extensions/gsd/tests/safety-harness-false-positives.test.ts +38 -0
  218. package/src/resources/extensions/gsd/tests/session-switch-clears-pending-autostart.test.ts +108 -0
  219. package/src/resources/extensions/gsd/tests/single-writer-invariant.test.ts +43 -6
  220. package/src/resources/extensions/gsd/tests/state-transition-matrix.test.ts +36 -0
  221. package/src/resources/extensions/gsd/tests/status-guards.test.ts +38 -0
  222. package/src/resources/extensions/gsd/tests/worktree-lifecycle.test.ts +41 -4
  223. package/src/resources/extensions/gsd/tests/worktree-manager.test.ts +22 -1
  224. package/src/resources/extensions/gsd/tests/worktree-placement.test.ts +113 -0
  225. package/src/resources/extensions/gsd/tests/worktree-reentry.test.ts +1 -1
  226. package/src/resources/extensions/gsd/tests/worktree-safety.test.ts +3 -1
  227. package/src/resources/extensions/gsd/tests/worktree-symlink-removal.test.ts +12 -6
  228. package/src/resources/extensions/gsd/tests/worktree-teardown-safety.test.ts +2 -2
  229. package/src/resources/extensions/gsd/tests/write-gate.test.ts +42 -0
  230. package/src/resources/extensions/gsd/tools/complete-slice.ts +23 -58
  231. package/src/resources/extensions/gsd/tools/exec-tool.ts +5 -5
  232. package/src/resources/extensions/gsd/tools/reopen-milestone.ts +11 -38
  233. package/src/resources/extensions/gsd/tools/reopen-slice.ts +14 -42
  234. package/src/resources/extensions/gsd/tools/skip-slice.ts +18 -44
  235. package/src/resources/extensions/gsd/undo.ts +9 -8
  236. package/src/resources/extensions/gsd/worktree-git-recovery.ts +308 -0
  237. package/src/resources/extensions/gsd/worktree-lifecycle.ts +10 -1
  238. package/src/resources/extensions/gsd/worktree-manager.ts +47 -28
  239. package/src/resources/extensions/gsd/worktree-placement.ts +63 -0
  240. package/src/resources/extensions/gsd/worktree-reentry.ts +10 -7
  241. package/src/resources/extensions/gsd/worktree-root.ts +17 -6
  242. package/src/resources/extensions/gsd/worktree-safety.ts +8 -5
  243. package/src/resources/extensions/gsd/worktree-session-state.ts +12 -10
  244. package/src/resources/skills/gsd-browser/SKILL.md +1 -1
  245. /package/dist/web/standalone/.next/static/{DUFWcMFRH3iXh7d2fbrOF → C24pqUd-aru-l0Dp0gLZP}/_buildManifest.js +0 -0
  246. /package/dist/web/standalone/.next/static/{DUFWcMFRH3iXh7d2fbrOF → C24pqUd-aru-l0Dp0gLZP}/_ssgManifest.js +0 -0
@@ -5517,6 +5517,129 @@ test("dispatch Worktree Safety wins before stuck detection for execute-task with
5517
5517
  );
5518
5518
  });
5519
5519
 
5520
+ test("dispatch Worktree Safety honors degraded branch fallback instead of demanding the canonical worktree root", async (t) => {
5521
+ _resetPendingResolve();
5522
+
5523
+ const ctx = makeMockCtx();
5524
+ const pi = makeMockPi();
5525
+ const notifications: string[] = [];
5526
+ ctx.ui.notify = (msg: string) => { notifications.push(msg); };
5527
+
5528
+ // Worktree creation failed and the lifecycle fell back to the milestone
5529
+ // branch in the project root. The safety gate must validate against that
5530
+ // effective branch mode, not the configured worktree mode.
5531
+ const projectRoot = mkdtempSync(join(tmpdir(), "gsd-wt-safety-degraded-"));
5532
+ t.after(() => rmSync(projectRoot, { recursive: true, force: true }));
5533
+
5534
+ const s = makeLoopSession({
5535
+ basePath: projectRoot,
5536
+ originalBasePath: projectRoot,
5537
+ canonicalProjectRoot: projectRoot,
5538
+ isolationDegraded: true,
5539
+ });
5540
+ const deps = makeMockDeps({
5541
+ getIsolationMode: () => "worktree",
5542
+ });
5543
+ const result = await runDispatch(
5544
+ {
5545
+ ctx,
5546
+ pi,
5547
+ s,
5548
+ deps,
5549
+ prefs: undefined,
5550
+ iteration: 1,
5551
+ flowId: "test-flow",
5552
+ nextSeq: () => 1,
5553
+ },
5554
+ {
5555
+ state: {
5556
+ phase: "executing",
5557
+ activeMilestone: { id: "M001", title: "Test", status: "active" },
5558
+ activeSlice: { id: "S01", title: "Slice 1" },
5559
+ activeTask: { id: "T01" },
5560
+ registry: [{ id: "M001", status: "active" }],
5561
+ blockers: [],
5562
+ } as any,
5563
+ mid: "M001",
5564
+ midTitle: "Test",
5565
+ },
5566
+ {
5567
+ recentUnits: [],
5568
+ stuckRecoveryAttempts: 0,
5569
+ consecutiveFinalizeTimeouts: 0,
5570
+ },
5571
+ );
5572
+
5573
+ assert.equal(result.action, "next", "dispatch must proceed under degraded branch isolation");
5574
+ assert.ok(
5575
+ !notifications.some((n) => n.includes("Worktree Safety failed")),
5576
+ "degraded branch fallback must not trip a false invalid-root",
5577
+ );
5578
+ assert.ok(!deps.callLog.includes("stopAuto"), "auto-mode must not stop on the degraded fallback");
5579
+ });
5580
+
5581
+ test("dispatch Worktree Safety honors stranded branch recovery instead of demanding the canonical worktree root", async (t) => {
5582
+ _resetPendingResolve();
5583
+
5584
+ const ctx = makeMockCtx();
5585
+ const pi = makeMockPi();
5586
+ const notifications: string[] = [];
5587
+ ctx.ui.notify = (msg: string) => { notifications.push(msg); };
5588
+
5589
+ // Bootstrap adopted stranded work by checking out the milestone branch in
5590
+ // the project root (strandedRecoveryIsolationMode = "branch"). Isolation is
5591
+ // NOT degraded — the adoption is intentional. The safety gate must validate
5592
+ // against the effective branch mode, not the configured worktree mode.
5593
+ const projectRoot = mkdtempSync(join(tmpdir(), "gsd-wt-safety-stranded-"));
5594
+ t.after(() => rmSync(projectRoot, { recursive: true, force: true }));
5595
+
5596
+ const s = makeLoopSession({
5597
+ basePath: projectRoot,
5598
+ originalBasePath: projectRoot,
5599
+ canonicalProjectRoot: projectRoot,
5600
+ strandedRecoveryIsolationMode: "branch",
5601
+ });
5602
+ const deps = makeMockDeps({
5603
+ getIsolationMode: () => "worktree",
5604
+ });
5605
+ const result = await runDispatch(
5606
+ {
5607
+ ctx,
5608
+ pi,
5609
+ s,
5610
+ deps,
5611
+ prefs: undefined,
5612
+ iteration: 1,
5613
+ flowId: "test-flow",
5614
+ nextSeq: () => 1,
5615
+ },
5616
+ {
5617
+ state: {
5618
+ phase: "executing",
5619
+ activeMilestone: { id: "M001", title: "Test", status: "active" },
5620
+ activeSlice: { id: "S01", title: "Slice 1" },
5621
+ activeTask: { id: "T01" },
5622
+ registry: [{ id: "M001", status: "active" }],
5623
+ blockers: [],
5624
+ } as any,
5625
+ mid: "M001",
5626
+ midTitle: "Test",
5627
+ },
5628
+ {
5629
+ recentUnits: [],
5630
+ stuckRecoveryAttempts: 0,
5631
+ consecutiveFinalizeTimeouts: 0,
5632
+ },
5633
+ );
5634
+
5635
+ assert.equal(result.action, "next", "dispatch must proceed under stranded branch recovery");
5636
+ assert.ok(
5637
+ !notifications.some((n) => n.includes("Worktree Safety failed")),
5638
+ "stranded branch recovery must not trip a false invalid-root",
5639
+ );
5640
+ assert.ok(!deps.callLog.includes("stopAuto"), "auto-mode must not stop on stranded branch recovery");
5641
+ });
5642
+
5520
5643
  test("runDispatch runs stuck detection while artifact verification retry is pending (#5719)", async (t) => {
5521
5644
  _resetPendingResolve();
5522
5645
 
@@ -306,7 +306,9 @@ test("pauseAuto records the expected worktree path when paused from project root
306
306
 
307
307
  const meta = readPausedSessionMetadata(base);
308
308
  assert.ok(meta);
309
- assert.equal(meta.worktreePath, join(base, ".gsd", "worktrees", "M001"));
309
+ // No worktree exists yet, so the recorded path is the canonical
310
+ // .gsd-worktrees/ creation location (worktree-placement seam).
311
+ assert.equal(meta.worktreePath, join(base, ".gsd-worktrees", "M001"));
310
312
  } finally {
311
313
  autoSession.reset();
312
314
  try {
@@ -88,3 +88,49 @@ test("detects session execution tools supported by the evidence collector", () =
88
88
 
89
89
  assert.equal(_hasExecutionToolCallsInSessionForTest(entries), true);
90
90
  });
91
+
92
+ test("detects execution tool calls in bare agent-end messages (no session-entry wrapper)", () => {
93
+ // The auto loop passes opts.agentEndMessages as bare {role, content}
94
+ // messages — not {type: "message", message} session-manager entries.
95
+ const entries = [
96
+ {
97
+ role: "assistant",
98
+ content: [
99
+ {
100
+ type: "toolCall",
101
+ name: "Bash",
102
+ arguments: { command: "test -s index.html && grep -q localStorage index.html" },
103
+ },
104
+ ],
105
+ },
106
+ ];
107
+
108
+ assert.equal(_hasExecutionToolCallsInSessionForTest(entries), true);
109
+ });
110
+
111
+ test("does not suppress for bare agent-end messages without execution tools", () => {
112
+ const entries = [
113
+ {
114
+ role: "assistant",
115
+ content: [
116
+ { type: "text", text: "Task complete." },
117
+ { type: "toolCall", name: "Write", arguments: { file_path: "index.html" } },
118
+ ],
119
+ },
120
+ ];
121
+
122
+ assert.equal(_hasExecutionToolCallsInSessionForTest(entries), false);
123
+ });
124
+
125
+ test("ignores bare user messages with toolCall-shaped content", () => {
126
+ const entries = [
127
+ {
128
+ role: "user",
129
+ content: [
130
+ { type: "toolCall", name: "bash", arguments: { command: "echo hi" } },
131
+ ],
132
+ },
133
+ ];
134
+
135
+ assert.equal(_hasExecutionToolCallsInSessionForTest(entries), false);
136
+ });
@@ -184,7 +184,7 @@ describe("auto-worktree workspace registry", () => {
184
184
  git(["commit", "-m", "add milestone"], tempDir);
185
185
 
186
186
  createAutoWorktree(tempDir, "M003");
187
- const wtDir = join(tempDir, ".gsd", "worktrees", "M003");
187
+ const wtDir = join(tempDir, ".gsd-worktrees", "M003");
188
188
  writeFileSync(join(wtDir, "feature.txt"), "implemented\n");
189
189
  git(["add", "feature.txt"], wtDir);
190
190
  git(["commit", "-m", "feat: implement M003"], wtDir);
@@ -216,7 +216,7 @@ describe("auto-worktree workspace registry", () => {
216
216
  git(["commit", "-m", "add milestone"], tempDir);
217
217
 
218
218
  createAutoWorktree(tempDir, "M004");
219
- const wtDir = join(tempDir, ".gsd", "worktrees", "M004");
219
+ const wtDir = join(tempDir, ".gsd-worktrees", "M004");
220
220
  writeFileSync(join(wtDir, "feature.txt"), "implemented\n");
221
221
  git(["add", "feature.txt"], wtDir);
222
222
  git(["commit", "-m", "feat: implement M004"], wtDir);
@@ -35,7 +35,8 @@ test("repair target accepts a missing expected milestone worktree", () => {
35
35
 
36
36
  assert.equal(result.ok, true);
37
37
  if (result.ok) {
38
- assert.equal(result.expectedPath, join(base, ".gsd", "worktrees", "M001"));
38
+ // No worktree exists yet, so the expected path is the canonical container.
39
+ assert.equal(result.expectedPath, join(base, ".gsd-worktrees", "M001"));
39
40
  }
40
41
  } finally {
41
42
  cleanup(base);
@@ -192,7 +193,8 @@ test("paused metadata path resolves to the expected worktree while paused at pro
192
193
  baseIsAutoWorktree: false,
193
194
  });
194
195
 
195
- assert.equal(result, join(base, ".gsd", "worktrees", "M001"));
196
+ // No worktree exists yet, so resolution lands at the canonical container.
197
+ assert.equal(result, join(base, ".gsd-worktrees", "M001"));
196
198
  } finally {
197
199
  cleanup(base);
198
200
  }
@@ -73,4 +73,26 @@ describe("clear stale pending auto-start (#3667)", () => {
73
73
  "pending auto-start gate must clear stale map entries for completed discussions",
74
74
  );
75
75
  });
76
+
77
+ test("guided-flow does not treat a live discuss turn as a stale pending entry", () => {
78
+ const source = readFileSync(join(__dirname, "..", "guided-flow.ts"), "utf-8");
79
+ assert.ok(
80
+ source.includes("!isAgentTurnInFlight(ctx)"),
81
+ "stale-entry deletion must be gated on no agent turn being in flight — a dispatched " +
82
+ "discuss turn can think for over 30s before writing its first artifact, and deleting " +
83
+ "its entry re-dispatches the workflow (duplicate interview + duplicate completion message)",
84
+ );
85
+ assert.ok(
86
+ source.includes('const milestoneHasDraft = !!resolveMilestoneFile(basePath, entry.milestoneId, "CONTEXT-DRAFT");'),
87
+ "stale-entry check must treat an existing CONTEXT-DRAFT as proof of an in-progress interview",
88
+ );
89
+ assert.ok(
90
+ source.includes("!milestoneHasDraft"),
91
+ "stale-entry deletion must require the CONTEXT-DRAFT to be absent",
92
+ );
93
+ assert.ok(
94
+ source.includes("ctx.hasPendingMessages"),
95
+ "in-flight detection must also cover dispatched-but-not-yet-started queued messages",
96
+ );
97
+ });
76
98
  });
@@ -0,0 +1,157 @@
1
+ // Project/App: gsd-pi
2
+ // File Purpose: Regression tests for evidence cross-referencing of gsd_exec /
3
+ // gsd_uat_exec tool calls. Mirrors the live false-positive where an
4
+ // execute-task agent ran its verification commands through gsd_exec (script
5
+ // body in the `script` argument) and the cross-referencer reported
6
+ // "No bash tool call found" despite successful execution.
7
+
8
+ import test from "node:test";
9
+ import assert from "node:assert/strict";
10
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
11
+ import { tmpdir } from "node:os";
12
+ import { join } from "node:path";
13
+
14
+ import {
15
+ resetEvidence,
16
+ getEvidence,
17
+ recordToolCall,
18
+ recordToolResult,
19
+ isExecutionToolName,
20
+ type BashEvidence,
21
+ } from "../safety/evidence-collector.ts";
22
+ import { crossReferenceEvidence } from "../safety/evidence-cross-ref.ts";
23
+
24
+ function gsdExecResult(exitCode: number, id = "4858202d-2ed7-4a0a-9ef7-4e159e65da83"): unknown {
25
+ return {
26
+ content: [{
27
+ type: "text",
28
+ text: JSON.stringify({
29
+ operation: "gsd_exec",
30
+ id,
31
+ runtime: "bash",
32
+ exit_code: exitCode,
33
+ signal: null,
34
+ timed_out: false,
35
+ duration_ms: 272,
36
+ stdout_bytes: 592,
37
+ stderr_bytes: 0,
38
+ meta_path: `/tmp/does-not-exist/.gsd/exec/${id}.meta.json`,
39
+ }),
40
+ }],
41
+ };
42
+ }
43
+
44
+ test("evidence-xref: verification run through gsd_exec script matches the claimed command", () => {
45
+ resetEvidence();
46
+
47
+ // The live false positive: agent runs `node --test tests/verify-s01.test.js`
48
+ // inside a gsd_exec script with a cd prefix and exit-code echo suffix.
49
+ recordToolCall("tc-exec-1", "gsd_exec", {
50
+ script: 'cd /work/.gsd/worktrees/M001 && node --test tests/verify-s01.test.js; echo "EXIT=$?"',
51
+ purpose: "T02: run node --test contract checks against T01 index.html",
52
+ });
53
+ recordToolResult("tc-exec-1", "gsd_exec", gsdExecResult(0), false);
54
+
55
+ const mismatches = crossReferenceEvidence(
56
+ [{ command: "node --test tests/verify-s01.test.js", exitCode: 0, verdict: "passed" }],
57
+ getEvidence(),
58
+ );
59
+
60
+ assert.deepEqual(mismatches, [], "gsd_exec-executed verification must not be flagged as missing");
61
+ });
62
+
63
+ test("evidence-xref: multi-line gsd_exec script matches claims for each embedded command", () => {
64
+ resetEvidence();
65
+
66
+ recordToolCall("tc-exec-2", "gsd_exec", {
67
+ script: [
68
+ "cd /work/.gsd/worktrees/M001",
69
+ "sed -i '' \"s/'todos'/'tasks-v1'/\" index.html",
70
+ "node --test tests/verify-s01.test.js > /dev/null 2>&1",
71
+ 'echo "BROKEN_EXIT=$?"',
72
+ ].join("\n"),
73
+ purpose: "T02: deliberate contract break must fail, then restore",
74
+ });
75
+ recordToolResult("tc-exec-2", "gsd_exec", gsdExecResult(0), false);
76
+
77
+ const mismatches = crossReferenceEvidence(
78
+ [{ command: "node --test tests/verify-s01.test.js > /dev/null 2>&1", exitCode: 0, verdict: "passed" }],
79
+ getEvidence(),
80
+ );
81
+
82
+ assert.deepEqual(mismatches, [], "command embedded in a multi-line script must match");
83
+ });
84
+
85
+ test("evidence-xref: claimed pass with failing gsd_exec exit_code is still an error", () => {
86
+ resetEvidence();
87
+
88
+ recordToolCall("tc-exec-3", "gsd_exec", {
89
+ script: "node --test tests/verify-s01.test.js",
90
+ purpose: "verification",
91
+ });
92
+ // gsd_exec reports failures via the JSON envelope's exit_code (and isError).
93
+ recordToolResult("tc-exec-3", "gsd_exec", gsdExecResult(1), true);
94
+
95
+ const mismatches = crossReferenceEvidence(
96
+ [{ command: "node --test tests/verify-s01.test.js", exitCode: 0, verdict: "passed" }],
97
+ getEvidence(),
98
+ );
99
+
100
+ assert.equal(mismatches.length, 1);
101
+ assert.equal(mismatches[0].severity, "error");
102
+ assert.match(mismatches[0].reason, /Claimed exitCode=0 but actual exitCode=1/);
103
+ });
104
+
105
+ test("evidence-collector: gsd_uat_exec and MCP-namespaced variants are execution tools", () => {
106
+ assert.equal(isExecutionToolName("gsd_uat_exec"), true);
107
+ assert.equal(isExecutionToolName("mcp__gsd-workflow__gsd_uat_exec"), true);
108
+ assert.equal(isExecutionToolName("mcp__gsd-workflow__gsd_exec"), true);
109
+
110
+ resetEvidence();
111
+ recordToolCall("tc-uat-1", "gsd_uat_exec", { script: "curl -fsS http://localhost:3000/health" });
112
+ const bash = getEvidence().filter((e): e is BashEvidence => e.kind === "bash");
113
+ assert.equal(bash.length, 1, "gsd_uat_exec must record bash evidence");
114
+ assert.equal(bash[0].command, "curl -fsS http://localhost:3000/health");
115
+ });
116
+
117
+ test("evidence-xref: blank-command evidence does not satisfy arbitrary claims", () => {
118
+ // Before script extraction existed, gsd_exec calls were recorded with
119
+ // command: "" — and `"x".includes("")` made them match every claim,
120
+ // masking genuine fabrications. Blank entries must never match.
121
+ const mismatches = crossReferenceEvidence(
122
+ [{ command: "node --test tests/verify-s01.test.js", exitCode: 0, verdict: "passed" }],
123
+ [{
124
+ kind: "bash",
125
+ toolCallId: "tc-blank",
126
+ command: "",
127
+ exitCode: 0,
128
+ outputSnippet: "",
129
+ timestamp: 1,
130
+ }],
131
+ );
132
+
133
+ assert.equal(mismatches.length, 1);
134
+ assert.equal(mismatches[0].severity, "warning");
135
+ assert.match(mismatches[0].reason, /No bash tool call found/);
136
+ });
137
+
138
+ test("evidence-collector: exit code falls back to .gsd/exec meta.json when result text omits it", (t) => {
139
+ const dir = mkdtempSync(join(tmpdir(), "gsd-exec-meta-"));
140
+ t.after(() => rmSync(dir, { recursive: true, force: true }));
141
+
142
+ const metaPath = join(dir, "run-1.meta.json");
143
+ writeFileSync(metaPath, JSON.stringify({ id: "run-1", exit_code: 7 }));
144
+
145
+ resetEvidence();
146
+ recordToolCall("tc-meta-1", "gsd_exec", { script: "exit 7" });
147
+ // Truncated result: meta_path survives but exit_code was cut off.
148
+ recordToolResult(
149
+ "tc-meta-1",
150
+ "gsd_exec",
151
+ { content: [{ type: "text", text: `{"operation":"gsd_exec","meta_path":${JSON.stringify(metaPath)}` }] },
152
+ false,
153
+ );
154
+
155
+ const bash = getEvidence().filter((e): e is BashEvidence => e.kind === "bash");
156
+ assert.equal(bash[0].exitCode, 7, "exit code must be recovered from meta.json");
157
+ });
@@ -5,7 +5,7 @@ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
5
5
  import { tmpdir } from "node:os";
6
6
  import { join } from "node:path";
7
7
 
8
- import { validateFileChanges } from "../safety/file-change-validator.ts";
8
+ import { validateFileChanges, effectiveFileChangeAllowlist } from "../safety/file-change-validator.ts";
9
9
 
10
10
  function git(cwd: string, ...args: string[]): string {
11
11
  return execFileSync("git", args, {
@@ -106,3 +106,35 @@ test("validateFileChanges ignores inline descriptions in expected output paths",
106
106
  "described expected output should not trigger unexpected-file warnings",
107
107
  );
108
108
  });
109
+
110
+ test("effectiveFileChangeAllowlist includes .gitignore when GSD manages it", () => {
111
+ assert.deepEqual(effectiveFileChangeAllowlist([], undefined), [".gitignore"]);
112
+ assert.deepEqual(effectiveFileChangeAllowlist(["docs/**"], true), ["docs/**", ".gitignore"]);
113
+ });
114
+
115
+ test("effectiveFileChangeAllowlist keeps .gitignore auditable when management is disabled", () => {
116
+ assert.deepEqual(effectiveFileChangeAllowlist(["docs/**"], false), ["docs/**"]);
117
+ });
118
+
119
+ test("GSD-managed .gitignore edit swept into a task commit is not flagged", (t) => {
120
+ const base = mkdtempSync(join(tmpdir(), "gsd-file-change-validator-"));
121
+ t.after(() => rmSync(base, { recursive: true, force: true }));
122
+
123
+ git(base, "init");
124
+ git(base, "config", "user.email", "test@example.com");
125
+ git(base, "config", "user.name", "Test User");
126
+ writeFileSync(join(base, "index.html"), "<main></main>\n");
127
+ writeFileSync(join(base, ".gitignore"), "# ── GSD baseline (auto-generated) ──\n.gsd\n");
128
+ git(base, "add", ".");
129
+ git(base, "commit", "-m", "task commit with swept gitignore");
130
+
131
+ const audit = validateFileChanges(
132
+ base,
133
+ ["index.html"],
134
+ [],
135
+ effectiveFileChangeAllowlist([], undefined),
136
+ );
137
+
138
+ assert.ok(audit, "audit should be produced");
139
+ assert.deepEqual(audit.unexpectedFiles, [], ".gitignore must not be flagged when GSD manages it");
140
+ });
@@ -746,7 +746,7 @@ describe("auto-worktree-milestone-merge", { timeout: 300_000 }, () => {
746
746
  });
747
747
 
748
748
  test("#2156: mergeMilestoneToMain removes external-state worktrees using the milestone branch name", () => {
749
- const { repo, externalState } = freshRepoWithExternalGsd();
749
+ const { repo } = freshRepoWithExternalGsd();
750
750
  const wtPath = createAutoWorktree(repo, "M215");
751
751
 
752
752
  addSliceToMilestone(repo, wtPath, "M215", "S01", "External cleanup", [
@@ -754,9 +754,10 @@ describe("auto-worktree-milestone-merge", { timeout: 300_000 }, () => {
754
754
  ]);
755
755
 
756
756
  const realWtPath = realpathSync(wtPath);
757
- assert.ok(
758
- realWtPath.startsWith(externalState),
759
- `worktree should be registered under external .gsd state, got ${realWtPath}`,
757
+ assert.equal(
758
+ realWtPath,
759
+ join(repo, ".gsd-worktrees", "M215"),
760
+ `worktree should use canonical path under project root, got ${realWtPath}`,
760
761
  );
761
762
 
762
763
  // Recreate the exact divergence from #1852: local .gsd/ is replaced with a
@@ -157,7 +157,7 @@ describe("auto-worktree lifecycle", () => {
157
157
  try {
158
158
  const wtPath = createAutoWorktree(tempDir, "M001");
159
159
  const realWtPath = realpathSync(wtPath);
160
- assert.ok(realWtPath.startsWith(storage), "git registered the symlink-resolved worktree path");
160
+ assert.equal(realWtPath, join(tempDir, ".gsd-worktrees", "M001"), "worktree uses canonical path under project root, not through the .gsd symlink");
161
161
 
162
162
  _resetAutoWorktreeOriginalBaseForTests();
163
163
  process.chdir(realWtPath);
@@ -293,11 +293,12 @@ describe('git-service', async () => {
293
293
 
294
294
  assert.deepStrictEqual(
295
295
  RUNTIME_EXCLUSION_PATHS.length,
296
- 16,
297
- "exactly 16 runtime exclusion paths"
296
+ 17,
297
+ "exactly 17 runtime exclusion paths"
298
298
  );
299
299
 
300
300
  const expectedPaths = [
301
+ ".gsd-worktrees/",
301
302
  ".gsd/activity/",
302
303
  ".gsd/audit/",
303
304
  ".gsd/forensics/",
@@ -1,6 +1,6 @@
1
1
  import assert from "node:assert/strict";
2
2
  import { createRequire } from "node:module";
3
- import { copyFileSync, mkdtempSync, renameSync, rmSync } from "node:fs";
3
+ import { copyFileSync, mkdirSync, mkdtempSync, renameSync, rmSync, writeFileSync } from "node:fs";
4
4
  import { join } from "node:path";
5
5
  import { tmpdir } from "node:os";
6
6
  import test from "node:test";
@@ -288,6 +288,90 @@ test("migration auto-check refreshes a stale open DB handle before comparing", a
288
288
  }
289
289
  });
290
290
 
291
+ function writeScratchMilestoneDir(base: string, milestoneId: string, file?: string): void {
292
+ const dir = join(base, ".gsd", "milestones", milestoneId);
293
+ mkdirSync(dir, { recursive: true });
294
+ if (file) writeFileSync(join(dir, file), `# ${milestoneId} discussion context\n`);
295
+ }
296
+
297
+ test("migration auto-check ignores discussion-scratch milestone dirs (CONTEXT only, no DB row)", async () => {
298
+ const base = makeBase();
299
+ try {
300
+ await writeGSDDirectory(projectFixture(), base); // markdown: M001 / S01 / T01
301
+ assert.equal(await ensureDbOpen(base), true);
302
+ insertMilestone({ id: "M001", title: "Legacy Milestone", status: "active" });
303
+ insertSlice({ id: "S01", milestoneId: "M001", title: "Legacy Slice", status: "pending", risk: "medium", depends: [], demo: "Legacy slice demo", sequence: 1 });
304
+ insertTask({ id: "T01", sliceId: "S01", milestoneId: "M001", title: "Legacy Task", status: "pending" });
305
+
306
+ // Mid-discussion artifacts: dirs with no ROADMAP and no DB row. The queued
307
+ // DB row is only inserted at discussion handoff, so these are expected to
308
+ // be DB-less — not drift, and recover must not be recommended (it would
309
+ // import them as ghost active milestones).
310
+ writeScratchMilestoneDir(base, "M002", "M002-CONTEXT.md");
311
+ writeScratchMilestoneDir(base, "M003", "M003-CONTEXT-DRAFT.md");
312
+ writeScratchMilestoneDir(base, "M004"); // empty dir
313
+
314
+ const result = await checkMarkdownHierarchyAgainstDb(base);
315
+ assert.equal(result.action, "none");
316
+ assert.equal(result.reason, "in-sync");
317
+ assert.deepEqual(result.markdown, { milestones: 1, slices: 1, tasks: 1 });
318
+ } finally {
319
+ cleanup(base);
320
+ }
321
+ });
322
+
323
+ test("migration auto-check stays quiet mid-first-discussion (scratch dir over empty DB)", async () => {
324
+ const base = makeBase();
325
+ try {
326
+ await writeGSDDirectory({ projectContent: "# P\n", decisionsContent: "", requirements: [], milestones: [] }, base);
327
+ assert.equal(await ensureDbOpen(base), true);
328
+ writeScratchMilestoneDir(base, "M001", "M001-CONTEXT.md");
329
+
330
+ const result = await checkMarkdownHierarchyAgainstDb(base);
331
+ assert.equal(result.action, "none");
332
+ assert.equal(result.reason, "no-markdown");
333
+ } finally {
334
+ cleanup(base);
335
+ }
336
+ });
337
+
338
+ test("migration auto-check still reports real drift with scratch dirs excluded from counts", async () => {
339
+ const base = makeBase();
340
+ try {
341
+ await writeGSDDirectory(projectFixture(), base); // markdown: M001 / S01 / T01, DB empty
342
+ assert.equal(await ensureDbOpen(base), true);
343
+ writeScratchMilestoneDir(base, "M002", "M002-CONTEXT.md");
344
+
345
+ const result = await checkMarkdownHierarchyAgainstDb(base);
346
+ assert.equal(result.action, "recovery-required");
347
+ assert.equal(result.reason, "db-empty");
348
+ assert.equal(result.recoveryCommand, "/gsd recover --confirm");
349
+ // The scratch dir must not inflate the reported markdown count.
350
+ assert.deepEqual(result.markdown, { milestones: 1, slices: 1, tasks: 1 });
351
+ } finally {
352
+ cleanup(base);
353
+ }
354
+ });
355
+
356
+ test("migration auto-check still compares a roadmapless milestone that HAS a DB row", async () => {
357
+ const base = makeBase();
358
+ try {
359
+ await writeGSDDirectory({ projectContent: "# P\n", decisionsContent: "", requirements: [], milestones: [] }, base);
360
+ assert.equal(await ensureDbOpen(base), true);
361
+ // Post-handoff queued milestone: CONTEXT-only dir WITH a DB row. It must
362
+ // stay in the comparison (both sides have it → in-sync).
363
+ insertMilestone({ id: "M001", title: "M001", status: "queued" });
364
+ writeScratchMilestoneDir(base, "M001", "M001-CONTEXT.md");
365
+
366
+ const result = await checkMarkdownHierarchyAgainstDb(base);
367
+ assert.equal(result.action, "none");
368
+ assert.equal(result.reason, "in-sync");
369
+ assert.deepEqual(result.markdown, { milestones: 1, slices: 0, tasks: 0 });
370
+ } finally {
371
+ cleanup(base);
372
+ }
373
+ });
374
+
291
375
  test("rebuildMarkdownProjectionsFromDb realigns markdown when DB holds extra rows", async () => {
292
376
  const base = makeBase();
293
377
  try {
@@ -0,0 +1,30 @@
1
+ // GSD — recovery-classification: illegal-transition kind (ADR-030)
2
+
3
+ import test from "node:test";
4
+ import assert from "node:assert/strict";
5
+
6
+ import { classifyFailure } from "../recovery-classification.ts";
7
+ import { IllegalPhaseTransitionError } from "../state-transition-matrix.ts";
8
+
9
+ test("classifyFailure recognizes IllegalPhaseTransitionError by class and escalates", () => {
10
+ const classification = classifyFailure({
11
+ error: new IllegalPhaseTransitionError("executing", "complete"),
12
+ unitType: "execute-task",
13
+ unitId: "T-1",
14
+ });
15
+
16
+ assert.equal(classification.failureKind, "illegal-transition");
17
+ assert.equal(classification.action, "escalate");
18
+ assert.equal(classification.exitReason, "illegal-transition");
19
+ assert.match(classification.reason, /Illegal phase transition/);
20
+ });
21
+
22
+ test("classifyFailure routes an explicit illegal-transition failureKind to the same case", () => {
23
+ const classification = classifyFailure({
24
+ error: new Error("derived edge rejected"),
25
+ failureKind: "illegal-transition",
26
+ });
27
+
28
+ assert.equal(classification.failureKind, "illegal-transition");
29
+ assert.equal(classification.action, "escalate");
30
+ });