gsd-pi 2.80.0-dev.c5c38454b → 2.80.0-dev.cf9433f56

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 (266) hide show
  1. package/README.md +4 -2
  2. package/dist/resources/.managed-resources-content-hash +1 -1
  3. package/dist/resources/GSD-WORKFLOW.md +2 -2
  4. package/dist/resources/extensions/gsd/auto/loop.js +32 -1
  5. package/dist/resources/extensions/gsd/auto/phases.js +66 -45
  6. package/dist/resources/extensions/gsd/auto/resolve.js +17 -0
  7. package/dist/resources/extensions/gsd/auto/run-unit.js +13 -1
  8. package/dist/resources/extensions/gsd/auto-post-unit.js +10 -10
  9. package/dist/resources/extensions/gsd/auto-prompts.js +124 -2
  10. package/dist/resources/extensions/gsd/auto-recovery.js +43 -1
  11. package/dist/resources/extensions/gsd/auto-supervisor.js +8 -1
  12. package/dist/resources/extensions/gsd/auto-timeout-recovery.js +2 -2
  13. package/dist/resources/extensions/gsd/auto.js +77 -5
  14. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +21 -2
  15. package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +27 -20
  16. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +21 -0
  17. package/dist/resources/extensions/gsd/clean-root-preflight.js +42 -4
  18. package/dist/resources/extensions/gsd/commands/dispatcher.js +5 -0
  19. package/dist/resources/extensions/gsd/context-budget.js +37 -2
  20. package/dist/resources/extensions/gsd/crash-recovery.js +56 -10
  21. package/dist/resources/extensions/gsd/db/unit-dispatches.js +39 -0
  22. package/dist/resources/extensions/gsd/db-base-schema.js +4 -2
  23. package/dist/resources/extensions/gsd/db-migration-steps.js +6 -0
  24. package/dist/resources/extensions/gsd/detection.js +106 -0
  25. package/dist/resources/extensions/gsd/git-service.js +36 -4
  26. package/dist/resources/extensions/gsd/gsd-db.js +46 -13
  27. package/dist/resources/extensions/gsd/guided-flow.js +80 -14
  28. package/dist/resources/extensions/gsd/memory-store.js +69 -12
  29. package/dist/resources/extensions/gsd/migrate/command.js +40 -1
  30. package/dist/resources/extensions/gsd/migration-auto-check.js +87 -0
  31. package/dist/resources/extensions/gsd/planning-path-scope.js +26 -0
  32. package/dist/resources/extensions/gsd/pre-execution-checks.js +7 -0
  33. package/dist/resources/extensions/gsd/prompt-loader.js +28 -2
  34. package/dist/resources/extensions/gsd/prompts/complete-milestone.md +19 -19
  35. package/dist/resources/extensions/gsd/prompts/parallel-research-slices.md +1 -1
  36. package/dist/resources/extensions/gsd/prompts/plan-milestone.md +3 -1
  37. package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  38. package/dist/resources/extensions/gsd/prompts/quick-task.md +1 -5
  39. package/dist/resources/extensions/gsd/prompts/validate-milestone.md +2 -2
  40. package/dist/resources/extensions/gsd/quick.js +34 -2
  41. package/dist/resources/extensions/gsd/safety/evidence-collector.js +10 -2
  42. package/dist/resources/extensions/gsd/tools/context-mode-tool-result.js +15 -0
  43. package/dist/resources/extensions/gsd/tools/exec-search-tool.js +5 -0
  44. package/dist/resources/extensions/gsd/tools/exec-tool.js +3 -15
  45. package/dist/resources/extensions/gsd/tools/memory-tools.js +1 -0
  46. package/dist/resources/extensions/gsd/tools/plan-slice.js +9 -0
  47. package/dist/resources/extensions/gsd/tools/plan-task.js +9 -0
  48. package/dist/resources/extensions/gsd/tools/resume-tool.js +5 -0
  49. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +1 -1
  50. package/dist/resources/extensions/gsd/unit-context-composer.js +12 -3
  51. package/dist/resources/extensions/gsd/unit-runtime.js +22 -0
  52. package/dist/resources/extensions/gsd/worktree-manager.js +16 -14
  53. package/dist/resources/extensions/gsd/worktree-resolver.js +33 -17
  54. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  55. package/dist/web/standalone/.next/BUILD_ID +1 -1
  56. package/dist/web/standalone/.next/app-path-routes-manifest.json +7 -7
  57. package/dist/web/standalone/.next/build-manifest.json +2 -2
  58. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  59. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  60. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  64. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  65. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  66. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  67. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  68. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  69. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  70. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  71. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  72. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  73. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  74. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  75. package/dist/web/standalone/.next/server/app/index.html +1 -1
  76. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  77. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  78. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  79. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  80. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  81. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  82. package/dist/web/standalone/.next/server/app-paths-manifest.json +7 -7
  83. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  84. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  85. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  86. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  87. package/package.json +3 -3
  88. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  89. package/packages/mcp-server/dist/workflow-tools.js +22 -17
  90. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  91. package/packages/mcp-server/src/workflow-tools.test.ts +75 -2
  92. package/packages/mcp-server/src/workflow-tools.ts +30 -16
  93. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
  94. package/packages/native/tsconfig.tsbuildinfo +1 -1
  95. package/packages/pi-coding-agent/dist/core/agent-session-abort-order.test.js +32 -0
  96. package/packages/pi-coding-agent/dist/core/agent-session-abort-order.test.js.map +1 -1
  97. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  98. package/packages/pi-coding-agent/dist/core/agent-session.js +8 -0
  99. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  100. package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js +32 -0
  101. package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js.map +1 -1
  102. package/packages/pi-coding-agent/dist/core/compaction/compaction.d.ts +11 -0
  103. package/packages/pi-coding-agent/dist/core/compaction/compaction.d.ts.map +1 -1
  104. package/packages/pi-coding-agent/dist/core/compaction/compaction.js +9 -0
  105. package/packages/pi-coding-agent/dist/core/compaction/compaction.js.map +1 -1
  106. package/packages/pi-coding-agent/dist/core/compaction-threshold.test.d.ts +2 -0
  107. package/packages/pi-coding-agent/dist/core/compaction-threshold.test.d.ts.map +1 -0
  108. package/packages/pi-coding-agent/dist/core/compaction-threshold.test.js +103 -0
  109. package/packages/pi-coding-agent/dist/core/compaction-threshold.test.js.map +1 -0
  110. package/packages/pi-coding-agent/dist/core/db-snapshot.d.ts +15 -0
  111. package/packages/pi-coding-agent/dist/core/db-snapshot.d.ts.map +1 -0
  112. package/packages/pi-coding-agent/dist/core/db-snapshot.js +66 -0
  113. package/packages/pi-coding-agent/dist/core/db-snapshot.js.map +1 -0
  114. package/packages/pi-coding-agent/dist/core/db-snapshot.test.d.ts +2 -0
  115. package/packages/pi-coding-agent/dist/core/db-snapshot.test.d.ts.map +1 -0
  116. package/packages/pi-coding-agent/dist/core/db-snapshot.test.js +24 -0
  117. package/packages/pi-coding-agent/dist/core/db-snapshot.test.js.map +1 -0
  118. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts +1 -0
  119. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
  120. package/packages/pi-coding-agent/dist/core/extensions/runner.js +3 -0
  121. package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
  122. package/packages/pi-coding-agent/dist/core/extensions/runner.test.js +2 -0
  123. package/packages/pi-coding-agent/dist/core/extensions/runner.test.js.map +1 -1
  124. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +7 -0
  125. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
  126. package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
  127. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +20 -0
  128. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  129. package/packages/pi-coding-agent/dist/core/settings-manager.js +25 -0
  130. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  131. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts +1 -0
  132. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  133. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +3 -0
  134. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
  135. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  136. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +15 -5
  137. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  138. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.test.js +53 -0
  139. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.test.js.map +1 -1
  140. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  141. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +3 -0
  142. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  143. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage-safety-guard.test.d.ts +2 -0
  144. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage-safety-guard.test.d.ts.map +1 -0
  145. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage-safety-guard.test.js +10 -0
  146. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage-safety-guard.test.js.map +1 -0
  147. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage.d.ts.map +1 -1
  148. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage.js +3 -2
  149. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage.js.map +1 -1
  150. package/packages/pi-coding-agent/src/core/agent-session-abort-order.test.ts +36 -0
  151. package/packages/pi-coding-agent/src/core/agent-session.ts +8 -0
  152. package/packages/pi-coding-agent/src/core/chat-controller-ordering.test.ts +38 -0
  153. package/packages/pi-coding-agent/src/core/compaction/compaction.ts +18 -0
  154. package/packages/pi-coding-agent/src/core/compaction-threshold.test.ts +121 -0
  155. package/packages/pi-coding-agent/src/core/db-snapshot.test.ts +32 -0
  156. package/packages/pi-coding-agent/src/core/db-snapshot.ts +66 -0
  157. package/packages/pi-coding-agent/src/core/extensions/runner.test.ts +2 -0
  158. package/packages/pi-coding-agent/src/core/extensions/runner.ts +3 -0
  159. package/packages/pi-coding-agent/src/core/extensions/types.ts +7 -0
  160. package/packages/pi-coding-agent/src/core/settings-manager.ts +39 -1
  161. package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +4 -0
  162. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.test.ts +56 -0
  163. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +24 -7
  164. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +3 -0
  165. package/packages/pi-coding-agent/src/resources/extensions/memory/storage-safety-guard.test.ts +14 -0
  166. package/packages/pi-coding-agent/src/resources/extensions/memory/storage.ts +3 -2
  167. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  168. package/packages/pi-tui/dist/tui.d.ts.map +1 -1
  169. package/packages/pi-tui/dist/tui.js +18 -8
  170. package/packages/pi-tui/dist/tui.js.map +1 -1
  171. package/packages/pi-tui/src/tui.ts +20 -8
  172. package/packages/pi-tui/tsconfig.tsbuildinfo +1 -1
  173. package/src/resources/GSD-WORKFLOW.md +2 -2
  174. package/src/resources/extensions/gsd/auto/loop-deps.ts +1 -0
  175. package/src/resources/extensions/gsd/auto/loop.ts +50 -8
  176. package/src/resources/extensions/gsd/auto/phases.ts +77 -48
  177. package/src/resources/extensions/gsd/auto/resolve.ts +23 -1
  178. package/src/resources/extensions/gsd/auto/run-unit.ts +18 -1
  179. package/src/resources/extensions/gsd/auto-post-unit.ts +10 -10
  180. package/src/resources/extensions/gsd/auto-prompts.ts +133 -2
  181. package/src/resources/extensions/gsd/auto-recovery.ts +54 -0
  182. package/src/resources/extensions/gsd/auto-supervisor.ts +7 -0
  183. package/src/resources/extensions/gsd/auto-timeout-recovery.ts +2 -2
  184. package/src/resources/extensions/gsd/auto.ts +92 -4
  185. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +21 -1
  186. package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +27 -19
  187. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +22 -0
  188. package/src/resources/extensions/gsd/clean-root-preflight.ts +41 -3
  189. package/src/resources/extensions/gsd/commands/dispatcher.ts +6 -0
  190. package/src/resources/extensions/gsd/context-budget.ts +44 -2
  191. package/src/resources/extensions/gsd/crash-recovery.ts +67 -10
  192. package/src/resources/extensions/gsd/db/unit-dispatches.ts +41 -0
  193. package/src/resources/extensions/gsd/db-base-schema.ts +4 -2
  194. package/src/resources/extensions/gsd/db-migration-steps.ts +8 -0
  195. package/src/resources/extensions/gsd/detection.ts +128 -0
  196. package/src/resources/extensions/gsd/git-service.ts +46 -8
  197. package/src/resources/extensions/gsd/gsd-db.ts +50 -13
  198. package/src/resources/extensions/gsd/guided-flow.ts +96 -14
  199. package/src/resources/extensions/gsd/memory-store.ts +77 -12
  200. package/src/resources/extensions/gsd/migrate/command.ts +47 -1
  201. package/src/resources/extensions/gsd/migration-auto-check.ts +129 -0
  202. package/src/resources/extensions/gsd/planning-path-scope.ts +35 -0
  203. package/src/resources/extensions/gsd/pre-execution-checks.ts +7 -0
  204. package/src/resources/extensions/gsd/preferences-types.ts +1 -1
  205. package/src/resources/extensions/gsd/prompt-loader.ts +27 -2
  206. package/src/resources/extensions/gsd/prompts/complete-milestone.md +19 -19
  207. package/src/resources/extensions/gsd/prompts/parallel-research-slices.md +1 -1
  208. package/src/resources/extensions/gsd/prompts/plan-milestone.md +3 -1
  209. package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  210. package/src/resources/extensions/gsd/prompts/quick-task.md +1 -5
  211. package/src/resources/extensions/gsd/prompts/validate-milestone.md +2 -2
  212. package/src/resources/extensions/gsd/quick.ts +37 -2
  213. package/src/resources/extensions/gsd/safety/evidence-collector.ts +11 -2
  214. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +72 -1
  215. package/src/resources/extensions/gsd/tests/auto-phases-lifecycle.test.ts +56 -13
  216. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +14 -1
  217. package/src/resources/extensions/gsd/tests/clean-root-preflight.test.ts +88 -2
  218. package/src/resources/extensions/gsd/tests/compaction-snapshot.test.ts +14 -1
  219. package/src/resources/extensions/gsd/tests/context-budget.test.ts +10 -1
  220. package/src/resources/extensions/gsd/tests/crash-handler-secondary.test.ts +55 -0
  221. package/src/resources/extensions/gsd/tests/crash-recovery-via-db.test.ts +22 -0
  222. package/src/resources/extensions/gsd/tests/detection.test.ts +140 -0
  223. package/src/resources/extensions/gsd/tests/dispatch-rule-coverage.test.ts +313 -0
  224. package/src/resources/extensions/gsd/tests/exec-history.test.ts +15 -0
  225. package/src/resources/extensions/gsd/tests/exec-sandbox.test.ts +65 -0
  226. package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +54 -0
  227. package/src/resources/extensions/gsd/tests/journal-integration.test.ts +234 -0
  228. package/src/resources/extensions/gsd/tests/memory-decay-factor.test.ts +90 -0
  229. package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +48 -0
  230. package/src/resources/extensions/gsd/tests/migration-auto-check.test.ts +127 -0
  231. package/src/resources/extensions/gsd/tests/plan-slice.test.ts +50 -0
  232. package/src/resources/extensions/gsd/tests/plan-task.test.ts +21 -0
  233. package/src/resources/extensions/gsd/tests/pre-execution-checks.test.ts +38 -0
  234. package/src/resources/extensions/gsd/tests/prompt-path-audit.test.ts +40 -0
  235. package/src/resources/extensions/gsd/tests/prompt-step-ordering.test.ts +19 -0
  236. package/src/resources/extensions/gsd/tests/quick-external-gsd.test.ts +40 -0
  237. package/src/resources/extensions/gsd/tests/right-sized-workflow-prompts.test.ts +192 -0
  238. package/src/resources/extensions/gsd/tests/safety-harness-false-positives.test.ts +29 -0
  239. package/src/resources/extensions/gsd/tests/schema-v27-v28-sequence.test.ts +156 -0
  240. package/src/resources/extensions/gsd/tests/signal-handlers.test.ts +27 -0
  241. package/src/resources/extensions/gsd/tests/smart-entry-complete.test.ts +38 -0
  242. package/src/resources/extensions/gsd/tests/stalled-tool-recovery.test.ts +49 -1
  243. package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +101 -2
  244. package/src/resources/extensions/gsd/tests/status-db-open.test.ts +9 -0
  245. package/src/resources/extensions/gsd/tests/unit-context-composer.test.ts +136 -4
  246. package/src/resources/extensions/gsd/tests/unit-dispatches.test.ts +30 -0
  247. package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +37 -0
  248. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +3 -0
  249. package/src/resources/extensions/gsd/tests/worktree-health-dispatch.test.ts +37 -6
  250. package/src/resources/extensions/gsd/tests/worktree-manager.test.ts +7 -0
  251. package/src/resources/extensions/gsd/tests/worktree-nested-git-safety.test.ts +9 -2
  252. package/src/resources/extensions/gsd/tests/worktree-resolver.test.ts +63 -1
  253. package/src/resources/extensions/gsd/tools/context-mode-tool-result.ts +25 -0
  254. package/src/resources/extensions/gsd/tools/exec-search-tool.ts +7 -7
  255. package/src/resources/extensions/gsd/tools/exec-tool.ts +4 -23
  256. package/src/resources/extensions/gsd/tools/memory-tools.ts +1 -0
  257. package/src/resources/extensions/gsd/tools/plan-slice.ts +13 -0
  258. package/src/resources/extensions/gsd/tools/plan-task.ts +10 -0
  259. package/src/resources/extensions/gsd/tools/resume-tool.ts +7 -7
  260. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +1 -1
  261. package/src/resources/extensions/gsd/unit-context-composer.ts +19 -4
  262. package/src/resources/extensions/gsd/unit-runtime.ts +25 -0
  263. package/src/resources/extensions/gsd/worktree-manager.ts +15 -4
  264. package/src/resources/extensions/gsd/worktree-resolver.ts +36 -15
  265. /package/dist/web/standalone/.next/static/{TCSim36ZpcPu2WgeoC45g → -5nHJWzSdG-WkPMul_khA}/_buildManifest.js +0 -0
  266. /package/dist/web/standalone/.next/static/{TCSim36ZpcPu2WgeoC45g → -5nHJWzSdG-WkPMul_khA}/_ssgManifest.js +0 -0
@@ -10,8 +10,8 @@
10
10
  */
11
11
 
12
12
  import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent";
13
- import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
14
- import { join } from "node:path";
13
+ import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, writeFileSync } from "node:fs";
14
+ import { isAbsolute, join, relative } from "node:path";
15
15
  import { loadPrompt } from "./prompt-loader.js";
16
16
  import { gsdRoot } from "./paths.js";
17
17
  import { GitServiceImpl, runGit } from "./git-service.js";
@@ -77,6 +77,40 @@ function ensureQuickDir(basePath: string, taskNum: number, slug: string): string
77
77
  return taskDir;
78
78
  }
79
79
 
80
+ function isPathInside(parent: string, child: string): boolean {
81
+ const rel = relative(parent, child);
82
+ return rel === "" || (!!rel && !rel.startsWith("..") && !isAbsolute(rel));
83
+ }
84
+
85
+ function isExternalGsdRoot(basePath: string, root: string): boolean {
86
+ try {
87
+ return !isPathInside(realpathSync(basePath), realpathSync(root));
88
+ } catch {
89
+ return !isPathInside(basePath, root);
90
+ }
91
+ }
92
+
93
+ export function buildQuickCommitInstruction(basePath: string, root: string): string {
94
+ const externalState = isExternalGsdRoot(basePath, root);
95
+ if (externalState) {
96
+ return [
97
+ "Commit repo changes atomically, but do not stage or commit `.gsd/quick/...`:",
98
+ " - `.gsd/` resolves outside this git repository, so Git cannot stage quick-task summary files from the project repo.",
99
+ " - Write the quick summary file directly at the requested path; that file is persisted by GSD external state.",
100
+ " - Stage and commit only implementation/test/docs files that live inside the repository.",
101
+ " - If the task only writes quick-task research/summary files and no repository files changed, do not run `git commit`; report that there was nothing in the project repo to commit.",
102
+ ].join("\n");
103
+ }
104
+
105
+ return [
106
+ "Commit your changes atomically:",
107
+ " - Use conventional commit messages (feat:, fix:, refactor:, etc.)",
108
+ " - Stage only relevant files — never commit secrets or runtime files.",
109
+ " - Commit logical units separately if the task involves distinct changes.",
110
+ " - Quick tasks run outside the auto-mode lifecycle — there is no system auto-commit, so commit directly here.",
111
+ ].join("\n");
112
+ }
113
+
80
114
  function quickReturnStatePath(basePath: string): string {
81
115
  return join(gsdRoot(basePath), "runtime", "quick-return.json");
82
116
  }
@@ -246,6 +280,7 @@ export async function handleQuick(
246
280
  taskDir: taskDirRel,
247
281
  branch: actualBranch,
248
282
  summaryPath,
283
+ commitInstruction: buildQuickCommitInstruction(basePath, root),
249
284
  date,
250
285
  taskNum: String(taskNum),
251
286
  slug,
@@ -50,6 +50,15 @@ export interface FileEditEvidence {
50
50
 
51
51
  export type EvidenceEntry = BashEvidence | FileWriteEvidence | FileEditEvidence;
52
52
 
53
+ const EXECUTION_TOOL_NAMES = new Set([
54
+ "bash",
55
+ "Bash",
56
+ "gsd_exec",
57
+ "gsd_exec_search",
58
+ "mcp__gsd-workflow__gsd_exec",
59
+ "mcp__gsd-workflow__gsd_exec_search",
60
+ ]);
61
+
53
62
  // ─── Module State ───────────────────────────────────────────────────────────
54
63
 
55
64
  let unitEvidence: EvidenceEntry[] = [];
@@ -188,11 +197,11 @@ export function clearEvidenceFromDisk(
188
197
  * Exit codes and output are filled in by recordToolResult after execution.
189
198
  */
190
199
  export function recordToolCall(toolCallId: string, toolName: string, input: Record<string, unknown>): void {
191
- if (toolName === "bash" || toolName === "Bash") {
200
+ if (EXECUTION_TOOL_NAMES.has(toolName)) {
192
201
  unitEvidence.push({
193
202
  kind: "bash",
194
203
  toolCallId,
195
- command: String(input.command ?? ""),
204
+ command: String(input.command ?? input.cmd ?? input.query ?? ""),
196
205
  exitCode: -1,
197
206
  outputSnippet: "",
198
207
  timestamp: Date.now(),
@@ -10,6 +10,8 @@ import {
10
10
  _resetPendingResolve,
11
11
  _hasPendingResolveForTest,
12
12
  _setActiveSession,
13
+ _setSessionSwitchInFlight,
14
+ _consumePendingSwitchCancellation,
13
15
  isSessionSwitchInFlight,
14
16
  } from "../auto/resolve.js";
15
17
  import { runUnit } from "../auto/run-unit.js";
@@ -206,6 +208,28 @@ test("runUnit returns cancelled when session creation fails", async () => {
206
208
  assert.equal(pi.calls.length, 0);
207
209
  });
208
210
 
211
+ test("runUnit clears queued switch cancellation when session creation fails", async () => {
212
+ _resetPendingResolve();
213
+
214
+ const ctx = makeMockCtx();
215
+ const pi = makeMockPi();
216
+ const s = makeMockSession({
217
+ newSessionThrows: "connection refused",
218
+ onNewSessionStart: () => {
219
+ resolveAgentEndCancelled({
220
+ message: "Claude Code process aborted by user",
221
+ category: "aborted",
222
+ isTransient: false,
223
+ });
224
+ },
225
+ });
226
+
227
+ const result = await runUnit(ctx, pi, s, "task", "T01", "prompt");
228
+
229
+ assert.equal(result.status, "cancelled");
230
+ assert.equal(_consumePendingSwitchCancellation(), null);
231
+ });
232
+
209
233
  test("runUnit returns cancelled when session creation times out", async () => {
210
234
  _resetPendingResolve();
211
235
 
@@ -221,6 +245,34 @@ test("runUnit returns cancelled when session creation times out", async () => {
221
245
  assert.equal(pi.calls.length, 0);
222
246
  });
223
247
 
248
+ test("runUnit consumes a cancellation queued during session switch before dispatch", async () => {
249
+ _resetPendingResolve();
250
+
251
+ const ctx = makeMockCtx();
252
+ const pi = makeMockPi();
253
+ let cancellationQueued = false;
254
+ const s = makeMockSession({
255
+ newSessionDelayMs: 10,
256
+ onNewSessionStart: () => {
257
+ setTimeout(() => {
258
+ cancellationQueued = !resolveAgentEndCancelled({
259
+ message: "Claude Code process aborted by user",
260
+ category: "aborted",
261
+ isTransient: false,
262
+ });
263
+ }, 0);
264
+ },
265
+ });
266
+
267
+ const result = await runUnit(ctx, pi, s, "plan-slice", "M009/S01", "prompt");
268
+
269
+ assert.equal(cancellationQueued, true);
270
+ assert.equal(result.status, "cancelled");
271
+ assert.equal(result.errorContext?.category, "aborted");
272
+ assert.equal(result.errorContext?.message, "Claude Code process aborted by user");
273
+ assert.equal(pi.calls.length, 0, "queued switch cancellation must prevent prompt dispatch");
274
+ });
275
+
224
276
  test("runUnit keeps the session-switch guard across a late newSession settlement", async () => {
225
277
  _resetPendingResolve();
226
278
  mock.timers.enable();
@@ -2089,6 +2141,25 @@ test("resolveAgentEndCancelled without args produces no errorContext field", asy
2089
2141
  assert.equal(resolved.errorContext, undefined, "errorContext must not be present when no args passed");
2090
2142
  });
2091
2143
 
2144
+ test("resolveAgentEndCancelled queues cancellation that arrives during session switch", () => {
2145
+ _resetPendingResolve();
2146
+
2147
+ _setSessionSwitchInFlight(true);
2148
+ const resolved = resolveAgentEndCancelled({
2149
+ message: "Claude Code process aborted by user",
2150
+ category: "aborted",
2151
+ isTransient: false,
2152
+ });
2153
+
2154
+ assert.equal(resolved, false);
2155
+ const pending = _consumePendingSwitchCancellation();
2156
+ assert.ok(pending?.errorContext, "queued cancellation should preserve errorContext");
2157
+ assert.equal(pending.errorContext.category, "aborted");
2158
+ assert.equal(pending.errorContext.message, "Claude Code process aborted by user");
2159
+ assert.equal(_consumePendingSwitchCancellation(), null);
2160
+ _resetPendingResolve();
2161
+ });
2162
+
2092
2163
  // ─── #1571: artifact verification retry ──────────────────────────────────────
2093
2164
 
2094
2165
  test("autoLoop re-iterates when postUnitPreVerification returns retry (#1571)", async () => {
@@ -2556,7 +2627,7 @@ test("autoLoop warns but proceeds for greenfield project (no project files) (#18
2556
2627
  "should not stop with health check failure for greenfield project",
2557
2628
  );
2558
2629
  const greenfieldWarning = notifications.find(
2559
- (n) => n.includes("no recognized project files") && n.includes("greenfield"),
2630
+ (n) => n.includes("no project content yet") && n.includes("greenfield"),
2560
2631
  );
2561
2632
  assert.ok(
2562
2633
  greenfieldWarning,
@@ -1,17 +1,20 @@
1
1
  import test from "node:test";
2
2
  import assert from "node:assert/strict";
3
+ import { mkdtempSync, rmSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { tmpdir } from "node:os";
3
6
 
4
7
  import { runFinalize } from "../auto/phases.ts";
5
8
  import { AutoSession } from "../auto/session.ts";
9
+ import { readUnitRuntimeRecord, writeUnitRuntimeRecord } from "../unit-runtime.ts";
6
10
 
7
- test("runFinalize clears currentUnit after successful finalize", async () => {
8
- const s = new AutoSession();
9
- s.basePath = "/tmp/gsd-finalize-current-unit";
10
- s.currentUnit = {
11
- type: "execute-task",
12
- id: "M001/S01/T01",
13
- startedAt: Date.now(),
14
- };
11
+ async function runSuccessfulFinalize(s: AutoSession) {
12
+ const unit = s.currentUnit;
13
+ assert.ok(unit, "test setup must provide currentUnit");
14
+
15
+ writeUnitRuntimeRecord(s.basePath, unit.type, unit.id, unit.startedAt, {
16
+ phase: "dispatched",
17
+ });
15
18
 
16
19
  const deps = {
17
20
  clearUnitTimeout() {},
@@ -26,7 +29,7 @@ test("runFinalize clears currentUnit after successful finalize", async () => {
26
29
  postUnitPostVerification: async () => "continue",
27
30
  };
28
31
 
29
- const result = await runFinalize(
32
+ return runFinalize(
30
33
  {
31
34
  ctx: { ui: { notify() {} } },
32
35
  pi: {},
@@ -38,8 +41,8 @@ test("runFinalize clears currentUnit after successful finalize", async () => {
38
41
  nextSeq: () => 1,
39
42
  } as any,
40
43
  {
41
- unitType: "execute-task",
42
- unitId: "M001/S01/T01",
44
+ unitType: unit.type,
45
+ unitId: unit.id,
43
46
  prompt: "",
44
47
  finalPrompt: "",
45
48
  pauseAfterUatDispatch: false,
@@ -55,7 +58,47 @@ test("runFinalize clears currentUnit after successful finalize", async () => {
55
58
  consecutiveFinalizeTimeouts: 0,
56
59
  },
57
60
  );
61
+ }
62
+
63
+ test("runFinalize clears currentUnit after successful finalize", async () => {
64
+ const base = mkdtempSync(join(tmpdir(), "gsd-finalize-current-unit-"));
65
+ const s = new AutoSession();
66
+ s.basePath = base;
67
+ s.currentUnit = {
68
+ type: "execute-task",
69
+ id: "M001/S01/T01",
70
+ startedAt: Date.now(),
71
+ };
72
+
73
+ try {
74
+ const result = await runSuccessfulFinalize(s);
75
+
76
+ assert.equal(result.action, "next");
77
+ assert.equal(s.currentUnit, null);
78
+ } finally {
79
+ rmSync(base, { recursive: true, force: true });
80
+ }
81
+ });
82
+
83
+ test("runFinalize marks unit runtime finalized after successful finalize", async () => {
84
+ const base = mkdtempSync(join(tmpdir(), "gsd-finalize-runtime-"));
85
+ const s = new AutoSession();
86
+ const startedAt = Date.now();
87
+ s.basePath = base;
88
+ s.currentUnit = {
89
+ type: "complete-milestone",
90
+ id: "M001",
91
+ startedAt,
92
+ };
93
+
94
+ try {
95
+ const result = await runSuccessfulFinalize(s);
96
+ const runtime = readUnitRuntimeRecord(base, "complete-milestone", "M001");
58
97
 
59
- assert.equal(result.action, "next");
60
- assert.equal(s.currentUnit, null);
98
+ assert.equal(result.action, "next");
99
+ assert.equal(runtime?.phase, "finalized");
100
+ assert.equal(runtime?.lastProgressKind, "finalize-success");
101
+ } finally {
102
+ rmSync(base, { recursive: true, force: true });
103
+ }
61
104
  });
@@ -5,7 +5,7 @@ import { join } from "node:path";
5
5
  import { tmpdir } from "node:os";
6
6
  import { randomUUID } from "node:crypto";
7
7
 
8
- import { verifyExpectedArtifact, hasImplementationArtifacts, resolveExpectedArtifactPath, diagnoseExpectedArtifact, buildLoopRemediationSteps, writeBlockerPlaceholder } from "../auto-recovery.ts";
8
+ import { verifyExpectedArtifact, hasImplementationArtifacts, resolveExpectedArtifactPath, diagnoseExpectedArtifact, buildLoopRemediationSteps, writeBlockerPlaceholder, refreshRecoveryDbForArtifact } from "../auto-recovery.ts";
9
9
  import { resolveMilestoneFile } from "../paths.ts";
10
10
  import { openDatabase, closeDatabase, insertMilestone, insertSlice, insertGateRow, insertTask, getMilestoneCommitAttributionShas } from "../gsd-db.ts";
11
11
  import { clearParseCache } from "../files.ts";
@@ -175,6 +175,19 @@ test("resolveExpectedArtifactPath returns correct path for all slice-level types
175
175
  }
176
176
  });
177
177
 
178
+ test("refreshRecoveryDbForArtifact treats missing execute-task DB rows as fatal mismatches", () => {
179
+ makeTmpProject();
180
+
181
+ const result = refreshRecoveryDbForArtifact("execute-task", "M001/S01/T01");
182
+
183
+ assert.deepEqual(result, {
184
+ ok: false,
185
+ fatal: true,
186
+ reason: "execute-task-artifact-db-missing",
187
+ message: "Stuck recovery found execute-task M001/S01/T01 artifacts, but no matching DB task row exists after refresh.",
188
+ });
189
+ });
190
+
178
191
  // ─── diagnoseExpectedArtifact ─────────────────────────────────────────────
179
192
 
180
193
  test("diagnoseExpectedArtifact returns description for known types", () => {
@@ -131,7 +131,7 @@ test("postflightPopStash — restores stashed changes and emits info notificatio
131
131
  run('git commit -m "simulate merge"', repo);
132
132
 
133
133
  const postNotifications: Array<{ msg: string; level: string }> = [];
134
- postflightPopStash(repo, "M004", (msg, level) => {
134
+ postflightPopStash(repo, "M004", preflight.stashMarker, (msg, level) => {
135
135
  postNotifications.push({ msg, level });
136
136
  });
137
137
 
@@ -171,7 +171,7 @@ test("preflight + merge + postflight round-trip preserves uncommitted changes",
171
171
  run('git commit -m "feat: add feature"', repo);
172
172
 
173
173
  // Postflight: pop stash
174
- postflightPopStash(repo, "M005", () => {});
174
+ postflightPopStash(repo, "M005", preflight.stashMarker, () => {});
175
175
 
176
176
  // README.md must still have our local content
177
177
  const restored = readFileSync(join(repo, "README.md"), "utf-8");
@@ -184,3 +184,89 @@ test("preflight + merge + postflight round-trip preserves uncommitted changes",
184
184
  try { rmSync(repo, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 }); } catch { /* ignore */ }
185
185
  }
186
186
  });
187
+
188
+ test("postflightPopStash conflict warning names the exact stash ref", () => {
189
+ const repo = createTempRepo();
190
+ try {
191
+ writeFileSync(join(repo, "README.md"), "# local work\n");
192
+ const preflight = preflightCleanRoot(repo, "M005C", () => {});
193
+ assert.equal(preflight.stashPushed, true, "must have stashed");
194
+
195
+ writeFileSync(join(repo, "README.md"), "# merged work\n");
196
+ run("git add README.md", repo);
197
+ run('git commit -m "simulate conflicting merge"', repo);
198
+
199
+ const notifications: Array<{ msg: string; level: string }> = [];
200
+ postflightPopStash(repo, "M005C", preflight.stashMarker, (msg, level) => {
201
+ notifications.push({ msg, level });
202
+ });
203
+
204
+ const warning = notifications.find((n) => n.level === "warning")?.msg ?? "";
205
+ assert.match(warning, /git stash pop stash@\{\d+\}/);
206
+ assert.match(warning, /git stash apply stash@\{\d+\}/);
207
+ } finally {
208
+ try { rmSync(repo, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 }); } catch { /* ignore */ }
209
+ }
210
+ });
211
+
212
+ test("postflightPopStash restores the matching GSD stash, not stash@{0}", () => {
213
+ const repo = createTempRepo();
214
+ try {
215
+ writeFileSync(join(repo, "README.md"), "# target stash\n");
216
+ const preflight = preflightCleanRoot(repo, "M006", () => {});
217
+ assert.equal(preflight.stashPushed, true, "must have stashed target change");
218
+
219
+ writeFileSync(join(repo, "other.txt"), "other stash\n");
220
+ run('git stash push --include-untracked -m "unrelated newer stash"', repo);
221
+
222
+ postflightPopStash(repo, "M006", preflight.stashMarker, () => {});
223
+
224
+ const content = readFileSync(join(repo, "README.md"), "utf-8");
225
+ assert.equal(content.replace(/\r\n/g, "\n"), "# target stash\n");
226
+ const stashList = run("git stash list", repo);
227
+ assert.ok(stashList.includes("unrelated newer stash"), "unrelated newer stash must remain");
228
+ assert.ok(!stashList.includes("gsd-preflight-stash [gsd-preflight-stash:M006"), "target stash should be consumed");
229
+ } finally {
230
+ try { rmSync(repo, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 }); } catch { /* ignore */ }
231
+ }
232
+ });
233
+
234
+ test("postflightPopStash restores the exact preflight marker when another same-milestone stash exists", () => {
235
+ const repo = createTempRepo();
236
+ try {
237
+ writeFileSync(join(repo, "README.md"), "# target stash\n");
238
+ const preflight = preflightCleanRoot(repo, "M007", () => {});
239
+ assert.equal(preflight.stashPushed, true, "must have stashed target change");
240
+ assert.ok(preflight.stashMarker, "preflight must expose exact stash marker");
241
+
242
+ writeFileSync(join(repo, "same-milestone.txt"), "newer same milestone stash\n");
243
+ run('git stash push --include-untracked -m "gsd-preflight-stash [gsd-preflight-stash:M007:other]"', repo);
244
+
245
+ postflightPopStash(repo, "M007", preflight.stashMarker, () => {});
246
+
247
+ const content = readFileSync(join(repo, "README.md"), "utf-8");
248
+ assert.equal(content.replace(/\r\n/g, "\n"), "# target stash\n");
249
+ const stashList = run("git stash list", repo);
250
+ assert.ok(stashList.includes("gsd-preflight-stash:M007:other"), "newer same-milestone stash must remain");
251
+ assert.ok(!stashList.includes(preflight.stashMarker), "exact target stash should be consumed");
252
+ } finally {
253
+ try { rmSync(repo, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 }); } catch { /* ignore */ }
254
+ }
255
+ });
256
+
257
+ test("postflightPopStash falls back to milestone marker prefix when exact marker is unavailable", () => {
258
+ const repo = createTempRepo();
259
+ try {
260
+ writeFileSync(join(repo, "README.md"), "# fallback stash\n");
261
+ run('git stash push --include-untracked -m "gsd-preflight-stash [gsd-preflight-stash:M008:fallback]"', repo);
262
+
263
+ postflightPopStash(repo, "M008", undefined, () => {});
264
+
265
+ const content = readFileSync(join(repo, "README.md"), "utf-8");
266
+ assert.equal(content.replace(/\r\n/g, "\n"), "# fallback stash\n");
267
+ const stashList = run("git stash list", repo);
268
+ assert.ok(!stashList.includes("gsd-preflight-stash:M008:fallback"), "fallback stash should be consumed");
269
+ } finally {
270
+ try { rmSync(repo, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 }); } catch { /* ignore */ }
271
+ }
272
+ });
@@ -29,7 +29,8 @@ test('buildSnapshot: renders memories, exec history, and active context', () =>
29
29
  memories: [
30
30
  { id: 'MEM001', category: 'gotcha', content: 'FTS5 needs Porter tokenizer', confidence: 0.9,
31
31
  source_unit_type: null, source_unit_id: null, created_at: '', updated_at: '',
32
- superseded_by: null, hit_count: 0, scope: 'project', seq: 1, tags: [], structured_fields: null },
32
+ superseded_by: null, hit_count: 0, scope: 'project', seq: 1, tags: [], structured_fields: null,
33
+ last_hit_at: null },
33
34
  ],
34
35
  execHistory: [
35
36
  {
@@ -65,6 +66,7 @@ test('buildSnapshot: enforces the byte cap with a truncation marker', () => {
65
66
  seq: i,
66
67
  tags: [] as string[],
67
68
  structured_fields: null,
69
+ last_hit_at: null,
68
70
  }));
69
71
  const snap = buildSnapshot(
70
72
  { generatedAt: new Date(), memories: longMemories, execHistory: [] },
@@ -121,3 +123,14 @@ test('executeResume: reports friendly empty state when no snapshot exists', () =
121
123
  cleanup(base);
122
124
  }
123
125
  });
126
+
127
+ test('executeResume: returns disabled error when context_mode.enabled=false', () => {
128
+ const base = freshBase();
129
+ try {
130
+ const result = executeResume({}, { baseDir: base, preferences: { context_mode: { enabled: false } } });
131
+ assert.equal(result.isError, true);
132
+ assert.equal((result.details as { error?: string }).error, 'context_mode_disabled');
133
+ } finally {
134
+ cleanup(base);
135
+ }
136
+ });
@@ -4,7 +4,7 @@
4
4
  * No I/O, no extension context, no global state.
5
5
  */
6
6
 
7
- import { describe, it } from "node:test";
7
+ import { describe, it, beforeEach } from "node:test";
8
8
  import assert from "node:assert/strict";
9
9
 
10
10
  import {
@@ -16,8 +16,17 @@ import {
16
16
  computeBudgets,
17
17
  truncateAtSectionBoundary,
18
18
  resolveExecutorContextWindow,
19
+ _resetEmpiricalCacheForTest,
19
20
  } from "../context-budget.js";
20
21
 
22
+ // Reset the per-provider empirical chars-per-token cache before each test.
23
+ // The hardcoded char-ratio assertions below assume the static fallback path
24
+ // (3.5 / 4.0 chars/token) is used. Without this guard, a prior test that
25
+ // warms tiktoken would populate the cache and silently break these tests.
26
+ beforeEach(() => {
27
+ _resetEmpiricalCacheForTest();
28
+ });
29
+
21
30
  import type { TokenProvider } from "../token-counter.js";
22
31
 
23
32
  // ─── Test helpers ─────────────────────────────────────────────────────────────
@@ -232,4 +232,59 @@ describe('emitCrashRecoveredUnitEnd (#3348)', () => {
232
232
  rmSync(base, { recursive: true, force: true });
233
233
  }
234
234
  });
235
+
236
+ test('emitOpenUnitEndForUnit closes the latest open start with error context', async () => {
237
+ const base = makeTmpBase();
238
+ try {
239
+ const { emitJournalEvent, queryJournal } = await import('../journal.ts');
240
+ const { emitOpenUnitEndForUnit } = await import('../crash-recovery.ts');
241
+
242
+ const firstFlowId = randomUUID();
243
+ const secondFlowId = randomUUID();
244
+ emitJournalEvent(base, {
245
+ ts: new Date().toISOString(),
246
+ flowId: firstFlowId,
247
+ seq: 1,
248
+ eventType: 'unit-start',
249
+ data: { unitType: 'execute-task', unitId: 'M008/S04/T02' },
250
+ });
251
+ emitJournalEvent(base, {
252
+ ts: new Date().toISOString(),
253
+ flowId: firstFlowId,
254
+ seq: 2,
255
+ eventType: 'unit-end',
256
+ data: { unitType: 'execute-task', unitId: 'M008/S04/T02', status: 'completed' },
257
+ causedBy: { flowId: firstFlowId, seq: 1 },
258
+ });
259
+ emitJournalEvent(base, {
260
+ ts: new Date().toISOString(),
261
+ flowId: secondFlowId,
262
+ seq: 3,
263
+ eventType: 'unit-start',
264
+ data: { unitType: 'execute-task', unitId: 'M008/S04/T02' },
265
+ });
266
+
267
+ const emitted = emitOpenUnitEndForUnit(
268
+ base,
269
+ 'execute-task',
270
+ 'M008/S04/T02',
271
+ 'cancelled',
272
+ { message: 'runUnitPhase exploded', category: 'unit-exception', isTransient: false },
273
+ );
274
+
275
+ assert.equal(emitted, true, 'open unit should be closed');
276
+ const ends = queryJournal(base).filter((e) => e.eventType === 'unit-end');
277
+ assert.equal(ends.length, 2, 'should preserve existing end and add one new end');
278
+ const newEnd = ends.find((e) => e.causedBy?.flowId === secondFlowId);
279
+ assert.ok(newEnd, 'new end should close the latest open start');
280
+ assert.equal(newEnd!.data?.status, 'cancelled');
281
+ assert.deepEqual(newEnd!.data?.errorContext, {
282
+ message: 'runUnitPhase exploded',
283
+ category: 'unit-exception',
284
+ isTransient: false,
285
+ });
286
+ } finally {
287
+ rmSync(base, { recursive: true, force: true });
288
+ }
289
+ });
235
290
  });
@@ -30,6 +30,7 @@ import {
30
30
  isLockProcessAlive,
31
31
  } from "../crash-recovery.ts";
32
32
  import { normalizeRealPath } from "../paths.ts";
33
+ import { writeUnitRuntimeRecord } from "../unit-runtime.ts";
33
34
 
34
35
  function makeBase(): string {
35
36
  const base = mkdtempSync(join(tmpdir(), "gsd-crash-recovery-"));
@@ -102,6 +103,27 @@ test("readCrashLock synthesizes LockData from a stale dead worker (no dispatches
102
103
  assert.ok(lock!.startedAt, "startedAt populated from workers.started_at");
103
104
  });
104
105
 
106
+ test("readCrashLock falls back to latest in-flight runtime record when dispatch claim is missing", (t) => {
107
+ const base = makeBase();
108
+ t.after(() => cleanup(base));
109
+ openDatabase(join(base, ".gsd", "gsd.db"));
110
+ const projectRoot = normalizeRealPath(base);
111
+ const workerId = registerAutoWorker({ projectRootRealpath: projectRoot });
112
+ writeUnitRuntimeRecord(base, "execute-task", "M008/S04/T02", 1778069087937, {
113
+ phase: "dispatched",
114
+ lastProgressAt: 1778069087937,
115
+ lastProgressKind: "dispatch",
116
+ });
117
+ setWorkerPid(workerId, 99999);
118
+ expireWorker(workerId);
119
+
120
+ const lock = readCrashLock(base);
121
+ assert.ok(lock, "stale worker surfaced as a crash lock");
122
+ assert.equal(lock!.unitType, "execute-task");
123
+ assert.equal(lock!.unitId, "M008/S04/T02");
124
+ assert.equal(lock!.unitStartedAt, new Date(1778069087937).toISOString());
125
+ });
126
+
105
127
  test("readCrashLock includes the most recent dispatch as unitType/unitId", (t) => {
106
128
  const base = makeBase();
107
129
  t.after(() => cleanup(base));