gsd-pi 2.67.0-dev.1cd1e0f → 2.67.0-dev.2367d7e

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 (257) hide show
  1. package/README.md +1 -1
  2. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +155 -70
  3. package/dist/resources/extensions/gsd/auto/phases.js +17 -0
  4. package/dist/resources/extensions/gsd/auto/session.js +10 -0
  5. package/dist/resources/extensions/gsd/auto-direct-dispatch.js +12 -0
  6. package/dist/resources/extensions/gsd/auto-dispatch.js +1 -1
  7. package/dist/resources/extensions/gsd/auto-start.js +16 -30
  8. package/dist/resources/extensions/gsd/auto-worktree.js +62 -15
  9. package/dist/resources/extensions/gsd/auto.js +121 -59
  10. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +11 -435
  11. package/dist/resources/extensions/gsd/bootstrap/dynamic-tools.js +1 -4
  12. package/dist/resources/extensions/gsd/bootstrap/query-tools.js +7 -64
  13. package/dist/resources/extensions/gsd/bootstrap/system-context.js +7 -2
  14. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +88 -8
  15. package/dist/resources/extensions/gsd/commands/catalog.js +2 -1
  16. package/dist/resources/extensions/gsd/commands/handlers/core.js +39 -25
  17. package/dist/resources/extensions/gsd/commands/index.js +8 -1
  18. package/dist/resources/extensions/gsd/commands-mcp-status.js +43 -7
  19. package/dist/resources/extensions/gsd/doctor-git-checks.js +4 -4
  20. package/dist/resources/extensions/gsd/doctor-proactive.js +3 -3
  21. package/dist/resources/extensions/gsd/doctor.js +8 -4
  22. package/dist/resources/extensions/gsd/gsd-db.js +11 -0
  23. package/dist/resources/extensions/gsd/guided-flow.js +56 -31
  24. package/dist/resources/extensions/gsd/init-wizard.js +37 -0
  25. package/dist/resources/extensions/gsd/interrupted-session.js +146 -0
  26. package/dist/resources/extensions/gsd/mcp-project-config.js +83 -0
  27. package/dist/resources/extensions/gsd/state.js +7 -2
  28. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +508 -0
  29. package/dist/resources/extensions/gsd/workflow-logger.js +18 -3
  30. package/dist/resources/extensions/gsd/workflow-mcp.js +261 -0
  31. package/dist/web/standalone/.next/BUILD_ID +1 -1
  32. package/dist/web/standalone/.next/app-path-routes-manifest.json +8 -8
  33. package/dist/web/standalone/.next/build-manifest.json +3 -3
  34. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  35. package/dist/web/standalone/.next/react-loadable-manifest.json +2 -2
  36. package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  37. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  38. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  46. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  47. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/index.html +1 -1
  55. package/dist/web/standalone/.next/server/app/index.rsc +2 -2
  56. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  57. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +2 -2
  58. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  62. package/dist/web/standalone/.next/server/app-paths-manifest.json +8 -8
  63. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  64. package/dist/web/standalone/.next/server/middleware-react-loadable-manifest.js +1 -1
  65. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  66. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  67. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  68. package/dist/web/standalone/.next/static/chunks/2826.821e01b07d92e948.js +9 -0
  69. package/dist/web/standalone/.next/static/chunks/app/{page-0c485498795110d6.js → page-f1e30ab6bb269149.js} +1 -1
  70. package/dist/web/standalone/.next/static/chunks/{webpack-b49b09f97429b5d0.js → webpack-6e4d7e9a4f57bed4.js} +1 -1
  71. package/package.json +4 -2
  72. package/packages/mcp-server/README.md +38 -0
  73. package/packages/mcp-server/dist/cli.d.ts +9 -0
  74. package/packages/mcp-server/dist/cli.d.ts.map +1 -0
  75. package/packages/mcp-server/dist/cli.js +58 -0
  76. package/packages/mcp-server/dist/cli.js.map +1 -0
  77. package/packages/mcp-server/dist/index.d.ts +20 -0
  78. package/packages/mcp-server/dist/index.d.ts.map +1 -0
  79. package/packages/mcp-server/dist/index.js +14 -0
  80. package/packages/mcp-server/dist/index.js.map +1 -0
  81. package/packages/mcp-server/dist/readers/captures.d.ts +25 -0
  82. package/packages/mcp-server/dist/readers/captures.d.ts.map +1 -0
  83. package/packages/mcp-server/dist/readers/captures.js +67 -0
  84. package/packages/mcp-server/dist/readers/captures.js.map +1 -0
  85. package/packages/mcp-server/dist/readers/doctor-lite.d.ts +20 -0
  86. package/packages/mcp-server/dist/readers/doctor-lite.d.ts.map +1 -0
  87. package/packages/mcp-server/dist/readers/doctor-lite.js +173 -0
  88. package/packages/mcp-server/dist/readers/doctor-lite.js.map +1 -0
  89. package/packages/mcp-server/dist/readers/index.d.ts +14 -0
  90. package/packages/mcp-server/dist/readers/index.d.ts.map +1 -0
  91. package/packages/mcp-server/dist/readers/index.js +10 -0
  92. package/packages/mcp-server/dist/readers/index.js.map +1 -0
  93. package/packages/mcp-server/dist/readers/knowledge.d.ts +18 -0
  94. package/packages/mcp-server/dist/readers/knowledge.d.ts.map +1 -0
  95. package/packages/mcp-server/dist/readers/knowledge.js +82 -0
  96. package/packages/mcp-server/dist/readers/knowledge.js.map +1 -0
  97. package/packages/mcp-server/dist/readers/metrics.d.ts +32 -0
  98. package/packages/mcp-server/dist/readers/metrics.d.ts.map +1 -0
  99. package/packages/mcp-server/dist/readers/metrics.js +74 -0
  100. package/packages/mcp-server/dist/readers/metrics.js.map +1 -0
  101. package/packages/mcp-server/dist/readers/paths.d.ts +42 -0
  102. package/packages/mcp-server/dist/readers/paths.d.ts.map +1 -0
  103. package/packages/mcp-server/dist/readers/paths.js +199 -0
  104. package/packages/mcp-server/dist/readers/paths.js.map +1 -0
  105. package/packages/mcp-server/dist/readers/roadmap.d.ts +26 -0
  106. package/packages/mcp-server/dist/readers/roadmap.d.ts.map +1 -0
  107. package/packages/mcp-server/dist/readers/roadmap.js +194 -0
  108. package/packages/mcp-server/dist/readers/roadmap.js.map +1 -0
  109. package/packages/mcp-server/dist/readers/state.d.ts +43 -0
  110. package/packages/mcp-server/dist/readers/state.d.ts.map +1 -0
  111. package/packages/mcp-server/dist/readers/state.js +184 -0
  112. package/packages/mcp-server/dist/readers/state.js.map +1 -0
  113. package/packages/mcp-server/dist/server.d.ts +28 -0
  114. package/packages/mcp-server/dist/server.d.ts.map +1 -0
  115. package/packages/mcp-server/dist/server.js +319 -0
  116. package/packages/mcp-server/dist/server.js.map +1 -0
  117. package/packages/mcp-server/dist/session-manager.d.ts +54 -0
  118. package/packages/mcp-server/dist/session-manager.d.ts.map +1 -0
  119. package/packages/mcp-server/dist/session-manager.js +284 -0
  120. package/packages/mcp-server/dist/session-manager.js.map +1 -0
  121. package/packages/mcp-server/dist/types.d.ts +61 -0
  122. package/packages/mcp-server/dist/types.d.ts.map +1 -0
  123. package/packages/mcp-server/dist/types.js +11 -0
  124. package/packages/mcp-server/dist/types.js.map +1 -0
  125. package/packages/mcp-server/dist/workflow-tools.d.ts +9 -0
  126. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -0
  127. package/packages/mcp-server/dist/workflow-tools.js +532 -0
  128. package/packages/mcp-server/dist/workflow-tools.js.map +1 -0
  129. package/packages/mcp-server/src/server.ts +6 -2
  130. package/packages/mcp-server/src/workflow-tools.test.ts +976 -0
  131. package/packages/mcp-server/src/workflow-tools.ts +997 -0
  132. package/packages/mcp-server/tsconfig.json +1 -1
  133. package/packages/pi-agent-core/dist/agent-loop.js +14 -6
  134. package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
  135. package/packages/pi-agent-core/src/agent-loop.test.ts +53 -0
  136. package/packages/pi-agent-core/src/agent-loop.ts +20 -6
  137. package/packages/pi-coding-agent/dist/core/contextual-tips.d.ts +43 -0
  138. package/packages/pi-coding-agent/dist/core/contextual-tips.d.ts.map +1 -0
  139. package/packages/pi-coding-agent/dist/core/contextual-tips.js +208 -0
  140. package/packages/pi-coding-agent/dist/core/contextual-tips.js.map +1 -0
  141. package/packages/pi-coding-agent/dist/core/contextual-tips.test.d.ts +2 -0
  142. package/packages/pi-coding-agent/dist/core/contextual-tips.test.d.ts.map +1 -0
  143. package/packages/pi-coding-agent/dist/core/contextual-tips.test.js +227 -0
  144. package/packages/pi-coding-agent/dist/core/contextual-tips.test.js.map +1 -0
  145. package/packages/pi-coding-agent/dist/core/index.d.ts +1 -0
  146. package/packages/pi-coding-agent/dist/core/index.d.ts.map +1 -1
  147. package/packages/pi-coding-agent/dist/core/index.js +1 -0
  148. package/packages/pi-coding-agent/dist/core/index.js.map +1 -1
  149. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.d.ts +2 -0
  150. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.d.ts.map +1 -0
  151. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.js +28 -0
  152. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.js.map +1 -0
  153. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts +1 -0
  154. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  155. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +17 -12
  156. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
  157. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  158. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +19 -0
  159. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  160. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.d.ts +4 -0
  161. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.d.ts.map +1 -1
  162. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.js +14 -0
  163. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.js.map +1 -1
  164. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +3 -0
  165. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  166. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +15 -12
  167. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  168. package/packages/pi-coding-agent/src/core/contextual-tips.test.ts +259 -0
  169. package/packages/pi-coding-agent/src/core/contextual-tips.ts +232 -0
  170. package/packages/pi-coding-agent/src/core/index.ts +2 -0
  171. package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/tool-execution.test.ts +54 -0
  172. package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +18 -12
  173. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +21 -0
  174. package/packages/pi-coding-agent/src/modes/interactive/controllers/input-controller.ts +19 -0
  175. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +19 -15
  176. package/packages/rpc-client/dist/index.d.ts +10 -0
  177. package/packages/rpc-client/dist/index.d.ts.map +1 -0
  178. package/packages/rpc-client/dist/index.js +9 -0
  179. package/packages/rpc-client/dist/index.js.map +1 -0
  180. package/packages/rpc-client/dist/jsonl.d.ts +17 -0
  181. package/packages/rpc-client/dist/jsonl.d.ts.map +1 -0
  182. package/packages/rpc-client/dist/jsonl.js +54 -0
  183. package/packages/rpc-client/dist/jsonl.js.map +1 -0
  184. package/packages/rpc-client/dist/rpc-client.d.ts +259 -0
  185. package/packages/rpc-client/dist/rpc-client.d.ts.map +1 -0
  186. package/packages/rpc-client/dist/rpc-client.js +541 -0
  187. package/packages/rpc-client/dist/rpc-client.js.map +1 -0
  188. package/packages/rpc-client/dist/rpc-client.test.d.ts +2 -0
  189. package/packages/rpc-client/dist/rpc-client.test.d.ts.map +1 -0
  190. package/packages/rpc-client/dist/rpc-client.test.js +477 -0
  191. package/packages/rpc-client/dist/rpc-client.test.js.map +1 -0
  192. package/packages/rpc-client/dist/rpc-types.d.ts +566 -0
  193. package/packages/rpc-client/dist/rpc-types.d.ts.map +1 -0
  194. package/packages/rpc-client/dist/rpc-types.js +12 -0
  195. package/packages/rpc-client/dist/rpc-types.js.map +1 -0
  196. package/scripts/ensure-workspace-builds.cjs +2 -0
  197. package/scripts/link-workspace-packages.cjs +21 -14
  198. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +193 -93
  199. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +173 -79
  200. package/src/resources/extensions/gsd/auto/phases.ts +25 -0
  201. package/src/resources/extensions/gsd/auto/session.ts +10 -0
  202. package/src/resources/extensions/gsd/auto-direct-dispatch.ts +20 -0
  203. package/src/resources/extensions/gsd/auto-dispatch.ts +1 -1
  204. package/src/resources/extensions/gsd/auto-start.ts +23 -55
  205. package/src/resources/extensions/gsd/auto-worktree.ts +59 -15
  206. package/src/resources/extensions/gsd/auto.ts +133 -64
  207. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +22 -435
  208. package/src/resources/extensions/gsd/bootstrap/dynamic-tools.ts +1 -5
  209. package/src/resources/extensions/gsd/bootstrap/query-tools.ts +7 -72
  210. package/src/resources/extensions/gsd/bootstrap/system-context.ts +8 -2
  211. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +122 -6
  212. package/src/resources/extensions/gsd/commands/catalog.ts +2 -1
  213. package/src/resources/extensions/gsd/commands/handlers/core.ts +53 -26
  214. package/src/resources/extensions/gsd/commands/index.ts +7 -1
  215. package/src/resources/extensions/gsd/commands-mcp-status.ts +53 -7
  216. package/src/resources/extensions/gsd/doctor-git-checks.ts +4 -4
  217. package/src/resources/extensions/gsd/doctor-proactive.ts +3 -3
  218. package/src/resources/extensions/gsd/doctor.ts +9 -5
  219. package/src/resources/extensions/gsd/gsd-db.ts +12 -0
  220. package/src/resources/extensions/gsd/guided-flow.ts +66 -36
  221. package/src/resources/extensions/gsd/init-wizard.ts +40 -0
  222. package/src/resources/extensions/gsd/interrupted-session.ts +224 -0
  223. package/src/resources/extensions/gsd/mcp-project-config.ts +128 -0
  224. package/src/resources/extensions/gsd/state.ts +7 -1
  225. package/src/resources/extensions/gsd/tests/auto-project-root-env.test.ts +29 -0
  226. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +668 -2
  227. package/src/resources/extensions/gsd/tests/cold-resume-db-reopen.test.ts +14 -4
  228. package/src/resources/extensions/gsd/tests/copy-planning-artifacts-samepath.test.ts +21 -0
  229. package/src/resources/extensions/gsd/tests/core-overlay-fallback.test.ts +101 -0
  230. package/src/resources/extensions/gsd/tests/crash-recovery.test.ts +380 -2
  231. package/src/resources/extensions/gsd/tests/ensure-db-open.test.ts +66 -0
  232. package/src/resources/extensions/gsd/tests/forensics-context-persist.test.ts +30 -0
  233. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +12 -0
  234. package/src/resources/extensions/gsd/tests/guided-flow-session-isolation.test.ts +2 -2
  235. package/src/resources/extensions/gsd/tests/integration/doctor-fixlevel.test.ts +52 -1
  236. package/src/resources/extensions/gsd/tests/integration/doctor-git.test.ts +2 -9
  237. package/src/resources/extensions/gsd/tests/integration/doctor-proactive.test.ts +0 -33
  238. package/src/resources/extensions/gsd/tests/integration/merge-cwd-restore.test.ts +169 -0
  239. package/src/resources/extensions/gsd/tests/interrupted-session-auto.test.ts +146 -0
  240. package/src/resources/extensions/gsd/tests/interrupted-session-ui.test.ts +136 -0
  241. package/src/resources/extensions/gsd/tests/mcp-project-config.test.ts +85 -0
  242. package/src/resources/extensions/gsd/tests/mcp-status.test.ts +15 -0
  243. package/src/resources/extensions/gsd/tests/verification-operational-gate.test.ts +11 -0
  244. package/src/resources/extensions/gsd/tests/workflow-logger.test.ts +16 -0
  245. package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +500 -0
  246. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +625 -0
  247. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +629 -0
  248. package/src/resources/extensions/gsd/workflow-logger.ts +19 -3
  249. package/src/resources/extensions/gsd/workflow-mcp.ts +320 -0
  250. package/dist/web/standalone/.next/static/chunks/6502.b804e48b7919f55e.js +0 -9
  251. package/packages/pi-coding-agent/dist/modes/interactive/provider-auth-setup.d.ts +0 -13
  252. package/packages/pi-coding-agent/dist/modes/interactive/provider-auth-setup.d.ts.map +0 -1
  253. package/packages/pi-coding-agent/dist/modes/interactive/provider-auth-setup.js +0 -27
  254. package/packages/pi-coding-agent/dist/modes/interactive/provider-auth-setup.js.map +0 -1
  255. package/packages/pi-coding-agent/src/modes/interactive/provider-auth-setup.ts +0 -40
  256. /package/dist/web/standalone/.next/static/{PHqEommYRR8CRn3i84CGM → WMDT_0C0XDkBKtsAI_AX4}/_buildManifest.js +0 -0
  257. /package/dist/web/standalone/.next/static/{PHqEommYRR8CRn3i84CGM → WMDT_0C0XDkBKtsAI_AX4}/_ssgManifest.js +0 -0
@@ -18,22 +18,33 @@ const { assertTrue, report } = createTestContext();
18
18
 
19
19
  const autoSrc = readFileSync(join(import.meta.dirname, "..", "auto.ts"), "utf-8");
20
20
 
21
- console.log("\n=== #2940: resume path opens DB before rebuildState/deriveState ===");
21
+ console.log("\n=== resume path refreshes resources and opens DB before rebuildState/deriveState ===");
22
22
 
23
23
  // The resume block is the `if (s.paused) { ... }` section that calls rebuildState/deriveState.
24
24
  // Locate the resume section by finding `s.paused = false;` followed by `rebuildState`.
25
25
  const resumeSectionStart = autoSrc.indexOf("if (s.paused) {", autoSrc.indexOf("// If resuming from paused state"));
26
26
  assertTrue(resumeSectionStart > 0, "auto.ts has the paused-session resume block");
27
27
 
28
- const resumeSection = autoSrc.slice(resumeSectionStart, resumeSectionStart + 3000);
28
+ const resumeSectionEnd = autoSrc.indexOf("await autoLoop(", resumeSectionStart);
29
+ assertTrue(resumeSectionEnd > resumeSectionStart, "resume block reaches autoLoop");
29
30
 
30
- // The resume path must open the DB before rebuildState/deriveState
31
+ const resumeSection = autoSrc.slice(resumeSectionStart, resumeSectionEnd);
32
+
33
+ // The resume path must refresh managed resources and open the DB before
34
+ // rebuildState/deriveState so resumed auto-mode uses current extension code.
31
35
  const rebuildIdx = resumeSection.indexOf("rebuildState(");
32
36
  assertTrue(rebuildIdx > 0, "resume block calls rebuildState");
33
37
 
34
38
  const deriveIdx = resumeSection.indexOf("deriveState(");
35
39
  assertTrue(deriveIdx > 0, "resume block calls deriveState");
36
40
 
41
+ const preDeriveSection = resumeSection.slice(0, rebuildIdx);
42
+
43
+ assertTrue(
44
+ preDeriveSection.includes("initResources("),
45
+ "resume path must refresh managed resources before rebuildState/deriveState (#3761)",
46
+ );
47
+
37
48
  // There must be a DB open call before the first rebuildState call
38
49
  const dbOpenPatterns = [
39
50
  "openProjectDbIfPresent(",
@@ -41,7 +52,6 @@ const dbOpenPatterns = [
41
52
  "ensureDbOpen(",
42
53
  ];
43
54
 
44
- const preDeriveSection = resumeSection.slice(0, rebuildIdx);
45
55
  const hasDbOpen = dbOpenPatterns.some(pat => preDeriveSection.includes(pat));
46
56
  assertTrue(
47
57
  hasDbOpen,
@@ -0,0 +1,21 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { readFileSync } from "node:fs";
4
+ import { join } from "node:path";
5
+
6
+ test("copyPlanningArtifacts skips when source and destination .gsd resolve to the same path", () => {
7
+ const srcPath = join(import.meta.dirname, "..", "auto-worktree.ts");
8
+ const src = readFileSync(srcPath, "utf-8");
9
+
10
+ const fnIdx = src.indexOf("function copyPlanningArtifacts");
11
+ assert.ok(fnIdx !== -1, "copyPlanningArtifacts function exists");
12
+
13
+ const fnBody = src.slice(fnIdx, fnIdx + 2400);
14
+
15
+ const guardIdx = fnBody.indexOf("if (isSamePath(srcGsd, dstGsd)) return;");
16
+ const copyIdx = fnBody.indexOf("safeCopyRecursive(join(srcGsd, \"milestones\")");
17
+
18
+ assert.ok(guardIdx !== -1, "copyPlanningArtifacts should guard same-path .gsd copies");
19
+ assert.ok(copyIdx !== -1, "copyPlanningArtifacts should still copy milestones when paths differ");
20
+ assert.ok(guardIdx < copyIdx, "same-path guard should run before any copy attempt");
21
+ });
@@ -74,3 +74,104 @@ test("model command resolves and persists exact provider-qualified selection", a
74
74
  assert.deepEqual(applied, selectedModel);
75
75
  assert.match(notices[0]!.message, /openai\/gpt-5\.4/);
76
76
  });
77
+
78
+ test("interactive model picker chooses provider first, then model", async () => {
79
+ const selectedModel = { provider: "openai", id: "gpt-5.4" };
80
+ let applied: typeof selectedModel | null = null;
81
+ const selects: Array<{ title: string; options: string[] }> = [];
82
+ const notices: Array<{ message: string; type?: string }> = [];
83
+
84
+ const ctx = {
85
+ hasUI: true,
86
+ model: { provider: "anthropic", id: "claude-sonnet-4-6" },
87
+ modelRegistry: {
88
+ getAvailable: () => [
89
+ { provider: "openai", id: "gpt-5.4" },
90
+ { provider: "anthropic", id: "claude-opus-4-6" },
91
+ { provider: "openai", id: "gpt-5.3-mini" },
92
+ { provider: "anthropic", id: "claude-sonnet-4-6" },
93
+ ],
94
+ },
95
+ ui: {
96
+ select: async (title: string, options: string[]) => {
97
+ selects.push({ title, options });
98
+ return selects.length === 1 ? "openai (2 models)" : "gpt-5.4";
99
+ },
100
+ notify: (message: string, type?: string) => {
101
+ notices.push({ message, type });
102
+ },
103
+ },
104
+ } as any;
105
+
106
+ const pi = {
107
+ setModel: async (model: typeof selectedModel) => {
108
+ applied = model;
109
+ return true;
110
+ },
111
+ } as any;
112
+
113
+ const handled = await handleCoreCommand("model", ctx, pi);
114
+ assert.equal(handled, true);
115
+ assert.deepEqual(selects, [
116
+ {
117
+ title: "Select session model: — choose provider:",
118
+ options: ["anthropic (2 models)", "openai (2 models)", "(cancel)"],
119
+ },
120
+ {
121
+ title: "Select session model: — openai:",
122
+ options: ["gpt-5.3-mini", "gpt-5.4", "(cancel)"],
123
+ },
124
+ ]);
125
+ assert.deepEqual(applied, selectedModel);
126
+ assert.match(notices[0]!.message, /openai\/gpt-5\.4/);
127
+ });
128
+
129
+ test("ambiguous typed model selection chooses provider first, then model", async () => {
130
+ const selectedModel = { provider: "github-copilot", id: "gpt-5" };
131
+ let applied: typeof selectedModel | null = null;
132
+ const selects: Array<{ title: string; options: string[] }> = [];
133
+ const notices: Array<{ message: string; type?: string }> = [];
134
+
135
+ const ctx = {
136
+ hasUI: true,
137
+ model: { provider: "anthropic", id: "claude-sonnet-4-6" },
138
+ modelRegistry: {
139
+ getAvailable: () => [
140
+ { provider: "openai", id: "gpt-5" },
141
+ { provider: "github-copilot", id: "gpt-5" },
142
+ { provider: "openai", id: "gpt-5-mini" },
143
+ ],
144
+ },
145
+ ui: {
146
+ select: async (title: string, options: string[]) => {
147
+ selects.push({ title, options });
148
+ return selects.length === 1 ? "github-copilot (1 model)" : "gpt-5";
149
+ },
150
+ notify: (message: string, type?: string) => {
151
+ notices.push({ message, type });
152
+ },
153
+ },
154
+ } as any;
155
+
156
+ const pi = {
157
+ setModel: async (model: typeof selectedModel) => {
158
+ applied = model;
159
+ return true;
160
+ },
161
+ } as any;
162
+
163
+ const handled = await handleCoreCommand("model gpt", ctx, pi);
164
+ assert.equal(handled, true);
165
+ assert.deepEqual(selects, [
166
+ {
167
+ title: "Multiple models match \"gpt\" — choose provider:",
168
+ options: ["github-copilot (1 model)", "openai (2 models)", "(cancel)"],
169
+ },
170
+ {
171
+ title: "Multiple models match \"gpt\" — github-copilot:",
172
+ options: ["gpt-5", "(cancel)"],
173
+ },
174
+ ]);
175
+ assert.deepEqual(applied, selectedModel);
176
+ assert.match(notices[0]!.message, /github-copilot\/gpt-5/);
177
+ });
@@ -1,6 +1,6 @@
1
1
  import test from "node:test";
2
2
  import assert from "node:assert/strict";
3
- import { mkdirSync, existsSync, readFileSync, rmSync } from "node:fs";
3
+ import { mkdirSync, rmSync, writeFileSync } from "node:fs";
4
4
  import { join } from "node:path";
5
5
  import { tmpdir } from "node:os";
6
6
  import { randomUUID } from "node:crypto";
@@ -13,6 +13,14 @@ import {
13
13
  formatCrashInfo,
14
14
  type LockData,
15
15
  } from "../crash-recovery.ts";
16
+ import {
17
+ assessInterruptedSession,
18
+ hasResumableDerivedState,
19
+ isBootstrapCrashLock,
20
+ readPausedSessionMetadata,
21
+ } from "../interrupted-session.ts";
22
+ import { gsdRoot } from "../paths.ts";
23
+ import type { GSDState } from "../types.ts";
16
24
 
17
25
  function makeTmpBase(): string {
18
26
  const base = join(tmpdir(), `gsd-test-${randomUUID()}`);
@@ -24,6 +32,376 @@ function cleanup(base: string): void {
24
32
  try { rmSync(base, { recursive: true, force: true }); } catch { /* */ }
25
33
  }
26
34
 
35
+ function writeTestLock(
36
+ base: string,
37
+ unitType: string,
38
+ unitId: string,
39
+ sessionFile?: string,
40
+ ): void {
41
+ writeFileSync(
42
+ join(gsdRoot(base), "auto.lock"),
43
+ JSON.stringify({
44
+ pid: 999999999,
45
+ startedAt: new Date().toISOString(),
46
+ unitType,
47
+ unitId,
48
+ unitStartedAt: new Date().toISOString(),
49
+ sessionFile,
50
+ }, null, 2),
51
+ "utf-8",
52
+ );
53
+ }
54
+
55
+ function writeRoadmap(base: string, checked = false): void {
56
+ const milestoneDir = join(base, ".gsd", "milestones", "M001");
57
+ mkdirSync(join(milestoneDir, "slices", "S01", "tasks"), { recursive: true });
58
+ writeFileSync(
59
+ join(milestoneDir, "M001-ROADMAP.md"),
60
+ [
61
+ "# M001: Test Milestone",
62
+ "",
63
+ "## Vision",
64
+ "",
65
+ "Test milestone.",
66
+ "",
67
+ "## Success Criteria",
68
+ "",
69
+ "- It works.",
70
+ "",
71
+ "## Slices",
72
+ "",
73
+ `- [${checked ? "x" : " "}] **S01: Test slice** \`risk:low\``,
74
+ " After this: Demo",
75
+ "",
76
+ "## Boundary Map",
77
+ "",
78
+ "- S01 → terminal",
79
+ " - Produces: done",
80
+ " - Consumes: nothing",
81
+ ].join("\n"),
82
+ "utf-8",
83
+ );
84
+ }
85
+
86
+ function writeCompleteSliceArtifacts(base: string): void {
87
+ const sliceDir = join(base, ".gsd", "milestones", "M001", "slices", "S01");
88
+ mkdirSync(sliceDir, { recursive: true });
89
+ writeFileSync(join(sliceDir, "S01-SUMMARY.md"), "# Summary\nDone.\n", "utf-8");
90
+ writeFileSync(join(sliceDir, "S01-UAT.md"), "# UAT\nPassed.\n", "utf-8");
91
+ }
92
+
93
+ function writeCompleteMilestoneSummary(base: string): void {
94
+ const milestoneDir = join(base, ".gsd", "milestones", "M001");
95
+ mkdirSync(milestoneDir, { recursive: true });
96
+ writeFileSync(join(milestoneDir, "M001-SUMMARY.md"), "# Milestone Summary\nDone.\n", "utf-8");
97
+ }
98
+
99
+ function writePausedSession(
100
+ base: string,
101
+ milestoneId = "M001",
102
+ stepMode = false,
103
+ worktreePath?: string,
104
+ unitType?: string,
105
+ unitId?: string,
106
+ ): void {
107
+ const runtimeDir = join(base, ".gsd", "runtime");
108
+ mkdirSync(runtimeDir, { recursive: true });
109
+ writeFileSync(
110
+ join(runtimeDir, "paused-session.json"),
111
+ JSON.stringify({ milestoneId, originalBasePath: base, stepMode, worktreePath, unitType, unitId }, null, 2),
112
+ "utf-8",
113
+ );
114
+ }
115
+
116
+ function writeActivityLog(base: string, entries: Record<string, unknown>[]): void {
117
+ const activityDir = join(base, ".gsd", "activity");
118
+ mkdirSync(activityDir, { recursive: true });
119
+ writeFileSync(
120
+ join(activityDir, "001-execute-task-M001-S01-T01.jsonl"),
121
+ entries.map((entry) => JSON.stringify(entry)).join("\n") + "\n",
122
+ "utf-8",
123
+ );
124
+ }
125
+
126
+ function makeState(phase: GSDState["phase"], activeMilestone = true): GSDState {
127
+ return {
128
+ activeMilestone: activeMilestone ? { id: "M001", title: "Test" } : null,
129
+ activeSlice: null,
130
+ activeTask: null,
131
+ phase,
132
+ recentDecisions: [],
133
+ blockers: [],
134
+ nextAction: "",
135
+ registry: [],
136
+ };
137
+ }
138
+
139
+ // ─── interrupted-session helpers ───────────────────────────────────────────
140
+
141
+ test("hasResumableDerivedState treats only unfinished active work as resumable", () => {
142
+ assert.equal(hasResumableDerivedState(makeState("executing")), true);
143
+ assert.equal(hasResumableDerivedState(makeState("complete")), false);
144
+ assert.equal(hasResumableDerivedState(makeState("pre-planning", false)), false);
145
+ });
146
+
147
+ test("isBootstrapCrashLock detects starting/bootstrap special case", () => {
148
+ const bootstrap: LockData = {
149
+ pid: 999999999,
150
+ startedAt: new Date().toISOString(),
151
+ unitType: "starting",
152
+ unitId: "bootstrap",
153
+ unitStartedAt: new Date().toISOString(),
154
+ };
155
+ assert.equal(isBootstrapCrashLock(bootstrap), true);
156
+ assert.equal(isBootstrapCrashLock({ ...bootstrap, unitType: "execute-task" }), false);
157
+ });
158
+
159
+ test("readPausedSessionMetadata reads paused-session metadata when present", () => {
160
+ const base = makeTmpBase();
161
+ try {
162
+ writePausedSession(base, "M009");
163
+ const meta = readPausedSessionMetadata(base);
164
+ assert.equal(meta?.milestoneId, "M009");
165
+ } finally {
166
+ cleanup(base);
167
+ }
168
+ });
169
+
170
+ test("readPausedSessionMetadata preserves unitType and unitId through round-trip", () => {
171
+ const base = makeTmpBase();
172
+ try {
173
+ writePausedSession(base, "M001", false, undefined, "execute-task", "M001/S01/T02");
174
+ const meta = readPausedSessionMetadata(base);
175
+ assert.equal(meta?.unitType, "execute-task");
176
+ assert.equal(meta?.unitId, "M001/S01/T02");
177
+ } finally {
178
+ cleanup(base);
179
+ }
180
+ });
181
+
182
+ test("readPausedSessionMetadata handles legacy metadata without unitType/unitId", () => {
183
+ const base = makeTmpBase();
184
+ try {
185
+ // Write metadata without unitType/unitId (simulates older version)
186
+ const runtimeDir = join(base, ".gsd", "runtime");
187
+ mkdirSync(runtimeDir, { recursive: true });
188
+ writeFileSync(
189
+ join(runtimeDir, "paused-session.json"),
190
+ JSON.stringify({ milestoneId: "M001", originalBasePath: base }),
191
+ "utf-8",
192
+ );
193
+ const meta = readPausedSessionMetadata(base);
194
+ assert.equal(meta?.milestoneId, "M001");
195
+ assert.equal(meta?.unitType, undefined);
196
+ assert.equal(meta?.unitId, undefined);
197
+ } finally {
198
+ cleanup(base);
199
+ }
200
+ });
201
+
202
+ test("assessInterruptedSession returns none when no lock and no paused session exist", async () => {
203
+ const base = makeTmpBase();
204
+ try {
205
+ const assessment = await assessInterruptedSession(base);
206
+ assert.equal(assessment.classification, "none");
207
+ assert.equal(assessment.lock, null);
208
+ assert.equal(assessment.pausedSession, null);
209
+ assert.equal(assessment.state, null);
210
+ assert.equal(assessment.recovery, null);
211
+ assert.equal(assessment.recoveryPrompt, null);
212
+ assert.equal(assessment.recoveryToolCallCount, 0);
213
+ assert.equal(assessment.artifactSatisfied, false);
214
+ assert.equal(assessment.hasResumableDiskState, false);
215
+ assert.equal(assessment.isBootstrapCrash, false);
216
+ } finally {
217
+ cleanup(base);
218
+ }
219
+ });
220
+
221
+ test("assessInterruptedSession classifies stale complete repo as stale and suppresses recovery", async () => {
222
+ const base = makeTmpBase();
223
+ try {
224
+ writeRoadmap(base, true);
225
+ writeCompleteSliceArtifacts(base);
226
+ writeCompleteMilestoneSummary(base);
227
+ writeTestLock(base, "execute-task", "M001/S01/T01");
228
+
229
+ const assessment = await assessInterruptedSession(base);
230
+ assert.equal(assessment.classification, "stale");
231
+ assert.equal(assessment.hasResumableDiskState, false);
232
+ assert.equal(assessment.recoveryPrompt, null);
233
+ } finally {
234
+ cleanup(base);
235
+ }
236
+ });
237
+
238
+ test("assessInterruptedSession suppresses prompt when expected artifact already exists and no resumable state remains", async () => {
239
+ const base = makeTmpBase();
240
+ try {
241
+ writeRoadmap(base, true);
242
+ writeCompleteSliceArtifacts(base);
243
+ writeCompleteMilestoneSummary(base);
244
+ writeTestLock(base, "complete-slice", "M001/S01");
245
+
246
+ const assessment = await assessInterruptedSession(base);
247
+ assert.equal(assessment.classification, "stale");
248
+ assert.equal(assessment.artifactSatisfied, true);
249
+ } finally {
250
+ cleanup(base);
251
+ }
252
+ });
253
+
254
+ test("assessInterruptedSession keeps paused-session resume recoverable when disk state is unfinished", async () => {
255
+ const base = makeTmpBase();
256
+ try {
257
+ writeRoadmap(base, false);
258
+ writePausedSession(base);
259
+ writeTestLock(base, "execute-task", "M001/S01/T01");
260
+
261
+ const assessment = await assessInterruptedSession(base);
262
+ assert.equal(assessment.classification, "recoverable");
263
+ assert.equal(assessment.pausedSession?.milestoneId, "M001");
264
+ } finally {
265
+ cleanup(base);
266
+ }
267
+ });
268
+
269
+ test("assessInterruptedSession marks stale paused-session metadata as stale when no work remains", async () => {
270
+ const base = makeTmpBase();
271
+ try {
272
+ writeRoadmap(base, true);
273
+ writeCompleteSliceArtifacts(base);
274
+ writeCompleteMilestoneSummary(base);
275
+ writePausedSession(base, "M999");
276
+
277
+ const assessment = await assessInterruptedSession(base);
278
+ assert.equal(assessment.classification, "stale");
279
+ assert.equal(assessment.hasResumableDiskState, false);
280
+ } finally {
281
+ cleanup(base);
282
+ }
283
+ });
284
+
285
+ test("assessInterruptedSession classifies paused session without lock as recoverable when disk state is resumable", async () => {
286
+ const base = makeTmpBase();
287
+ try {
288
+ writeRoadmap(base, false);
289
+ writePausedSession(base, "M001", true);
290
+
291
+ const assessment = await assessInterruptedSession(base);
292
+ assert.equal(assessment.classification, "recoverable");
293
+ assert.equal(assessment.lock, null);
294
+ assert.equal(assessment.pausedSession?.milestoneId, "M001");
295
+ assert.equal(assessment.hasResumableDiskState, true);
296
+ assert.equal(assessment.isBootstrapCrash, false);
297
+ } finally {
298
+ cleanup(base);
299
+ }
300
+ });
301
+
302
+ test("assessInterruptedSession falls back to basePath when worktreePath no longer exists", async () => {
303
+ const base = makeTmpBase();
304
+ try {
305
+ writeRoadmap(base, false);
306
+ // Reference a worktree that doesn't exist on disk
307
+ writePausedSession(base, "M001", false, "/nonexistent/worktree");
308
+
309
+ const assessment = await assessInterruptedSession(base);
310
+ // Should use basePath (which has an unfinished roadmap) instead of the missing worktree
311
+ assert.equal(assessment.classification, "recoverable");
312
+ assert.equal(assessment.hasResumableDiskState, true);
313
+ } finally {
314
+ cleanup(base);
315
+ }
316
+ });
317
+
318
+ test("assessInterruptedSession prefers paused worktree state when worktreePath is recorded", async () => {
319
+ const base = makeTmpBase();
320
+ const worktree = join(base, "worktree-copy");
321
+ try {
322
+ writeRoadmap(base, false);
323
+ writeRoadmap(worktree, true);
324
+ writeCompleteSliceArtifacts(worktree);
325
+ writeCompleteMilestoneSummary(worktree);
326
+ writePausedSession(base, "M001", false, worktree);
327
+
328
+ const assessment = await assessInterruptedSession(base);
329
+ assert.equal(assessment.classification, "stale");
330
+ assert.equal(assessment.hasResumableDiskState, false);
331
+ } finally {
332
+ cleanup(base);
333
+ }
334
+ });
335
+
336
+ test("assessInterruptedSession keeps unfinished derived state recoverable without trace", async () => {
337
+ const base = makeTmpBase();
338
+ try {
339
+ writeRoadmap(base, false);
340
+ writeTestLock(base, "plan-slice", "M001/S01");
341
+
342
+ const assessment = await assessInterruptedSession(base);
343
+ assert.equal(assessment.classification, "recoverable");
344
+ assert.equal(assessment.hasResumableDiskState, true);
345
+ assert.equal(assessment.recoveryPrompt, null);
346
+ } finally {
347
+ cleanup(base);
348
+ }
349
+ });
350
+
351
+ test("assessInterruptedSession preserves crash trace when activity log has tool calls", async () => {
352
+ const base = makeTmpBase();
353
+ try {
354
+ writeRoadmap(base, false);
355
+ writeTestLock(base, "execute-task", "M001/S01/T01");
356
+ writeActivityLog(base, [
357
+ {
358
+ type: "message",
359
+ message: {
360
+ role: "assistant",
361
+ content: [
362
+ {
363
+ type: "toolCall",
364
+ id: "1",
365
+ name: "bash",
366
+ arguments: { command: "npm test" },
367
+ },
368
+ ],
369
+ },
370
+ },
371
+ {
372
+ type: "message",
373
+ message: {
374
+ role: "toolResult",
375
+ toolCallId: "1",
376
+ toolName: "bash",
377
+ isError: false,
378
+ content: [{ type: "text", text: "ok" }],
379
+ },
380
+ },
381
+ ]);
382
+
383
+ const assessment = await assessInterruptedSession(base);
384
+ assert.equal(assessment.classification, "recoverable");
385
+ assert.ok(assessment.recoveryToolCallCount > 0);
386
+ assert.ok(assessment.recoveryPrompt?.includes("Recovery Briefing"));
387
+ } finally {
388
+ cleanup(base);
389
+ }
390
+ });
391
+
392
+ test("assessInterruptedSession treats bootstrap crash as stale without paused metadata", async () => {
393
+ const base = makeTmpBase();
394
+ try {
395
+ writeTestLock(base, "starting", "bootstrap");
396
+
397
+ const assessment = await assessInterruptedSession(base);
398
+ assert.equal(assessment.classification, "stale");
399
+ assert.equal(assessment.isBootstrapCrash, true);
400
+ } finally {
401
+ cleanup(base);
402
+ }
403
+ });
404
+
27
405
  // ─── writeLock / readCrashLock ────────────────────────────────────────────
28
406
 
29
407
  test("writeLock creates lock file and readCrashLock reads it", (t) => {
@@ -84,7 +462,7 @@ test("#2470: isLockProcessAlive returns true for own PID (we hold the lock)", ()
84
462
 
85
463
  test("isLockProcessAlive returns false for dead PID", () => {
86
464
  const lock: LockData = {
87
- pid: 999999999, // almost certainly not running
465
+ pid: 999999999,
88
466
  startedAt: new Date().toISOString(),
89
467
  unitType: "execute-task",
90
468
  unitId: "M001/S01/T01",
@@ -77,6 +77,36 @@ describe('ensure-db-open', () => {
77
77
  }
78
78
  });
79
79
 
80
+ test('ensureDbOpen: explicit basePath opens target project without cwd override', async () => {
81
+ const tmpDir = makeTmpDir();
82
+ const gsdDir = path.join(tmpDir, '.gsd');
83
+ fs.mkdirSync(gsdDir, { recursive: true });
84
+ fs.writeFileSync(path.join(gsdDir, 'DECISIONS.md'), `# Decisions
85
+
86
+ | # | When | Scope | Decision | Choice | Rationale | Revisable |
87
+ |---|------|-------|----------|--------|-----------|-----------|
88
+ | D777 | M001 | architecture | Use explicit basePath | BasePath | Avoid cwd coupling | Yes |
89
+ `);
90
+
91
+ try {
92
+ closeDatabase();
93
+ } catch { /* ok */ }
94
+
95
+ const originalCwd = process.cwd();
96
+ try {
97
+ const { ensureDbOpen } = await import('../bootstrap/dynamic-tools.ts');
98
+ const result = await ensureDbOpen(tmpDir);
99
+
100
+ assert.ok(result === true, 'ensureDbOpen should honor explicit basePath');
101
+ assert.equal(process.cwd(), originalCwd, 'ensureDbOpen should not mutate process.cwd');
102
+ assert.ok(isDbAvailable(), 'DB should be available after explicit open');
103
+ assert.ok(getDecisionById('D777') !== null, 'explicit basePath DB should be opened');
104
+ } finally {
105
+ closeDatabase();
106
+ cleanupDir(tmpDir);
107
+ }
108
+ });
109
+
80
110
  // ═══════════════════════════════════════════════════════════════════════════
81
111
  // ensureDbOpen returns false when no .gsd/ exists
82
112
  // ═══════════════════════════════════════════════════════════════════════════
@@ -159,6 +189,42 @@ describe('ensure-db-open', () => {
159
189
  }
160
190
  });
161
191
 
192
+ test('ensureDbOpen: switches open database when basePath changes', async () => {
193
+ const firstDir = makeTmpDir();
194
+ const secondDir = makeTmpDir();
195
+ fs.mkdirSync(path.join(firstDir, '.gsd'), { recursive: true });
196
+ fs.mkdirSync(path.join(secondDir, '.gsd'), { recursive: true });
197
+ fs.writeFileSync(path.join(firstDir, '.gsd', 'DECISIONS.md'), `# Decisions
198
+
199
+ | # | When | Scope | Decision | Choice | Rationale | Revisable |
200
+ |---|------|-------|----------|--------|-----------|-----------|
201
+ | D101 | M001 | architecture | First DB | First | First rationale | Yes |
202
+ `);
203
+ fs.writeFileSync(path.join(secondDir, '.gsd', 'DECISIONS.md'), `# Decisions
204
+
205
+ | # | When | Scope | Decision | Choice | Rationale | Revisable |
206
+ |---|------|-------|----------|--------|-----------|-----------|
207
+ | D202 | M001 | architecture | Second DB | Second | Second rationale | Yes |
208
+ `);
209
+
210
+ try {
211
+ closeDatabase();
212
+ } catch { /* ok */ }
213
+
214
+ try {
215
+ const { ensureDbOpen } = await import('../bootstrap/dynamic-tools.ts');
216
+ assert.equal(await ensureDbOpen(firstDir), true);
217
+ assert.ok(getDecisionById('D101') !== null, 'first DB should be active');
218
+ assert.equal(await ensureDbOpen(secondDir), true);
219
+ assert.ok(getDecisionById('D202') !== null, 'second DB should be active after switch');
220
+ assert.equal(getDecisionById('D101'), null, 'first DB should no longer be active after switch');
221
+ } finally {
222
+ closeDatabase();
223
+ cleanupDir(firstDir);
224
+ cleanupDir(secondDir);
225
+ }
226
+ });
227
+
162
228
  // ═══════════════════════════════════════════════════════════════════════════
163
229
 
164
230
  });
@@ -126,4 +126,34 @@ describe("forensics context persistence (#2941)", () => {
126
126
  // Should not throw
127
127
  clearForensicsMarker(join(tmpBase, "nonexistent"));
128
128
  });
129
+
130
+ it("buildForensicsContextInjection keeps marker for low-entropy resume prompts", async () => {
131
+ const { buildForensicsContextInjection } = await import("../bootstrap/system-context.ts");
132
+
133
+ const markerPath = join(tmpBase, ".gsd", "runtime", "active-forensics.json");
134
+ writeFileSync(markerPath, JSON.stringify({
135
+ reportPath: "/some/report.md",
136
+ promptContent: "forensics prompt",
137
+ createdAt: new Date().toISOString(),
138
+ }), "utf-8");
139
+
140
+ const result = buildForensicsContextInjection(tmpBase, "continue");
141
+ assert.equal(result, "forensics prompt");
142
+ assert.ok(existsSync(markerPath), "resume-like follow-up should keep marker intact");
143
+ });
144
+
145
+ it("buildForensicsContextInjection clears marker on unrelated user prompts", async () => {
146
+ const { buildForensicsContextInjection } = await import("../bootstrap/system-context.ts");
147
+
148
+ const markerPath = join(tmpBase, ".gsd", "runtime", "active-forensics.json");
149
+ writeFileSync(markerPath, JSON.stringify({
150
+ reportPath: "/some/report.md",
151
+ promptContent: "forensics prompt",
152
+ createdAt: new Date().toISOString(),
153
+ }), "utf-8");
154
+
155
+ const result = buildForensicsContextInjection(tmpBase, "please summarize the README");
156
+ assert.equal(result, null);
157
+ assert.ok(!existsSync(markerPath), "unrelated follow-up should clear the stale marker");
158
+ });
129
159
  });