gsd-pi 2.63.0-dev.026d309 → 2.63.0-dev.351157b

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 (312) hide show
  1. package/README.md +46 -134
  2. package/dist/cli.js +44 -6
  3. package/dist/help-text.js +4 -1
  4. package/dist/onboarding.js +15 -8
  5. package/dist/resource-loader.js +18 -3
  6. package/dist/resources/extensions/cmux/index.js +21 -12
  7. package/dist/resources/extensions/gsd/auto/finalize-timeout.js +40 -0
  8. package/dist/resources/extensions/gsd/auto/loop.js +4 -0
  9. package/dist/resources/extensions/gsd/auto/phases.js +123 -22
  10. package/dist/resources/extensions/gsd/auto/session.js +8 -0
  11. package/dist/resources/extensions/gsd/auto-dashboard.js +9 -3
  12. package/dist/resources/extensions/gsd/auto-post-unit.js +45 -10
  13. package/dist/resources/extensions/gsd/auto-prompts.js +25 -0
  14. package/dist/resources/extensions/gsd/auto-recovery.js +15 -7
  15. package/dist/resources/extensions/gsd/auto-start.js +10 -21
  16. package/dist/resources/extensions/gsd/auto-tool-tracking.js +17 -0
  17. package/dist/resources/extensions/gsd/auto-worktree.js +13 -7
  18. package/dist/resources/extensions/gsd/auto.js +19 -2
  19. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +73 -60
  20. package/dist/resources/extensions/gsd/bootstrap/dynamic-tools.js +13 -0
  21. package/dist/resources/extensions/gsd/bootstrap/query-tools.js +85 -0
  22. package/dist/resources/extensions/gsd/bootstrap/register-extension.js +3 -0
  23. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +9 -1
  24. package/dist/resources/extensions/gsd/bootstrap/sanitize-complete-milestone.js +54 -0
  25. package/dist/resources/extensions/gsd/commands-handlers.js +9 -4
  26. package/dist/resources/extensions/gsd/constants.js +42 -0
  27. package/dist/resources/extensions/gsd/db-writer.js +72 -4
  28. package/dist/resources/extensions/gsd/forensics.js +20 -4
  29. package/dist/resources/extensions/gsd/gsd-db.js +64 -17
  30. package/dist/resources/extensions/gsd/guided-flow.js +19 -0
  31. package/dist/resources/extensions/gsd/metrics.js +27 -1
  32. package/dist/resources/extensions/gsd/native-git-bridge.js +5 -3
  33. package/dist/resources/extensions/gsd/preferences-types.js +1 -0
  34. package/dist/resources/extensions/gsd/preferences.js +7 -2
  35. package/dist/resources/extensions/gsd/prompts/complete-milestone.md +2 -0
  36. package/dist/resources/extensions/gsd/prompts/complete-slice.md +2 -0
  37. package/dist/resources/extensions/gsd/prompts/doctor-heal.md +1 -0
  38. package/dist/resources/extensions/gsd/prompts/forensics.md +2 -0
  39. package/dist/resources/extensions/gsd/prompts/reassess-roadmap.md +2 -0
  40. package/dist/resources/extensions/gsd/prompts/system.md +1 -0
  41. package/dist/resources/extensions/gsd/prompts/validate-milestone.md +2 -0
  42. package/dist/resources/extensions/gsd/roadmap-mutations.js +1 -1
  43. package/dist/resources/extensions/gsd/roadmap-slices.js +9 -5
  44. package/dist/resources/extensions/gsd/slice-parallel-conflict.js +67 -0
  45. package/dist/resources/extensions/gsd/slice-parallel-eligibility.js +51 -0
  46. package/dist/resources/extensions/gsd/slice-parallel-orchestrator.js +378 -0
  47. package/dist/resources/extensions/gsd/state.js +74 -14
  48. package/dist/resources/extensions/gsd/status-guards.js +11 -0
  49. package/dist/resources/extensions/gsd/tools/complete-milestone.js +17 -12
  50. package/dist/resources/extensions/gsd/tools/complete-slice.js +40 -26
  51. package/dist/resources/extensions/gsd/tools/complete-task.js +12 -12
  52. package/dist/resources/extensions/gsd/tools/plan-milestone.js +33 -25
  53. package/dist/resources/extensions/gsd/tools/plan-slice.js +5 -8
  54. package/dist/resources/extensions/gsd/workflow-projections.js +21 -5
  55. package/dist/resources/extensions/gsd/worktree-manager.js +82 -29
  56. package/dist/resources/extensions/gsd/worktree-resolver.js +4 -3
  57. package/dist/resources/extensions/mcp-client/auth.js +101 -0
  58. package/dist/resources/extensions/mcp-client/index.js +10 -1
  59. package/dist/resources/extensions/ollama/index.js +6 -12
  60. package/dist/resources/extensions/ollama/model-capabilities.js +37 -34
  61. package/dist/resources/extensions/ollama/ndjson-stream.js +54 -0
  62. package/dist/resources/extensions/ollama/ollama-chat-provider.js +380 -0
  63. package/dist/resources/extensions/ollama/ollama-client.js +23 -32
  64. package/dist/resources/extensions/ollama/ollama-discovery.js +2 -7
  65. package/dist/resources/extensions/ollama/ollama-tool.js +62 -0
  66. package/dist/resources/extensions/ollama/thinking-parser.js +104 -0
  67. package/dist/web/standalone/.next/BUILD_ID +1 -1
  68. package/dist/web/standalone/.next/app-path-routes-manifest.json +14 -14
  69. package/dist/web/standalone/.next/build-manifest.json +2 -2
  70. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  71. package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
  72. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  73. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  74. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  75. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  76. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  77. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  78. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  79. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  80. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  81. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  82. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  83. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  84. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  85. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  86. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  87. package/dist/web/standalone/.next/server/app/api/boot/route.js +1 -1
  88. package/dist/web/standalone/.next/server/app/api/boot/route.js.nft.json +1 -1
  89. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js +1 -1
  90. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js.nft.json +1 -1
  91. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js +1 -1
  92. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js.nft.json +1 -1
  93. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js +2 -2
  94. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js.nft.json +1 -1
  95. package/dist/web/standalone/.next/server/app/api/captures/route.js +1 -1
  96. package/dist/web/standalone/.next/server/app/api/captures/route.js.nft.json +1 -1
  97. package/dist/web/standalone/.next/server/app/api/cleanup/route.js +1 -1
  98. package/dist/web/standalone/.next/server/app/api/cleanup/route.js.nft.json +1 -1
  99. package/dist/web/standalone/.next/server/app/api/doctor/route.js +1 -1
  100. package/dist/web/standalone/.next/server/app/api/doctor/route.js.nft.json +1 -1
  101. package/dist/web/standalone/.next/server/app/api/export-data/route.js +1 -1
  102. package/dist/web/standalone/.next/server/app/api/export-data/route.js.nft.json +1 -1
  103. package/dist/web/standalone/.next/server/app/api/files/route.js +1 -1
  104. package/dist/web/standalone/.next/server/app/api/files/route.js.nft.json +1 -1
  105. package/dist/web/standalone/.next/server/app/api/forensics/route.js +1 -1
  106. package/dist/web/standalone/.next/server/app/api/forensics/route.js.nft.json +1 -1
  107. package/dist/web/standalone/.next/server/app/api/git/route.js +1 -1
  108. package/dist/web/standalone/.next/server/app/api/git/route.js.nft.json +1 -1
  109. package/dist/web/standalone/.next/server/app/api/history/route.js +1 -1
  110. package/dist/web/standalone/.next/server/app/api/history/route.js.nft.json +1 -1
  111. package/dist/web/standalone/.next/server/app/api/hooks/route.js +1 -1
  112. package/dist/web/standalone/.next/server/app/api/hooks/route.js.nft.json +1 -1
  113. package/dist/web/standalone/.next/server/app/api/inspect/route.js +1 -1
  114. package/dist/web/standalone/.next/server/app/api/inspect/route.js.nft.json +1 -1
  115. package/dist/web/standalone/.next/server/app/api/knowledge/route.js +1 -1
  116. package/dist/web/standalone/.next/server/app/api/knowledge/route.js.nft.json +1 -1
  117. package/dist/web/standalone/.next/server/app/api/live-state/route.js +1 -1
  118. package/dist/web/standalone/.next/server/app/api/live-state/route.js.nft.json +1 -1
  119. package/dist/web/standalone/.next/server/app/api/onboarding/route.js +1 -1
  120. package/dist/web/standalone/.next/server/app/api/onboarding/route.js.nft.json +1 -1
  121. package/dist/web/standalone/.next/server/app/api/projects/route.js +1 -1
  122. package/dist/web/standalone/.next/server/app/api/projects/route.js.nft.json +1 -1
  123. package/dist/web/standalone/.next/server/app/api/recovery/route.js +1 -1
  124. package/dist/web/standalone/.next/server/app/api/recovery/route.js.nft.json +1 -1
  125. package/dist/web/standalone/.next/server/app/api/session/browser/route.js +1 -1
  126. package/dist/web/standalone/.next/server/app/api/session/browser/route.js.nft.json +1 -1
  127. package/dist/web/standalone/.next/server/app/api/session/command/route.js +1 -1
  128. package/dist/web/standalone/.next/server/app/api/session/command/route.js.nft.json +1 -1
  129. package/dist/web/standalone/.next/server/app/api/session/events/route.js +2 -2
  130. package/dist/web/standalone/.next/server/app/api/session/events/route.js.nft.json +1 -1
  131. package/dist/web/standalone/.next/server/app/api/session/manage/route.js +1 -1
  132. package/dist/web/standalone/.next/server/app/api/session/manage/route.js.nft.json +1 -1
  133. package/dist/web/standalone/.next/server/app/api/settings-data/route.js +1 -1
  134. package/dist/web/standalone/.next/server/app/api/settings-data/route.js.nft.json +1 -1
  135. package/dist/web/standalone/.next/server/app/api/skill-health/route.js +1 -1
  136. package/dist/web/standalone/.next/server/app/api/skill-health/route.js.nft.json +1 -1
  137. package/dist/web/standalone/.next/server/app/api/steer/route.js +1 -1
  138. package/dist/web/standalone/.next/server/app/api/steer/route.js.nft.json +1 -1
  139. package/dist/web/standalone/.next/server/app/api/switch-root/route.js +1 -1
  140. package/dist/web/standalone/.next/server/app/api/switch-root/route.js.nft.json +1 -1
  141. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js +2 -2
  142. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js.nft.json +1 -1
  143. package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js +2 -2
  144. package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js.nft.json +1 -1
  145. package/dist/web/standalone/.next/server/app/api/undo/route.js +1 -1
  146. package/dist/web/standalone/.next/server/app/api/undo/route.js.nft.json +1 -1
  147. package/dist/web/standalone/.next/server/app/api/visualizer/route.js +1 -1
  148. package/dist/web/standalone/.next/server/app/api/visualizer/route.js.nft.json +1 -1
  149. package/dist/web/standalone/.next/server/app/index.html +1 -1
  150. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  151. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  152. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  153. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  154. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  155. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  156. package/dist/web/standalone/.next/server/app-paths-manifest.json +14 -14
  157. package/dist/web/standalone/.next/server/chunks/6897.js +12 -0
  158. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  159. package/dist/web/standalone/.next/server/pages/500.html +2 -2
  160. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  161. package/package.json +1 -1
  162. package/packages/pi-agent-core/dist/agent-loop.d.ts +8 -0
  163. package/packages/pi-agent-core/dist/agent-loop.d.ts.map +1 -1
  164. package/packages/pi-agent-core/dist/agent-loop.js +50 -0
  165. package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
  166. package/packages/pi-agent-core/src/agent-loop.test.ts +221 -5
  167. package/packages/pi-agent-core/src/agent-loop.ts +53 -0
  168. package/packages/pi-ai/dist/types.d.ts +16 -1
  169. package/packages/pi-ai/dist/types.d.ts.map +1 -1
  170. package/packages/pi-ai/dist/types.js.map +1 -1
  171. package/packages/pi-ai/src/types.ts +18 -1
  172. package/packages/pi-coding-agent/dist/core/auth-storage.d.ts +9 -0
  173. package/packages/pi-coding-agent/dist/core/auth-storage.d.ts.map +1 -1
  174. package/packages/pi-coding-agent/dist/core/auth-storage.js +50 -1
  175. package/packages/pi-coding-agent/dist/core/auth-storage.js.map +1 -1
  176. package/packages/pi-coding-agent/dist/core/auth-storage.test.js +41 -0
  177. package/packages/pi-coding-agent/dist/core/auth-storage.test.js.map +1 -1
  178. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts +7 -0
  179. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  180. package/packages/pi-coding-agent/dist/core/extensions/loader.js +31 -4
  181. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  182. package/packages/pi-coding-agent/dist/core/extensions/loader.test.js +28 -1
  183. package/packages/pi-coding-agent/dist/core/extensions/loader.test.js.map +1 -1
  184. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +2 -0
  185. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
  186. package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
  187. package/packages/pi-coding-agent/dist/core/model-registry.d.ts +1 -0
  188. package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
  189. package/packages/pi-coding-agent/dist/core/model-registry.js +1 -0
  190. package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
  191. package/packages/pi-coding-agent/dist/core/model-resolver.js +3 -3
  192. package/packages/pi-coding-agent/dist/core/model-resolver.js.map +1 -1
  193. package/packages/pi-coding-agent/dist/core/resource-loader.d.ts +23 -1
  194. package/packages/pi-coding-agent/dist/core/resource-loader.d.ts.map +1 -1
  195. package/packages/pi-coding-agent/dist/core/resource-loader.js +80 -56
  196. package/packages/pi-coding-agent/dist/core/resource-loader.js.map +1 -1
  197. package/packages/pi-coding-agent/dist/core/sdk.d.ts.map +1 -1
  198. package/packages/pi-coding-agent/dist/core/sdk.js +10 -0
  199. package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
  200. package/packages/pi-coding-agent/src/core/auth-storage.test.ts +53 -0
  201. package/packages/pi-coding-agent/src/core/auth-storage.ts +66 -1
  202. package/packages/pi-coding-agent/src/core/extensions/loader.test.ts +39 -1
  203. package/packages/pi-coding-agent/src/core/extensions/loader.ts +34 -4
  204. package/packages/pi-coding-agent/src/core/extensions/types.ts +2 -0
  205. package/packages/pi-coding-agent/src/core/model-registry.ts +2 -0
  206. package/packages/pi-coding-agent/src/core/model-resolver.ts +3 -3
  207. package/packages/pi-coding-agent/src/core/resource-loader.ts +89 -56
  208. package/packages/pi-coding-agent/src/core/sdk.ts +11 -0
  209. package/src/resources/extensions/cmux/index.ts +18 -12
  210. package/src/resources/extensions/gsd/auto/finalize-timeout.ts +46 -0
  211. package/src/resources/extensions/gsd/auto/loop.ts +5 -0
  212. package/src/resources/extensions/gsd/auto/phases.ts +156 -34
  213. package/src/resources/extensions/gsd/auto/session.ts +9 -0
  214. package/src/resources/extensions/gsd/auto-dashboard.ts +11 -3
  215. package/src/resources/extensions/gsd/auto-post-unit.ts +53 -12
  216. package/src/resources/extensions/gsd/auto-prompts.ts +21 -0
  217. package/src/resources/extensions/gsd/auto-recovery.ts +9 -8
  218. package/src/resources/extensions/gsd/auto-start.ts +11 -20
  219. package/src/resources/extensions/gsd/auto-tool-tracking.ts +19 -0
  220. package/src/resources/extensions/gsd/auto-worktree.ts +14 -6
  221. package/src/resources/extensions/gsd/auto.ts +22 -1
  222. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +74 -60
  223. package/src/resources/extensions/gsd/bootstrap/dynamic-tools.ts +15 -0
  224. package/src/resources/extensions/gsd/bootstrap/query-tools.ts +98 -0
  225. package/src/resources/extensions/gsd/bootstrap/register-extension.ts +4 -0
  226. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +9 -1
  227. package/src/resources/extensions/gsd/bootstrap/sanitize-complete-milestone.ts +57 -0
  228. package/src/resources/extensions/gsd/commands-handlers.ts +10 -4
  229. package/src/resources/extensions/gsd/constants.ts +44 -0
  230. package/src/resources/extensions/gsd/db-writer.ts +78 -4
  231. package/src/resources/extensions/gsd/forensics.ts +21 -5
  232. package/src/resources/extensions/gsd/gsd-db.ts +64 -17
  233. package/src/resources/extensions/gsd/guided-flow.ts +22 -0
  234. package/src/resources/extensions/gsd/metrics.ts +28 -1
  235. package/src/resources/extensions/gsd/native-git-bridge.ts +5 -3
  236. package/src/resources/extensions/gsd/preferences-types.ts +3 -0
  237. package/src/resources/extensions/gsd/preferences.ts +9 -2
  238. package/src/resources/extensions/gsd/prompts/complete-milestone.md +2 -0
  239. package/src/resources/extensions/gsd/prompts/complete-slice.md +2 -0
  240. package/src/resources/extensions/gsd/prompts/doctor-heal.md +1 -0
  241. package/src/resources/extensions/gsd/prompts/forensics.md +2 -0
  242. package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +2 -0
  243. package/src/resources/extensions/gsd/prompts/system.md +1 -0
  244. package/src/resources/extensions/gsd/prompts/validate-milestone.md +2 -0
  245. package/src/resources/extensions/gsd/roadmap-mutations.ts +1 -1
  246. package/src/resources/extensions/gsd/roadmap-slices.ts +10 -5
  247. package/src/resources/extensions/gsd/slice-parallel-conflict.ts +86 -0
  248. package/src/resources/extensions/gsd/slice-parallel-eligibility.ts +73 -0
  249. package/src/resources/extensions/gsd/slice-parallel-orchestrator.ts +477 -0
  250. package/src/resources/extensions/gsd/state.ts +67 -12
  251. package/src/resources/extensions/gsd/status-guards.ts +13 -0
  252. package/src/resources/extensions/gsd/tests/artifact-corruption-2630.test.ts +288 -0
  253. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +34 -13
  254. package/src/resources/extensions/gsd/tests/cmux.test.ts +58 -0
  255. package/src/resources/extensions/gsd/tests/cold-resume-db-reopen.test.ts +51 -0
  256. package/src/resources/extensions/gsd/tests/complete-milestone.test.ts +140 -0
  257. package/src/resources/extensions/gsd/tests/complete-task.test.ts +39 -0
  258. package/src/resources/extensions/gsd/tests/dashboard-model-label-ordering.test.ts +107 -0
  259. package/src/resources/extensions/gsd/tests/db-access-guardrails.test.ts +109 -0
  260. package/src/resources/extensions/gsd/tests/db-path-worktree-symlink.test.ts +13 -9
  261. package/src/resources/extensions/gsd/tests/db-writer.test.ts +134 -0
  262. package/src/resources/extensions/gsd/tests/deferred-slice-dispatch.test.ts +203 -0
  263. package/src/resources/extensions/gsd/tests/discuss-tool-scoping.test.ts +130 -0
  264. package/src/resources/extensions/gsd/tests/doctor-fix-flag.test.ts +92 -0
  265. package/src/resources/extensions/gsd/tests/finalize-timeout-guard.test.ts +116 -0
  266. package/src/resources/extensions/gsd/tests/forensics-stuck-loops.test.ts +103 -0
  267. package/src/resources/extensions/gsd/tests/insert-slice-no-wipe.test.ts +88 -0
  268. package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +27 -7
  269. package/src/resources/extensions/gsd/tests/integration/idle-recovery.test.ts +34 -0
  270. package/src/resources/extensions/gsd/tests/metrics.test.ts +116 -1
  271. package/src/resources/extensions/gsd/tests/milestone-status-tool.test.ts +201 -0
  272. package/src/resources/extensions/gsd/tests/plan-milestone-title.test.ts +2 -1
  273. package/src/resources/extensions/gsd/tests/plan-milestone.test.ts +82 -18
  274. package/src/resources/extensions/gsd/tests/preferences.test.ts +10 -0
  275. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +25 -0
  276. package/src/resources/extensions/gsd/tests/roadmap-slices.test.ts +69 -0
  277. package/src/resources/extensions/gsd/tests/shared-wal.test.ts +30 -0
  278. package/src/resources/extensions/gsd/tests/slice-context-injection.test.ts +50 -0
  279. package/src/resources/extensions/gsd/tests/slice-parallel-conflict.test.ts +92 -0
  280. package/src/resources/extensions/gsd/tests/slice-parallel-eligibility.test.ts +95 -0
  281. package/src/resources/extensions/gsd/tests/slice-parallel-orchestrator.test.ts +83 -0
  282. package/src/resources/extensions/gsd/tests/tool-invocation-error-loop-break.test.ts +103 -0
  283. package/src/resources/extensions/gsd/tests/tool-param-optionality.test.ts +349 -0
  284. package/src/resources/extensions/gsd/tests/worktree-health-dispatch.test.ts +35 -2
  285. package/src/resources/extensions/gsd/tests/worktree-health-monorepo.test.ts +73 -0
  286. package/src/resources/extensions/gsd/tests/worktree-resolver.test.ts +34 -0
  287. package/src/resources/extensions/gsd/tests/worktree-submodule-safety.test.ts +1 -1
  288. package/src/resources/extensions/gsd/tests/worktree-teardown-safety.test.ts +148 -0
  289. package/src/resources/extensions/gsd/tools/complete-milestone.ts +34 -20
  290. package/src/resources/extensions/gsd/tools/complete-slice.ts +41 -26
  291. package/src/resources/extensions/gsd/tools/complete-task.ts +12 -12
  292. package/src/resources/extensions/gsd/tools/plan-milestone.ts +55 -30
  293. package/src/resources/extensions/gsd/tools/plan-slice.ts +13 -8
  294. package/src/resources/extensions/gsd/types.ts +44 -22
  295. package/src/resources/extensions/gsd/workflow-projections.ts +23 -5
  296. package/src/resources/extensions/gsd/worktree-manager.ts +76 -28
  297. package/src/resources/extensions/gsd/worktree-resolver.ts +4 -3
  298. package/src/resources/extensions/mcp-client/auth.ts +149 -0
  299. package/src/resources/extensions/mcp-client/index.ts +16 -1
  300. package/src/resources/extensions/ollama/index.ts +6 -14
  301. package/src/resources/extensions/ollama/model-capabilities.ts +41 -34
  302. package/src/resources/extensions/ollama/ndjson-stream.ts +63 -0
  303. package/src/resources/extensions/ollama/ollama-chat-provider.ts +459 -0
  304. package/src/resources/extensions/ollama/ollama-client.ts +30 -30
  305. package/src/resources/extensions/ollama/ollama-discovery.ts +5 -8
  306. package/src/resources/extensions/ollama/ollama-tool.ts +69 -0
  307. package/src/resources/extensions/ollama/tests/ollama-discovery.test.ts +0 -27
  308. package/src/resources/extensions/ollama/thinking-parser.ts +116 -0
  309. package/src/resources/extensions/ollama/types.ts +23 -0
  310. package/dist/web/standalone/.next/server/chunks/2229.js +0 -12
  311. /package/dist/web/standalone/.next/static/{TTlAguZQ5vR9EOv6G8cel → QmuF-eAbuU_2MQ03t38qr}/_buildManifest.js +0 -0
  312. /package/dist/web/standalone/.next/static/{TTlAguZQ5vR9EOv6G8cel → QmuF-eAbuU_2MQ03t38qr}/_ssgManifest.js +0 -0
@@ -13,13 +13,17 @@ import { runUnit } from "./run-unit.js";
13
13
  import { debugLog } from "../debug-logger.js";
14
14
  import { PROJECT_FILES } from "../detection.js";
15
15
  import { MergeConflictError } from "../git-service.js";
16
- import { join, basename } from "node:path";
17
- import { existsSync, cpSync } from "node:fs";
16
+ import { join, basename, dirname, parse as parsePath } from "node:path";
17
+ import { existsSync, cpSync, readdirSync } from "node:fs";
18
18
  import { logWarning, logError } from "../workflow-logger.js";
19
19
  import { gsdRoot } from "../paths.js";
20
20
  import { atomicWriteSync } from "../atomic-write.js";
21
21
  import { verifyExpectedArtifact, diagnoseExpectedArtifact, buildLoopRemediationSteps } from "../auto-recovery.js";
22
22
  import { writeUnitRuntimeRecord } from "../unit-runtime.js";
23
+ import { withTimeout, FINALIZE_POST_TIMEOUT_MS } from "./finalize-timeout.js";
24
+ import { getEligibleSlices } from "../slice-parallel-eligibility.js";
25
+ import { startSliceParallel } from "../slice-parallel-orchestrator.js";
26
+ import { isDbAvailable, getMilestoneSlices } from "../gsd-db.js";
23
27
  // ─── generateMilestoneReport ──────────────────────────────────────────────────
24
28
  /**
25
29
  * Resolve the base path for milestone reports.
@@ -142,6 +146,50 @@ export async function runPreDispatch(ic, loopState) {
142
146
  mid,
143
147
  statePhase: state.phase,
144
148
  });
149
+ // ── Slice-level parallelism gate (#2340) ─────────────────────────────
150
+ // When slice_parallel is enabled, check if multiple slices are eligible
151
+ // for parallel execution. If so, dispatch them in parallel and stop the
152
+ // sequential loop. Workers are spawned via slice-parallel-orchestrator.ts.
153
+ if (prefs?.slice_parallel?.enabled &&
154
+ mid &&
155
+ !process.env.GSD_PARALLEL_WORKER &&
156
+ isDbAvailable()) {
157
+ try {
158
+ const dbSlices = getMilestoneSlices(mid);
159
+ if (dbSlices.length > 0) {
160
+ const doneIds = new Set(dbSlices.filter(sl => sl.status === "complete" || sl.status === "done").map(sl => sl.id));
161
+ const sliceInputs = dbSlices.map(sl => ({
162
+ id: sl.id,
163
+ done: doneIds.has(sl.id),
164
+ depends: sl.depends ?? [],
165
+ }));
166
+ const eligible = getEligibleSlices(sliceInputs, doneIds);
167
+ if (eligible.length > 1) {
168
+ debugLog("autoLoop", {
169
+ phase: "slice-parallel-dispatch",
170
+ iteration: ic.iteration,
171
+ mid,
172
+ eligibleSlices: eligible.map(e => e.id),
173
+ });
174
+ ctx.ui.notify(`Slice-parallel: dispatching ${eligible.length} eligible slices for ${mid}.`, "info");
175
+ const result = await startSliceParallel(s.basePath, mid, eligible, { maxWorkers: prefs.slice_parallel.max_workers ?? 2 });
176
+ if (result.started.length > 0) {
177
+ ctx.ui.notify(`Slice-parallel: started ${result.started.length} worker(s): ${result.started.join(", ")}.`, "info");
178
+ await deps.stopAuto(ctx, pi, `Slice-parallel dispatched for ${mid}`);
179
+ return { action: "break", reason: "slice-parallel-dispatched" };
180
+ }
181
+ // Fall through to sequential if no workers started
182
+ }
183
+ }
184
+ }
185
+ catch (err) {
186
+ debugLog("autoLoop", {
187
+ phase: "slice-parallel-check-error",
188
+ error: err instanceof Error ? err.message : String(err),
189
+ });
190
+ // Non-fatal — fall through to sequential dispatch
191
+ }
192
+ }
145
193
  // ── Milestone transition ────────────────────────────────────────────
146
194
  if (mid && s.currentMilestoneId && mid !== s.currentMilestoneId) {
147
195
  deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: ic.nextSeq(), eventType: "milestone-transition", data: { from: s.currentMilestoneId, to: mid } });
@@ -669,11 +717,40 @@ export async function runUnitPhase(ic, iterData, loopState, sidecarItem) {
669
717
  }
670
718
  const hasProjectFile = PROJECT_FILES.some((f) => deps.existsSync(join(s.basePath, f)));
671
719
  const hasSrcDir = deps.existsSync(join(s.basePath, "src"));
672
- if (!hasProjectFile && !hasSrcDir) {
720
+ // Xcode bundles have project-specific names (*.xcodeproj, *.xcworkspace)
721
+ // that cannot be matched by exact filename — scan the directory by suffix.
722
+ let hasXcodeBundle = false;
723
+ try {
724
+ const entries = deps.existsSync(s.basePath) ? readdirSync(s.basePath) : [];
725
+ hasXcodeBundle = entries.some((e) => e.endsWith(".xcodeproj") || e.endsWith(".xcworkspace"));
726
+ }
727
+ catch (err) {
728
+ debugLog("runUnitPhase", { phase: "xcode-bundle-scan-failed", basePath: s.basePath, error: String(err) });
729
+ }
730
+ // Monorepo support (#2347): if no project files in the worktree directory,
731
+ // walk parent directories up to the filesystem root. In monorepos,
732
+ // package.json / Cargo.toml etc. live in a parent directory.
733
+ let hasProjectFileInParent = false;
734
+ if (!hasProjectFile && !hasSrcDir && !hasXcodeBundle) {
735
+ let checkDir = dirname(s.basePath);
736
+ const { root } = parsePath(checkDir);
737
+ while (checkDir !== root) {
738
+ // Stop at git repository boundary — ancestors above the repo root
739
+ // (e.g. ~ or /usr/local) may contain unrelated project files.
740
+ if (deps.existsSync(join(checkDir, ".git")))
741
+ break;
742
+ if (PROJECT_FILES.some((f) => deps.existsSync(join(checkDir, f)))) {
743
+ hasProjectFileInParent = true;
744
+ break;
745
+ }
746
+ checkDir = dirname(checkDir);
747
+ }
748
+ }
749
+ if (!hasProjectFile && !hasSrcDir && !hasXcodeBundle && !hasProjectFileInParent) {
673
750
  // Greenfield projects won't have project files yet — the first task creates them.
674
751
  // Log a warning but allow execution to proceed. The .git check above is sufficient
675
752
  // to ensure we're in a valid working directory.
676
- debugLog("runUnitPhase", { phase: "worktree-health-warn-greenfield", basePath: s.basePath, hasProjectFile, hasSrcDir });
753
+ debugLog("runUnitPhase", { phase: "worktree-health-warn-greenfield", basePath: s.basePath, hasProjectFile, hasSrcDir, hasXcodeBundle });
677
754
  ctx.ui.notify(`Warning: ${s.basePath} has no recognized project files — proceeding as greenfield project`, "warning");
678
755
  }
679
756
  }
@@ -683,6 +760,7 @@ export async function runUnitPhase(ic, iterData, loopState, sidecarItem) {
683
760
  s.currentUnit.id === unitId);
684
761
  const previousTier = s.currentUnitRouting?.tier;
685
762
  s.currentUnit = { type: unitType, id: unitId, startedAt: Date.now() };
763
+ s.lastToolInvocationError = null; // #2883: clear stale error from previous unit
686
764
  const unitStartSeq = ic.nextSeq();
687
765
  deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: unitStartSeq, eventType: "unit-start", data: { unitType, unitId } });
688
766
  deps.captureAvailableSkills();
@@ -695,18 +773,10 @@ export async function runUnitPhase(ic, iterData, loopState, sidecarItem) {
695
773
  lastProgressKind: "dispatch",
696
774
  recoveryAttempts: 0, // Reset so re-dispatched units get full recovery budget (#2322)
697
775
  });
698
- // Select and apply model (with tier escalation on retrynormal units only)
699
- const modelResult = await deps.selectAndApplyModel(ctx, pi, unitType, unitId, s.basePath, prefs, s.verbose, s.autoModeStartModel, sidecarItem ? undefined : { isRetry, previousTier });
700
- s.currentUnitRouting =
701
- modelResult.routing;
702
- s.currentUnitModel =
703
- modelResult.appliedModel;
704
- // Status bar + progress widget
776
+ // Status bar (widget + preconditions deferred until after model selection see #2899)
705
777
  ctx.ui.setStatus("gsd-auto", "auto");
706
778
  if (mid)
707
779
  deps.updateSliceProgressCache(s.basePath, mid, state.activeSlice?.id);
708
- deps.updateProgressWidget(ctx, unitType, unitId, state);
709
- deps.ensurePreconditions(unitType, unitId, s.basePath, state);
710
780
  // Prompt injection
711
781
  let finalPrompt = prompt;
712
782
  if (s.pendingVerificationRetry) {
@@ -764,6 +834,12 @@ export async function runUnitPhase(ic, iterData, loopState, sidecarItem) {
764
834
  const msg = reorderErr instanceof Error ? reorderErr.message : String(reorderErr);
765
835
  logWarning("engine", "Prompt reorder failed", { error: msg });
766
836
  }
837
+ // Select and apply model (with tier escalation on retry — normal units only)
838
+ const modelResult = await deps.selectAndApplyModel(ctx, pi, unitType, unitId, s.basePath, prefs, s.verbose, s.autoModeStartModel, sidecarItem ? undefined : { isRetry, previousTier });
839
+ s.currentUnitRouting =
840
+ modelResult.routing;
841
+ s.currentUnitModel =
842
+ modelResult.appliedModel;
767
843
  // Apply sidecar/pre-dispatch hook model override (takes priority over standard model selection)
768
844
  const hookModelOverride = sidecarItem?.model ?? iterData.hookModelOverride;
769
845
  if (hookModelOverride) {
@@ -784,6 +860,15 @@ export async function runUnitPhase(ic, iterData, loopState, sidecarItem) {
784
860
  `Ensure the model is defined in models.json and has auth configured.`, "warning");
785
861
  }
786
862
  }
863
+ // Store the final dispatched model ID so the dashboard can read it (#2899).
864
+ // This accounts for hook model overrides applied after selectAndApplyModel.
865
+ s.currentDispatchedModelId = s.currentUnitModel
866
+ ? `${s.currentUnitModel.provider ?? ""}/${s.currentUnitModel.id ?? ""}`
867
+ : null;
868
+ // Progress widget + preconditions — deferred to after model selection so the
869
+ // widget's first render tick shows the correct model (#2899).
870
+ deps.updateProgressWidget(ctx, unitType, unitId, state);
871
+ deps.ensurePreconditions(unitType, unitId, s.basePath, state);
787
872
  // Start unit supervision
788
873
  deps.clearUnitTimeout();
789
874
  deps.startUnitSupervision({
@@ -860,11 +945,13 @@ export async function runUnitPhase(ic, iterData, loopState, sidecarItem) {
860
945
  if (s.currentUnit) {
861
946
  await deps.closeoutUnit(ctx, s.basePath, unitType, unitId, s.currentUnit.startedAt, deps.buildSnapshotOpts(unitType, unitId));
862
947
  }
863
- // ── Zero tool-call guard (#1833) ──────────────────────────────────
864
- // An execute-task agent that completes with 0 tool calls made no
865
- // real changes its summary is hallucinated. Treat as failed so
866
- // the task is retried instead of silently marked complete.
867
- if (unitType === "execute-task") {
948
+ // ── Zero tool-call guard (#1833, #2653) ──────────────────────────
949
+ // Any unit that completes with 0 tool calls made no real progress —
950
+ // likely context exhaustion where all tool calls errored out. Treat
951
+ // as failed so the unit is retried in a fresh context instead of
952
+ // silently passing through to artifact verification (which loops
953
+ // forever when the unit never produced its artifact).
954
+ {
868
955
  const currentLedger = deps.getLedger();
869
956
  if (currentLedger?.units) {
870
957
  const lastUnit = [...currentLedger.units].reverse().find((u) => u.type === unitType && u.id === unitId && u.startedAt === s.currentUnit?.startedAt);
@@ -873,11 +960,11 @@ export async function runUnitPhase(ic, iterData, loopState, sidecarItem) {
873
960
  phase: "zero-tool-calls",
874
961
  unitType,
875
962
  unitId,
876
- warning: "Task completed with 0 tool calls — likely hallucinated, marking as failed",
963
+ warning: "Unit completed with 0 tool calls — likely context exhaustion, marking as failed",
877
964
  });
878
- ctx.ui.notify(`${unitType} ${unitId} completed with 0 tool calls — hallucinated summary, will retry`, "warning");
965
+ ctx.ui.notify(`${unitType} ${unitId} completed with 0 tool calls — context exhaustion, will retry`, "warning");
879
966
  // Fall through to next iteration where dispatch will re-derive
880
- // and re-dispatch this task.
967
+ // and re-dispatch this unit.
881
968
  return { action: "next", data: { unitStartedAt: s.currentUnit?.startedAt } };
882
969
  }
883
970
  }
@@ -993,7 +1080,21 @@ export async function runFinalize(ic, iterData, sidecarItem) {
993
1080
  }
994
1081
  }
995
1082
  // Post-verification processing (DB dual-write, hooks, triage, quick-tasks)
996
- const postResult = await deps.postUnitPostVerification(postUnitCtx);
1083
+ // Timeout guard: if postUnitPostVerification hangs (e.g., module import
1084
+ // deadlock, SQLite transaction hang), force-continue after timeout so the
1085
+ // auto-loop is not permanently frozen (#2344).
1086
+ const postResultGuard = await withTimeout(deps.postUnitPostVerification(postUnitCtx), FINALIZE_POST_TIMEOUT_MS, "postUnitPostVerification");
1087
+ if (postResultGuard.timedOut) {
1088
+ debugLog("autoLoop", {
1089
+ phase: "post-verification-timeout",
1090
+ iteration: ic.iteration,
1091
+ unitType: iterData.unitType,
1092
+ unitId: iterData.unitId,
1093
+ });
1094
+ ctx.ui.notify(`postUnitPostVerification timed out after ${FINALIZE_POST_TIMEOUT_MS / 1000}s for ${iterData.unitType} ${iterData.unitId} — continuing to next iteration`, "warning");
1095
+ return { action: "next", data: undefined };
1096
+ }
1097
+ const postResult = postResultGuard.value;
997
1098
  if (postResult === "stopped") {
998
1099
  debugLog("autoLoop", {
999
1100
  phase: "exit",
@@ -50,6 +50,8 @@ export class AutoSession {
50
50
  // ── Model state ──────────────────────────────────────────────────────────
51
51
  autoModeStartModel = null;
52
52
  currentUnitModel = null;
53
+ /** Fully-qualified model ID (provider/id) set after selectAndApplyModel + hook overrides (#2899). */
54
+ currentDispatchedModelId = null;
53
55
  originalModelId = null;
54
56
  originalModelProvider = null;
55
57
  lastBudgetAlertLevel = 0;
@@ -62,6 +64,10 @@ export class AutoSession {
62
64
  lastStateRebuildAt = 0;
63
65
  // ── Sidecar queue ─────────────────────────────────────────────────────
64
66
  sidecarQueue = [];
67
+ // ── Tool invocation errors (#2883) ──────────────────────────────────
68
+ /** Set when a GSD tool execution ends with isError due to malformed/truncated
69
+ * JSON arguments. Checked by postUnitPreVerification to break retry loops. */
70
+ lastToolInvocationError = null;
65
71
  // ── Isolation degradation ────────────────────────────────────────────
66
72
  /** Set to true when worktree creation fails; prevents merge of nonexistent branch. */
67
73
  isolationDegraded = false;
@@ -132,6 +138,7 @@ export class AutoSession {
132
138
  // Model
133
139
  this.autoModeStartModel = null;
134
140
  this.currentUnitModel = null;
141
+ this.currentDispatchedModelId = null;
135
142
  this.originalModelId = null;
136
143
  this.originalModelProvider = null;
137
144
  this.lastBudgetAlertLevel = 0;
@@ -149,6 +156,7 @@ export class AutoSession {
149
156
  this.pendingQuickTasks = [];
150
157
  this.sidecarQueue = [];
151
158
  this.rewriteAttemptCount = 0;
159
+ this.lastToolInvocationError = null;
152
160
  this.isolationDegraded = false;
153
161
  this.milestoneMergedInPhases = false;
154
162
  // Signal handler
@@ -516,9 +516,15 @@ export function updateProgressWidget(ctx, unitType, unitId, state, accessors, ti
516
516
  const cxWindow = cxUsage?.contextWindow ?? cmdCtx?.model?.contextWindow ?? 0;
517
517
  const cxPctVal = cxUsage?.percent ?? 0;
518
518
  const cxPct = cxUsage?.percent !== null ? cxPctVal.toFixed(1) : "?";
519
- // Model display — shown in context section, not stats
520
- const modelId = cmdCtx?.model?.id ?? "";
521
- const modelProvider = cmdCtx?.model?.provider ?? "";
519
+ // Model display — prefer dispatched model ID (set after selectAndApplyModel
520
+ // + hook overrides) over cmdCtx?.model which can be stale (#2899).
521
+ const dispatchedModelId = accessors.getCurrentDispatchedModelId();
522
+ const modelId = dispatchedModelId
523
+ ? dispatchedModelId.split("/").slice(1).join("/") || dispatchedModelId
524
+ : (cmdCtx?.model?.id ?? "");
525
+ const modelProvider = dispatchedModelId
526
+ ? dispatchedModelId.split("/")[0] || ""
527
+ : (cmdCtx?.model?.provider ?? "");
522
528
  const tierIcon = resolveServiceTierIcon(effectiveServiceTier, modelId);
523
529
  const modelDisplay = (modelProvider && modelId
524
530
  ? `${modelProvider}/${modelId}`
@@ -19,7 +19,7 @@ import { invalidateAllCaches } from "./cache.js";
19
19
  import { parseUnitId } from "./unit-id.js";
20
20
  import { closeoutUnit } from "./auto-unit-closeout.js";
21
21
  import { autoCommitCurrentBranch, } from "./worktree.js";
22
- import { verifyExpectedArtifact, resolveExpectedArtifactPath, diagnoseExpectedArtifact, } from "./auto-recovery.js";
22
+ import { verifyExpectedArtifact, resolveExpectedArtifactPath, writeBlockerPlaceholder, diagnoseExpectedArtifact, } from "./auto-recovery.js";
23
23
  import { regenerateIfMissing } from "./workflow-projections.js";
24
24
  import { syncStateToProjectRoot } from "./auto-worktree.js";
25
25
  import { isDbAvailable, getTask, getSlice, getMilestone, updateTaskStatus, _getAdapter } from "./gsd-db.js";
@@ -29,6 +29,8 @@ import { checkPostUnitHooks, isRetryPending, consumeRetryTrigger, persistHookSta
29
29
  import { hasPendingCaptures, loadPendingCaptures, revertExecutorResolvedCaptures } from "./captures.js";
30
30
  import { debugLog } from "./debug-logger.js";
31
31
  import { runSafely } from "./auto-utils.js";
32
+ /** Maximum verification retry attempts before escalating to blocker placeholder (#2653). */
33
+ const MAX_VERIFICATION_RETRIES = 3;
32
34
  /** Enqueue a sidecar item (hook, triage, or quick-task) for the main loop to
33
35
  * drain via runUnit. Logs the enqueue event and notifies the UI. */
34
36
  function enqueueSidecar(s, ctx, entry, debugExtra, notification) {
@@ -374,6 +376,8 @@ export async function postUnitPreVerification(pctx, opts) {
374
376
  // When artifact verification fails for a unit type that has a known expected
375
377
  // artifact, return "retry" so the caller re-dispatches with failure context
376
378
  // instead of blindly re-dispatching the same unit (#1571).
379
+ // After MAX_VERIFICATION_RETRIES, escalate to writeBlockerPlaceholder so the
380
+ // pipeline can advance instead of looping forever (#2653).
377
381
  //
378
382
  // HOWEVER, if the DB is unavailable (db_unavailable), the artifact was never
379
383
  // written because the completion tool failed at the infra level. Retrying
@@ -387,20 +391,51 @@ export async function postUnitPreVerification(pctx, opts) {
387
391
  ctx.ui.notify(`Artifact missing for ${s.currentUnit.type} ${s.currentUnit.id} — DB unavailable, skipping retry.${dbSkipDiag ? ` Expected: ${dbSkipDiag}` : ""}`, "error");
388
392
  }
389
393
  else if (!triggerArtifactVerified) {
394
+ // #2883: If the artifact is missing because the tool invocation itself
395
+ // failed (malformed/truncated JSON arguments), retrying will produce the
396
+ // same failure. Pause auto-mode instead of entering a stuck retry loop.
397
+ if (s.lastToolInvocationError) {
398
+ const errMsg = `Tool invocation failed for ${s.currentUnit.type}: ${s.lastToolInvocationError}. Structured argument generation failed — pausing auto-mode.`;
399
+ debugLog("postUnit", { phase: "tool-invocation-error-pause", unitType: s.currentUnit.type, unitId: s.currentUnit.id, error: s.lastToolInvocationError });
400
+ ctx.ui.notify(errMsg, "error");
401
+ s.lastToolInvocationError = null;
402
+ await pauseAuto(ctx, pi);
403
+ return "dispatched";
404
+ }
390
405
  const hasExpectedArtifact = resolveExpectedArtifactPath(s.currentUnit.type, s.currentUnit.id, s.basePath) !== null;
391
406
  if (hasExpectedArtifact) {
392
407
  const retryKey = `${s.currentUnit.type}:${s.currentUnit.id}`;
393
408
  const attempt = (s.verificationRetryCount.get(retryKey) ?? 0) + 1;
394
409
  s.verificationRetryCount.set(retryKey, attempt);
395
- const retryDiag = diagnoseExpectedArtifact(s.currentUnit.type, s.currentUnit.id, s.basePath);
396
- s.pendingVerificationRetry = {
397
- unitId: s.currentUnit.id,
398
- failureContext: `Artifact verification failed: expected artifact for ${s.currentUnit.type} "${s.currentUnit.id}" was not found on disk after unit execution (attempt ${attempt}).${retryDiag ? ` Expected: ${retryDiag}` : ""}`,
399
- attempt,
400
- };
401
- debugLog("postUnit", { phase: "artifact-verify-retry", unitType: s.currentUnit.type, unitId: s.currentUnit.id, attempt });
402
- ctx.ui.notify(`Artifact missing for ${s.currentUnit.type} ${s.currentUnit.id} — retrying (attempt ${attempt}).${retryDiag ? ` Expected: ${retryDiag}` : ""}`, "warning");
403
- return "retry";
410
+ if (attempt > MAX_VERIFICATION_RETRIES) {
411
+ // Retries exhausted — write a blocker placeholder so the pipeline
412
+ // can advance past this stuck unit (#2653).
413
+ debugLog("postUnit", {
414
+ phase: "artifact-verify-escalate",
415
+ unitType: s.currentUnit.type,
416
+ unitId: s.currentUnit.id,
417
+ attempt,
418
+ maxRetries: MAX_VERIFICATION_RETRIES,
419
+ });
420
+ const reason = `Artifact verification failed after ${MAX_VERIFICATION_RETRIES} retries for ${s.currentUnit.type} "${s.currentUnit.id}".`;
421
+ writeBlockerPlaceholder(s.currentUnit.type, s.currentUnit.id, s.basePath, reason);
422
+ ctx.ui.notify(`${s.currentUnit.type} ${s.currentUnit.id} — verification retries exhausted (${MAX_VERIFICATION_RETRIES}), wrote blocker placeholder to advance pipeline`, "warning");
423
+ // Reset retry count and fall through to "continue" so the loop
424
+ // re-derives state with the placeholder in place.
425
+ s.verificationRetryCount.delete(retryKey);
426
+ s.pendingVerificationRetry = null;
427
+ // Do NOT return "retry" — fall through to "continue" below.
428
+ }
429
+ else {
430
+ s.pendingVerificationRetry = {
431
+ unitId: s.currentUnit.id,
432
+ failureContext: `Artifact verification failed: expected artifact for ${s.currentUnit.type} "${s.currentUnit.id}" was not found on disk after unit execution (attempt ${attempt}).`,
433
+ attempt,
434
+ };
435
+ debugLog("postUnit", { phase: "artifact-verify-retry", unitType: s.currentUnit.type, unitId: s.currentUnit.id, attempt });
436
+ ctx.ui.notify(`Artifact missing for ${s.currentUnit.type} ${s.currentUnit.id} — retrying (attempt ${attempt})`, "warning");
437
+ return "retry";
438
+ }
404
439
  }
405
440
  }
406
441
  }
@@ -880,11 +880,16 @@ export async function buildResearchSlicePrompt(mid, _midTitle, sid, sTitle, base
880
880
  const contextRel = relMilestoneFile(base, mid, "CONTEXT");
881
881
  const milestoneResearchPath = resolveMilestoneFile(base, mid, "RESEARCH");
882
882
  const milestoneResearchRel = relMilestoneFile(base, mid, "RESEARCH");
883
+ const sliceContextPath = resolveSliceFile(base, mid, sid, "CONTEXT");
884
+ const sliceContextRel = relSliceFile(base, mid, sid, "CONTEXT");
883
885
  const inlined = [];
884
886
  inlined.push(await inlineFile(roadmapPath, roadmapRel, "Milestone Roadmap"));
885
887
  const contextInline = await inlineFileOptional(contextPath, contextRel, "Milestone Context");
886
888
  if (contextInline)
887
889
  inlined.push(contextInline);
890
+ const sliceCtxInline = await inlineFileOptional(sliceContextPath, sliceContextRel, "Slice Context (from discussion)");
891
+ if (sliceCtxInline)
892
+ inlined.push(sliceCtxInline);
888
893
  const researchInline = await inlineFileOptional(milestoneResearchPath, milestoneResearchRel, "Milestone Research");
889
894
  if (researchInline)
890
895
  inlined.push(researchInline);
@@ -931,12 +936,17 @@ export async function buildPlanSlicePrompt(mid, _midTitle, sid, sTitle, base, le
931
936
  const roadmapRel = relMilestoneFile(base, mid, "ROADMAP");
932
937
  const researchPath = resolveSliceFile(base, mid, sid, "RESEARCH");
933
938
  const researchRel = relSliceFile(base, mid, sid, "RESEARCH");
939
+ const sliceContextPath = resolveSliceFile(base, mid, sid, "CONTEXT");
940
+ const sliceContextRel = relSliceFile(base, mid, sid, "CONTEXT");
934
941
  const inlined = [];
935
942
  // Inject phase handoff anchor from research phase (if available)
936
943
  const researchSliceAnchor = readPhaseAnchor(base, mid, "research-slice");
937
944
  if (researchSliceAnchor)
938
945
  inlined.push(formatAnchorForPrompt(researchSliceAnchor));
939
946
  inlined.push(await inlineFile(roadmapPath, roadmapRel, "Milestone Roadmap"));
947
+ const sliceCtxInline = await inlineFileOptional(sliceContextPath, sliceContextRel, "Slice Context (from discussion)");
948
+ if (sliceCtxInline)
949
+ inlined.push(sliceCtxInline);
940
950
  const researchInline = await inlineFileOptional(researchPath, researchRel, "Slice Research");
941
951
  if (researchInline)
942
952
  inlined.push(researchInline);
@@ -1097,8 +1107,13 @@ export async function buildCompleteSlicePrompt(mid, _midTitle, sid, sTitle, base
1097
1107
  const roadmapRel = relMilestoneFile(base, mid, "ROADMAP");
1098
1108
  const slicePlanPath = resolveSliceFile(base, mid, sid, "PLAN");
1099
1109
  const slicePlanRel = relSliceFile(base, mid, sid, "PLAN");
1110
+ const sliceContextPath = resolveSliceFile(base, mid, sid, "CONTEXT");
1111
+ const sliceContextRel = relSliceFile(base, mid, sid, "CONTEXT");
1100
1112
  const inlined = [];
1101
1113
  inlined.push(await inlineFile(roadmapPath, roadmapRel, "Milestone Roadmap"));
1114
+ const sliceCtxInline = await inlineFileOptional(sliceContextPath, sliceContextRel, "Slice Context (from discussion)");
1115
+ if (sliceCtxInline)
1116
+ inlined.push(sliceCtxInline);
1102
1117
  inlined.push(await inlineFile(slicePlanPath, slicePlanRel, "Slice Plan"));
1103
1118
  if (inlineLevel !== "minimal") {
1104
1119
  const requirementsInline = await inlineRequirementsFromDb(base, sid, inlineLevel);
@@ -1351,8 +1366,13 @@ export async function buildReplanSlicePrompt(mid, midTitle, sid, sTitle, base) {
1351
1366
  const roadmapRel = relMilestoneFile(base, mid, "ROADMAP");
1352
1367
  const slicePlanPath = resolveSliceFile(base, mid, sid, "PLAN");
1353
1368
  const slicePlanRel = relSliceFile(base, mid, sid, "PLAN");
1369
+ const sliceContextPath = resolveSliceFile(base, mid, sid, "CONTEXT");
1370
+ const sliceContextRel = relSliceFile(base, mid, sid, "CONTEXT");
1354
1371
  const inlined = [];
1355
1372
  inlined.push(await inlineFile(roadmapPath, roadmapRel, "Milestone Roadmap"));
1373
+ const sliceCtxInline = await inlineFileOptional(sliceContextPath, sliceContextRel, "Slice Context (from discussion)");
1374
+ if (sliceCtxInline)
1375
+ inlined.push(sliceCtxInline);
1356
1376
  inlined.push(await inlineFile(slicePlanPath, slicePlanRel, "Current Slice Plan"));
1357
1377
  // Find the blocker task summary — the completed task with blocker_discovered: true
1358
1378
  let blockerTaskId = "";
@@ -1454,8 +1474,13 @@ export async function buildReassessRoadmapPrompt(mid, midTitle, completedSliceId
1454
1474
  const roadmapRel = relMilestoneFile(base, mid, "ROADMAP");
1455
1475
  const summaryPath = resolveSliceFile(base, mid, completedSliceId, "SUMMARY");
1456
1476
  const summaryRel = relSliceFile(base, mid, completedSliceId, "SUMMARY");
1477
+ const sliceContextPath = resolveSliceFile(base, mid, completedSliceId, "CONTEXT");
1478
+ const sliceContextRel = relSliceFile(base, mid, completedSliceId, "CONTEXT");
1457
1479
  const inlined = [];
1458
1480
  inlined.push(await inlineFile(roadmapPath, roadmapRel, "Current Roadmap"));
1481
+ const sliceCtxInline = await inlineFileOptional(sliceContextPath, sliceContextRel, "Slice Context (from discussion)");
1482
+ if (sliceCtxInline)
1483
+ inlined.push(sliceCtxInline);
1459
1484
  inlined.push(await inlineFile(summaryPath, summaryRel, `${completedSliceId} Summary`));
1460
1485
  if (inlineLevel !== "minimal") {
1461
1486
  const projectInline = await inlineProjectFromDb(base);
@@ -9,7 +9,7 @@
9
9
  import { parseUnitId } from "./unit-id.js";
10
10
  import { clearParseCache } from "./files.js";
11
11
  import { parseRoadmap as parseLegacyRoadmap, parsePlan as parseLegacyPlan } from "./parsers-legacy.js";
12
- import { isDbAvailable, getTask, getSlice, getSliceTasks, updateTaskStatus } from "./gsd-db.js";
12
+ import { isDbAvailable, getTask, getSlice, getSliceTasks, updateTaskStatus, updateSliceStatus } from "./gsd-db.js";
13
13
  import { isValidationTerminal } from "./state.js";
14
14
  import { getErrorMessage } from "./error-utils.js";
15
15
  import { logWarning, logError } from "./workflow-logger.js";
@@ -377,17 +377,25 @@ export function writeBlockerPlaceholder(unitType, unitId, base, reason) {
377
377
  `Review and replace this file before relying on downstream artifacts.`,
378
378
  ].join("\n");
379
379
  writeFileSync(absPath, content, "utf-8");
380
- // Mark the task as complete in the DB so verifyExpectedArtifact passes.
380
+ // Mark the task/slice as complete in the DB so verifyExpectedArtifact passes.
381
381
  // Without this, the DB status stays "pending" and the dispatch loop
382
- // re-derives the same task indefinitely (#2531).
383
- if (unitType === "execute-task" && isDbAvailable()) {
382
+ // re-derives the same unit indefinitely (#2531, #2653).
383
+ if (isDbAvailable()) {
384
384
  const { milestone: mid, slice: sid, task: tid } = parseUnitId(unitId);
385
- if (mid && sid && tid) {
385
+ if (unitType === "execute-task" && mid && sid && tid) {
386
386
  try {
387
387
  updateTaskStatus(mid, sid, tid, "complete", new Date().toISOString());
388
388
  }
389
- catch (err) { /* non-fatal */
390
- logError("recovery", `DB status update failed: ${err instanceof Error ? err.message : String(err)}`);
389
+ catch (e) {
390
+ logWarning("recovery", `updateTaskStatus failed during context exhaustion: ${e instanceof Error ? e.message : String(e)}`);
391
+ }
392
+ }
393
+ if (unitType === "complete-slice" && mid && sid) {
394
+ try {
395
+ updateSliceStatus(mid, sid, "complete", new Date().toISOString());
396
+ }
397
+ catch (e) {
398
+ logWarning("recovery", `updateSliceStatus failed during context exhaustion: ${e instanceof Error ? e.message : String(e)}`);
391
399
  }
392
400
  }
393
401
  }
@@ -30,7 +30,7 @@ import { initRoutingHistory } from "./routing-history.js";
30
30
  import { restoreHookState, resetHookState } from "./post-unit-hooks.js";
31
31
  import { resetProactiveHealing, setLevelChangeCallback } from "./doctor-proactive.js";
32
32
  import { snapshotSkills } from "./skill-discovery.js";
33
- import { isDbAvailable, getMilestone } from "./gsd-db.js";
33
+ import { isDbAvailable, getMilestone, openDatabase } from "./gsd-db.js";
34
34
  import { hideFooter } from "./auto-dashboard.js";
35
35
  import { debugLog, enableDebug, isDebugEnabled, getDebugLogPath, } from "./debug-logger.js";
36
36
  import { logWarning, logError } from "./workflow-logger.js";
@@ -48,34 +48,23 @@ import { resolveDefaultSessionModel } from "./preferences-models.js";
48
48
  * Returns false if the bootstrap aborted (e.g., guided flow returned,
49
49
  * concurrent session detected). Returns true when ready to dispatch.
50
50
  */
51
- /**
52
- * Open the project-root DB before the first deriveState call (#2841).
53
- * When auto-mode starts cold (no prior DB handle), state derivation that
54
- * touches DB-backed helpers (queue-order, task status) silently falls back
55
- * to markdown-only data, producing stale or incomplete state. Opening the
56
- * DB first ensures deriveState sees the full picture on its very first run.
57
- */
58
- async function openProjectDbIfPresent(basePath) {
51
+ /** Guard: tracks consecutive bootstrap attempts that found phase === "complete".
52
+ * Prevents the recursive dialog loop described in #1348 where
53
+ * bootstrapAutoSession showSmartEntry checkAutoStartAfterDiscuss startAuto
54
+ * cycles indefinitely when the discuss workflow doesn't produce a milestone. */
55
+ let _consecutiveCompleteBootstraps = 0;
56
+ const MAX_CONSECUTIVE_COMPLETE_BOOTSTRAPS = 2;
57
+ export async function openProjectDbIfPresent(basePath) {
59
58
  const gsdDbPath = resolveProjectRootDbPath(basePath);
60
- if (!existsSync(gsdDbPath))
61
- return;
62
- if (isDbAvailable())
59
+ if (!existsSync(gsdDbPath) || isDbAvailable())
63
60
  return;
64
61
  try {
65
- const { openDatabase } = await import("./gsd-db.js");
66
62
  openDatabase(gsdDbPath);
67
63
  }
68
64
  catch (err) {
69
- /* non-fatal DB lifecycle block below will retry */
70
- logWarning("engine", `DB open failed: ${err instanceof Error ? err.message : String(err)}`);
65
+ logWarning("engine", `gsd-db: failed to open existing database: ${err instanceof Error ? err.message : String(err)}`);
71
66
  }
72
67
  }
73
- /** Guard: tracks consecutive bootstrap attempts that found phase === "complete".
74
- * Prevents the recursive dialog loop described in #1348 where
75
- * bootstrapAutoSession → showSmartEntry → checkAutoStartAfterDiscuss → startAuto
76
- * cycles indefinitely when the discuss workflow doesn't produce a milestone. */
77
- let _consecutiveCompleteBootstraps = 0;
78
- const MAX_CONSECUTIVE_COMPLETE_BOOTSTRAPS = 2;
79
68
  export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, requestedStepMode, deps) {
80
69
  const { shouldUseWorktreeIsolation, registerSigtermHandler, lockBase, buildResolver, } = deps;
81
70
  const lockResult = acquireSessionLock(base);
@@ -75,3 +75,20 @@ export function hasInteractiveToolInFlight() {
75
75
  export function clearInFlightTools() {
76
76
  inFlightTools.clear();
77
77
  }
78
+ // ─── Tool invocation error classification (#2883) ────────────────────────
79
+ /**
80
+ * Patterns that indicate a tool invocation failed due to malformed or truncated
81
+ * JSON arguments — as opposed to a normal business-logic error from the tool
82
+ * handler. When these errors occur, retrying the same unit will produce the same
83
+ * failure, so the retry loop must be broken.
84
+ */
85
+ const TOOL_INVOCATION_ERROR_RE = /Validation failed for tool|Expected ',' or '\}' in JSON|Unexpected end of JSON|Unexpected token.*in JSON/i;
86
+ /**
87
+ * Returns true if the error message indicates a tool invocation failure due to
88
+ * malformed/truncated arguments (as opposed to a normal tool execution error).
89
+ */
90
+ export function isToolInvocationError(errorMsg) {
91
+ if (!errorMsg)
92
+ return false;
93
+ return TOOL_INVOCATION_ERROR_RE.test(errorMsg);
94
+ }
@@ -14,7 +14,7 @@ import { atomicWriteSync } from "./atomic-write.js";
14
14
  import { execFileSync } from "node:child_process";
15
15
  import { safeCopy, safeCopyRecursive } from "./safe-fs.js";
16
16
  import { gsdRoot } from "./paths.js";
17
- import { createWorktree, removeWorktree, resolveGitDir, worktreePath, } from "./worktree-manager.js";
17
+ import { createWorktree, removeWorktree, resolveGitDir, worktreePath, isInsideWorktreesDir, } from "./worktree-manager.js";
18
18
  import { detectWorktreeName, nudgeGitBranchCache, } from "./worktree.js";
19
19
  import { MergeConflictError, readIntegrationBranch, RUNTIME_EXCLUSION_PATHS } from "./git-service.js";
20
20
  import { debugLog } from "./debug-logger.js";
@@ -1025,13 +1025,19 @@ export function teardownAutoWorktree(originalBasePath, milestoneId, opts = {}) {
1025
1025
  logWarning("reconcile", `Worktree directory still exists after teardown: ${wtDir}. ` +
1026
1026
  `This is likely an orphaned directory consuming disk space. ` +
1027
1027
  `Remove it manually with: rm -rf "${wtDir.replaceAll("\\", "/")}"`, { worktree: milestoneId });
1028
- // Attempt a direct filesystem removal as a fallback
1029
- try {
1030
- rmSync(wtDir, { recursive: true, force: true });
1028
+ // Attempt a direct filesystem removal as a fallback — but ONLY if the
1029
+ // path is safely inside .gsd/worktrees/ to prevent #2365 data loss.
1030
+ if (isInsideWorktreesDir(originalBasePath, wtDir)) {
1031
+ try {
1032
+ rmSync(wtDir, { recursive: true, force: true });
1033
+ }
1034
+ catch (err) {
1035
+ // Non-fatal — the warning above tells the user how to clean up
1036
+ logWarning("worktree", `worktree directory removal failed: ${err instanceof Error ? err.message : String(err)}`);
1037
+ }
1031
1038
  }
1032
- catch (err) {
1033
- // Non-fatal the warning above tells the user how to clean up
1034
- logWarning("worktree", `worktree directory removal failed: ${err instanceof Error ? err.message : String(err)}`);
1039
+ else {
1040
+ console.error(`[GSD] REFUSING fallback rmSync path is outside .gsd/worktrees/: ${wtDir}`);
1035
1041
  }
1036
1042
  }
1037
1043
  }