gsd-pi 2.63.0 → 2.64.0

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 (353) hide show
  1. package/README.md +46 -134
  2. package/dist/cli.js +48 -6
  3. package/dist/headless-query.js +11 -1
  4. package/dist/help-text.js +4 -1
  5. package/dist/onboarding.js +15 -8
  6. package/dist/resource-loader.js +18 -3
  7. package/dist/resources/extensions/cmux/index.js +21 -12
  8. package/dist/resources/extensions/gsd/auto/detect-stuck.js +27 -0
  9. package/dist/resources/extensions/gsd/auto/finalize-timeout.js +40 -0
  10. package/dist/resources/extensions/gsd/auto/loop.js +4 -0
  11. package/dist/resources/extensions/gsd/auto/phases.js +157 -22
  12. package/dist/resources/extensions/gsd/auto/session.js +12 -0
  13. package/dist/resources/extensions/gsd/auto-dashboard.js +9 -3
  14. package/dist/resources/extensions/gsd/auto-model-selection.js +32 -0
  15. package/dist/resources/extensions/gsd/auto-post-unit.js +124 -10
  16. package/dist/resources/extensions/gsd/auto-prompts.js +25 -0
  17. package/dist/resources/extensions/gsd/auto-recovery.js +15 -7
  18. package/dist/resources/extensions/gsd/auto-start.js +10 -21
  19. package/dist/resources/extensions/gsd/auto-timers.js +2 -1
  20. package/dist/resources/extensions/gsd/auto-tool-tracking.js +17 -0
  21. package/dist/resources/extensions/gsd/auto-worktree.js +13 -7
  22. package/dist/resources/extensions/gsd/auto.js +19 -2
  23. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +147 -75
  24. package/dist/resources/extensions/gsd/bootstrap/dynamic-tools.js +13 -0
  25. package/dist/resources/extensions/gsd/bootstrap/query-tools.js +85 -0
  26. package/dist/resources/extensions/gsd/bootstrap/register-extension.js +3 -0
  27. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +32 -1
  28. package/dist/resources/extensions/gsd/bootstrap/sanitize-complete-milestone.js +54 -0
  29. package/dist/resources/extensions/gsd/bootstrap/system-context.js +30 -2
  30. package/dist/resources/extensions/gsd/commands-handlers.js +9 -4
  31. package/dist/resources/extensions/gsd/constants.js +42 -0
  32. package/dist/resources/extensions/gsd/db-writer.js +72 -4
  33. package/dist/resources/extensions/gsd/forensics.js +20 -4
  34. package/dist/resources/extensions/gsd/gsd-db.js +64 -17
  35. package/dist/resources/extensions/gsd/guided-flow.js +19 -0
  36. package/dist/resources/extensions/gsd/metrics.js +27 -1
  37. package/dist/resources/extensions/gsd/native-git-bridge.js +5 -3
  38. package/dist/resources/extensions/gsd/preferences-types.js +2 -0
  39. package/dist/resources/extensions/gsd/preferences.js +7 -2
  40. package/dist/resources/extensions/gsd/prompt-loader.js +7 -0
  41. package/dist/resources/extensions/gsd/prompts/complete-milestone.md +2 -0
  42. package/dist/resources/extensions/gsd/prompts/complete-slice.md +2 -0
  43. package/dist/resources/extensions/gsd/prompts/doctor-heal.md +1 -0
  44. package/dist/resources/extensions/gsd/prompts/forensics.md +2 -0
  45. package/dist/resources/extensions/gsd/prompts/reassess-roadmap.md +2 -0
  46. package/dist/resources/extensions/gsd/prompts/system.md +4 -7
  47. package/dist/resources/extensions/gsd/prompts/validate-milestone.md +2 -0
  48. package/dist/resources/extensions/gsd/roadmap-mutations.js +1 -1
  49. package/dist/resources/extensions/gsd/roadmap-slices.js +9 -5
  50. package/dist/resources/extensions/gsd/safety/content-validator.js +73 -0
  51. package/dist/resources/extensions/gsd/safety/destructive-guard.js +34 -0
  52. package/dist/resources/extensions/gsd/safety/evidence-collector.js +109 -0
  53. package/dist/resources/extensions/gsd/safety/evidence-cross-ref.js +83 -0
  54. package/dist/resources/extensions/gsd/safety/file-change-validator.js +71 -0
  55. package/dist/resources/extensions/gsd/safety/git-checkpoint.js +91 -0
  56. package/dist/resources/extensions/gsd/safety/safety-harness.js +64 -0
  57. package/dist/resources/extensions/gsd/slice-parallel-conflict.js +67 -0
  58. package/dist/resources/extensions/gsd/slice-parallel-eligibility.js +51 -0
  59. package/dist/resources/extensions/gsd/slice-parallel-orchestrator.js +378 -0
  60. package/dist/resources/extensions/gsd/state.js +74 -14
  61. package/dist/resources/extensions/gsd/status-guards.js +11 -0
  62. package/dist/resources/extensions/gsd/tools/complete-milestone.js +17 -12
  63. package/dist/resources/extensions/gsd/tools/complete-slice.js +40 -26
  64. package/dist/resources/extensions/gsd/tools/complete-task.js +12 -12
  65. package/dist/resources/extensions/gsd/tools/plan-milestone.js +33 -25
  66. package/dist/resources/extensions/gsd/tools/plan-slice.js +5 -8
  67. package/dist/resources/extensions/gsd/workflow-projections.js +21 -5
  68. package/dist/resources/extensions/gsd/worktree-manager.js +82 -29
  69. package/dist/resources/extensions/gsd/worktree-resolver.js +4 -3
  70. package/dist/resources/extensions/mcp-client/auth.js +101 -0
  71. package/dist/resources/extensions/mcp-client/index.js +10 -1
  72. package/dist/resources/extensions/ollama/index.js +28 -22
  73. package/dist/resources/extensions/ollama/model-capabilities.js +37 -34
  74. package/dist/resources/extensions/ollama/ndjson-stream.js +54 -0
  75. package/dist/resources/extensions/ollama/ollama-chat-provider.js +380 -0
  76. package/dist/resources/extensions/ollama/ollama-client.js +23 -32
  77. package/dist/resources/extensions/ollama/ollama-discovery.js +2 -7
  78. package/dist/resources/extensions/ollama/ollama-tool.js +62 -0
  79. package/dist/resources/extensions/ollama/thinking-parser.js +104 -0
  80. package/dist/update-cmd.js +4 -2
  81. package/dist/web/standalone/.next/BUILD_ID +1 -1
  82. package/dist/web/standalone/.next/app-path-routes-manifest.json +12 -12
  83. package/dist/web/standalone/.next/build-manifest.json +2 -2
  84. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  85. package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
  86. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  87. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  88. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  89. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  90. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  91. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  92. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  93. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  94. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  95. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  96. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  97. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  98. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  99. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  100. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  101. package/dist/web/standalone/.next/server/app/api/boot/route.js +1 -1
  102. package/dist/web/standalone/.next/server/app/api/boot/route.js.nft.json +1 -1
  103. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js +1 -1
  104. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js.nft.json +1 -1
  105. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js +1 -1
  106. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js.nft.json +1 -1
  107. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js +2 -2
  108. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js.nft.json +1 -1
  109. package/dist/web/standalone/.next/server/app/api/captures/route.js +1 -1
  110. package/dist/web/standalone/.next/server/app/api/captures/route.js.nft.json +1 -1
  111. package/dist/web/standalone/.next/server/app/api/cleanup/route.js +1 -1
  112. package/dist/web/standalone/.next/server/app/api/cleanup/route.js.nft.json +1 -1
  113. package/dist/web/standalone/.next/server/app/api/doctor/route.js +1 -1
  114. package/dist/web/standalone/.next/server/app/api/doctor/route.js.nft.json +1 -1
  115. package/dist/web/standalone/.next/server/app/api/export-data/route.js +1 -1
  116. package/dist/web/standalone/.next/server/app/api/export-data/route.js.nft.json +1 -1
  117. package/dist/web/standalone/.next/server/app/api/files/route.js +1 -1
  118. package/dist/web/standalone/.next/server/app/api/files/route.js.nft.json +1 -1
  119. package/dist/web/standalone/.next/server/app/api/forensics/route.js +1 -1
  120. package/dist/web/standalone/.next/server/app/api/forensics/route.js.nft.json +1 -1
  121. package/dist/web/standalone/.next/server/app/api/git/route.js +1 -1
  122. package/dist/web/standalone/.next/server/app/api/git/route.js.nft.json +1 -1
  123. package/dist/web/standalone/.next/server/app/api/history/route.js +1 -1
  124. package/dist/web/standalone/.next/server/app/api/history/route.js.nft.json +1 -1
  125. package/dist/web/standalone/.next/server/app/api/hooks/route.js +1 -1
  126. package/dist/web/standalone/.next/server/app/api/hooks/route.js.nft.json +1 -1
  127. package/dist/web/standalone/.next/server/app/api/inspect/route.js +1 -1
  128. package/dist/web/standalone/.next/server/app/api/inspect/route.js.nft.json +1 -1
  129. package/dist/web/standalone/.next/server/app/api/knowledge/route.js +1 -1
  130. package/dist/web/standalone/.next/server/app/api/knowledge/route.js.nft.json +1 -1
  131. package/dist/web/standalone/.next/server/app/api/live-state/route.js +1 -1
  132. package/dist/web/standalone/.next/server/app/api/live-state/route.js.nft.json +1 -1
  133. package/dist/web/standalone/.next/server/app/api/onboarding/route.js +1 -1
  134. package/dist/web/standalone/.next/server/app/api/onboarding/route.js.nft.json +1 -1
  135. package/dist/web/standalone/.next/server/app/api/projects/route.js +1 -1
  136. package/dist/web/standalone/.next/server/app/api/projects/route.js.nft.json +1 -1
  137. package/dist/web/standalone/.next/server/app/api/recovery/route.js +1 -1
  138. package/dist/web/standalone/.next/server/app/api/recovery/route.js.nft.json +1 -1
  139. package/dist/web/standalone/.next/server/app/api/session/browser/route.js +1 -1
  140. package/dist/web/standalone/.next/server/app/api/session/browser/route.js.nft.json +1 -1
  141. package/dist/web/standalone/.next/server/app/api/session/command/route.js +1 -1
  142. package/dist/web/standalone/.next/server/app/api/session/command/route.js.nft.json +1 -1
  143. package/dist/web/standalone/.next/server/app/api/session/events/route.js +2 -2
  144. package/dist/web/standalone/.next/server/app/api/session/events/route.js.nft.json +1 -1
  145. package/dist/web/standalone/.next/server/app/api/session/manage/route.js +1 -1
  146. package/dist/web/standalone/.next/server/app/api/session/manage/route.js.nft.json +1 -1
  147. package/dist/web/standalone/.next/server/app/api/settings-data/route.js +1 -1
  148. package/dist/web/standalone/.next/server/app/api/settings-data/route.js.nft.json +1 -1
  149. package/dist/web/standalone/.next/server/app/api/skill-health/route.js +1 -1
  150. package/dist/web/standalone/.next/server/app/api/skill-health/route.js.nft.json +1 -1
  151. package/dist/web/standalone/.next/server/app/api/steer/route.js +1 -1
  152. package/dist/web/standalone/.next/server/app/api/steer/route.js.nft.json +1 -1
  153. package/dist/web/standalone/.next/server/app/api/switch-root/route.js +1 -1
  154. package/dist/web/standalone/.next/server/app/api/switch-root/route.js.nft.json +1 -1
  155. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js +1 -1
  156. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js.nft.json +1 -1
  157. package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js +2 -2
  158. package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js.nft.json +1 -1
  159. package/dist/web/standalone/.next/server/app/api/undo/route.js +1 -1
  160. package/dist/web/standalone/.next/server/app/api/undo/route.js.nft.json +1 -1
  161. package/dist/web/standalone/.next/server/app/api/visualizer/route.js +1 -1
  162. package/dist/web/standalone/.next/server/app/api/visualizer/route.js.nft.json +1 -1
  163. package/dist/web/standalone/.next/server/app/index.html +1 -1
  164. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  165. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  166. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  167. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  168. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  169. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  170. package/dist/web/standalone/.next/server/app-paths-manifest.json +12 -12
  171. package/dist/web/standalone/.next/server/chunks/6897.js +12 -0
  172. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  173. package/dist/web/standalone/.next/server/pages/500.html +2 -2
  174. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  175. package/dist/welcome-screen.js +1 -1
  176. package/package.json +1 -1
  177. package/packages/pi-agent-core/dist/agent-loop.d.ts +8 -0
  178. package/packages/pi-agent-core/dist/agent-loop.d.ts.map +1 -1
  179. package/packages/pi-agent-core/dist/agent-loop.js +50 -0
  180. package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
  181. package/packages/pi-agent-core/src/agent-loop.test.ts +221 -5
  182. package/packages/pi-agent-core/src/agent-loop.ts +53 -0
  183. package/packages/pi-ai/dist/types.d.ts +16 -1
  184. package/packages/pi-ai/dist/types.d.ts.map +1 -1
  185. package/packages/pi-ai/dist/types.js.map +1 -1
  186. package/packages/pi-ai/src/types.ts +18 -1
  187. package/packages/pi-coding-agent/dist/core/auth-storage.d.ts +9 -0
  188. package/packages/pi-coding-agent/dist/core/auth-storage.d.ts.map +1 -1
  189. package/packages/pi-coding-agent/dist/core/auth-storage.js +50 -1
  190. package/packages/pi-coding-agent/dist/core/auth-storage.js.map +1 -1
  191. package/packages/pi-coding-agent/dist/core/auth-storage.test.js +41 -0
  192. package/packages/pi-coding-agent/dist/core/auth-storage.test.js.map +1 -1
  193. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts +7 -0
  194. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  195. package/packages/pi-coding-agent/dist/core/extensions/loader.js +31 -4
  196. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  197. package/packages/pi-coding-agent/dist/core/extensions/loader.test.js +28 -1
  198. package/packages/pi-coding-agent/dist/core/extensions/loader.test.js.map +1 -1
  199. package/packages/pi-coding-agent/dist/core/extensions/provider-registration.test.d.ts +2 -0
  200. package/packages/pi-coding-agent/dist/core/extensions/provider-registration.test.d.ts.map +1 -0
  201. package/packages/pi-coding-agent/dist/core/extensions/provider-registration.test.js +46 -0
  202. package/packages/pi-coding-agent/dist/core/extensions/provider-registration.test.js.map +1 -0
  203. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +2 -0
  204. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
  205. package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
  206. package/packages/pi-coding-agent/dist/core/model-registry.d.ts +1 -0
  207. package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
  208. package/packages/pi-coding-agent/dist/core/model-registry.js +12 -0
  209. package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
  210. package/packages/pi-coding-agent/dist/core/model-resolver.js +3 -3
  211. package/packages/pi-coding-agent/dist/core/model-resolver.js.map +1 -1
  212. package/packages/pi-coding-agent/dist/core/resource-loader.d.ts +23 -1
  213. package/packages/pi-coding-agent/dist/core/resource-loader.d.ts.map +1 -1
  214. package/packages/pi-coding-agent/dist/core/resource-loader.js +80 -56
  215. package/packages/pi-coding-agent/dist/core/resource-loader.js.map +1 -1
  216. package/packages/pi-coding-agent/dist/core/sdk.d.ts.map +1 -1
  217. package/packages/pi-coding-agent/dist/core/sdk.js +9 -0
  218. package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
  219. package/packages/pi-coding-agent/package.json +1 -1
  220. package/packages/pi-coding-agent/src/core/auth-storage.test.ts +53 -0
  221. package/packages/pi-coding-agent/src/core/auth-storage.ts +66 -1
  222. package/packages/pi-coding-agent/src/core/extensions/loader.test.ts +39 -1
  223. package/packages/pi-coding-agent/src/core/extensions/loader.ts +34 -4
  224. package/packages/pi-coding-agent/src/core/extensions/provider-registration.test.ts +81 -0
  225. package/packages/pi-coding-agent/src/core/extensions/types.ts +2 -0
  226. package/packages/pi-coding-agent/src/core/model-registry.ts +14 -0
  227. package/packages/pi-coding-agent/src/core/model-resolver.ts +3 -3
  228. package/packages/pi-coding-agent/src/core/resource-loader.ts +89 -56
  229. package/packages/pi-coding-agent/src/core/sdk.ts +10 -0
  230. package/pkg/package.json +1 -1
  231. package/src/resources/extensions/cmux/index.ts +18 -12
  232. package/src/resources/extensions/gsd/auto/detect-stuck.ts +27 -0
  233. package/src/resources/extensions/gsd/auto/finalize-timeout.ts +46 -0
  234. package/src/resources/extensions/gsd/auto/loop.ts +5 -0
  235. package/src/resources/extensions/gsd/auto/phases.ts +194 -33
  236. package/src/resources/extensions/gsd/auto/session.ts +14 -0
  237. package/src/resources/extensions/gsd/auto-dashboard.ts +11 -3
  238. package/src/resources/extensions/gsd/auto-model-selection.ts +36 -0
  239. package/src/resources/extensions/gsd/auto-post-unit.ts +141 -12
  240. package/src/resources/extensions/gsd/auto-prompts.ts +21 -0
  241. package/src/resources/extensions/gsd/auto-recovery.ts +9 -8
  242. package/src/resources/extensions/gsd/auto-start.ts +11 -20
  243. package/src/resources/extensions/gsd/auto-timers.ts +2 -1
  244. package/src/resources/extensions/gsd/auto-tool-tracking.ts +19 -0
  245. package/src/resources/extensions/gsd/auto-worktree.ts +14 -6
  246. package/src/resources/extensions/gsd/auto.ts +22 -1
  247. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +160 -88
  248. package/src/resources/extensions/gsd/bootstrap/dynamic-tools.ts +15 -0
  249. package/src/resources/extensions/gsd/bootstrap/query-tools.ts +98 -0
  250. package/src/resources/extensions/gsd/bootstrap/register-extension.ts +4 -0
  251. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +36 -1
  252. package/src/resources/extensions/gsd/bootstrap/sanitize-complete-milestone.ts +57 -0
  253. package/src/resources/extensions/gsd/bootstrap/system-context.ts +31 -2
  254. package/src/resources/extensions/gsd/commands-handlers.ts +10 -4
  255. package/src/resources/extensions/gsd/constants.ts +44 -0
  256. package/src/resources/extensions/gsd/db-writer.ts +78 -4
  257. package/src/resources/extensions/gsd/forensics.ts +21 -5
  258. package/src/resources/extensions/gsd/gsd-db.ts +64 -17
  259. package/src/resources/extensions/gsd/guided-flow.ts +22 -0
  260. package/src/resources/extensions/gsd/metrics.ts +28 -1
  261. package/src/resources/extensions/gsd/native-git-bridge.ts +5 -3
  262. package/src/resources/extensions/gsd/preferences-types.ts +16 -0
  263. package/src/resources/extensions/gsd/preferences.ts +9 -2
  264. package/src/resources/extensions/gsd/prompt-loader.ts +8 -0
  265. package/src/resources/extensions/gsd/prompts/complete-milestone.md +2 -0
  266. package/src/resources/extensions/gsd/prompts/complete-slice.md +2 -0
  267. package/src/resources/extensions/gsd/prompts/doctor-heal.md +1 -0
  268. package/src/resources/extensions/gsd/prompts/forensics.md +2 -0
  269. package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +2 -0
  270. package/src/resources/extensions/gsd/prompts/system.md +4 -7
  271. package/src/resources/extensions/gsd/prompts/validate-milestone.md +2 -0
  272. package/src/resources/extensions/gsd/roadmap-mutations.ts +1 -1
  273. package/src/resources/extensions/gsd/roadmap-slices.ts +10 -5
  274. package/src/resources/extensions/gsd/safety/content-validator.ts +98 -0
  275. package/src/resources/extensions/gsd/safety/destructive-guard.ts +49 -0
  276. package/src/resources/extensions/gsd/safety/evidence-collector.ts +151 -0
  277. package/src/resources/extensions/gsd/safety/evidence-cross-ref.ts +120 -0
  278. package/src/resources/extensions/gsd/safety/file-change-validator.ts +108 -0
  279. package/src/resources/extensions/gsd/safety/git-checkpoint.ts +106 -0
  280. package/src/resources/extensions/gsd/safety/safety-harness.ts +105 -0
  281. package/src/resources/extensions/gsd/slice-parallel-conflict.ts +86 -0
  282. package/src/resources/extensions/gsd/slice-parallel-eligibility.ts +73 -0
  283. package/src/resources/extensions/gsd/slice-parallel-orchestrator.ts +477 -0
  284. package/src/resources/extensions/gsd/state.ts +67 -12
  285. package/src/resources/extensions/gsd/status-guards.ts +13 -0
  286. package/src/resources/extensions/gsd/tests/artifact-corruption-2630.test.ts +288 -0
  287. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +34 -13
  288. package/src/resources/extensions/gsd/tests/cmux.test.ts +58 -0
  289. package/src/resources/extensions/gsd/tests/cold-resume-db-reopen.test.ts +51 -0
  290. package/src/resources/extensions/gsd/tests/complete-milestone.test.ts +140 -0
  291. package/src/resources/extensions/gsd/tests/complete-slice-string-coercion.test.ts +211 -0
  292. package/src/resources/extensions/gsd/tests/complete-task.test.ts +39 -0
  293. package/src/resources/extensions/gsd/tests/dashboard-model-label-ordering.test.ts +107 -0
  294. package/src/resources/extensions/gsd/tests/db-access-guardrails.test.ts +109 -0
  295. package/src/resources/extensions/gsd/tests/db-path-worktree-symlink.test.ts +13 -9
  296. package/src/resources/extensions/gsd/tests/db-writer.test.ts +134 -0
  297. package/src/resources/extensions/gsd/tests/deferred-slice-dispatch.test.ts +203 -0
  298. package/src/resources/extensions/gsd/tests/discuss-tool-scoping.test.ts +130 -0
  299. package/src/resources/extensions/gsd/tests/doctor-fix-flag.test.ts +92 -0
  300. package/src/resources/extensions/gsd/tests/finalize-timeout-guard.test.ts +116 -0
  301. package/src/resources/extensions/gsd/tests/flat-rate-routing-guard.test.ts +50 -0
  302. package/src/resources/extensions/gsd/tests/forensics-stuck-loops.test.ts +103 -0
  303. package/src/resources/extensions/gsd/tests/git-checkpoint.test.ts +94 -0
  304. package/src/resources/extensions/gsd/tests/insert-slice-no-wipe.test.ts +88 -0
  305. package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +27 -7
  306. package/src/resources/extensions/gsd/tests/integration/idle-recovery.test.ts +34 -0
  307. package/src/resources/extensions/gsd/tests/metrics.test.ts +116 -1
  308. package/src/resources/extensions/gsd/tests/milestone-status-tool.test.ts +201 -0
  309. package/src/resources/extensions/gsd/tests/plan-milestone-title.test.ts +2 -1
  310. package/src/resources/extensions/gsd/tests/plan-milestone.test.ts +82 -18
  311. package/src/resources/extensions/gsd/tests/preferences.test.ts +10 -0
  312. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +25 -0
  313. package/src/resources/extensions/gsd/tests/roadmap-slices.test.ts +69 -0
  314. package/src/resources/extensions/gsd/tests/shared-wal.test.ts +30 -0
  315. package/src/resources/extensions/gsd/tests/slice-context-injection.test.ts +50 -0
  316. package/src/resources/extensions/gsd/tests/slice-parallel-conflict.test.ts +92 -0
  317. package/src/resources/extensions/gsd/tests/slice-parallel-eligibility.test.ts +95 -0
  318. package/src/resources/extensions/gsd/tests/slice-parallel-orchestrator.test.ts +83 -0
  319. package/src/resources/extensions/gsd/tests/stuck-detection-coverage.test.ts +42 -0
  320. package/src/resources/extensions/gsd/tests/tool-invocation-error-loop-break.test.ts +103 -0
  321. package/src/resources/extensions/gsd/tests/tool-param-optionality.test.ts +349 -0
  322. package/src/resources/extensions/gsd/tests/worktree-health-dispatch.test.ts +35 -2
  323. package/src/resources/extensions/gsd/tests/worktree-health-monorepo.test.ts +73 -0
  324. package/src/resources/extensions/gsd/tests/worktree-resolver.test.ts +34 -0
  325. package/src/resources/extensions/gsd/tests/worktree-submodule-safety.test.ts +1 -1
  326. package/src/resources/extensions/gsd/tests/worktree-teardown-safety.test.ts +148 -0
  327. package/src/resources/extensions/gsd/tools/complete-milestone.ts +34 -20
  328. package/src/resources/extensions/gsd/tools/complete-slice.ts +41 -26
  329. package/src/resources/extensions/gsd/tools/complete-task.ts +12 -12
  330. package/src/resources/extensions/gsd/tools/plan-milestone.ts +55 -30
  331. package/src/resources/extensions/gsd/tools/plan-slice.ts +13 -8
  332. package/src/resources/extensions/gsd/types.ts +44 -22
  333. package/src/resources/extensions/gsd/workflow-logger.ts +2 -1
  334. package/src/resources/extensions/gsd/workflow-projections.ts +23 -5
  335. package/src/resources/extensions/gsd/worktree-manager.ts +76 -28
  336. package/src/resources/extensions/gsd/worktree-resolver.ts +4 -3
  337. package/src/resources/extensions/mcp-client/auth.ts +149 -0
  338. package/src/resources/extensions/mcp-client/index.ts +16 -1
  339. package/src/resources/extensions/ollama/index.ts +26 -25
  340. package/src/resources/extensions/ollama/model-capabilities.ts +41 -34
  341. package/src/resources/extensions/ollama/ndjson-stream.ts +63 -0
  342. package/src/resources/extensions/ollama/ollama-auth-mode.test.ts +20 -0
  343. package/src/resources/extensions/ollama/ollama-chat-provider.ts +459 -0
  344. package/src/resources/extensions/ollama/ollama-client.ts +30 -30
  345. package/src/resources/extensions/ollama/ollama-discovery.ts +5 -8
  346. package/src/resources/extensions/ollama/ollama-tool.ts +69 -0
  347. package/src/resources/extensions/ollama/tests/ollama-chat-provider-stream.test.ts +82 -0
  348. package/src/resources/extensions/ollama/tests/ollama-discovery.test.ts +0 -27
  349. package/src/resources/extensions/ollama/thinking-parser.ts +116 -0
  350. package/src/resources/extensions/ollama/types.ts +23 -0
  351. package/dist/web/standalone/.next/server/chunks/2229.js +0 -12
  352. /package/dist/web/standalone/.next/static/{5FLUBNdqolRyyehCyChPd → eebXKteM9EaWyseHKTjqp}/_buildManifest.js +0 -0
  353. /package/dist/web/standalone/.next/static/{5FLUBNdqolRyyehCyChPd → eebXKteM9EaWyseHKTjqp}/_ssgManifest.js +0 -0
@@ -13,13 +13,20 @@ 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";
27
+ import { resetEvidence } from "../safety/evidence-collector.js";
28
+ import { createCheckpoint, cleanupCheckpoint, rollbackToCheckpoint } from "../safety/git-checkpoint.js";
29
+ import { resolveSafetyHarnessConfig } from "../safety/safety-harness.js";
23
30
  // ─── generateMilestoneReport ──────────────────────────────────────────────────
24
31
  /**
25
32
  * Resolve the base path for milestone reports.
@@ -142,6 +149,50 @@ export async function runPreDispatch(ic, loopState) {
142
149
  mid,
143
150
  statePhase: state.phase,
144
151
  });
152
+ // ── Slice-level parallelism gate (#2340) ─────────────────────────────
153
+ // When slice_parallel is enabled, check if multiple slices are eligible
154
+ // for parallel execution. If so, dispatch them in parallel and stop the
155
+ // sequential loop. Workers are spawned via slice-parallel-orchestrator.ts.
156
+ if (prefs?.slice_parallel?.enabled &&
157
+ mid &&
158
+ !process.env.GSD_PARALLEL_WORKER &&
159
+ isDbAvailable()) {
160
+ try {
161
+ const dbSlices = getMilestoneSlices(mid);
162
+ if (dbSlices.length > 0) {
163
+ const doneIds = new Set(dbSlices.filter(sl => sl.status === "complete" || sl.status === "done").map(sl => sl.id));
164
+ const sliceInputs = dbSlices.map(sl => ({
165
+ id: sl.id,
166
+ done: doneIds.has(sl.id),
167
+ depends: sl.depends ?? [],
168
+ }));
169
+ const eligible = getEligibleSlices(sliceInputs, doneIds);
170
+ if (eligible.length > 1) {
171
+ debugLog("autoLoop", {
172
+ phase: "slice-parallel-dispatch",
173
+ iteration: ic.iteration,
174
+ mid,
175
+ eligibleSlices: eligible.map(e => e.id),
176
+ });
177
+ ctx.ui.notify(`Slice-parallel: dispatching ${eligible.length} eligible slices for ${mid}.`, "info");
178
+ const result = await startSliceParallel(s.basePath, mid, eligible, { maxWorkers: prefs.slice_parallel.max_workers ?? 2 });
179
+ if (result.started.length > 0) {
180
+ ctx.ui.notify(`Slice-parallel: started ${result.started.length} worker(s): ${result.started.join(", ")}.`, "info");
181
+ await deps.stopAuto(ctx, pi, `Slice-parallel dispatched for ${mid}`);
182
+ return { action: "break", reason: "slice-parallel-dispatched" };
183
+ }
184
+ // Fall through to sequential if no workers started
185
+ }
186
+ }
187
+ }
188
+ catch (err) {
189
+ debugLog("autoLoop", {
190
+ phase: "slice-parallel-check-error",
191
+ error: err instanceof Error ? err.message : String(err),
192
+ });
193
+ // Non-fatal — fall through to sequential dispatch
194
+ }
195
+ }
145
196
  // ── Milestone transition ────────────────────────────────────────────
146
197
  if (mid && s.currentMilestoneId && mid !== s.currentMilestoneId) {
147
198
  deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: ic.nextSeq(), eventType: "milestone-transition", data: { from: s.currentMilestoneId, to: mid } });
@@ -669,11 +720,40 @@ export async function runUnitPhase(ic, iterData, loopState, sidecarItem) {
669
720
  }
670
721
  const hasProjectFile = PROJECT_FILES.some((f) => deps.existsSync(join(s.basePath, f)));
671
722
  const hasSrcDir = deps.existsSync(join(s.basePath, "src"));
672
- if (!hasProjectFile && !hasSrcDir) {
723
+ // Xcode bundles have project-specific names (*.xcodeproj, *.xcworkspace)
724
+ // that cannot be matched by exact filename — scan the directory by suffix.
725
+ let hasXcodeBundle = false;
726
+ try {
727
+ const entries = deps.existsSync(s.basePath) ? readdirSync(s.basePath) : [];
728
+ hasXcodeBundle = entries.some((e) => e.endsWith(".xcodeproj") || e.endsWith(".xcworkspace"));
729
+ }
730
+ catch (err) {
731
+ debugLog("runUnitPhase", { phase: "xcode-bundle-scan-failed", basePath: s.basePath, error: String(err) });
732
+ }
733
+ // Monorepo support (#2347): if no project files in the worktree directory,
734
+ // walk parent directories up to the filesystem root. In monorepos,
735
+ // package.json / Cargo.toml etc. live in a parent directory.
736
+ let hasProjectFileInParent = false;
737
+ if (!hasProjectFile && !hasSrcDir && !hasXcodeBundle) {
738
+ let checkDir = dirname(s.basePath);
739
+ const { root } = parsePath(checkDir);
740
+ while (checkDir !== root) {
741
+ // Stop at git repository boundary — ancestors above the repo root
742
+ // (e.g. ~ or /usr/local) may contain unrelated project files.
743
+ if (deps.existsSync(join(checkDir, ".git")))
744
+ break;
745
+ if (PROJECT_FILES.some((f) => deps.existsSync(join(checkDir, f)))) {
746
+ hasProjectFileInParent = true;
747
+ break;
748
+ }
749
+ checkDir = dirname(checkDir);
750
+ }
751
+ }
752
+ if (!hasProjectFile && !hasSrcDir && !hasXcodeBundle && !hasProjectFileInParent) {
673
753
  // Greenfield projects won't have project files yet — the first task creates them.
674
754
  // Log a warning but allow execution to proceed. The .git check above is sufficient
675
755
  // to ensure we're in a valid working directory.
676
- debugLog("runUnitPhase", { phase: "worktree-health-warn-greenfield", basePath: s.basePath, hasProjectFile, hasSrcDir });
756
+ debugLog("runUnitPhase", { phase: "worktree-health-warn-greenfield", basePath: s.basePath, hasProjectFile, hasSrcDir, hasXcodeBundle });
677
757
  ctx.ui.notify(`Warning: ${s.basePath} has no recognized project files — proceeding as greenfield project`, "warning");
678
758
  }
679
759
  }
@@ -683,6 +763,7 @@ export async function runUnitPhase(ic, iterData, loopState, sidecarItem) {
683
763
  s.currentUnit.id === unitId);
684
764
  const previousTier = s.currentUnitRouting?.tier;
685
765
  s.currentUnit = { type: unitType, id: unitId, startedAt: Date.now() };
766
+ s.lastToolInvocationError = null; // #2883: clear stale error from previous unit
686
767
  const unitStartSeq = ic.nextSeq();
687
768
  deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: unitStartSeq, eventType: "unit-start", data: { unitType, unitId } });
688
769
  deps.captureAvailableSkills();
@@ -695,18 +776,22 @@ export async function runUnitPhase(ic, iterData, loopState, sidecarItem) {
695
776
  lastProgressKind: "dispatch",
696
777
  recoveryAttempts: 0, // Reset so re-dispatched units get full recovery budget (#2322)
697
778
  });
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
779
+ // Status bar (widget + preconditions deferred until after model selection see #2899)
705
780
  ctx.ui.setStatus("gsd-auto", "auto");
706
781
  if (mid)
707
782
  deps.updateSliceProgressCache(s.basePath, mid, state.activeSlice?.id);
708
- deps.updateProgressWidget(ctx, unitType, unitId, state);
709
- deps.ensurePreconditions(unitType, unitId, s.basePath, state);
783
+ // ── Safety harness: reset evidence + create checkpoint ──
784
+ const safetyConfig = resolveSafetyHarnessConfig(prefs?.safety_harness);
785
+ if (safetyConfig.enabled && safetyConfig.evidence_collection) {
786
+ resetEvidence();
787
+ }
788
+ // Only checkpoint code-executing units (not lifecycle/planning units)
789
+ if (safetyConfig.enabled && safetyConfig.checkpoints && unitType === "execute-task") {
790
+ s.checkpointSha = createCheckpoint(s.basePath, unitId);
791
+ if (s.checkpointSha) {
792
+ debugLog("runUnitPhase", { phase: "checkpoint-created", unitId, sha: s.checkpointSha.slice(0, 8) });
793
+ }
794
+ }
710
795
  // Prompt injection
711
796
  let finalPrompt = prompt;
712
797
  if (s.pendingVerificationRetry) {
@@ -764,6 +849,12 @@ export async function runUnitPhase(ic, iterData, loopState, sidecarItem) {
764
849
  const msg = reorderErr instanceof Error ? reorderErr.message : String(reorderErr);
765
850
  logWarning("engine", "Prompt reorder failed", { error: msg });
766
851
  }
852
+ // Select and apply model (with tier escalation on retry — normal units only)
853
+ const modelResult = await deps.selectAndApplyModel(ctx, pi, unitType, unitId, s.basePath, prefs, s.verbose, s.autoModeStartModel, sidecarItem ? undefined : { isRetry, previousTier });
854
+ s.currentUnitRouting =
855
+ modelResult.routing;
856
+ s.currentUnitModel =
857
+ modelResult.appliedModel;
767
858
  // Apply sidecar/pre-dispatch hook model override (takes priority over standard model selection)
768
859
  const hookModelOverride = sidecarItem?.model ?? iterData.hookModelOverride;
769
860
  if (hookModelOverride) {
@@ -784,6 +875,15 @@ export async function runUnitPhase(ic, iterData, loopState, sidecarItem) {
784
875
  `Ensure the model is defined in models.json and has auth configured.`, "warning");
785
876
  }
786
877
  }
878
+ // Store the final dispatched model ID so the dashboard can read it (#2899).
879
+ // This accounts for hook model overrides applied after selectAndApplyModel.
880
+ s.currentDispatchedModelId = s.currentUnitModel
881
+ ? `${s.currentUnitModel.provider ?? ""}/${s.currentUnitModel.id ?? ""}`
882
+ : null;
883
+ // Progress widget + preconditions — deferred to after model selection so the
884
+ // widget's first render tick shows the correct model (#2899).
885
+ deps.updateProgressWidget(ctx, unitType, unitId, state);
886
+ deps.ensurePreconditions(unitType, unitId, s.basePath, state);
787
887
  // Start unit supervision
788
888
  deps.clearUnitTimeout();
789
889
  deps.startUnitSupervision({
@@ -860,11 +960,13 @@ export async function runUnitPhase(ic, iterData, loopState, sidecarItem) {
860
960
  if (s.currentUnit) {
861
961
  await deps.closeoutUnit(ctx, s.basePath, unitType, unitId, s.currentUnit.startedAt, deps.buildSnapshotOpts(unitType, unitId));
862
962
  }
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") {
963
+ // ── Zero tool-call guard (#1833, #2653) ──────────────────────────
964
+ // Any unit that completes with 0 tool calls made no real progress —
965
+ // likely context exhaustion where all tool calls errored out. Treat
966
+ // as failed so the unit is retried in a fresh context instead of
967
+ // silently passing through to artifact verification (which loops
968
+ // forever when the unit never produced its artifact).
969
+ {
868
970
  const currentLedger = deps.getLedger();
869
971
  if (currentLedger?.units) {
870
972
  const lastUnit = [...currentLedger.units].reverse().find((u) => u.type === unitType && u.id === unitId && u.startedAt === s.currentUnit?.startedAt);
@@ -873,11 +975,11 @@ export async function runUnitPhase(ic, iterData, loopState, sidecarItem) {
873
975
  phase: "zero-tool-calls",
874
976
  unitType,
875
977
  unitId,
876
- warning: "Task completed with 0 tool calls — likely hallucinated, marking as failed",
978
+ warning: "Unit completed with 0 tool calls — likely context exhaustion, marking as failed",
877
979
  });
878
- ctx.ui.notify(`${unitType} ${unitId} completed with 0 tool calls — hallucinated summary, will retry`, "warning");
980
+ ctx.ui.notify(`${unitType} ${unitId} completed with 0 tool calls — context exhaustion, will retry`, "warning");
879
981
  // Fall through to next iteration where dispatch will re-derive
880
- // and re-dispatch this task.
982
+ // and re-dispatch this unit.
881
983
  return { action: "next", data: { unitStartedAt: s.currentUnit?.startedAt } };
882
984
  }
883
985
  }
@@ -912,6 +1014,25 @@ export async function runUnitPhase(ic, iterData, loopState, sidecarItem) {
912
1014
  }
913
1015
  }
914
1016
  deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: ic.nextSeq(), eventType: "unit-end", data: { unitType, unitId, status: unitResult.status, artifactVerified, ...(unitResult.errorContext ? { errorContext: unitResult.errorContext } : {}) }, causedBy: { flowId: ic.flowId, seq: unitStartSeq } });
1017
+ // ── Safety harness: checkpoint cleanup or rollback ──
1018
+ if (s.checkpointSha) {
1019
+ if (unitResult.status === "error" && safetyConfig.auto_rollback) {
1020
+ const rolled = rollbackToCheckpoint(s.basePath, unitId, s.checkpointSha);
1021
+ if (rolled) {
1022
+ ctx.ui.notify(`Rolled back to pre-unit checkpoint for ${unitId}`, "info");
1023
+ debugLog("runUnitPhase", { phase: "checkpoint-rollback", unitId });
1024
+ }
1025
+ }
1026
+ else if (unitResult.status === "error") {
1027
+ ctx.ui.notify(`Unit ${unitId} failed. Pre-unit checkpoint available at ${s.checkpointSha.slice(0, 8)}`, "warning");
1028
+ }
1029
+ else {
1030
+ // Success — clean up checkpoint ref
1031
+ cleanupCheckpoint(s.basePath, unitId);
1032
+ debugLog("runUnitPhase", { phase: "checkpoint-cleaned", unitId });
1033
+ }
1034
+ s.checkpointSha = null;
1035
+ }
915
1036
  return { action: "next", data: { unitStartedAt: s.currentUnit?.startedAt } };
916
1037
  }
917
1038
  // ─── runFinalize ──────────────────────────────────────────────────────────────
@@ -993,7 +1114,21 @@ export async function runFinalize(ic, iterData, sidecarItem) {
993
1114
  }
994
1115
  }
995
1116
  // Post-verification processing (DB dual-write, hooks, triage, quick-tasks)
996
- const postResult = await deps.postUnitPostVerification(postUnitCtx);
1117
+ // Timeout guard: if postUnitPostVerification hangs (e.g., module import
1118
+ // deadlock, SQLite transaction hang), force-continue after timeout so the
1119
+ // auto-loop is not permanently frozen (#2344).
1120
+ const postResultGuard = await withTimeout(deps.postUnitPostVerification(postUnitCtx), FINALIZE_POST_TIMEOUT_MS, "postUnitPostVerification");
1121
+ if (postResultGuard.timedOut) {
1122
+ debugLog("autoLoop", {
1123
+ phase: "post-verification-timeout",
1124
+ iteration: ic.iteration,
1125
+ unitType: iterData.unitType,
1126
+ unitId: iterData.unitId,
1127
+ });
1128
+ ctx.ui.notify(`postUnitPostVerification timed out after ${FINALIZE_POST_TIMEOUT_MS / 1000}s for ${iterData.unitType} ${iterData.unitId} — continuing to next iteration`, "warning");
1129
+ return { action: "next", data: undefined };
1130
+ }
1131
+ const postResult = postResultGuard.value;
997
1132
  if (postResult === "stopped") {
998
1133
  debugLog("autoLoop", {
999
1134
  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;
@@ -76,6 +82,9 @@ export class AutoSession {
76
82
  lastPromptCharCount;
77
83
  lastBaselineCharCount;
78
84
  pendingQuickTasks = [];
85
+ // ── Safety harness ───────────────────────────────────────────────────────
86
+ /** SHA of the pre-unit git checkpoint ref. Cleared on success or rollback. */
87
+ checkpointSha = null;
79
88
  // ── Signal handler ───────────────────────────────────────────────────────
80
89
  sigtermHandler = null;
81
90
  // ── Loop promise state ──────────────────────────────────────────────────
@@ -132,6 +141,7 @@ export class AutoSession {
132
141
  // Model
133
142
  this.autoModeStartModel = null;
134
143
  this.currentUnitModel = null;
144
+ this.currentDispatchedModelId = null;
135
145
  this.originalModelId = null;
136
146
  this.originalModelProvider = null;
137
147
  this.lastBudgetAlertLevel = 0;
@@ -149,8 +159,10 @@ export class AutoSession {
149
159
  this.pendingQuickTasks = [];
150
160
  this.sidecarQueue = [];
151
161
  this.rewriteAttemptCount = 0;
162
+ this.lastToolInvocationError = null;
152
163
  this.isolationDegraded = false;
153
164
  this.milestoneMergedInPhases = false;
165
+ this.checkpointSha = null;
154
166
  // Signal handler
155
167
  this.sigtermHandler = null;
156
168
  // Loop promise state lives in auto-loop.ts module scope
@@ -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}`
@@ -15,6 +15,9 @@ export function resolvePreferredModelConfig(unitType, autoModeStartModel) {
15
15
  const routingConfig = resolveDynamicRoutingConfig();
16
16
  if (!routingConfig.enabled || !routingConfig.tier_models)
17
17
  return undefined;
18
+ // Don't synthesize a routing config for flat-rate providers (#3453).
19
+ if (autoModeStartModel && isFlatRateProvider(autoModeStartModel.provider))
20
+ return undefined;
18
21
  const ceilingModel = routingConfig.tier_models.heavy
19
22
  ?? (autoModeStartModel ? `${autoModeStartModel.provider}/${autoModeStartModel.id}` : undefined);
20
23
  if (!ceilingModel)
@@ -41,6 +44,25 @@ export async function selectAndApplyModel(ctx, pi, unitType, unitId, basePath, p
41
44
  const routingConfig = resolveDynamicRoutingConfig();
42
45
  let effectiveModelConfig = modelConfig;
43
46
  let routingTierLabel = "";
47
+ // Disable routing for flat-rate providers like GitHub Copilot (#3453).
48
+ // All models cost the same per request, so downgrading to a cheaper
49
+ // model provides no cost benefit — it only degrades quality.
50
+ // Fail-closed: if primary model can't be resolved, fall back to
51
+ // provider-level signals rather than allowing unwanted downgrades.
52
+ if (routingConfig.enabled) {
53
+ const primaryModel = resolveModelId(modelConfig.primary, availableModels, ctx.model?.provider);
54
+ if (primaryModel) {
55
+ if (isFlatRateProvider(primaryModel.provider)) {
56
+ routingConfig.enabled = false;
57
+ }
58
+ }
59
+ else if ((autoModeStartModel && isFlatRateProvider(autoModeStartModel.provider))
60
+ || (ctx.model?.provider && isFlatRateProvider(ctx.model.provider))) {
61
+ // Primary model unresolvable but provider signals indicate flat-rate —
62
+ // disable routing to prevent quality degradation.
63
+ routingConfig.enabled = false;
64
+ }
65
+ }
44
66
  if (routingConfig.enabled) {
45
67
  let budgetPct;
46
68
  if (routingConfig.budget_pressure !== false) {
@@ -244,3 +266,13 @@ export function resolveModelId(modelId, availableModels, currentProvider) {
244
266
  // Fall back to first non-extension candidate, or any candidate
245
267
  return candidates.find(m => !EXTENSION_PROVIDERS.has(m.provider)) ?? candidates[0];
246
268
  }
269
+ /**
270
+ * Flat-rate providers charge the same per request regardless of model.
271
+ * Dynamic routing provides no cost benefit — it only degrades quality (#3453).
272
+ * Uses case-insensitive matching with alias support to prevent fail-open on
273
+ * provider naming variations (e.g. "copilot" vs "github-copilot").
274
+ */
275
+ const FLAT_RATE_PROVIDERS = new Set(["github-copilot", "copilot"]);
276
+ export function isFlatRateProvider(provider) {
277
+ return FLAT_RATE_PROVIDERS.has(provider.toLowerCase());
278
+ }
@@ -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,15 @@ 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
+ import { getEvidence } from "./safety/evidence-collector.js";
33
+ import { validateFileChanges } from "./safety/file-change-validator.js";
34
+ // crossReferenceEvidence available for future use when verification_evidence is stored in DB
35
+ // import { crossReferenceEvidence, type ClaimedEvidence } from "./safety/evidence-cross-ref.js";
36
+ import { validateContent } from "./safety/content-validator.js";
37
+ import { resolveSafetyHarnessConfig } from "./safety/safety-harness.js";
38
+ import { resolveExpectedArtifactPath as resolveArtifactForContent } from "./auto-artifact-paths.js";
39
+ /** Maximum verification retry attempts before escalating to blocker placeholder (#2653). */
40
+ const MAX_VERIFICATION_RETRIES = 3;
32
41
  /** Enqueue a sidecar item (hook, triage, or quick-task) for the main loop to
33
42
  * drain via runUnit. Logs the enqueue event and notifies the UI. */
34
43
  function enqueueSidecar(s, ctx, entry, debugExtra, notification) {
@@ -339,6 +348,78 @@ export async function postUnitPreVerification(pctx, opts) {
339
348
  catch (e) {
340
349
  debugLog("postUnit", { phase: "rogue-detection", error: String(e) });
341
350
  }
351
+ // ── Safety harness: post-unit validation ──
352
+ try {
353
+ const { loadEffectiveGSDPreferences } = await import("./preferences.js");
354
+ const prefs = loadEffectiveGSDPreferences()?.preferences;
355
+ const safetyConfig = resolveSafetyHarnessConfig(prefs?.safety_harness);
356
+ if (safetyConfig.enabled) {
357
+ const { milestone: sMid, slice: sSid, task: sTid } = parseUnitId(s.currentUnit.id);
358
+ // File change validation (execute-task only, after auto-commit)
359
+ if (safetyConfig.file_change_validation && s.currentUnit.type === "execute-task" && sMid && sSid && sTid && isDbAvailable()) {
360
+ try {
361
+ const taskRow = getTask(sMid, sSid, sTid);
362
+ if (taskRow) {
363
+ const expectedOutput = taskRow.expected_output ?? [];
364
+ const plannedFiles = taskRow.files ?? [];
365
+ const audit = validateFileChanges(s.basePath, expectedOutput, plannedFiles);
366
+ if (audit && audit.violations.length > 0) {
367
+ const warnings = audit.violations.filter(v => v.severity === "warning");
368
+ for (const v of warnings) {
369
+ logWarning("safety", `file-change: ${v.file} — ${v.reason}`);
370
+ }
371
+ if (warnings.length > 0) {
372
+ ctx.ui.notify(`Safety: ${warnings.length} unexpected file change(s) outside task plan`, "warning");
373
+ }
374
+ }
375
+ }
376
+ }
377
+ catch (e) {
378
+ debugLog("postUnit", { phase: "safety-file-change", error: String(e) });
379
+ }
380
+ }
381
+ // Evidence cross-reference (execute-task only)
382
+ // Verification evidence is passed via the complete-task tool call and
383
+ // stored in the SUMMARY.md on disk — not available as structured data
384
+ // in the DB. The evidence collector tracks actual bash tool calls, so
385
+ // we can still detect units that claimed success but ran no commands.
386
+ if (safetyConfig.evidence_cross_reference && s.currentUnit.type === "execute-task") {
387
+ try {
388
+ const actual = getEvidence();
389
+ const bashCalls = actual.filter(e => e.kind === "bash");
390
+ // If the task is marked complete but zero bash commands were run,
391
+ // it's suspicious — the LLM may have fabricated results.
392
+ if (sMid && sSid && sTid && isDbAvailable()) {
393
+ const taskRow = getTask(sMid, sSid, sTid);
394
+ if (taskRow?.status === "complete" && taskRow.verify && bashCalls.length === 0) {
395
+ logWarning("safety", "task marked complete with verification commands but no bash calls were executed");
396
+ ctx.ui.notify(`Safety: task ${sTid} has verification commands but no bash calls were recorded`, "warning");
397
+ }
398
+ }
399
+ }
400
+ catch (e) {
401
+ debugLog("postUnit", { phase: "safety-evidence-xref", error: String(e) });
402
+ }
403
+ }
404
+ // Content validation (plan-slice, plan-milestone)
405
+ if (safetyConfig.content_validation) {
406
+ try {
407
+ const artifactPath = resolveArtifactForContent(s.currentUnit.type, s.currentUnit.id, s.basePath);
408
+ const contentViolations = validateContent(s.currentUnit.type, artifactPath);
409
+ for (const v of contentViolations) {
410
+ logWarning("safety", `content: ${v.reason}`);
411
+ ctx.ui.notify(`Content validation: ${v.reason}`, "warning");
412
+ }
413
+ }
414
+ catch (e) {
415
+ debugLog("postUnit", { phase: "safety-content-validation", error: String(e) });
416
+ }
417
+ }
418
+ }
419
+ }
420
+ catch (e) {
421
+ debugLog("postUnit", { phase: "safety-harness", error: String(e) });
422
+ }
342
423
  // Artifact verification
343
424
  let triggerArtifactVerified = false;
344
425
  if (!s.currentUnit.type.startsWith("hook/")) {
@@ -374,6 +455,8 @@ export async function postUnitPreVerification(pctx, opts) {
374
455
  // When artifact verification fails for a unit type that has a known expected
375
456
  // artifact, return "retry" so the caller re-dispatches with failure context
376
457
  // instead of blindly re-dispatching the same unit (#1571).
458
+ // After MAX_VERIFICATION_RETRIES, escalate to writeBlockerPlaceholder so the
459
+ // pipeline can advance instead of looping forever (#2653).
377
460
  //
378
461
  // HOWEVER, if the DB is unavailable (db_unavailable), the artifact was never
379
462
  // written because the completion tool failed at the infra level. Retrying
@@ -387,20 +470,51 @@ export async function postUnitPreVerification(pctx, opts) {
387
470
  ctx.ui.notify(`Artifact missing for ${s.currentUnit.type} ${s.currentUnit.id} — DB unavailable, skipping retry.${dbSkipDiag ? ` Expected: ${dbSkipDiag}` : ""}`, "error");
388
471
  }
389
472
  else if (!triggerArtifactVerified) {
473
+ // #2883: If the artifact is missing because the tool invocation itself
474
+ // failed (malformed/truncated JSON arguments), retrying will produce the
475
+ // same failure. Pause auto-mode instead of entering a stuck retry loop.
476
+ if (s.lastToolInvocationError) {
477
+ const errMsg = `Tool invocation failed for ${s.currentUnit.type}: ${s.lastToolInvocationError}. Structured argument generation failed — pausing auto-mode.`;
478
+ debugLog("postUnit", { phase: "tool-invocation-error-pause", unitType: s.currentUnit.type, unitId: s.currentUnit.id, error: s.lastToolInvocationError });
479
+ ctx.ui.notify(errMsg, "error");
480
+ s.lastToolInvocationError = null;
481
+ await pauseAuto(ctx, pi);
482
+ return "dispatched";
483
+ }
390
484
  const hasExpectedArtifact = resolveExpectedArtifactPath(s.currentUnit.type, s.currentUnit.id, s.basePath) !== null;
391
485
  if (hasExpectedArtifact) {
392
486
  const retryKey = `${s.currentUnit.type}:${s.currentUnit.id}`;
393
487
  const attempt = (s.verificationRetryCount.get(retryKey) ?? 0) + 1;
394
488
  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";
489
+ if (attempt > MAX_VERIFICATION_RETRIES) {
490
+ // Retries exhausted — write a blocker placeholder so the pipeline
491
+ // can advance past this stuck unit (#2653).
492
+ debugLog("postUnit", {
493
+ phase: "artifact-verify-escalate",
494
+ unitType: s.currentUnit.type,
495
+ unitId: s.currentUnit.id,
496
+ attempt,
497
+ maxRetries: MAX_VERIFICATION_RETRIES,
498
+ });
499
+ const reason = `Artifact verification failed after ${MAX_VERIFICATION_RETRIES} retries for ${s.currentUnit.type} "${s.currentUnit.id}".`;
500
+ writeBlockerPlaceholder(s.currentUnit.type, s.currentUnit.id, s.basePath, reason);
501
+ ctx.ui.notify(`${s.currentUnit.type} ${s.currentUnit.id} — verification retries exhausted (${MAX_VERIFICATION_RETRIES}), wrote blocker placeholder to advance pipeline`, "warning");
502
+ // Reset retry count and fall through to "continue" so the loop
503
+ // re-derives state with the placeholder in place.
504
+ s.verificationRetryCount.delete(retryKey);
505
+ s.pendingVerificationRetry = null;
506
+ // Do NOT return "retry" — fall through to "continue" below.
507
+ }
508
+ else {
509
+ s.pendingVerificationRetry = {
510
+ unitId: s.currentUnit.id,
511
+ failureContext: `Artifact verification failed: expected artifact for ${s.currentUnit.type} "${s.currentUnit.id}" was not found on disk after unit execution (attempt ${attempt}).`,
512
+ attempt,
513
+ };
514
+ debugLog("postUnit", { phase: "artifact-verify-retry", unitType: s.currentUnit.type, unitId: s.currentUnit.id, attempt });
515
+ ctx.ui.notify(`Artifact missing for ${s.currentUnit.type} ${s.currentUnit.id} — retrying (attempt ${attempt})`, "warning");
516
+ return "retry";
517
+ }
404
518
  }
405
519
  }
406
520
  }
@@ -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);