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
@@ -36,13 +36,14 @@ import {
36
36
 
37
37
  import { findMilestoneIds } from './milestone-ids.js';
38
38
  import { loadQueueOrder, sortByQueueOrder } from './queue-order.js';
39
+ import { isClosedStatus, isDeferredStatus } from './status-guards.js';
39
40
  import { nativeBatchParseGsdFiles, type BatchParsedFile } from './native-parser-bridge.js';
40
41
 
41
42
  import { join, resolve } from 'path';
42
43
  import { existsSync, readdirSync, readFileSync } from 'node:fs';
43
44
  import { debugCount, debugTime } from './debug-logger.js';
44
- import { extractVerdict } from './verdict-parser.js';
45
45
  import { logWarning, logError } from './workflow-logger.js';
46
+ import { extractVerdict } from './verdict-parser.js';
46
47
 
47
48
  import {
48
49
  isDbAvailable,
@@ -674,12 +675,39 @@ export async function deriveStateFromDb(basePath: string): Promise<GSDState> {
674
675
  let activeSlice: ActiveRef | null = null;
675
676
  let activeSliceRow: SliceRow | null = null;
676
677
 
677
- for (const s of activeMilestoneSlices) {
678
- if (isStatusDone(s.status)) continue;
679
- if (s.depends.every(dep => doneSliceIds.has(dep))) {
680
- activeSlice = { id: s.id, title: s.title };
681
- activeSliceRow = s;
682
- break;
678
+ // ── Slice-level parallel worker isolation ─────────────────────────────
679
+ // When GSD_SLICE_LOCK is set, this process is a parallel worker scoped
680
+ // to a single slice. Override activeSlice to only the locked slice ID.
681
+ const sliceLock = process.env.GSD_SLICE_LOCK;
682
+ if (sliceLock) {
683
+ const lockedSlice = activeMilestoneSlices.find(s => s.id === sliceLock);
684
+ if (lockedSlice) {
685
+ activeSlice = { id: lockedSlice.id, title: lockedSlice.title };
686
+ activeSliceRow = lockedSlice;
687
+ } else {
688
+ logWarning("state", `GSD_SLICE_LOCK=${sliceLock} not found in active slices — worker has no assigned work`);
689
+ // Don't silently continue — this is a dispatch error
690
+ return {
691
+ activeMilestone, activeSlice: null, activeTask: null,
692
+ phase: 'blocked',
693
+ recentDecisions: [], blockers: [`GSD_SLICE_LOCK=${sliceLock} not found in active milestone slices`],
694
+ nextAction: 'Slice lock references a non-existent slice — check orchestrator dispatch.',
695
+ registry, requirements,
696
+ progress: { milestones: milestoneProgress, slices: sliceProgress },
697
+ };
698
+ }
699
+ } else {
700
+ for (const s of activeMilestoneSlices) {
701
+ if (isStatusDone(s.status)) continue;
702
+ // #2661: Skip deferred slices — a decision explicitly deferred this work.
703
+ // Without this guard the dispatcher would keep dispatching deferred slices
704
+ // because DECISIONS.md is only contextual, not authoritative for dispatch.
705
+ if (isDeferredStatus(s.status)) continue;
706
+ if (s.depends.every(dep => doneSliceIds.has(dep))) {
707
+ activeSlice = { id: s.id, title: s.title };
708
+ activeSliceRow = s;
709
+ break;
710
+ }
683
711
  }
684
712
  }
685
713
 
@@ -1320,11 +1348,38 @@ export async function _deriveStateImpl(basePath: string): Promise<GSDState> {
1320
1348
  const doneSliceIds = new Set(activeRoadmap.slices.filter(s => s.done).map(s => s.id));
1321
1349
  let activeSlice: ActiveRef | null = null;
1322
1350
 
1323
- for (const s of activeRoadmap.slices) {
1324
- if (s.done) continue;
1325
- if (s.depends.every(dep => doneSliceIds.has(dep))) {
1326
- activeSlice = { id: s.id, title: s.title };
1327
- break;
1351
+ // ── Slice-level parallel worker isolation ─────────────────────────────
1352
+ // When GSD_SLICE_LOCK is set, override activeSlice to only the locked slice.
1353
+ const sliceLockLegacy = process.env.GSD_SLICE_LOCK;
1354
+ if (sliceLockLegacy) {
1355
+ const lockedSlice = activeRoadmap.slices.find(s => s.id === sliceLockLegacy);
1356
+ if (lockedSlice) {
1357
+ activeSlice = { id: lockedSlice.id, title: lockedSlice.title };
1358
+ } else {
1359
+ logWarning("state", `GSD_SLICE_LOCK=${sliceLockLegacy} not found in active slices — worker has no assigned work`);
1360
+ return {
1361
+ activeMilestone,
1362
+ activeSlice: null,
1363
+ activeTask: null,
1364
+ phase: 'blocked',
1365
+ recentDecisions: [],
1366
+ blockers: [`GSD_SLICE_LOCK=${sliceLockLegacy} not found in active milestone slices`],
1367
+ nextAction: 'Slice lock references a non-existent slice — check orchestrator dispatch.',
1368
+ registry,
1369
+ requirements,
1370
+ progress: {
1371
+ milestones: milestoneProgress,
1372
+ slices: sliceProgress,
1373
+ },
1374
+ };
1375
+ }
1376
+ } else {
1377
+ for (const s of activeRoadmap.slices) {
1378
+ if (s.done) continue;
1379
+ if (s.depends.every(dep => doneSliceIds.has(dep))) {
1380
+ activeSlice = { id: s.id, title: s.title };
1381
+ break;
1382
+ }
1328
1383
  }
1329
1384
  }
1330
1385
 
@@ -12,3 +12,16 @@
12
12
  export function isClosedStatus(status: string): boolean {
13
13
  return status === "complete" || status === "done" || status === "skipped";
14
14
  }
15
+
16
+ /** Returns true when a slice status indicates it was deferred by a decision. */
17
+ export function isDeferredStatus(status: string): boolean {
18
+ return status === "deferred";
19
+ }
20
+
21
+ /**
22
+ * Returns true when a slice should be skipped during active-slice selection.
23
+ * This includes both closed (complete/done) and deferred slices.
24
+ */
25
+ export function isInactiveStatus(status: string): boolean {
26
+ return isClosedStatus(status) || isDeferredStatus(status);
27
+ }
@@ -0,0 +1,288 @@
1
+ // GSD — regression tests for issue #2630
2
+ // Milestone/slice artifact rendering must not corrupt existing markdown.
3
+ // Three bugs: (A) milestone title double-prefix, (B) full_uat_md demo fallback,
4
+ // (C) STATE.md title double-prefix.
5
+
6
+ import test from 'node:test';
7
+ import assert from 'node:assert/strict';
8
+
9
+ import {
10
+ renderPlanContent,
11
+ renderRoadmapContent,
12
+ renderStateContent,
13
+ } from '../workflow-projections.ts';
14
+ import type { SliceRow, TaskRow, MilestoneRow } from '../gsd-db.ts';
15
+ import type { GSDState } from '../types.ts';
16
+
17
+ // ─── Helpers ─────────────────────────────────────────────────────────────
18
+
19
+ function makeSliceRow(overrides?: Partial<SliceRow>): SliceRow {
20
+ return {
21
+ milestone_id: 'M001',
22
+ id: 'S04',
23
+ title: 'Dependency-driven scene pipeline and state truth',
24
+ status: 'complete',
25
+ risk: 'high',
26
+ depends: ['S03'],
27
+ demo: '',
28
+ created_at: '2026-01-01T00:00:00Z',
29
+ completed_at: '2026-01-15T00:00:00Z',
30
+ full_summary_md: '',
31
+ full_uat_md: `# S04: Dependency-driven scene pipeline and state truth — UAT
32
+
33
+ **Milestone:** M001
34
+ **Written:** 2026-01-15
35
+
36
+ ## UAT Type: Functional
37
+
38
+ ### Scenario 1: Pipeline processes dependencies
39
+ **Given** a scene with dependencies
40
+ **When** the pipeline runs
41
+ **Then** dependencies are resolved in order`,
42
+ goal: 'Build dependency-driven scene pipeline',
43
+ success_criteria: '',
44
+ proof_level: '',
45
+ integration_closure: '',
46
+ observability_impact: '',
47
+ sequence: 4,
48
+ replan_triggered_at: null,
49
+ ...overrides,
50
+ };
51
+ }
52
+
53
+ function makeTaskRow(overrides?: Partial<TaskRow>): TaskRow {
54
+ return {
55
+ milestone_id: 'M001',
56
+ slice_id: 'S04',
57
+ id: 'T01',
58
+ title: 'Test Task',
59
+ status: 'done',
60
+ one_liner: '',
61
+ narrative: '',
62
+ verification_result: '',
63
+ duration: '',
64
+ completed_at: null,
65
+ blocker_discovered: false,
66
+ deviations: '',
67
+ known_issues: '',
68
+ key_files: [],
69
+ key_decisions: [],
70
+ full_summary_md: '',
71
+ full_plan_md: '',
72
+ description: 'Test description',
73
+ estimate: '30m',
74
+ files: [],
75
+ verify: 'npm test',
76
+ inputs: [],
77
+ expected_output: [],
78
+ observability_impact: '',
79
+ sequence: 0,
80
+ ...overrides,
81
+ };
82
+ }
83
+
84
+ function makeMilestoneRow(overrides?: Partial<MilestoneRow>): MilestoneRow {
85
+ return {
86
+ id: 'M001',
87
+ title: 'Topic-to-pipeline foundation',
88
+ status: 'active',
89
+ depends_on: [],
90
+ created_at: '2026-01-01T00:00:00Z',
91
+ completed_at: null,
92
+ vision: 'Build the topic-to-pipeline foundation',
93
+ success_criteria: [],
94
+ key_risks: [],
95
+ proof_strategy: [],
96
+ verification_contract: '',
97
+ verification_integration: '',
98
+ verification_operational: '',
99
+ verification_uat: '',
100
+ definition_of_done: [],
101
+ requirement_coverage: '',
102
+ boundary_map_markdown: '',
103
+ ...overrides,
104
+ };
105
+ }
106
+
107
+ function makeGSDState(overrides?: Partial<GSDState>): GSDState {
108
+ return {
109
+ activeMilestone: { id: 'M001', title: 'Topic-to-pipeline foundation' },
110
+ activeSlice: { id: 'S01', title: 'Auth Layer' },
111
+ activeTask: null,
112
+ phase: 'executing',
113
+ recentDecisions: [],
114
+ blockers: [],
115
+ nextAction: 'Continue execution',
116
+ registry: [],
117
+ requirements: undefined,
118
+ ...overrides,
119
+ };
120
+ }
121
+
122
+ // ─── Bug A: milestone title double-prefix ────────────────────────────────
123
+ // When params.title already contains "M001: ", the H1 should NOT become
124
+ // "# M001: M001: Topic-to-pipeline foundation"
125
+
126
+ test('#2630 renderRoadmapContent: milestone title with pre-existing ID prefix renders without duplication', () => {
127
+ const milestone = makeMilestoneRow({ title: 'M001: Topic-to-pipeline foundation' });
128
+ const content = renderRoadmapContent(milestone, []);
129
+
130
+ // The H1 must be exactly "# M001: Topic-to-pipeline foundation", not "# M001: M001: ..."
131
+ assert.ok(
132
+ content.includes('# M001: Topic-to-pipeline foundation'),
133
+ `expected single prefix in H1, got: ${content.split('\n')[0]}`,
134
+ );
135
+ assert.ok(
136
+ !content.includes('M001: M001:'),
137
+ `found double prefix in roadmap H1: ${content.split('\n')[0]}`,
138
+ );
139
+ });
140
+
141
+ test('#2630 renderStateContent: active milestone title with pre-existing ID prefix renders without duplication', () => {
142
+ const state = makeGSDState({
143
+ activeMilestone: { id: 'M001', title: 'M001: Topic-to-pipeline foundation' },
144
+ });
145
+ const content = renderStateContent(state);
146
+
147
+ assert.ok(
148
+ !content.includes('M001: M001:'),
149
+ `found double prefix in STATE.md: ${content}`,
150
+ );
151
+ assert.ok(
152
+ content.includes('**Active Milestone:** M001: Topic-to-pipeline foundation'),
153
+ `expected single prefix, got: ${content}`,
154
+ );
155
+ });
156
+
157
+ test('#2630 renderStateContent: registry entry with pre-existing ID prefix renders without duplication', () => {
158
+ const state = makeGSDState({
159
+ registry: [
160
+ { id: 'M001', title: 'M001: Topic-to-pipeline foundation', status: 'active' },
161
+ ],
162
+ });
163
+ const content = renderStateContent(state);
164
+
165
+ assert.ok(
166
+ !content.includes('M001: M001:'),
167
+ `found double prefix in registry: ${content}`,
168
+ );
169
+ });
170
+
171
+ // ─── Bug D: PLAN.md slice title double-prefix ──────────────────────────────
172
+ // When sliceRow.title already contains "S04: ", the H1 should NOT become
173
+ // "# S04: S04: Dependency-driven scene pipeline and state truth"
174
+
175
+ test('#2630 renderPlanContent: slice title with pre-existing ID prefix renders without duplication', () => {
176
+ const slice = makeSliceRow({ title: 'S04: Dependency-driven scene pipeline and state truth' });
177
+ const content = renderPlanContent(slice, []);
178
+
179
+ // The H1 must be exactly "# S04: Dependency-driven scene pipeline and state truth"
180
+ assert.ok(
181
+ content.includes('# S04: Dependency-driven scene pipeline and state truth'),
182
+ `expected single prefix in H1, got: ${content.split('\n')[0]}`,
183
+ );
184
+ assert.ok(
185
+ !content.includes('S04: S04:'),
186
+ `found double prefix in PLAN.md H1: ${content.split('\n')[0]}`,
187
+ );
188
+ });
189
+
190
+ test('#2630 renderPlanContent: slice title without prefix still renders correctly', () => {
191
+ const slice = makeSliceRow({ title: 'Dependency-driven scene pipeline and state truth' });
192
+ const content = renderPlanContent(slice, []);
193
+
194
+ assert.ok(
195
+ content.startsWith('# S04: Dependency-driven scene pipeline and state truth'),
196
+ `expected prefixed H1, got: ${content.split('\n')[0]}`,
197
+ );
198
+ });
199
+
200
+ // ─── Bug B: full_uat_md as demo fallback ─────────────────────────────────
201
+ // When slice.demo is empty and full_uat_md is a multi-line UAT document,
202
+ // the renderers must NOT inject the entire UAT body.
203
+
204
+ test('#2630 renderPlanContent: empty demo must not inject full_uat_md body into plan', () => {
205
+ const slice = makeSliceRow({ demo: '' });
206
+ const content = renderPlanContent(slice, []);
207
+
208
+ // The **Demo:** line must be a single line, not multi-line UAT content
209
+ const demoLine = content.split('\n').find(l => l.startsWith('**Demo:**'));
210
+ assert.ok(demoLine, 'should have a Demo line');
211
+
212
+ // Must not contain UAT headings or body
213
+ assert.ok(
214
+ !content.includes('## UAT Type'),
215
+ `plan contains UAT body content: ${content}`,
216
+ );
217
+ assert.ok(
218
+ !content.includes('**Milestone:** M001'),
219
+ `plan contains UAT metadata: ${content}`,
220
+ );
221
+
222
+ // The Demo line must not contain newlines (single line only)
223
+ assert.ok(
224
+ !demoLine!.includes('\n'),
225
+ `Demo line must be single line, got: ${demoLine}`,
226
+ );
227
+ });
228
+
229
+ test('#2630 renderPlanContent: null demo must not inject full_uat_md body into plan', () => {
230
+ const slice = makeSliceRow({ demo: null as unknown as string });
231
+ const content = renderPlanContent(slice, []);
232
+
233
+ assert.ok(
234
+ !content.includes('## UAT Type'),
235
+ `plan contains UAT body content when demo is null`,
236
+ );
237
+ });
238
+
239
+ test('#2630 renderRoadmapContent: empty demo must not inject full_uat_md into roadmap table', () => {
240
+ const milestone = makeMilestoneRow();
241
+ const slices = [makeSliceRow({ demo: '' })];
242
+
243
+ const content = renderRoadmapContent(milestone, slices);
244
+
245
+ // Roadmap table cell for "After this" must be single-line
246
+ assert.ok(
247
+ !content.includes('## UAT Type'),
248
+ `roadmap contains UAT body content: ${content}`,
249
+ );
250
+ assert.ok(
251
+ !content.includes('**Milestone:** M001'),
252
+ `roadmap contains UAT metadata: ${content}`,
253
+ );
254
+
255
+ // The table row containing S04 must be a single line
256
+ const s04Line = content.split('\n').find(l => l.includes('| S04 |'));
257
+ assert.ok(s04Line, 'should have S04 table row');
258
+ assert.ok(
259
+ !s04Line!.includes('# S04:'),
260
+ `roadmap table cell contains UAT heading: ${s04Line}`,
261
+ );
262
+ });
263
+
264
+ test('#2630 renderRoadmapContent: null demo must not inject full_uat_md into roadmap table', () => {
265
+ const milestone = makeMilestoneRow();
266
+ const slices = [makeSliceRow({ demo: null as unknown as string })];
267
+
268
+ const content = renderRoadmapContent(milestone, slices);
269
+
270
+ assert.ok(
271
+ !content.includes('## UAT Type'),
272
+ `roadmap contains UAT body content when demo is null`,
273
+ );
274
+ });
275
+
276
+ test('#2630 renderPlanContent: with valid demo string does not use full_uat_md', () => {
277
+ const slice = makeSliceRow({ demo: 'Login flow works end-to-end' });
278
+ const content = renderPlanContent(slice, []);
279
+
280
+ assert.ok(
281
+ content.includes('**Demo:** After this: Login flow works end-to-end'),
282
+ `expected demo text, got: ${content}`,
283
+ );
284
+ assert.ok(
285
+ !content.includes('UAT'),
286
+ `should not contain UAT when demo is provided`,
287
+ );
288
+ });
@@ -325,7 +325,7 @@ test("auto/phases.ts: selectAndApplyModel called exactly once and before updateP
325
325
  // Extract the runUnitPhase function body
326
326
  const fnStart = src.indexOf("export async function runUnitPhase");
327
327
  assert.ok(fnStart > 0, "runUnitPhase should exist in phases.ts");
328
- const fnBody = src.slice(fnStart, fnStart + 8000);
328
+ const fnBody = src.slice(fnStart, fnStart + 12000);
329
329
 
330
330
  // selectAndApplyModel must appear exactly once
331
331
  const allOccurrences = [...fnBody.matchAll(/selectAndApplyModel\(/g)];
@@ -2107,11 +2107,11 @@ test("autoLoop rejects execute-task with 0 tool calls as hallucinated (#1833)",
2107
2107
  // The task should NOT have been added to completedUnits on the first iteration
2108
2108
  // (0 tool calls), but SHOULD be added on the second iteration (5 tool calls)
2109
2109
  const warningNotification = notifications.find(
2110
- (n) => n.includes("0 tool calls") && n.includes("hallucinated"),
2110
+ (n) => n.includes("0 tool calls") && n.includes("context exhaustion"),
2111
2111
  );
2112
2112
  assert.ok(
2113
2113
  warningNotification,
2114
- "should notify about 0 tool calls hallucination",
2114
+ "should notify about 0 tool calls context exhaustion",
2115
2115
  );
2116
2116
 
2117
2117
  // Verify deriveState was called at least twice (two iterations)
@@ -2122,7 +2122,7 @@ test("autoLoop rejects execute-task with 0 tool calls as hallucinated (#1833)",
2122
2122
  );
2123
2123
  });
2124
2124
 
2125
- test("autoLoop does NOT reject non-execute-task units with 0 tool calls (#1833)", async () => {
2125
+ test("autoLoop rejects complete-slice with 0 tool calls as context-exhausted (#2653)", async () => {
2126
2126
  _resetPendingResolve();
2127
2127
 
2128
2128
  const ctx = makeMockCtx();
@@ -2130,6 +2130,7 @@ test("autoLoop does NOT reject non-execute-task units with 0 tool calls (#1833)"
2130
2130
  ctx.sessionManager = { getSessionFile: () => "/tmp/session.json" };
2131
2131
  const pi = makeMockPi();
2132
2132
 
2133
+ let iterationCount = 0;
2133
2134
  const notifications: string[] = [];
2134
2135
  ctx.ui.notify = (msg: string) => { notifications.push(msg); };
2135
2136
 
@@ -2163,7 +2164,7 @@ test("autoLoop does NOT reject non-execute-task units with 0 tool calls (#1833)"
2163
2164
  };
2164
2165
  },
2165
2166
  closeoutUnit: async () => {
2166
- // complete-slice with 0 tool calls is fine (e.g. it may just update status)
2167
+ // complete-slice with 0 tool calls context exhausted, no progress
2167
2168
  mockLedger.units.push({
2168
2169
  type: "complete-slice",
2169
2170
  id: "M001/S01",
@@ -2177,31 +2178,51 @@ test("autoLoop does NOT reject non-execute-task units with 0 tool calls (#1833)"
2177
2178
  getLedger: () => mockLedger,
2178
2179
  postUnitPostVerification: async () => {
2179
2180
  deps.callLog.push("postUnitPostVerification");
2180
- s.active = false;
2181
+ iterationCount++;
2182
+ // Deactivate after 2nd iteration
2183
+ s.active = iterationCount < 2;
2181
2184
  return "continue" as const;
2182
2185
  },
2183
2186
  });
2184
2187
 
2185
2188
  const loopPromise = autoLoop(ctx, pi, s, deps);
2186
2189
 
2190
+ // First iteration: complete-slice with 0 tool calls → rejected
2187
2191
  await new Promise((r) => setTimeout(r, 50));
2188
2192
  resolveAgentEnd(makeEvent());
2189
2193
 
2194
+ // Second iteration: re-dispatched, this time with tool calls
2195
+ await new Promise((r) => setTimeout(r, 50));
2196
+ mockLedger.units.length = 0;
2197
+ (deps as any).closeoutUnit = async () => {
2198
+ mockLedger.units.push({
2199
+ type: "complete-slice",
2200
+ id: "M001/S01",
2201
+ startedAt: s.currentUnit?.startedAt ?? Date.now(),
2202
+ toolCalls: 3,
2203
+ assistantMessages: 2,
2204
+ tokens: { input: 200, output: 400, total: 600, cacheRead: 0, cacheWrite: 0 },
2205
+ cost: 0.30,
2206
+ });
2207
+ };
2208
+ resolveAgentEnd(makeEvent());
2209
+
2190
2210
  await loopPromise;
2191
2211
 
2192
- // Should NOT have a hallucination warning for non-execute-task units
2212
+ // Should have a warning about 0 tool calls for complete-slice
2193
2213
  const warningNotification = notifications.find(
2194
- (n) => n.includes("0 tool calls") && n.includes("hallucinated"),
2214
+ (n) => n.includes("0 tool calls"),
2195
2215
  );
2196
2216
  assert.ok(
2197
- !warningNotification,
2198
- "should NOT flag non-execute-task units with 0 tool calls",
2217
+ warningNotification,
2218
+ "should flag complete-slice with 0 tool calls as failed (#2653)",
2199
2219
  );
2200
2220
 
2201
- // Verify the loop ran to completion (postUnitPostVerification was called)
2221
+ // Verify deriveState was called at least twice (two iterations: rejected + retry)
2222
+ const deriveCount = deps.callLog.filter((c) => c === "deriveState").length;
2202
2223
  assert.ok(
2203
- deps.callLog.includes("postUnitPostVerification"),
2204
- "complete-slice with 0 tool calls should still complete the post-unit pipeline",
2224
+ deriveCount >= 2,
2225
+ `deriveState should be called at least 2 times for retry (got ${deriveCount})`,
2205
2226
  );
2206
2227
  });
2207
2228
 
@@ -193,6 +193,64 @@ describe("createGridLayout", () => {
193
193
  });
194
194
  });
195
195
 
196
+ describe("CmuxClient stdio isolation", () => {
197
+ test("runSync and runAsync explicitly set stdio to prevent terminal interference", () => {
198
+ // Read the cmux index source and verify that execFileSync/spawn calls
199
+ // inside runSync/runAsync include stdio options that isolate stdin and stderr.
200
+ // This prevents the cmux CLI child process from inheriting the parent's
201
+ // stdin/stderr, which can steal keyboard input or corrupt TUI rendering (#1922).
202
+ const cmuxIndexPath = path.resolve(
203
+ path.dirname(fileURLToPath(import.meta.url)),
204
+ "../../cmux/index.ts",
205
+ );
206
+ const source = fs.readFileSync(cmuxIndexPath, "utf-8");
207
+
208
+ // Extract runSync method body
209
+ const runSyncMatch = source.match(/private runSync\(args: string\[\]\)[^{]*\{([\s\S]*?)\n \}/);
210
+ assert.ok(runSyncMatch, "runSync method must exist");
211
+ const runSyncBody = runSyncMatch[1];
212
+ assert.ok(
213
+ runSyncBody.includes('stdio:'),
214
+ "runSync must explicitly set stdio to prevent terminal interference (see #1922)",
215
+ );
216
+ assert.ok(
217
+ runSyncBody.includes('"ignore"'),
218
+ "runSync stdio must ignore stdin to prevent stealing keyboard input from TUI",
219
+ );
220
+
221
+ // Extract runAsync method body
222
+ const runAsyncMatch = source.match(/private async runAsync\(args: string\[\]\)[^{]*\{([\s\S]*?)\n \}/);
223
+ assert.ok(runAsyncMatch, "runAsync method must exist");
224
+ const runAsyncBody = runAsyncMatch[1];
225
+ assert.ok(
226
+ runAsyncBody.includes('stdio:'),
227
+ "runAsync must explicitly set stdio to prevent terminal interference (see #1922)",
228
+ );
229
+ assert.ok(
230
+ runAsyncBody.includes('"ignore"'),
231
+ "runAsync stdio must ignore stdin to prevent stealing keyboard input from TUI",
232
+ );
233
+ });
234
+
235
+ test("isCmuxCliAvailable uses stdio ignore to prevent terminal interference", () => {
236
+ const cmuxIndexPath = path.resolve(
237
+ path.dirname(fileURLToPath(import.meta.url)),
238
+ "../../cmux/index.ts",
239
+ );
240
+ const source = fs.readFileSync(cmuxIndexPath, "utf-8");
241
+
242
+ // Find isCmuxCliAvailable or the cli-check function body
243
+ const fnMatch = source.match(/function isCmuxCliAvailable[\s\S]*?\{([\s\S]*?)\n\}/);
244
+ if (!fnMatch) return; // function may be inlined or renamed — skip rather than fail
245
+
246
+ const fnBody = fnMatch[1];
247
+ assert.ok(
248
+ fnBody.includes('"ignore"') || !fnBody.includes('execFileSync'),
249
+ "isCmuxCliAvailable must not inherit parent stdio (see #1922)",
250
+ );
251
+ });
252
+ });
253
+
196
254
  describe("cmux extension discovery opt-out", () => {
197
255
  test("cmux directory has package.json with pi manifest to prevent auto-discovery as extension", () => {
198
256
  const cmuxDir = path.resolve(
@@ -0,0 +1,51 @@
1
+ /**
2
+ * cold-resume-db-reopen.test.ts — Regression test for #2940.
3
+ *
4
+ * Validates that the paused-session resume path in auto.ts opens the project
5
+ * database before calling rebuildState() / deriveState(), matching the fresh
6
+ * bootstrap path in auto-start.ts.
7
+ *
8
+ * Without this, cold resume falls back to markdown parsing which misreads
9
+ * done cells and redispatches wrong slices.
10
+ */
11
+
12
+ import { readFileSync } from "node:fs";
13
+ import { join } from "node:path";
14
+
15
+ import { createTestContext } from "./test-helpers.ts";
16
+
17
+ const { assertTrue, report } = createTestContext();
18
+
19
+ const autoSrc = readFileSync(join(import.meta.dirname, "..", "auto.ts"), "utf-8");
20
+
21
+ console.log("\n=== #2940: resume path opens DB before rebuildState/deriveState ===");
22
+
23
+ // The resume block is the `if (s.paused) { ... }` section that calls rebuildState/deriveState.
24
+ // Locate the resume section by finding `s.paused = false;` followed by `rebuildState`.
25
+ const resumeSectionStart = autoSrc.indexOf("if (s.paused) {", autoSrc.indexOf("// If resuming from paused state"));
26
+ assertTrue(resumeSectionStart > 0, "auto.ts has the paused-session resume block");
27
+
28
+ const resumeSection = autoSrc.slice(resumeSectionStart, resumeSectionStart + 3000);
29
+
30
+ // The resume path must open the DB before rebuildState/deriveState
31
+ const rebuildIdx = resumeSection.indexOf("rebuildState(");
32
+ assertTrue(rebuildIdx > 0, "resume block calls rebuildState");
33
+
34
+ const deriveIdx = resumeSection.indexOf("deriveState(");
35
+ assertTrue(deriveIdx > 0, "resume block calls deriveState");
36
+
37
+ // There must be a DB open call before the first rebuildState call
38
+ const dbOpenPatterns = [
39
+ "openProjectDbIfPresent(",
40
+ "openDatabase(",
41
+ "ensureDbOpen(",
42
+ ];
43
+
44
+ const preDeriveSection = resumeSection.slice(0, rebuildIdx);
45
+ const hasDbOpen = dbOpenPatterns.some(pat => preDeriveSection.includes(pat));
46
+ assertTrue(
47
+ hasDbOpen,
48
+ "resume path must open DB before rebuildState/deriveState (#2940)",
49
+ );
50
+
51
+ report();