gsd-pi 2.73.0 → 2.73.1-dev.06e4302

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 (300) hide show
  1. package/dist/cli-web-branch.d.ts +4 -3
  2. package/dist/cli-web-branch.js +10 -7
  3. package/dist/cli.js +99 -253
  4. package/dist/help-text.js +1 -1
  5. package/dist/logo.d.ts +1 -1
  6. package/dist/logo.js +1 -1
  7. package/dist/onboarding.js +59 -53
  8. package/dist/resource-loader.js +2 -2
  9. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +68 -4
  10. package/dist/resources/extensions/gsd/auto/phases.js +15 -9
  11. package/dist/resources/extensions/gsd/auto-dispatch.js +16 -6
  12. package/dist/resources/extensions/gsd/auto-model-selection.js +54 -11
  13. package/dist/resources/extensions/gsd/auto-post-unit.js +41 -1
  14. package/dist/resources/extensions/gsd/auto-prompts.js +9 -6
  15. package/dist/resources/extensions/gsd/auto-start.js +23 -6
  16. package/dist/resources/extensions/gsd/auto-timeout-recovery.js +13 -0
  17. package/dist/resources/extensions/gsd/auto-verification.js +88 -3
  18. package/dist/resources/extensions/gsd/auto.js +34 -9
  19. package/dist/resources/extensions/gsd/bootstrap/crash-log.js +31 -0
  20. package/dist/resources/extensions/gsd/bootstrap/register-extension.js +18 -7
  21. package/dist/resources/extensions/gsd/bootstrap/system-context.js +6 -1
  22. package/dist/resources/extensions/gsd/commands-handlers.js +8 -2
  23. package/dist/resources/extensions/gsd/crash-recovery.js +51 -0
  24. package/dist/resources/extensions/gsd/docs/preferences-reference.md +1 -1
  25. package/dist/resources/extensions/gsd/gsd-db.js +36 -2
  26. package/dist/resources/extensions/gsd/milestone-actions.js +19 -1
  27. package/dist/resources/extensions/gsd/notification-widget.js +2 -2
  28. package/dist/resources/extensions/gsd/preferences-models.js +43 -0
  29. package/dist/resources/extensions/gsd/preferences-types.js +1 -0
  30. package/dist/resources/extensions/gsd/preferences-validation.js +22 -0
  31. package/dist/resources/extensions/gsd/state.js +61 -14
  32. package/dist/startup-model-validation.js +8 -5
  33. package/dist/update-check.d.ts +1 -0
  34. package/dist/update-check.js +13 -5
  35. package/dist/update-cmd.js +4 -3
  36. package/dist/web/standalone/.next/BUILD_ID +1 -1
  37. package/dist/web/standalone/.next/app-path-routes-manifest.json +12 -12
  38. package/dist/web/standalone/.next/build-manifest.json +3 -3
  39. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  40. package/dist/web/standalone/.next/required-server-files.json +3 -3
  41. package/dist/web/standalone/.next/server/app/_global-error/page.js +3 -3
  42. package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  43. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  44. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_not-found/page.js +2 -2
  52. package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  53. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  54. package/dist/web/standalone/.next/server/app/_not-found.rsc +3 -3
  55. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
  56. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +3 -3
  58. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/api/boot/route.js +1 -1
  62. package/dist/web/standalone/.next/server/app/api/boot/route_client-reference-manifest.js +1 -1
  63. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js +1 -1
  64. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route_client-reference-manifest.js +1 -1
  65. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js +1 -1
  66. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route_client-reference-manifest.js +1 -1
  67. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js +2 -2
  68. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route_client-reference-manifest.js +1 -1
  69. package/dist/web/standalone/.next/server/app/api/browse-directories/route.js +1 -1
  70. package/dist/web/standalone/.next/server/app/api/browse-directories/route_client-reference-manifest.js +1 -1
  71. package/dist/web/standalone/.next/server/app/api/captures/route.js +1 -1
  72. package/dist/web/standalone/.next/server/app/api/captures/route_client-reference-manifest.js +1 -1
  73. package/dist/web/standalone/.next/server/app/api/cleanup/route.js +1 -1
  74. package/dist/web/standalone/.next/server/app/api/cleanup/route_client-reference-manifest.js +1 -1
  75. package/dist/web/standalone/.next/server/app/api/dev-mode/route.js +1 -1
  76. package/dist/web/standalone/.next/server/app/api/dev-mode/route_client-reference-manifest.js +1 -1
  77. package/dist/web/standalone/.next/server/app/api/doctor/route.js +1 -1
  78. package/dist/web/standalone/.next/server/app/api/doctor/route_client-reference-manifest.js +1 -1
  79. package/dist/web/standalone/.next/server/app/api/experimental/route.js +2 -2
  80. package/dist/web/standalone/.next/server/app/api/experimental/route_client-reference-manifest.js +1 -1
  81. package/dist/web/standalone/.next/server/app/api/export-data/route.js +1 -1
  82. package/dist/web/standalone/.next/server/app/api/export-data/route_client-reference-manifest.js +1 -1
  83. package/dist/web/standalone/.next/server/app/api/files/route.js +1 -1
  84. package/dist/web/standalone/.next/server/app/api/files/route_client-reference-manifest.js +1 -1
  85. package/dist/web/standalone/.next/server/app/api/forensics/route.js +1 -1
  86. package/dist/web/standalone/.next/server/app/api/forensics/route_client-reference-manifest.js +1 -1
  87. package/dist/web/standalone/.next/server/app/api/git/route.js +1 -1
  88. package/dist/web/standalone/.next/server/app/api/git/route_client-reference-manifest.js +1 -1
  89. package/dist/web/standalone/.next/server/app/api/history/route.js +1 -1
  90. package/dist/web/standalone/.next/server/app/api/history/route_client-reference-manifest.js +1 -1
  91. package/dist/web/standalone/.next/server/app/api/hooks/route.js +1 -1
  92. package/dist/web/standalone/.next/server/app/api/hooks/route_client-reference-manifest.js +1 -1
  93. package/dist/web/standalone/.next/server/app/api/inspect/route.js +1 -1
  94. package/dist/web/standalone/.next/server/app/api/inspect/route_client-reference-manifest.js +1 -1
  95. package/dist/web/standalone/.next/server/app/api/knowledge/route.js +1 -1
  96. package/dist/web/standalone/.next/server/app/api/knowledge/route_client-reference-manifest.js +1 -1
  97. package/dist/web/standalone/.next/server/app/api/live-state/route.js +1 -1
  98. package/dist/web/standalone/.next/server/app/api/live-state/route_client-reference-manifest.js +1 -1
  99. package/dist/web/standalone/.next/server/app/api/notifications/route.js +2 -2
  100. package/dist/web/standalone/.next/server/app/api/notifications/route_client-reference-manifest.js +1 -1
  101. package/dist/web/standalone/.next/server/app/api/onboarding/route.js +1 -1
  102. package/dist/web/standalone/.next/server/app/api/onboarding/route_client-reference-manifest.js +1 -1
  103. package/dist/web/standalone/.next/server/app/api/preferences/route.js +1 -1
  104. package/dist/web/standalone/.next/server/app/api/preferences/route_client-reference-manifest.js +1 -1
  105. package/dist/web/standalone/.next/server/app/api/projects/route.js +1 -1
  106. package/dist/web/standalone/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
  107. package/dist/web/standalone/.next/server/app/api/recovery/route.js +1 -1
  108. package/dist/web/standalone/.next/server/app/api/recovery/route_client-reference-manifest.js +1 -1
  109. package/dist/web/standalone/.next/server/app/api/remote-questions/route.js +2 -2
  110. package/dist/web/standalone/.next/server/app/api/remote-questions/route_client-reference-manifest.js +1 -1
  111. package/dist/web/standalone/.next/server/app/api/session/browser/route.js +1 -1
  112. package/dist/web/standalone/.next/server/app/api/session/browser/route_client-reference-manifest.js +1 -1
  113. package/dist/web/standalone/.next/server/app/api/session/command/route.js +1 -1
  114. package/dist/web/standalone/.next/server/app/api/session/command/route_client-reference-manifest.js +1 -1
  115. package/dist/web/standalone/.next/server/app/api/session/events/route.js +2 -2
  116. package/dist/web/standalone/.next/server/app/api/session/events/route_client-reference-manifest.js +1 -1
  117. package/dist/web/standalone/.next/server/app/api/session/manage/route.js +1 -1
  118. package/dist/web/standalone/.next/server/app/api/session/manage/route_client-reference-manifest.js +1 -1
  119. package/dist/web/standalone/.next/server/app/api/settings-data/route.js +1 -1
  120. package/dist/web/standalone/.next/server/app/api/settings-data/route_client-reference-manifest.js +1 -1
  121. package/dist/web/standalone/.next/server/app/api/shutdown/route.js +1 -1
  122. package/dist/web/standalone/.next/server/app/api/shutdown/route_client-reference-manifest.js +1 -1
  123. package/dist/web/standalone/.next/server/app/api/skill-health/route.js +1 -1
  124. package/dist/web/standalone/.next/server/app/api/skill-health/route_client-reference-manifest.js +1 -1
  125. package/dist/web/standalone/.next/server/app/api/steer/route.js +1 -1
  126. package/dist/web/standalone/.next/server/app/api/steer/route_client-reference-manifest.js +1 -1
  127. package/dist/web/standalone/.next/server/app/api/switch-root/route.js +1 -1
  128. package/dist/web/standalone/.next/server/app/api/switch-root/route_client-reference-manifest.js +1 -1
  129. package/dist/web/standalone/.next/server/app/api/terminal/input/route.js +2 -2
  130. package/dist/web/standalone/.next/server/app/api/terminal/input/route_client-reference-manifest.js +1 -1
  131. package/dist/web/standalone/.next/server/app/api/terminal/resize/route.js +2 -2
  132. package/dist/web/standalone/.next/server/app/api/terminal/resize/route_client-reference-manifest.js +1 -1
  133. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js +2 -2
  134. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route_client-reference-manifest.js +1 -1
  135. package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js +4 -4
  136. package/dist/web/standalone/.next/server/app/api/terminal/stream/route_client-reference-manifest.js +1 -1
  137. package/dist/web/standalone/.next/server/app/api/terminal/upload/route.js +1 -1
  138. package/dist/web/standalone/.next/server/app/api/terminal/upload/route_client-reference-manifest.js +1 -1
  139. package/dist/web/standalone/.next/server/app/api/undo/route.js +1 -1
  140. package/dist/web/standalone/.next/server/app/api/undo/route_client-reference-manifest.js +1 -1
  141. package/dist/web/standalone/.next/server/app/api/update/route.js +1 -1
  142. package/dist/web/standalone/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
  143. package/dist/web/standalone/.next/server/app/api/visualizer/route.js +1 -1
  144. package/dist/web/standalone/.next/server/app/api/visualizer/route_client-reference-manifest.js +1 -1
  145. package/dist/web/standalone/.next/server/app/index.html +1 -1
  146. package/dist/web/standalone/.next/server/app/index.rsc +4 -4
  147. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  148. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +4 -4
  149. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  150. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +3 -3
  151. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  152. package/dist/web/standalone/.next/server/app/page.js +2 -2
  153. package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  154. package/dist/web/standalone/.next/server/app-paths-manifest.json +12 -12
  155. package/dist/web/standalone/.next/server/chunks/63.js +3 -3
  156. package/dist/web/standalone/.next/server/chunks/6897.js +1 -1
  157. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  158. package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
  159. package/dist/web/standalone/.next/server/middleware.js +2 -2
  160. package/dist/web/standalone/.next/server/next-font-manifest.js +1 -1
  161. package/dist/web/standalone/.next/server/next-font-manifest.json +1 -1
  162. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  163. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  164. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  165. package/dist/web/standalone/.next/static/chunks/app/_not-found/{page-2f24283c162b6ab3.js → page-f2a7482d42a5614b.js} +1 -1
  166. package/dist/web/standalone/.next/static/chunks/app/{layout-9ecfd95f343793f0.js → layout-a16c7a7ecdf0c2cf.js} +1 -1
  167. package/dist/web/standalone/.next/static/chunks/app/page-f1e30ab6bb269149.js +1 -0
  168. package/dist/web/standalone/.next/static/chunks/main-app-fdab67f7802d7832.js +1 -0
  169. package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-459824ffb8c323dd.js +1 -0
  170. package/dist/web/standalone/node_modules/node-pty/build/Makefile +2 -2
  171. package/dist/web/standalone/node_modules/node-pty/build/Release/pty.node +0 -0
  172. package/dist/web/standalone/node_modules/node-pty/build/pty.target.mk +14 -14
  173. package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api.target.mk +14 -14
  174. package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api_except.target.mk +14 -14
  175. package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api_maybe.target.mk +14 -14
  176. package/dist/web/standalone/server.js +1 -1
  177. package/package.json +1 -2
  178. package/packages/pi-ai/dist/index.d.ts +1 -0
  179. package/packages/pi-ai/dist/index.d.ts.map +1 -1
  180. package/packages/pi-ai/dist/index.js +1 -0
  181. package/packages/pi-ai/dist/index.js.map +1 -1
  182. package/packages/pi-ai/dist/utils/overflow.d.ts.map +1 -1
  183. package/packages/pi-ai/dist/utils/overflow.js +12 -0
  184. package/packages/pi-ai/dist/utils/overflow.js.map +1 -1
  185. package/packages/pi-ai/dist/utils/tests/overflow.test.d.ts +2 -0
  186. package/packages/pi-ai/dist/utils/tests/overflow.test.d.ts.map +1 -0
  187. package/packages/pi-ai/dist/utils/tests/overflow.test.js +50 -0
  188. package/packages/pi-ai/dist/utils/tests/overflow.test.js.map +1 -0
  189. package/packages/pi-ai/src/index.ts +4 -0
  190. package/packages/pi-ai/src/utils/overflow.ts +14 -1
  191. package/packages/pi-ai/src/utils/tests/overflow.test.ts +58 -0
  192. package/packages/pi-coding-agent/dist/core/auth-storage.js +1 -1
  193. package/packages/pi-coding-agent/dist/core/auth-storage.js.map +1 -1
  194. package/packages/pi-coding-agent/dist/core/auth-storage.test.js +27 -0
  195. package/packages/pi-coding-agent/dist/core/auth-storage.test.js.map +1 -1
  196. package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js +313 -8
  197. package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js.map +1 -1
  198. package/packages/pi-coding-agent/dist/core/compaction/utils.js +5 -5
  199. package/packages/pi-coding-agent/dist/core/compaction/utils.js.map +1 -1
  200. package/packages/pi-coding-agent/dist/core/compaction-utils.test.d.ts +2 -0
  201. package/packages/pi-coding-agent/dist/core/compaction-utils.test.d.ts.map +1 -0
  202. package/packages/pi-coding-agent/dist/core/compaction-utils.test.js +45 -0
  203. package/packages/pi-coding-agent/dist/core/compaction-utils.test.js.map +1 -0
  204. package/packages/pi-coding-agent/dist/core/model-resolver.d.ts.map +1 -1
  205. package/packages/pi-coding-agent/dist/core/model-resolver.js +25 -68
  206. package/packages/pi-coding-agent/dist/core/model-resolver.js.map +1 -1
  207. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.d.ts +12 -2
  208. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  209. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js +61 -28
  210. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js.map +1 -1
  211. package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.d.ts +2 -1
  212. package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.d.ts.map +1 -1
  213. package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.js +9 -3
  214. package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.js.map +1 -1
  215. package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.test.d.ts +2 -0
  216. package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.test.d.ts.map +1 -0
  217. package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.test.js +52 -0
  218. package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.test.js.map +1 -0
  219. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts +1 -0
  220. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  221. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +116 -25
  222. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  223. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.test.d.ts +2 -0
  224. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.test.d.ts.map +1 -0
  225. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.test.js +63 -0
  226. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.test.js.map +1 -0
  227. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  228. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +11 -3
  229. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  230. package/packages/pi-coding-agent/package.json +1 -1
  231. package/packages/pi-coding-agent/src/core/auth-storage.test.ts +38 -0
  232. package/packages/pi-coding-agent/src/core/auth-storage.ts +1 -1
  233. package/packages/pi-coding-agent/src/core/chat-controller-ordering.test.ts +355 -8
  234. package/packages/pi-coding-agent/src/core/compaction/utils.ts +5 -5
  235. package/packages/pi-coding-agent/src/core/compaction-utils.test.ts +50 -0
  236. package/packages/pi-coding-agent/src/core/model-resolver.ts +26 -70
  237. package/packages/pi-coding-agent/src/modes/interactive/components/assistant-message.ts +74 -32
  238. package/packages/pi-coding-agent/src/modes/interactive/components/dynamic-border.test.ts +73 -0
  239. package/packages/pi-coding-agent/src/modes/interactive/components/dynamic-border.ts +9 -3
  240. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.test.ts +71 -0
  241. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +136 -30
  242. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +11 -3
  243. package/packages/pi-tui/dist/__tests__/tui.test.js +60 -1
  244. package/packages/pi-tui/dist/__tests__/tui.test.js.map +1 -1
  245. package/packages/pi-tui/dist/tui.d.ts +8 -0
  246. package/packages/pi-tui/dist/tui.d.ts.map +1 -1
  247. package/packages/pi-tui/dist/tui.js +32 -3
  248. package/packages/pi-tui/dist/tui.js.map +1 -1
  249. package/packages/pi-tui/src/__tests__/tui.test.ts +76 -1
  250. package/packages/pi-tui/src/tui.ts +31 -3
  251. package/pkg/package.json +1 -1
  252. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +107 -5
  253. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +111 -2
  254. package/src/resources/extensions/gsd/auto/phases.ts +22 -9
  255. package/src/resources/extensions/gsd/auto-dispatch.ts +15 -4
  256. package/src/resources/extensions/gsd/auto-model-selection.ts +85 -11
  257. package/src/resources/extensions/gsd/auto-post-unit.ts +47 -1
  258. package/src/resources/extensions/gsd/auto-prompts.ts +9 -3
  259. package/src/resources/extensions/gsd/auto-start.ts +30 -6
  260. package/src/resources/extensions/gsd/auto-timeout-recovery.ts +17 -0
  261. package/src/resources/extensions/gsd/auto-verification.ts +98 -3
  262. package/src/resources/extensions/gsd/auto.ts +36 -14
  263. package/src/resources/extensions/gsd/bootstrap/crash-log.ts +32 -0
  264. package/src/resources/extensions/gsd/bootstrap/register-extension.ts +19 -7
  265. package/src/resources/extensions/gsd/bootstrap/system-context.ts +8 -1
  266. package/src/resources/extensions/gsd/commands-handlers.ts +8 -2
  267. package/src/resources/extensions/gsd/crash-recovery.ts +59 -0
  268. package/src/resources/extensions/gsd/docs/preferences-reference.md +1 -1
  269. package/src/resources/extensions/gsd/gsd-db.ts +52 -2
  270. package/src/resources/extensions/gsd/milestone-actions.ts +19 -1
  271. package/src/resources/extensions/gsd/notification-widget.ts +2 -2
  272. package/src/resources/extensions/gsd/preferences-models.ts +41 -0
  273. package/src/resources/extensions/gsd/preferences-types.ts +12 -0
  274. package/src/resources/extensions/gsd/preferences-validation.ts +23 -0
  275. package/src/resources/extensions/gsd/state.ts +71 -15
  276. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +2 -2
  277. package/src/resources/extensions/gsd/tests/auto-post-unit-step-message.test.ts +53 -0
  278. package/src/resources/extensions/gsd/tests/auto-start-model-capture.test.ts +51 -2
  279. package/src/resources/extensions/gsd/tests/complete-milestone-false-merge.test.ts +142 -0
  280. package/src/resources/extensions/gsd/tests/completed-at-reconcile.test.ts +42 -0
  281. package/src/resources/extensions/gsd/tests/crash-handler-secondary.test.ts +235 -0
  282. package/src/resources/extensions/gsd/tests/derive-state-crossval.test.ts +3 -2
  283. package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +3 -2
  284. package/src/resources/extensions/gsd/tests/derive-state-helpers.test.ts +68 -8
  285. package/src/resources/extensions/gsd/tests/derive-state.test.ts +3 -3
  286. package/src/resources/extensions/gsd/tests/flat-rate-routing-guard.test.ts +137 -1
  287. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +59 -1
  288. package/src/resources/extensions/gsd/tests/integration/state-machine-edge-cases.test.ts +4 -2
  289. package/src/resources/extensions/gsd/tests/model-isolation.test.ts +91 -2
  290. package/src/resources/extensions/gsd/tests/park-milestone.test.ts +64 -0
  291. package/src/resources/extensions/gsd/tests/preferences.test.ts +47 -0
  292. package/src/resources/extensions/gsd/tests/state-machine-full-walkthrough.test.ts +5 -7
  293. package/src/resources/extensions/gsd/tests/subagent-model-dispatch.test.ts +267 -0
  294. package/src/resources/extensions/gsd/tests/token-profile.test.ts +1 -1
  295. package/src/resources/extensions/gsd/tests/validate-milestone-stuck-guard.test.ts +179 -0
  296. package/dist/web/standalone/.next/static/chunks/app/page-7115e62689b5fd84.js +0 -1
  297. package/dist/web/standalone/.next/static/chunks/main-app-d3d4c336195465f9.js +0 -1
  298. package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-ab5a8926e07ec673.js +0 -1
  299. /package/dist/web/standalone/.next/static/{KSZ2dcC3p4z6lOmUpPpzr → RXD20AQgB9BHSQJ07MDdd}/_buildManifest.js +0 -0
  300. /package/dist/web/standalone/.next/static/{KSZ2dcC3p4z6lOmUpPpzr → RXD20AQgB9BHSQJ07MDdd}/_ssgManifest.js +0 -0
@@ -38,7 +38,7 @@ import { existsSync, mkdirSync, readdirSync, rmSync, statSync, unlinkSync, } fro
38
38
  import { join } from "node:path";
39
39
  import { sep as pathSep } from "node:path";
40
40
  import { resolveProjectRootDbPath } from "./bootstrap/dynamic-tools.js";
41
- import { resolveDefaultSessionModel, resolveDynamicRoutingConfig } from "./preferences-models.js";
41
+ import { isCustomProvider, resolveDefaultSessionModel, resolveDynamicRoutingConfig, } from "./preferences-models.js";
42
42
  import { getSessionModelOverride } from "./session-model-override.js";
43
43
  /**
44
44
  * Bootstrap a fresh auto-mode session. Handles everything from git init
@@ -195,8 +195,18 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
195
195
  //
196
196
  // This preserves #3517 defaults while honoring explicit runtime model
197
197
  // selection for subsequent /gsd runs in the same session.
198
+ //
199
+ // Exception (#4122): when the session provider is a custom provider declared
200
+ // in ~/.gsd/agent/models.json (Ollama, vLLM, OpenAI-compatible proxy, etc.),
201
+ // PREFERENCES.md is skipped entirely. PREFERENCES.md cannot reference custom
202
+ // providers, so honoring it would silently reroute auto-mode to a built-in
203
+ // provider the user is not logged into and surface as "Not logged in · Please
204
+ // run /login" before pausing and resetting to claude-code/claude-sonnet-4-6.
198
205
  const manualSessionOverride = getSessionModelOverride(ctx.sessionManager.getSessionId());
199
- const preferredModel = resolveDefaultSessionModel(ctx.model?.provider);
206
+ const sessionProviderIsCustom = isCustomProvider(ctx.model?.provider);
207
+ const preferredModel = sessionProviderIsCustom
208
+ ? null
209
+ : resolveDefaultSessionModel(ctx.model?.provider);
200
210
  // Validate the preferred model against the live registry + provider auth so
201
211
  // an unconfigured PREFERENCES.md entry (no API key / OAuth) can't become the
202
212
  // start-model snapshot. Without this, every subsequent unit would try to
@@ -622,6 +632,9 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
622
632
  }
623
633
  ctx.ui.setStatus("gsd-auto", s.stepMode ? "next" : "auto");
624
634
  ctx.ui.setFooter(hideFooter);
635
+ // Hide gsd-health during AUTO — gsd-progress is the single source of truth
636
+ // for last-commit / cost / health signal while auto is running.
637
+ ctx.ui.setWidget("gsd-health", undefined);
625
638
  const modeLabel = s.stepMode ? "Step-mode" : "Auto-mode";
626
639
  const pendingCount = (state.registry ?? []).filter((m) => m.status !== "complete" && m.status !== "parked").length;
627
640
  const scopeMsg = pendingCount > 1
@@ -636,12 +649,16 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
636
649
  const startModelLabel = s.autoModeStartModel
637
650
  ? `${s.autoModeStartModel.provider}/${s.autoModeStartModel.id}`
638
651
  : ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "default";
639
- // Flat-rate providers (e.g. GitHub Copilot, claude-code) suppress routing
640
- // at dispatch time (#3453) reflect that in the banner.
641
- const { isFlatRateProvider } = await import("./auto-model-selection.js");
652
+ // Flat-rate providers (e.g. GitHub Copilot, claude-code, user-declared
653
+ // subscription proxies, externalCli CLIs) suppress routing at dispatch
654
+ // time (#3453) reflect that in the banner. Thread the same
655
+ // FlatRateContext used by selectAndApplyModel so user-declared
656
+ // flat-rate providers and externalCli auto-detection are respected.
657
+ const { isFlatRateProvider, buildFlatRateContext } = await import("./auto-model-selection.js");
658
+ const bannerPrefs = loadEffectiveGSDPreferences()?.preferences;
642
659
  const effectiveProvider = s.autoModeStartModel?.provider ?? ctx.model?.provider;
643
660
  const effectivelyEnabled = routingConfig.enabled
644
- && !(effectiveProvider && isFlatRateProvider(effectiveProvider));
661
+ && !(effectiveProvider && isFlatRateProvider(effectiveProvider, buildFlatRateContext(effectiveProvider, ctx, bannerPrefs)));
645
662
  // The actual ceiling may come from tier_models.heavy, not the start model.
646
663
  const effectiveCeiling = (routingConfig.enabled && routingConfig.tier_models?.heavy)
647
664
  ? routingConfig.tier_models.heavy
@@ -156,6 +156,19 @@ export async function recoverTimedOutUnit(ctx, pi, unitType, unitId, reason, rct
156
156
  ctx.ui.notify(`${reason === "idle" ? "Idle" : "Timeout"} recovery: steering ${unitType} ${unitId} to produce ${expected} (attempt ${attemptNumber}, session ${recoveryAttempts + 1}/${maxRecoveryAttempts}).`, "warning");
157
157
  return "recovered";
158
158
  }
159
+ // #4175: For complete-milestone, never write a blocker placeholder — a stub
160
+ // SUMMARY has no recovery value (milestone is terminal), it does not update
161
+ // DB status, and downstream merge paths can treat the stub as a legitimate
162
+ // completion signal. Pause instead so the worktree branch is preserved.
163
+ if (unitType === "complete-milestone") {
164
+ writeUnitRuntimeRecord(basePath, unitType, unitId, currentUnitStartedAt, {
165
+ phase: "paused",
166
+ recoveryAttempts: recoveryAttempts + 1,
167
+ lastRecoveryReason: reason,
168
+ });
169
+ ctx.ui.notify(`Milestone ${unitId} ${reason}-recovery exhausted ${maxRecoveryAttempts} attempt(s) — worktree branch preserved. Re-run /gsd auto once blockers are resolved.`, "error");
170
+ return "paused";
171
+ }
159
172
  // Retries exhausted — write a blocker placeholder and advance the pipeline
160
173
  // instead of silently stalling.
161
174
  const placeholder = writeBlockerPlaceholder(unitType, unitId, basePath, `${reason} recovery exhausted ${maxRecoveryAttempts} attempts without producing the artifact.`);
@@ -10,10 +10,15 @@
10
10
  * checks the result and handles control flow.
11
11
  */
12
12
  import { mkdirSync, writeFileSync } from "node:fs";
13
- import { resolveSlicePath } from "./paths.js";
13
+ import { resolveSlicePath, resolveMilestoneFile } from "./paths.js";
14
14
  import { parseUnitId } from "./unit-id.js";
15
- import { isDbAvailable, getTask, getSliceTasks } from "./gsd-db.js";
15
+ import { isDbAvailable, getTask, getSliceTasks, getMilestoneSlices } from "./gsd-db.js";
16
16
  import { loadEffectiveGSDPreferences } from "./preferences.js";
17
+ import { extractVerdict } from "./verdict-parser.js";
18
+ import { isClosedStatus } from "./status-guards.js";
19
+ import { loadFile } from "./files.js";
20
+ import { parseRoadmap } from "./parsers-legacy.js";
21
+ import { isMilestoneComplete } from "./state.js";
17
22
  import { runVerificationGate, formatFailureContext, captureRuntimeErrors, runDependencyAudit, } from "./verification-gate.js";
18
23
  import { writeVerificationJSON } from "./verification-evidence.js";
19
24
  import { logWarning } from "./workflow-logger.js";
@@ -22,6 +27,80 @@ import { join } from "node:path";
22
27
  function isInfraVerificationFailure(stderr) {
23
28
  return /\b(ENOENT|ENOTFOUND|ETIMEDOUT|ECONNRESET|EAI_AGAIN|spawn\s+\S+\s+ENOENT|command not found)\b/i.test(stderr);
24
29
  }
30
+ /**
31
+ * Post-unit guard for `validate-milestone` units (#4094).
32
+ *
33
+ * When validate-milestone writes verdict=needs-remediation, the agent is
34
+ * expected to also call gsd_reassess_roadmap in the same turn to add
35
+ * remediation slices. If they don't, the state machine re-derives
36
+ * `phase: validating-milestone` indefinitely (all slices still complete +
37
+ * verdict still needs-remediation), wasting ~3 dispatches before the stuck
38
+ * detector fires.
39
+ *
40
+ * This guard fires immediately on the first occurrence: if VALIDATION.md
41
+ * verdict is needs-remediation and no incomplete slices exist for the
42
+ * milestone, pause the auto-loop with a clear blocker.
43
+ */
44
+ async function runValidateMilestonePostCheck(vctx, pauseAuto) {
45
+ const { s, ctx, pi } = vctx;
46
+ if (!s.currentUnit)
47
+ return "continue";
48
+ const { milestone: mid } = parseUnitId(s.currentUnit.id);
49
+ if (!mid)
50
+ return "continue";
51
+ const validationFile = resolveMilestoneFile(s.basePath, mid, "VALIDATION");
52
+ if (!validationFile)
53
+ return "continue";
54
+ const validationContent = await loadFile(validationFile);
55
+ if (!validationContent)
56
+ return "continue";
57
+ const verdict = extractVerdict(validationContent);
58
+ if (verdict !== "needs-remediation")
59
+ return "continue";
60
+ const incompleteSliceCount = await countIncompleteSlices(s.basePath, mid);
61
+ // If any non-closed slices exist, the agent successfully queued remediation
62
+ // work — proceed normally. The state machine will execute those slices and
63
+ // re-validate per the #3596/#3670 fix.
64
+ if (incompleteSliceCount > 0)
65
+ return "continue";
66
+ ctx.ui.notify(`Milestone ${mid} validation returned verdict=needs-remediation but no remediation slices were added. Pausing for human review.`, "error");
67
+ process.stderr.write(`validate-milestone: pausing — verdict=needs-remediation with no incomplete slices for ${mid}. ` +
68
+ `The agent must call gsd_reassess_roadmap to add remediation slices before re-validation.\n`);
69
+ await pauseAuto(ctx, pi);
70
+ return "pause";
71
+ }
72
+ /**
73
+ * Count slices for a milestone that are not in a closed status.
74
+ * DB-backed projects are authoritative (#4094 peer review); falls back to
75
+ * roadmap parsing only when the DB is unavailable.
76
+ */
77
+ async function countIncompleteSlices(basePath, milestoneId) {
78
+ if (isDbAvailable()) {
79
+ const slices = getMilestoneSlices(milestoneId);
80
+ if (slices.length === 0) {
81
+ // No DB rows — treat as "unknown", do not pause.
82
+ return 1;
83
+ }
84
+ return slices.filter((slice) => !isClosedStatus(slice.status)).length;
85
+ }
86
+ // Filesystem fallback: parse the roadmap markdown.
87
+ try {
88
+ const roadmapFile = resolveMilestoneFile(basePath, milestoneId, "ROADMAP");
89
+ if (!roadmapFile)
90
+ return 1;
91
+ const roadmapContent = await loadFile(roadmapFile);
92
+ if (!roadmapContent)
93
+ return 1;
94
+ const roadmap = parseRoadmap(roadmapContent);
95
+ if (roadmap.slices.length === 0)
96
+ return 1;
97
+ return isMilestoneComplete(roadmap) ? 0 : 1;
98
+ }
99
+ catch {
100
+ // Parsing failures should not cause false-positive pauses.
101
+ return 1;
102
+ }
103
+ }
25
104
  /**
26
105
  * Run the verification gate for the current execute-task unit.
27
106
  * Returns:
@@ -31,7 +110,13 @@ function isInfraVerificationFailure(stderr) {
31
110
  */
32
111
  export async function runPostUnitVerification(vctx, pauseAuto) {
33
112
  const { s, ctx, pi } = vctx;
34
- if (!s.currentUnit || s.currentUnit.type !== "execute-task") {
113
+ if (!s.currentUnit) {
114
+ return "continue";
115
+ }
116
+ if (s.currentUnit.type === "validate-milestone") {
117
+ return await runValidateMilestonePostCheck(vctx, pauseAuto);
118
+ }
119
+ if (s.currentUnit.type !== "execute-task") {
35
120
  return "continue";
36
121
  }
37
122
  try {
@@ -19,7 +19,7 @@ import { gsdRoot, resolveMilestoneFile, resolveMilestonePath, resolveDir, milest
19
19
  import { invalidateAllCaches } from "./cache.js";
20
20
  import { clearActivityLogState } from "./activity-log.js";
21
21
  import { synthesizeCrashRecovery, getDeepDiagnostic, readActiveMilestoneId, } from "./session-forensics.js";
22
- import { writeLock, clearLock, readCrashLock, isLockProcessAlive, formatCrashInfo, } from "./crash-recovery.js";
22
+ import { writeLock, clearLock, readCrashLock, isLockProcessAlive, formatCrashInfo, emitCrashRecoveredUnitEnd, } from "./crash-recovery.js";
23
23
  import { acquireSessionLock, getSessionLockStatus, releaseSessionLock, updateSessionLock, } from "./session-lock.js";
24
24
  import { resolveAutoSupervisorConfig, loadEffectiveGSDPreferences, getIsolationMode, } from "./preferences.js";
25
25
  import { sendDesktopNotification } from "./notifications.js";
@@ -55,7 +55,7 @@ import { initRegistry, convertDispatchRules } from "./rule-registry.js";
55
55
  import { emitJournalEvent as _emitJournalEvent } from "./journal.js";
56
56
  import { updateProgressWidget as _updateProgressWidget, updateSliceProgressCache, clearSliceProgressCache, hideFooter, } from "./auto-dashboard.js";
57
57
  import { registerSigtermHandler as _registerSigtermHandler, deregisterSigtermHandler as _deregisterSigtermHandler, } from "./auto-supervisor.js";
58
- import { isDbAvailable } from "./gsd-db.js";
58
+ import { isDbAvailable, getMilestone } from "./gsd-db.js";
59
59
  import { countPendingCaptures } from "./captures.js";
60
60
  import { clearCmuxSidebar, logCmuxEvent, syncCmuxSidebar } from "../cmux/index.js";
61
61
  // ── Extracted modules ──────────────────────────────────────────────────────
@@ -63,6 +63,7 @@ import { startUnitSupervision } from "./auto-timers.js";
63
63
  import { runPostUnitVerification } from "./auto-verification.js";
64
64
  import { postUnitPreVerification, postUnitPostVerification, } from "./auto-post-unit.js";
65
65
  import { bootstrapAutoSession, openProjectDbIfPresent } from "./auto-start.js";
66
+ import { initHealthWidget } from "./health-widget.js";
66
67
  import { autoLoop, resolveAgentEnd, resolveAgentEndCancelled, _resetPendingResolve, isSessionSwitchInFlight } from "./auto-loop.js";
67
68
  import { WorktreeResolver, } from "./worktree-resolver.js";
68
69
  import { reorderForCaching } from "./prompt-ordering.js";
@@ -397,6 +398,8 @@ function handleLostSessionLock(ctx, lockStatus) {
397
398
  ctx?.ui.setStatus("gsd-auto", undefined);
398
399
  ctx?.ui.setWidget("gsd-progress", undefined);
399
400
  ctx?.ui.setFooter(undefined);
401
+ if (ctx)
402
+ initHealthWidget(ctx);
400
403
  }
401
404
  /**
402
405
  * Lightweight cleanup after autoLoop exits via step-wizard break.
@@ -431,6 +434,7 @@ function cleanupAfterLoopExit(ctx) {
431
434
  ctx.ui.setStatus("gsd-auto", undefined);
432
435
  ctx.ui.setWidget("gsd-progress", undefined);
433
436
  ctx.ui.setFooter(undefined);
437
+ initHealthWidget(ctx);
434
438
  }
435
439
  // Restore CWD out of worktree back to original project root
436
440
  if (s.originalBasePath) {
@@ -501,17 +505,30 @@ export async function stopAuto(ctx, pi, reason) {
501
505
  ? { notify: ctx.ui.notify.bind(ctx.ui) }
502
506
  : { notify: () => { } };
503
507
  const resolver = buildResolver();
504
- // Check if the milestone is complete SUMMARY file is the authoritative signal.
508
+ // Check if the milestone is complete. DB status is the authoritative
509
+ // signal — only a successful gsd_complete_milestone call flips it to
510
+ // "complete" (tools/complete-milestone.ts). SUMMARY file presence is
511
+ // NOT sufficient: a blocker placeholder stub or a partial write can
512
+ // leave a file behind without the milestone actually being done,
513
+ // which previously caused stopAuto to merge a failed milestone and
514
+ // emit a misleading metadata-only merge warning (#4175).
515
+ // DB-unavailable projects fall back to SUMMARY-file presence.
505
516
  let milestoneComplete = false;
506
517
  try {
507
- const summaryPath = resolveMilestoneFile(s.originalBasePath || s.basePath, s.currentMilestoneId, "SUMMARY");
508
- if (!summaryPath) {
509
- // Also check in the worktree path (SUMMARY may not be synced yet)
510
- const wtSummaryPath = resolveMilestoneFile(s.basePath, s.currentMilestoneId, "SUMMARY");
511
- milestoneComplete = wtSummaryPath !== null;
518
+ if (isDbAvailable()) {
519
+ const dbRow = getMilestone(s.currentMilestoneId);
520
+ milestoneComplete = dbRow?.status === "complete";
512
521
  }
513
522
  else {
514
- milestoneComplete = true;
523
+ const summaryPath = resolveMilestoneFile(s.originalBasePath || s.basePath, s.currentMilestoneId, "SUMMARY");
524
+ if (!summaryPath) {
525
+ // Also check in the worktree path (SUMMARY may not be synced yet)
526
+ const wtSummaryPath = resolveMilestoneFile(s.basePath, s.currentMilestoneId, "SUMMARY");
527
+ milestoneComplete = wtSummaryPath !== null;
528
+ }
529
+ else {
530
+ milestoneComplete = true;
531
+ }
515
532
  }
516
533
  }
517
534
  catch (err) {
@@ -676,6 +693,8 @@ export async function stopAuto(ctx, pi, reason) {
676
693
  ctx?.ui.setStatus("gsd-auto", undefined);
677
694
  ctx?.ui.setWidget("gsd-progress", undefined);
678
695
  ctx?.ui.setFooter(undefined);
696
+ if (ctx)
697
+ initHealthWidget(ctx);
679
698
  restoreProjectRootEnv();
680
699
  restoreMilestoneLockEnv();
681
700
  // Reset all session state in one call
@@ -762,6 +781,8 @@ export async function pauseAuto(ctx, _pi, _errorContext) {
762
781
  ctx?.ui.setStatus("gsd-auto", "paused");
763
782
  ctx?.ui.setWidget("gsd-progress", undefined);
764
783
  ctx?.ui.setFooter(undefined);
784
+ if (ctx)
785
+ initHealthWidget(ctx);
765
786
  const resumeCmd = s.stepMode ? "/gsd next" : "/gsd auto";
766
787
  ctx?.ui.notify(`${s.stepMode ? "Step" : "Auto"}-mode paused (Escape). Type to interact, or ${resumeCmd} to resume.`, "info");
767
788
  }
@@ -1014,6 +1035,10 @@ export async function startAuto(ctx, pi, base, verboseMode, options) {
1014
1035
  s.stepMode = requestedStepMode;
1015
1036
  }
1016
1037
  if (freshStartAssessment.lock) {
1038
+ // Emit a synthetic unit-end for any unit-start that has no closing event.
1039
+ // This closes the journal gap reported in #3348 where the worker wrote side
1040
+ // effects (SUMMARY.md, DB updates) but died before emitting unit-end.
1041
+ emitCrashRecoveredUnitEnd(base, freshStartAssessment.lock);
1017
1042
  clearLock(base);
1018
1043
  }
1019
1044
  if (!s.paused) {
@@ -0,0 +1,31 @@
1
+ /**
2
+ * crash-log.ts — Write crash diagnostics to ~/.gsd/crash/<timestamp>.log
3
+ *
4
+ * Zero cross-dependencies: only uses Node.js built-ins so it can be imported
5
+ * safely from uncaughtException / unhandledRejection handlers and from tests
6
+ * without pulling in the full extension dependency tree.
7
+ */
8
+ import { appendFileSync, mkdirSync } from "node:fs";
9
+ import { homedir } from "node:os";
10
+ import { join } from "node:path";
11
+ /**
12
+ * Write a crash log to ~/.gsd/crash/<timestamp>.log (or $GSD_HOME/crash/).
13
+ * Never throws — must be safe to call from any error handler.
14
+ */
15
+ export function writeCrashLog(err, source) {
16
+ try {
17
+ const crashDir = join(process.env.GSD_HOME ?? join(homedir(), ".gsd"), "crash");
18
+ mkdirSync(crashDir, { recursive: true });
19
+ const ts = new Date().toISOString().replace(/[:.]/g, "-");
20
+ const logPath = join(crashDir, `${ts}.log`);
21
+ const lines = [
22
+ `[gsd] ${source}: ${err.message}`,
23
+ `timestamp: ${new Date().toISOString()}`,
24
+ `pid: ${process.pid}`,
25
+ err.stack ?? "(no stack trace available)",
26
+ "",
27
+ ];
28
+ appendFileSync(logPath, lines.join("\n"));
29
+ }
30
+ catch { /* never throw from crash handler */ }
31
+ }
@@ -8,6 +8,8 @@ import { registerJournalTools } from "./journal-tools.js";
8
8
  import { registerQueryTools } from "./query-tools.js";
9
9
  import { registerHooks } from "./register-hooks.js";
10
10
  import { registerShortcuts } from "./register-shortcuts.js";
11
+ import { writeCrashLog } from "./crash-log.js";
12
+ export { writeCrashLog } from "./crash-log.js";
11
13
  export function handleRecoverableExtensionProcessError(err) {
12
14
  if (err.code === "EPIPE") {
13
15
  process.exit(0);
@@ -28,17 +30,26 @@ export function handleRecoverableExtensionProcessError(err) {
28
30
  function installEpipeGuard() {
29
31
  if (!process.listeners("uncaughtException").some((listener) => listener.name === "_gsdEpipeGuard")) {
30
32
  const _gsdEpipeGuard = (err) => {
31
- if (handleRecoverableExtensionProcessError(err)) {
33
+ if (handleRecoverableExtensionProcessError(err))
32
34
  return;
33
- }
34
- // Log unhandled errors instead of re-throwing throwing inside an
35
- // uncaughtException handler is a fatal double-fault in Node.js (#3163).
36
- process.stderr.write(`[gsd] uncaught extension error (non-fatal): ${err.message}\n`);
37
- if (err.stack)
38
- process.stderr.write(`${err.stack}\n`);
35
+ // Write crash log and exit cleanly for unrecoverable errors.
36
+ // Logging and continuing was the original double-fault fix (#3163), but
37
+ // continuing in an indeterminate state is worse than a clean exit (#3348).
38
+ writeCrashLog(err, "uncaughtException");
39
+ process.exit(1);
39
40
  };
40
41
  process.on("uncaughtException", _gsdEpipeGuard);
41
42
  }
43
+ if (!process.listeners("unhandledRejection").some((listener) => listener.name === "_gsdRejectionGuard")) {
44
+ const _gsdRejectionGuard = (reason, _promise) => {
45
+ const err = reason instanceof Error ? reason : new Error(String(reason));
46
+ if (handleRecoverableExtensionProcessError(err))
47
+ return;
48
+ writeCrashLog(err, "unhandledRejection");
49
+ process.exit(1);
50
+ };
51
+ process.on("unhandledRejection", _gsdRejectionGuard);
52
+ }
42
53
  }
43
54
  export function registerGsdExtension(pi) {
44
55
  registerGSDCommand(pi);
@@ -6,6 +6,7 @@ import { debugTime } from "../debug-logger.js";
6
6
  import { loadPrompt, getTemplatesDir } from "../prompt-loader.js";
7
7
  import { readForensicsMarker } from "../forensics.js";
8
8
  import { resolveAllSkillReferences, renderPreferencesForSystemPrompt, loadEffectiveGSDPreferences } from "../preferences.js";
9
+ import { resolveModelWithFallbacksForUnit } from "../preferences-models.js";
9
10
  import { resolveSkillReference } from "../preferences-skills.js";
10
11
  import { resolveGsdRootFile, resolveSliceFile, resolveSlicePath, resolveTaskFile, resolveTaskFiles, resolveTasksDir, relSliceFile, relSlicePath, relTaskFile } from "../paths.js";
11
12
  import { ensureCodebaseMapFresh, readCodebaseMap } from "../codebase-generator.js";
@@ -147,7 +148,11 @@ export async function buildBeforeAgentStartResult(event, ctx) {
147
148
  // Re-inject forensics context on follow-up turns (#2941)
148
149
  const forensicsInjection = !injection ? buildForensicsContextInjection(process.cwd(), event.prompt) : null;
149
150
  const worktreeBlock = buildWorktreeContextBlock();
150
- const fullSystem = `${event.systemPrompt}\n\n[SYSTEM CONTEXT — GSD]\n\n${systemContent}${preferenceBlock}${knowledgeBlock}${codebaseBlock}${memoryBlock}${newSkillsBlock}${worktreeBlock}`;
151
+ const subagentModelConfig = resolveModelWithFallbacksForUnit("subagent");
152
+ const subagentModelBlock = subagentModelConfig
153
+ ? `\n\n## Subagent Model\n\nWhen spawning subagents via the \`subagent\` tool, always pass \`model: "${subagentModelConfig.primary}"\` in the tool call parameters. Never omit this — always specify it explicitly.`
154
+ : "";
155
+ const fullSystem = `${event.systemPrompt}\n\n[SYSTEM CONTEXT — GSD]\n\n${systemContent}${preferenceBlock}${knowledgeBlock}${codebaseBlock}${memoryBlock}${newSkillsBlock}${worktreeBlock}${subagentModelBlock}`;
151
156
  stopContextTimer({
152
157
  systemPromptSize: fullSystem.length,
153
158
  injectionSize: injection?.length ?? forensicsInjection?.length ?? 0,
@@ -17,6 +17,11 @@ import { projectRoot } from "./commands/context.js";
17
17
  import { loadPrompt } from "./prompt-loader.js";
18
18
  const UPDATE_REGISTRY_URL = "https://registry.npmjs.org/gsd-pi/latest";
19
19
  const UPDATE_FETCH_TIMEOUT_MS = 5000;
20
+ function resolveInstallCommand(pkg) {
21
+ if ('bun' in process.versions)
22
+ return `bun add -g ${pkg}`;
23
+ return `npm install -g ${pkg}`;
24
+ }
20
25
  async function fetchLatestVersionForCommand() {
21
26
  const controller = new AbortController();
22
27
  const timeout = setTimeout(() => controller.abort(), UPDATE_FETCH_TIMEOUT_MS);
@@ -344,13 +349,14 @@ export async function handleUpdate(ctx) {
344
349
  return;
345
350
  }
346
351
  ctx.ui.notify(`Updating: v${current} → v${latest}...`, "info");
352
+ const installCmd = resolveInstallCommand(`${NPM_PACKAGE}@latest`);
347
353
  try {
348
- execSync(`npm install -g ${NPM_PACKAGE}@latest`, {
354
+ execSync(installCmd, {
349
355
  stdio: ["ignore", "pipe", "ignore"],
350
356
  });
351
357
  ctx.ui.notify(`Updated to v${latest}. Restart your GSD session to use the new version.`, "info");
352
358
  }
353
359
  catch {
354
- ctx.ui.notify(`Update failed. Try manually: npm install -g ${NPM_PACKAGE}@latest`, "error");
360
+ ctx.ui.notify(`Update failed. Try manually: ${installCmd}`, "error");
355
361
  }
356
362
  }
@@ -14,6 +14,7 @@ import { join } from "node:path";
14
14
  import { gsdRoot } from "./paths.js";
15
15
  import { atomicWriteSync } from "./atomic-write.js";
16
16
  import { effectiveLockFile } from "./session-lock.js";
17
+ import { emitJournalEvent, queryJournal } from "./journal.js";
17
18
  function lockPath(basePath) {
18
19
  return join(gsdRoot(basePath), effectiveLockFile());
19
20
  }
@@ -110,3 +111,53 @@ export function formatCrashInfo(lock) {
110
111
  }
111
112
  return lines.join("\n");
112
113
  }
114
+ /**
115
+ * Emit a synthetic unit-end event for a unit that crashed without emitting its own.
116
+ *
117
+ * Queries the journal to find the most recent unit-start for the crashed unit.
118
+ * If a matching unit-end already exists (e.g. the hard timeout fired), this is a
119
+ * no-op. Called during crash recovery, before clearing the stale lock.
120
+ *
121
+ * Addresses the gap reported in #3348 where `unit-start` was emitted but no
122
+ * `unit-end` followed — side effects landed but the worker died before closeout.
123
+ */
124
+ export function emitCrashRecoveredUnitEnd(basePath, lock) {
125
+ // Skip bootstrap / starting pseudo-units — they have no meaningful unit-start event.
126
+ if (!lock.unitType || !lock.unitId || lock.unitType === "starting")
127
+ return;
128
+ try {
129
+ const all = queryJournal(basePath);
130
+ // Find the most recent unit-start for this unitId
131
+ const starts = all.filter((e) => e.eventType === "unit-start" && e.data?.unitId === lock.unitId);
132
+ if (starts.length === 0)
133
+ return;
134
+ const lastStart = starts[starts.length - 1];
135
+ // Check if a unit-end was already emitted (e.g. hard timeout fired after the crash)
136
+ const alreadyClosed = all.some((e) => e.eventType === "unit-end" &&
137
+ e.data?.unitId === lock.unitId &&
138
+ e.causedBy?.flowId === lastStart.flowId &&
139
+ e.causedBy?.seq === lastStart.seq);
140
+ if (alreadyClosed)
141
+ return;
142
+ // Find the highest seq in this flow for monotonic ordering
143
+ const maxSeq = all
144
+ .filter((e) => e.flowId === lastStart.flowId)
145
+ .reduce((max, e) => Math.max(max, e.seq), lastStart.seq);
146
+ emitJournalEvent(basePath, {
147
+ ts: new Date().toISOString(),
148
+ flowId: lastStart.flowId,
149
+ seq: maxSeq + 1,
150
+ eventType: "unit-end",
151
+ data: {
152
+ unitType: lock.unitType,
153
+ unitId: lock.unitId,
154
+ status: "crash-recovered",
155
+ artifactVerified: false,
156
+ },
157
+ causedBy: { flowId: lastStart.flowId, seq: lastStart.seq },
158
+ });
159
+ }
160
+ catch {
161
+ // Never throw from crash recovery path — journal failure must not block recovery
162
+ }
163
+ }
@@ -157,7 +157,7 @@ Setting `prefer_skills: []` does **not** disable skill discovery — it just mea
157
157
 
158
158
  - `phases`: fine-grained control over which phases run. Usually set by `token_profile`, but can be overridden. Keys:
159
159
  - `skip_research`: boolean — skip milestone-level research. Default: `false`.
160
- - `reassess_after_slice`: boolean — run roadmap reassessment after each completed slice. Default: `false`.
160
+ - `reassess_after_slice`: boolean — run roadmap reassessment after each completed slice. Default: `true`.
161
161
  - `skip_reassess`: boolean — force-disable roadmap reassessment even if `reassess_after_slice` is enabled. Default: `false`.
162
162
  - `skip_slice_research`: boolean — skip per-slice research. Default: `false`.
163
163
 
@@ -1352,6 +1352,25 @@ export function setSliceSummaryMd(milestoneId, sliceId, summaryMd, uatMd) {
1352
1352
  throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
1353
1353
  currentDb.prepare(`UPDATE slices SET full_summary_md = :summary_md, full_uat_md = :uat_md WHERE milestone_id = :mid AND id = :sid`).run({ ":mid": milestoneId, ":sid": sliceId, ":summary_md": summaryMd, ":uat_md": uatMd });
1354
1354
  }
1355
+ function parseTaskArrayColumn(raw) {
1356
+ if (typeof raw !== "string" || raw.trim() === "")
1357
+ return [];
1358
+ try {
1359
+ const parsed = JSON.parse(raw);
1360
+ if (Array.isArray(parsed))
1361
+ return parsed.map((value) => String(value));
1362
+ if (parsed === null || parsed === undefined || parsed === "")
1363
+ return [];
1364
+ return [String(parsed)];
1365
+ }
1366
+ catch {
1367
+ // Older/corrupt rows may contain comma-separated strings instead of JSON.
1368
+ return raw
1369
+ .split(",")
1370
+ .map((value) => value.trim())
1371
+ .filter(Boolean);
1372
+ }
1373
+ }
1355
1374
  function rowToTask(row) {
1356
1375
  const parseTaskArray = (value) => {
1357
1376
  if (Array.isArray(value)) {
@@ -1390,8 +1409,8 @@ function rowToTask(row) {
1390
1409
  blocker_discovered: row["blocker_discovered"] === 1,
1391
1410
  deviations: row["deviations"],
1392
1411
  known_issues: row["known_issues"],
1393
- key_files: JSON.parse(row["key_files"] || "[]"),
1394
- key_decisions: JSON.parse(row["key_decisions"] || "[]"),
1412
+ key_files: parseTaskArrayColumn(row["key_files"]),
1413
+ key_decisions: parseTaskArrayColumn(row["key_decisions"]),
1395
1414
  full_summary_md: row["full_summary_md"],
1396
1415
  description: row["description"] ?? "",
1397
1416
  estimate: row["estimate"] ?? "",
@@ -1855,6 +1874,21 @@ export function deleteSlice(milestoneId, sliceId) {
1855
1874
  currentDb.prepare(`DELETE FROM slices WHERE milestone_id = :mid AND id = :sid`).run({ ":mid": milestoneId, ":sid": sliceId });
1856
1875
  });
1857
1876
  }
1877
+ export function deleteMilestone(milestoneId) {
1878
+ if (!currentDb)
1879
+ throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
1880
+ transaction(() => {
1881
+ currentDb.prepare(`DELETE FROM verification_evidence WHERE milestone_id = :mid`).run({ ":mid": milestoneId });
1882
+ currentDb.prepare(`DELETE FROM quality_gates WHERE milestone_id = :mid`).run({ ":mid": milestoneId });
1883
+ currentDb.prepare(`DELETE FROM tasks WHERE milestone_id = :mid`).run({ ":mid": milestoneId });
1884
+ currentDb.prepare(`DELETE FROM slice_dependencies WHERE milestone_id = :mid`).run({ ":mid": milestoneId });
1885
+ currentDb.prepare(`DELETE FROM slices WHERE milestone_id = :mid`).run({ ":mid": milestoneId });
1886
+ currentDb.prepare(`DELETE FROM replan_history WHERE milestone_id = :mid`).run({ ":mid": milestoneId });
1887
+ currentDb.prepare(`DELETE FROM assessments WHERE milestone_id = :mid`).run({ ":mid": milestoneId });
1888
+ currentDb.prepare(`DELETE FROM artifacts WHERE milestone_id = :mid`).run({ ":mid": milestoneId });
1889
+ currentDb.prepare(`DELETE FROM milestones WHERE id = :mid`).run({ ":mid": milestoneId });
1890
+ });
1891
+ }
1858
1892
  export function updateSliceFields(milestoneId, sliceId, fields) {
1859
1893
  if (!currentDb)
1860
1894
  throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
@@ -15,7 +15,8 @@ import { join } from "node:path";
15
15
  import { resolveMilestonePath, resolveMilestoneFile, buildMilestoneFileName, } from "./paths.js";
16
16
  import { invalidateAllCaches } from "./cache.js";
17
17
  import { loadQueueOrder, saveQueueOrder } from "./queue-order.js";
18
- import { getMilestone, isDbAvailable, updateMilestoneStatus } from "./gsd-db.js";
18
+ import { deleteMilestone, getMilestone, isDbAvailable, updateMilestoneStatus } from "./gsd-db.js";
19
+ import { removeWorktree } from "./worktree-manager.js";
19
20
  import { logWarning } from "./workflow-logger.js";
20
21
  // ─── Park ──────────────────────────────────────────────────────────────────
21
22
  /**
@@ -99,12 +100,29 @@ export function discardMilestone(basePath, milestoneId) {
99
100
  const mDir = resolveMilestonePath(basePath, milestoneId);
100
101
  if (!mDir || !existsSync(mDir))
101
102
  return false;
103
+ try {
104
+ removeWorktree(basePath, milestoneId, {
105
+ branch: `milestone/${milestoneId}`,
106
+ deleteBranch: true,
107
+ });
108
+ }
109
+ catch (err) {
110
+ logWarning("engine", `discardMilestone worktree cleanup failed for ${milestoneId}: ${err.message}`);
111
+ }
102
112
  rmSync(mDir, { recursive: true, force: true });
103
113
  // Prune from queue order if present
104
114
  const order = loadQueueOrder(basePath);
105
115
  if (order && order.includes(milestoneId)) {
106
116
  saveQueueOrder(basePath, order.filter(id => id !== milestoneId));
107
117
  }
118
+ if (isDbAvailable()) {
119
+ try {
120
+ deleteMilestone(milestoneId);
121
+ }
122
+ catch (err) {
123
+ logWarning("engine", `discardMilestone DB cleanup failed for ${milestoneId}: ${err.message}`);
124
+ }
125
+ }
108
126
  invalidateAllCaches();
109
127
  return true;
110
128
  }
@@ -1,6 +1,6 @@
1
1
  // GSD Extension — Notification Widget
2
2
  // Always-on ambient widget rendered belowEditor showing unread count and
3
- // the most recent notification message. Refreshes every 5 seconds.
3
+ // the most recent notification message. Refreshes every 30 seconds.
4
4
  // Widget key: "gsd-notifications", placement: "belowEditor"
5
5
  import { getUnreadCount, onNotificationStoreChange } from "./notification-store.js";
6
6
  import { formattedShortcutPair } from "./shortcut-defs.js";
@@ -12,7 +12,7 @@ export function buildNotificationWidgetLines() {
12
12
  return [` 🔔 Notifications: ${unread} unread (${formattedShortcutPair("notifications")})`];
13
13
  }
14
14
  // ─── Widget init ────────────────────────────────────────────────────────
15
- const REFRESH_INTERVAL_MS = 5_000;
15
+ const REFRESH_INTERVAL_MS = 30_000;
16
16
  /**
17
17
  * Initialize the always-on notification widget (belowEditor).
18
18
  * Call once from session_start after the notification store is initialized.