@opengsd/gsd-pi 1.1.1-dev.b2556262 → 1.2.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 (277) hide show
  1. package/dist/project-sessions.js +4 -2
  2. package/dist/resources/.managed-resources-content-hash +1 -1
  3. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +17 -9
  4. package/dist/resources/extensions/gsd/auto/contracts.js +8 -1
  5. package/dist/resources/extensions/gsd/auto/orchestrator.js +659 -57
  6. package/dist/resources/extensions/gsd/auto-prompts.js +110 -1
  7. package/dist/resources/extensions/gsd/auto-runtime-state.js +3 -0
  8. package/dist/resources/extensions/gsd/auto-tool-tracking.js +5 -0
  9. package/dist/resources/extensions/gsd/auto-unit-tool-scope.js +29 -0
  10. package/dist/resources/extensions/gsd/auto.js +62 -464
  11. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +4 -1
  12. package/dist/resources/extensions/gsd/debug-logger.js +10 -0
  13. package/dist/resources/extensions/gsd/doctor-proactive.js +7 -2
  14. package/dist/resources/extensions/gsd/guided-flow.js +2 -2
  15. package/dist/resources/extensions/gsd/markdown-renderer.js +31 -32
  16. package/dist/resources/extensions/gsd/mcp-filter.js +6 -0
  17. package/dist/resources/extensions/gsd/native-git-bridge.js +9 -0
  18. package/dist/resources/extensions/gsd/prompts/discuss.md +6 -7
  19. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +5 -7
  20. package/dist/resources/extensions/gsd/prompts/guided-discuss-project.md +3 -5
  21. package/dist/resources/extensions/gsd/prompts/guided-discuss-requirements.md +1 -2
  22. package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +5 -6
  23. package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  24. package/dist/resources/extensions/gsd/prompts/research-milestone.md +2 -2
  25. package/dist/resources/extensions/gsd/prompts/validate-milestone.md +5 -3
  26. package/dist/resources/extensions/gsd/schemas/parsers.js +6 -1
  27. package/dist/resources/extensions/gsd/state-reconciliation/drift/artifact-db.js +21 -1
  28. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +169 -20
  29. package/dist/resources/extensions/gsd/user-input-boundary.js +42 -4
  30. package/dist/web/standalone/.next/BUILD_ID +1 -1
  31. package/dist/web/standalone/.next/app-path-routes-manifest.json +6 -6
  32. package/dist/web/standalone/.next/build-manifest.json +3 -3
  33. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  34. package/dist/web/standalone/.next/required-server-files.json +3 -3
  35. package/dist/web/standalone/.next/server/app/_global-error/page.js +3 -3
  36. package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  37. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  38. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/_not-found/page.js +2 -2
  46. package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  47. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  48. package/dist/web/standalone/.next/server/app/_not-found.rsc +3 -3
  49. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
  50. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +3 -3
  52. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/api/boot/route.js +1 -1
  56. package/dist/web/standalone/.next/server/app/api/boot/route.js.nft.json +1 -1
  57. package/dist/web/standalone/.next/server/app/api/boot/route_client-reference-manifest.js +1 -1
  58. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js +1 -1
  59. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js.nft.json +1 -1
  60. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route_client-reference-manifest.js +1 -1
  61. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js +1 -1
  62. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js.nft.json +1 -1
  63. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route_client-reference-manifest.js +1 -1
  64. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js +2 -2
  65. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js.nft.json +1 -1
  66. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route_client-reference-manifest.js +1 -1
  67. package/dist/web/standalone/.next/server/app/api/browse-directories/route.js +1 -1
  68. package/dist/web/standalone/.next/server/app/api/browse-directories/route_client-reference-manifest.js +1 -1
  69. package/dist/web/standalone/.next/server/app/api/captures/route.js +1 -1
  70. package/dist/web/standalone/.next/server/app/api/captures/route.js.nft.json +1 -1
  71. package/dist/web/standalone/.next/server/app/api/captures/route_client-reference-manifest.js +1 -1
  72. package/dist/web/standalone/.next/server/app/api/cleanup/route.js +1 -1
  73. package/dist/web/standalone/.next/server/app/api/cleanup/route.js.nft.json +1 -1
  74. package/dist/web/standalone/.next/server/app/api/cleanup/route_client-reference-manifest.js +1 -1
  75. package/dist/web/standalone/.next/server/app/api/dev-mode/route.js +1 -1
  76. package/dist/web/standalone/.next/server/app/api/dev-mode/route_client-reference-manifest.js +1 -1
  77. package/dist/web/standalone/.next/server/app/api/doctor/route.js +1 -1
  78. package/dist/web/standalone/.next/server/app/api/doctor/route.js.nft.json +1 -1
  79. package/dist/web/standalone/.next/server/app/api/doctor/route_client-reference-manifest.js +1 -1
  80. package/dist/web/standalone/.next/server/app/api/experimental/route.js +2 -2
  81. package/dist/web/standalone/.next/server/app/api/experimental/route_client-reference-manifest.js +1 -1
  82. package/dist/web/standalone/.next/server/app/api/export-data/route.js +1 -1
  83. package/dist/web/standalone/.next/server/app/api/export-data/route.js.nft.json +1 -1
  84. package/dist/web/standalone/.next/server/app/api/export-data/route_client-reference-manifest.js +1 -1
  85. package/dist/web/standalone/.next/server/app/api/files/route.js +1 -1
  86. package/dist/web/standalone/.next/server/app/api/files/route.js.nft.json +1 -1
  87. package/dist/web/standalone/.next/server/app/api/files/route_client-reference-manifest.js +1 -1
  88. package/dist/web/standalone/.next/server/app/api/forensics/route.js +1 -1
  89. package/dist/web/standalone/.next/server/app/api/forensics/route.js.nft.json +1 -1
  90. package/dist/web/standalone/.next/server/app/api/forensics/route_client-reference-manifest.js +1 -1
  91. package/dist/web/standalone/.next/server/app/api/git/route.js +1 -1
  92. package/dist/web/standalone/.next/server/app/api/git/route.js.nft.json +1 -1
  93. package/dist/web/standalone/.next/server/app/api/git/route_client-reference-manifest.js +1 -1
  94. package/dist/web/standalone/.next/server/app/api/history/route.js +1 -1
  95. package/dist/web/standalone/.next/server/app/api/history/route.js.nft.json +1 -1
  96. package/dist/web/standalone/.next/server/app/api/history/route_client-reference-manifest.js +1 -1
  97. package/dist/web/standalone/.next/server/app/api/hooks/route.js +1 -1
  98. package/dist/web/standalone/.next/server/app/api/hooks/route.js.nft.json +1 -1
  99. package/dist/web/standalone/.next/server/app/api/hooks/route_client-reference-manifest.js +1 -1
  100. package/dist/web/standalone/.next/server/app/api/inspect/route.js +1 -1
  101. package/dist/web/standalone/.next/server/app/api/inspect/route.js.nft.json +1 -1
  102. package/dist/web/standalone/.next/server/app/api/inspect/route_client-reference-manifest.js +1 -1
  103. package/dist/web/standalone/.next/server/app/api/knowledge/route.js +1 -1
  104. package/dist/web/standalone/.next/server/app/api/knowledge/route.js.nft.json +1 -1
  105. package/dist/web/standalone/.next/server/app/api/knowledge/route_client-reference-manifest.js +1 -1
  106. package/dist/web/standalone/.next/server/app/api/live-state/route.js +1 -1
  107. package/dist/web/standalone/.next/server/app/api/live-state/route.js.nft.json +1 -1
  108. package/dist/web/standalone/.next/server/app/api/live-state/route_client-reference-manifest.js +1 -1
  109. package/dist/web/standalone/.next/server/app/api/mcp-connections/route.js +1 -1
  110. package/dist/web/standalone/.next/server/app/api/mcp-connections/route.js.nft.json +1 -1
  111. package/dist/web/standalone/.next/server/app/api/mcp-connections/route_client-reference-manifest.js +1 -1
  112. package/dist/web/standalone/.next/server/app/api/notifications/route.js +2 -2
  113. package/dist/web/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
  114. package/dist/web/standalone/.next/server/app/api/notifications/route_client-reference-manifest.js +1 -1
  115. package/dist/web/standalone/.next/server/app/api/onboarding/route.js +1 -1
  116. package/dist/web/standalone/.next/server/app/api/onboarding/route.js.nft.json +1 -1
  117. package/dist/web/standalone/.next/server/app/api/onboarding/route_client-reference-manifest.js +1 -1
  118. package/dist/web/standalone/.next/server/app/api/preferences/route.js +1 -1
  119. package/dist/web/standalone/.next/server/app/api/preferences/route_client-reference-manifest.js +1 -1
  120. package/dist/web/standalone/.next/server/app/api/projects/route.js +1 -1
  121. package/dist/web/standalone/.next/server/app/api/projects/route.js.nft.json +1 -1
  122. package/dist/web/standalone/.next/server/app/api/projects/route_client-reference-manifest.js +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/recovery/route_client-reference-manifest.js +1 -1
  126. package/dist/web/standalone/.next/server/app/api/remote-questions/route.js +2 -2
  127. package/dist/web/standalone/.next/server/app/api/remote-questions/route_client-reference-manifest.js +1 -1
  128. package/dist/web/standalone/.next/server/app/api/session/browser/route.js +1 -1
  129. package/dist/web/standalone/.next/server/app/api/session/browser/route.js.nft.json +1 -1
  130. package/dist/web/standalone/.next/server/app/api/session/browser/route_client-reference-manifest.js +1 -1
  131. package/dist/web/standalone/.next/server/app/api/session/command/route.js +1 -1
  132. package/dist/web/standalone/.next/server/app/api/session/command/route.js.nft.json +1 -1
  133. package/dist/web/standalone/.next/server/app/api/session/command/route_client-reference-manifest.js +1 -1
  134. package/dist/web/standalone/.next/server/app/api/session/events/route.js +2 -2
  135. package/dist/web/standalone/.next/server/app/api/session/events/route.js.nft.json +1 -1
  136. package/dist/web/standalone/.next/server/app/api/session/events/route_client-reference-manifest.js +1 -1
  137. package/dist/web/standalone/.next/server/app/api/session/manage/route.js +1 -1
  138. package/dist/web/standalone/.next/server/app/api/session/manage/route.js.nft.json +1 -1
  139. package/dist/web/standalone/.next/server/app/api/session/manage/route_client-reference-manifest.js +1 -1
  140. package/dist/web/standalone/.next/server/app/api/settings-data/route.js +1 -1
  141. package/dist/web/standalone/.next/server/app/api/settings-data/route.js.nft.json +1 -1
  142. package/dist/web/standalone/.next/server/app/api/settings-data/route_client-reference-manifest.js +1 -1
  143. package/dist/web/standalone/.next/server/app/api/shutdown/route.js +1 -1
  144. package/dist/web/standalone/.next/server/app/api/shutdown/route.js.nft.json +1 -1
  145. package/dist/web/standalone/.next/server/app/api/shutdown/route_client-reference-manifest.js +1 -1
  146. package/dist/web/standalone/.next/server/app/api/skill-health/route.js +1 -1
  147. package/dist/web/standalone/.next/server/app/api/skill-health/route.js.nft.json +1 -1
  148. package/dist/web/standalone/.next/server/app/api/skill-health/route_client-reference-manifest.js +1 -1
  149. package/dist/web/standalone/.next/server/app/api/steer/route.js +1 -1
  150. package/dist/web/standalone/.next/server/app/api/steer/route.js.nft.json +1 -1
  151. package/dist/web/standalone/.next/server/app/api/steer/route_client-reference-manifest.js +1 -1
  152. package/dist/web/standalone/.next/server/app/api/switch-root/route.js +1 -1
  153. package/dist/web/standalone/.next/server/app/api/switch-root/route.js.nft.json +1 -1
  154. package/dist/web/standalone/.next/server/app/api/switch-root/route_client-reference-manifest.js +1 -1
  155. package/dist/web/standalone/.next/server/app/api/terminal/input/route.js +1 -1
  156. package/dist/web/standalone/.next/server/app/api/terminal/input/route_client-reference-manifest.js +1 -1
  157. package/dist/web/standalone/.next/server/app/api/terminal/resize/route.js +2 -2
  158. package/dist/web/standalone/.next/server/app/api/terminal/resize/route_client-reference-manifest.js +1 -1
  159. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js +1 -1
  160. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js.nft.json +1 -1
  161. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route_client-reference-manifest.js +1 -1
  162. package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js +4 -4
  163. package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js.nft.json +1 -1
  164. package/dist/web/standalone/.next/server/app/api/terminal/stream/route_client-reference-manifest.js +1 -1
  165. package/dist/web/standalone/.next/server/app/api/terminal/upload/route.js +1 -1
  166. package/dist/web/standalone/.next/server/app/api/terminal/upload/route_client-reference-manifest.js +1 -1
  167. package/dist/web/standalone/.next/server/app/api/undo/route.js +1 -1
  168. package/dist/web/standalone/.next/server/app/api/undo/route.js.nft.json +1 -1
  169. package/dist/web/standalone/.next/server/app/api/undo/route_client-reference-manifest.js +1 -1
  170. package/dist/web/standalone/.next/server/app/api/update/route.js +1 -1
  171. package/dist/web/standalone/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
  172. package/dist/web/standalone/.next/server/app/api/visualizer/route.js +1 -1
  173. package/dist/web/standalone/.next/server/app/api/visualizer/route.js.nft.json +1 -1
  174. package/dist/web/standalone/.next/server/app/api/visualizer/route_client-reference-manifest.js +1 -1
  175. package/dist/web/standalone/.next/server/app/index.html +1 -1
  176. package/dist/web/standalone/.next/server/app/index.rsc +4 -4
  177. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  178. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +4 -4
  179. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  180. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +3 -3
  181. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  182. package/dist/web/standalone/.next/server/app/page.js +2 -2
  183. package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  184. package/dist/web/standalone/.next/server/app-paths-manifest.json +6 -6
  185. package/dist/web/standalone/.next/server/chunks/2842.js +4 -4
  186. package/dist/web/standalone/.next/server/chunks/5047.js +2 -0
  187. package/dist/web/standalone/.next/server/chunks/5124.js +1 -0
  188. package/dist/web/standalone/.next/server/chunks/8357.js +3 -3
  189. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  190. package/dist/web/standalone/.next/server/middleware.js +3 -3
  191. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  192. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  193. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  194. package/dist/web/standalone/.next/static/chunks/app/_not-found/page-49f565245e1e4afe.js +1 -0
  195. package/dist/web/standalone/.next/static/chunks/app/{layout-4ae2d68984392bbf.js → layout-b35cbfff38aaf4cf.js} +1 -1
  196. package/dist/web/standalone/.next/static/chunks/app/page-a48b7c48333b31c8.js +1 -0
  197. package/dist/web/standalone/.next/static/chunks/{main-app-90d1d8d5e5d2dc6b.js → main-app-590a74400e35f685.js} +1 -1
  198. package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-ec3eaffc9785ba48.js +1 -0
  199. package/dist/web/standalone/node_modules/@gsd/native/package.json +1 -1
  200. package/dist/web/standalone/node_modules/node-pty/build/Makefile +2 -2
  201. package/dist/web/standalone/node_modules/node-pty/build/Release/pty.node +0 -0
  202. package/dist/web/standalone/node_modules/node-pty/build/pty.target.mk +14 -14
  203. package/dist/web/standalone/server.js +1 -1
  204. package/package.json +8 -6
  205. package/packages/cloud-mcp-gateway/package.json +2 -2
  206. package/packages/contracts/package.json +1 -1
  207. package/packages/daemon/package.json +4 -4
  208. package/packages/gsd-agent-core/package.json +5 -5
  209. package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.js +1 -1
  210. package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.js.map +1 -1
  211. package/packages/gsd-agent-modes/package.json +7 -7
  212. package/packages/mcp-server/package.json +3 -3
  213. package/packages/native/package.json +1 -1
  214. package/packages/pi-agent-core/package.json +1 -1
  215. package/packages/pi-ai/dist/models.generated.d.ts +0 -34
  216. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  217. package/packages/pi-ai/dist/models.generated.js +0 -34
  218. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  219. package/packages/pi-ai/package.json +1 -1
  220. package/packages/pi-coding-agent/dist/core/auth-storage.d.ts.map +1 -1
  221. package/packages/pi-coding-agent/dist/core/auth-storage.js +11 -3
  222. package/packages/pi-coding-agent/dist/core/auth-storage.js.map +1 -1
  223. package/packages/pi-coding-agent/package.json +7 -7
  224. package/packages/pi-tui/package.json +2 -2
  225. package/packages/rpc-client/package.json +2 -2
  226. package/pkg/package.json +1 -1
  227. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +18 -8
  228. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +2 -2
  229. package/src/resources/extensions/gsd/auto/contracts.ts +8 -119
  230. package/src/resources/extensions/gsd/auto/orchestrator.ts +794 -58
  231. package/src/resources/extensions/gsd/auto-prompts.ts +114 -1
  232. package/src/resources/extensions/gsd/auto-runtime-state.ts +4 -0
  233. package/src/resources/extensions/gsd/auto-tool-tracking.ts +5 -0
  234. package/src/resources/extensions/gsd/auto-unit-tool-scope.ts +33 -0
  235. package/src/resources/extensions/gsd/auto.ts +81 -500
  236. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +4 -0
  237. package/src/resources/extensions/gsd/debug-logger.ts +11 -0
  238. package/src/resources/extensions/gsd/doctor-proactive.ts +8 -2
  239. package/src/resources/extensions/gsd/guided-flow.ts +2 -2
  240. package/src/resources/extensions/gsd/markdown-renderer.ts +38 -19
  241. package/src/resources/extensions/gsd/mcp-filter.ts +7 -0
  242. package/src/resources/extensions/gsd/native-git-bridge.ts +9 -0
  243. package/src/resources/extensions/gsd/prompts/discuss.md +6 -7
  244. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +5 -7
  245. package/src/resources/extensions/gsd/prompts/guided-discuss-project.md +3 -5
  246. package/src/resources/extensions/gsd/prompts/guided-discuss-requirements.md +1 -2
  247. package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +5 -6
  248. package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  249. package/src/resources/extensions/gsd/prompts/research-milestone.md +2 -2
  250. package/src/resources/extensions/gsd/prompts/validate-milestone.md +5 -3
  251. package/src/resources/extensions/gsd/schemas/parsers.ts +6 -1
  252. package/src/resources/extensions/gsd/state-reconciliation/drift/artifact-db.ts +31 -10
  253. package/src/resources/extensions/gsd/tests/artifact-db-drift-memo.test.ts +66 -0
  254. package/src/resources/extensions/gsd/tests/auto-dispatch-baseline-harness.test.ts +53 -0
  255. package/src/resources/extensions/gsd/tests/auto-orchestrator.test.ts +590 -855
  256. package/src/resources/extensions/gsd/tests/auto-paused-ui-cleanup.test.ts +38 -10
  257. package/src/resources/extensions/gsd/tests/debug-logger.test.ts +15 -0
  258. package/src/resources/extensions/gsd/tests/execute-summary-save-empty-project.test.ts +64 -1
  259. package/src/resources/extensions/gsd/tests/markdown-renderer-parse-cache.test.ts +75 -0
  260. package/src/resources/extensions/gsd/tests/orchestrator-legacy-parity.test.ts +127 -0
  261. package/src/resources/extensions/gsd/tests/parse-project-milestone-bridge.test.ts +77 -0
  262. package/src/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +4 -2
  263. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +29 -2
  264. package/src/resources/extensions/gsd/tests/research-milestone-composer.test.ts +65 -0
  265. package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +19 -5
  266. package/src/resources/extensions/gsd/tests/token-tool-gating.test.ts +38 -0
  267. package/src/resources/extensions/gsd/tests/user-input-boundary.test.ts +62 -0
  268. package/src/resources/extensions/gsd/tests/worktree-safety.test.ts +24 -0
  269. package/src/resources/extensions/gsd/tests/write-gate-planning-unit.test.ts +15 -3
  270. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +183 -21
  271. package/src/resources/extensions/gsd/user-input-boundary.ts +37 -5
  272. package/dist/web/standalone/.next/server/chunks/678.js +0 -2
  273. package/dist/web/standalone/.next/static/chunks/app/_not-found/page-a6fb1847f67f167c.js +0 -1
  274. package/dist/web/standalone/.next/static/chunks/app/page-6644fc6ee8ca1247.js +0 -1
  275. package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-1115d46ac61b9823.js +0 -1
  276. /package/dist/web/standalone/.next/static/{tJOKQbQRO-9MiFDO8DIDS → TA5o9SHAnCdK6Umm1MYxb}/_buildManifest.js +0 -0
  277. /package/dist/web/standalone/.next/static/{tJOKQbQRO-9MiFDO8DIDS → TA5o9SHAnCdK6Umm1MYxb}/_ssgManifest.js +0 -0
@@ -1,5 +1,45 @@
1
1
  // Project/App: gsd-pi
2
2
  // File Purpose: Auto Orchestration module implementation and ADR-015 invariant pipeline owner.
3
+ //
4
+ // Phase 2 of #442 collapsed the nine single-implementation adapter seams
5
+ // (DispatchAdapter, RecoveryAdapter, StateReconciliationAdapter,
6
+ // ToolContractAdapter, WorktreeAdapter, HealthAdapter, UokGateAdapter,
7
+ // RuntimePersistenceAdapter, NotificationAdapter) into this class. The
8
+ // orchestrator now constructs from the concrete extension context and calls
9
+ // the real collaborators (state-reconciliation, doctor-proactive,
10
+ // auto-dispatch, recovery-classification, tool-contract, worktree-safety,
11
+ // uok/gate-runner, journal, session-lock, ctx.ui.notify) directly.
12
+ import { debugCount, debugTime } from "../debug-logger.js";
13
+ import { reconcileBeforeDispatch } from "../state-reconciliation.js";
14
+ import { resolveDispatch } from "../auto-dispatch.js";
15
+ import { classifyFailure } from "../recovery-classification.js";
16
+ import { verifyExpectedArtifact, refreshRecoveryDbForArtifact } from "../auto-recovery.js";
17
+ import { invalidateAllCaches } from "../cache.js";
18
+ import { compileUnitToolContract } from "../tool-contract.js";
19
+ import { createWorktreeSafetyModule } from "../worktree-safety.js";
20
+ import { repairAutoWorktreeSafetyFailure } from "../auto-worktree-repair.js";
21
+ import { resolveManifest } from "../unit-context-manifest.js";
22
+ import { preDispatchHealthGate, recordHealthSnapshot, } from "../doctor-proactive.js";
23
+ import { checkResourcesStale, autoWorktreeBranch, mergeMilestoneToMain } from "../auto-worktree.js";
24
+ import { getSessionLockStatus } from "../session-lock.js";
25
+ import { resolveUokFlags } from "../uok/flags.js";
26
+ import { emitJournalEvent as _emitJournalEvent } from "../journal.js";
27
+ import { loadEffectiveGSDPreferences, getIsolationMode } from "../preferences.js";
28
+ import { detectWorktreeName, resolveProjectRoot } from "../worktree.js";
29
+ import { GitServiceImpl } from "../git-service.js";
30
+ import { WorktreeStateProjection } from "../worktree-state-projection.js";
31
+ import { WorktreeLifecycle } from "../worktree-lifecycle.js";
32
+ import { createWorkspace, scopeMilestone } from "../workspace.js";
33
+ import { supportsStructuredQuestions } from "../workflow-mcp.js";
34
+ import { getToolBaselineSnapshot } from "../auto-model-selection.js";
35
+ import { deriveState } from "../state.js";
36
+ import { parseUnitId } from "../unit-id.js";
37
+ import { isClosedStatus } from "../status-guards.js";
38
+ import { isDbAvailable, getSlice, getTask, refreshOpenDatabaseFromDisk, } from "../gsd-db.js";
39
+ import { getErrorMessage } from "../error-utils.js";
40
+ import { logWarning } from "../workflow-logger.js";
41
+ import { existsSync, readFileSync } from "node:fs";
42
+ import { join } from "node:path";
3
43
  function now() {
4
44
  return Date.now();
5
45
  }
@@ -18,34 +58,543 @@ function noRemainingUnitsReason(stateSnapshot) {
18
58
  }
19
59
  return "no remaining units";
20
60
  }
61
+ function getAlreadyClosedDispatchReason(unitType, unitId) {
62
+ if (!isDbAvailable())
63
+ return null;
64
+ refreshOpenDatabaseFromDisk();
65
+ const { milestone, slice, task } = parseUnitId(unitId);
66
+ if (unitType === "execute-task" && milestone && slice && task) {
67
+ const row = getTask(milestone, slice, task);
68
+ return row && isClosedStatus(row.status)
69
+ ? `execute-task ${unitId} is already ${row.status}`
70
+ : null;
71
+ }
72
+ if (unitType === "complete-slice" && milestone && slice) {
73
+ const row = getSlice(milestone, slice);
74
+ return row && isClosedStatus(row.status)
75
+ ? `complete-slice ${unitId} is already ${row.status}`
76
+ : null;
77
+ }
78
+ return null;
79
+ }
80
+ function shouldAdoptActiveMilestone(state, activeSession, activeDispatchBasePath) {
81
+ const activeMilestoneId = state.activeMilestone?.id;
82
+ const currentMilestoneId = activeSession?.currentMilestoneId;
83
+ if (!activeSession || !activeMilestoneId || !currentMilestoneId || activeMilestoneId === currentMilestoneId) {
84
+ return false;
85
+ }
86
+ const scopedWorktreeMilestone = (activeSession.basePath ? detectWorktreeName(activeSession.basePath) : null) ??
87
+ detectWorktreeName(activeDispatchBasePath);
88
+ if (scopedWorktreeMilestone && scopedWorktreeMilestone !== activeMilestoneId) {
89
+ return false;
90
+ }
91
+ const currentMilestone = state.registry.find((milestone) => milestone.id === currentMilestoneId);
92
+ return !!currentMilestone && isClosedStatus(currentMilestone.status);
93
+ }
94
+ /**
95
+ * Pure dispatch-decision function — formerly `createWiredDispatchAdapter`'s
96
+ * `decideNextUnit`. Folded out of the closure so the orchestrator can call it
97
+ * directly and tests can drive the exact dispatch decision logic against real
98
+ * fixtures without re-introducing an adapter seam.
99
+ *
100
+ * Derives session-derived dispatch inputs the same way phases.ts:runDispatch
101
+ * does (#5789): prefers caller-supplied values when present so test harnesses
102
+ * and alternative wirings can inject deterministic snapshots; otherwise pulls
103
+ * from the captured pi/ctx references.
104
+ */
105
+ export async function decideOrchestratorDispatch(ctx, pi, dispatchBasePath, session, input) {
106
+ const state = input.stateSnapshot;
107
+ const active = state.activeMilestone;
108
+ if (!active)
109
+ return null;
110
+ const activeSession = input.session ?? session;
111
+ const activeDispatchBasePath = activeSession?.basePath || dispatchBasePath;
112
+ if (activeSession && shouldAdoptActiveMilestone(state, activeSession, activeDispatchBasePath)) {
113
+ activeSession.currentMilestoneId = active.id;
114
+ }
115
+ const prefs = loadEffectiveGSDPreferences(activeDispatchBasePath)?.preferences;
116
+ // Derive session-derived dispatch inputs the same way phases.ts:runDispatch does
117
+ // (#5789). Prefer caller-supplied values when present so test harnesses and
118
+ // alternative wirings can inject deterministic snapshots; otherwise pull from
119
+ // the captured pi/ctx references.
120
+ const sessionProvider = input.sessionProvider ?? ctx.model?.provider;
121
+ const sessionContextWindow = input.sessionContextWindow ?? ctx.model?.contextWindow;
122
+ const modelRegistry = input.modelRegistry ?? ctx.modelRegistry;
123
+ const authMode = sessionProvider && typeof ctx.modelRegistry?.getProviderAuthMode === "function"
124
+ ? ctx.modelRegistry.getProviderAuthMode(sessionProvider)
125
+ : undefined;
126
+ // Use baseline snapshot — same reason as phases.ts:runDispatch: the live
127
+ // active set may be narrowed by the prior unit before selectAndApplyModel
128
+ // restores it, causing false transport-preflight failures (#477 follow-up).
129
+ const activeTools = getToolBaselineSnapshot(pi);
130
+ // Mirrors runDispatch: deep-planning keeps approval gates in plain chat
131
+ // because structured questions can be cancelled outside the chat turn on
132
+ // some transports.
133
+ const structuredQuestionsAvailable = input.structuredQuestionsAvailable ??
134
+ (prefs?.planning_depth === "deep"
135
+ ? "false"
136
+ : supportsStructuredQuestions(activeTools, {
137
+ authMode,
138
+ baseUrl: ctx.model?.baseUrl,
139
+ })
140
+ ? "true"
141
+ : "false");
142
+ const pendingRetry = session?.pendingVerificationRetryDispatch;
143
+ if (session && pendingRetry) {
144
+ session.pendingVerificationRetryDispatch = null;
145
+ const alreadyClosedReason = getAlreadyClosedDispatchReason(pendingRetry.unitType, pendingRetry.unitId);
146
+ if (alreadyClosedReason) {
147
+ session.pendingOrchestrationDispatch = null;
148
+ session.pendingVerificationRetry = null;
149
+ return { kind: "skipped", reason: alreadyClosedReason };
150
+ }
151
+ session.pendingOrchestrationDispatch = pendingRetry;
152
+ return {
153
+ unitType: pendingRetry.unitType,
154
+ unitId: pendingRetry.unitId,
155
+ reason: "verification-retry",
156
+ preconditions: [],
157
+ };
158
+ }
159
+ const action = await resolveDispatch({
160
+ basePath: activeDispatchBasePath,
161
+ mid: active.id,
162
+ midTitle: active.title,
163
+ state,
164
+ prefs,
165
+ session: activeSession,
166
+ structuredQuestionsAvailable,
167
+ sessionContextWindow,
168
+ sessionProvider,
169
+ modelRegistry,
170
+ activeTools,
171
+ sessionAuthMode: authMode,
172
+ sessionBaseUrl: ctx.model?.baseUrl,
173
+ });
174
+ if (action.action === "stop") {
175
+ if (session)
176
+ session.pendingOrchestrationDispatch = null;
177
+ return {
178
+ kind: "blocked",
179
+ reason: action.reason,
180
+ action: action.level === "warning" ? "pause" : "stop",
181
+ };
182
+ }
183
+ if (action.action !== "dispatch") {
184
+ if (session)
185
+ session.pendingOrchestrationDispatch = null;
186
+ return {
187
+ kind: "skipped",
188
+ reason: action.matchedRule ?? "dispatch-skip",
189
+ };
190
+ }
191
+ const alreadyClosedReason = getAlreadyClosedDispatchReason(action.unitType, action.unitId);
192
+ if (alreadyClosedReason) {
193
+ if (session) {
194
+ session.pendingOrchestrationDispatch = null;
195
+ session.pendingVerificationRetry = null;
196
+ }
197
+ return { kind: "skipped", reason: alreadyClosedReason };
198
+ }
199
+ if (session) {
200
+ const pending = {
201
+ unitType: action.unitType,
202
+ unitId: action.unitId,
203
+ prompt: action.prompt,
204
+ pauseAfterUatDispatch: action.pauseAfterDispatch ?? false,
205
+ state,
206
+ mid: active.id,
207
+ midTitle: active.title,
208
+ };
209
+ session.pendingOrchestrationDispatch = pending;
210
+ }
211
+ return {
212
+ unitType: action.unitType,
213
+ unitId: action.unitId,
214
+ reason: action.matchedRule ?? "dispatch",
215
+ preconditions: [],
216
+ };
217
+ }
21
218
  export class AutoOrchestrator {
22
219
  status = {
23
220
  phase: "idle",
24
221
  transitionCount: 0,
25
222
  };
26
- deps;
223
+ ctx;
224
+ pi;
225
+ dispatchBasePath;
226
+ runtimeBasePath;
227
+ s;
228
+ flowId;
229
+ seq = 0;
27
230
  lastAdvanceKey = null;
28
231
  lastFinalizedUnitKey = null;
29
232
  dispatchKeyWindow = [];
30
- constructor(deps) {
31
- this.deps = deps;
233
+ // #442: the unit key we last attempted graduated stuck-recovery for. Bounds
234
+ // recovery to one attempt per stuck episode per run (reset on start/resume/
235
+ // stop), mirroring the legacy Level-1-then-Level-2 escalation in phases.ts.
236
+ lastStuckRecoveryKey = null;
237
+ constructor(context) {
238
+ this.ctx = context.ctx;
239
+ this.pi = context.pi;
240
+ this.dispatchBasePath = context.dispatchBasePath;
241
+ this.runtimeBasePath = context.runtimeBasePath;
242
+ this.s = context.session;
243
+ this.flowId = `auto-orchestrator-${Date.now()}`;
244
+ }
245
+ // ── Live base-path resolution (was the wiring factory's getLiveDispatchBasePath) ──
246
+ getLiveDispatchBasePath() {
247
+ return resolveLiveOrchestratorBasePath({
248
+ capturedBasePath: this.dispatchBasePath,
249
+ runtimeBasePath: this.runtimeBasePath,
250
+ sessionBasePath: this.s.basePath,
251
+ originalBasePath: this.s.originalBasePath,
252
+ });
253
+ }
254
+ // ── RuntimePersistenceAdapter (folded) ───────────────────────────────────
255
+ ensureLockOwnership() {
256
+ const status = getSessionLockStatus(this.runtimeBasePath);
257
+ if (!status.valid || status.failureReason === "pid-mismatch") {
258
+ throw new Error("session lock held by another process");
259
+ }
260
+ }
261
+ /**
262
+ * Map an orchestrator lifecycle event name to its journal eventType and emit
263
+ * it. The name→eventType ternary is preserved byte-for-byte from the legacy
264
+ * wired RuntimePersistenceAdapter.journalTransition.
265
+ */
266
+ journalTransition(event) {
267
+ const eventType = event.name === "start"
268
+ ? "orchestrator-iteration-start"
269
+ : event.name === "resume"
270
+ ? "orchestrator-iteration-start"
271
+ : event.name === "advance"
272
+ ? "orchestrator-dispatch-match"
273
+ : event.name === "advance-blocked"
274
+ ? "orchestrator-guard-block"
275
+ : event.name === "advance-stopped"
276
+ ? "orchestrator-dispatch-stop"
277
+ : event.name === "advance-error"
278
+ ? "orchestrator-iteration-end"
279
+ : event.name === "advance-paused" || event.name === "advance-retry"
280
+ ? "orchestrator-guard-block"
281
+ : event.name === "stop"
282
+ ? "orchestrator-terminal"
283
+ : "orchestrator-iteration-end";
284
+ _emitJournalEvent(this.runtimeBasePath, {
285
+ ts: new Date().toISOString(),
286
+ flowId: this.flowId,
287
+ seq: ++this.seq,
288
+ eventType,
289
+ data: {
290
+ source: "auto-orchestrator",
291
+ name: event.name,
292
+ reason: event.reason,
293
+ unitType: event.unitType,
294
+ unitId: event.unitId,
295
+ },
296
+ });
297
+ }
298
+ // ── NotificationAdapter (folded) ─────────────────────────────────────────
299
+ notifyLifecycle(event) {
300
+ if (event.name === "error") {
301
+ this.ctx.ui.notify(event.detail ?? "auto orchestration error", "error");
302
+ }
303
+ }
304
+ // ── HealthAdapter (folded) ───────────────────────────────────────────────
305
+ checkResourcesStale() {
306
+ return checkResourcesStale(this.s.resourceVersionOnStart);
307
+ }
308
+ async preAdvanceGate() {
309
+ try {
310
+ const gate = await preDispatchHealthGate(this.getLiveDispatchBasePath());
311
+ if (gate.proceed) {
312
+ return {
313
+ kind: "pass",
314
+ fixesApplied: gate.fixesApplied,
315
+ };
316
+ }
317
+ return {
318
+ kind: "fail",
319
+ reason: gate.reason ?? "Pre-dispatch health check failed — run /gsd doctor for details.",
320
+ action: gate.severity ?? "pause",
321
+ };
322
+ }
323
+ catch (error) {
324
+ return { kind: "threw", error };
325
+ }
326
+ }
327
+ postAdvanceRecord(result) {
328
+ if (result.kind === "error") {
329
+ recordHealthSnapshot(1, 0, 0, [{
330
+ code: "orchestration-error",
331
+ message: result.reason ?? "orchestration error",
332
+ severity: "error",
333
+ unitId: "orchestration",
334
+ }], [], "orchestration");
335
+ }
336
+ else if (result.kind === "blocked") {
337
+ recordHealthSnapshot(0, 1, 0, [{
338
+ code: "orchestration-blocked",
339
+ message: result.reason ?? "orchestration blocked",
340
+ severity: "warning",
341
+ unitId: "orchestration",
342
+ }], [], "orchestration");
343
+ }
344
+ }
345
+ // ── UokGateAdapter (folded) ──────────────────────────────────────────────
346
+ async emitUokGate(input) {
347
+ const activeBasePath = this.getLiveDispatchBasePath();
348
+ const prefs = loadEffectiveGSDPreferences(activeBasePath)?.preferences;
349
+ const uokFlags = resolveUokFlags(prefs);
350
+ if (!uokFlags.gates)
351
+ return;
352
+ const milestoneId = input.milestoneId ?? this.s.currentMilestoneId ?? undefined;
353
+ try {
354
+ const { UokGateRunner } = await import("../uok/gate-runner.js");
355
+ const runner = new UokGateRunner();
356
+ runner.register({
357
+ id: input.gateId,
358
+ type: input.gateType,
359
+ execute: async () => ({
360
+ outcome: input.outcome,
361
+ failureClass: input.failureClass,
362
+ rationale: input.rationale,
363
+ findings: input.findings ?? "",
364
+ }),
365
+ });
366
+ await runner.run(input.gateId, {
367
+ basePath: activeBasePath,
368
+ traceId: `pre-dispatch:${this.flowId}`,
369
+ turnId: `orch-${this.seq}`,
370
+ milestoneId,
371
+ unitType: "pre-dispatch",
372
+ unitId: `orch-${this.seq}`,
373
+ });
374
+ }
375
+ catch (err) {
376
+ logWarning("engine", `uok gate emit failed: ${getErrorMessage(err)}`, {
377
+ file: "orchestrator.ts",
378
+ gateId: input.gateId,
379
+ gateType: input.gateType,
380
+ ...(milestoneId ? { milestoneId } : {}),
381
+ });
382
+ }
383
+ }
384
+ // ── StateReconciliationAdapter (folded) ──────────────────────────────────
385
+ async reconcileBeforeDispatch() {
386
+ const activeBasePath = this.getLiveDispatchBasePath();
387
+ const result = await reconcileBeforeDispatch(activeBasePath);
388
+ // Failure-path summaries written by gsd_summary_save create
389
+ // artifact-db-status-divergence blockers for tasks that are still
390
+ // pending (gsd_task_complete never ran). These tasks can still be
391
+ // dispatched and the drift self-heals once they complete successfully.
392
+ const hardBlockers = result.blockers.filter((b) => !b.includes("has SUMMARY artifact while DB status is") &&
393
+ !b.includes("has SUMMARY on disk while DB status is") &&
394
+ !b.includes("has task SUMMARY artifacts but no DB tasks"));
395
+ if (hardBlockers.length > 0) {
396
+ return {
397
+ ok: false,
398
+ reason: hardBlockers[0],
399
+ stateSnapshot: result.stateSnapshot,
400
+ };
401
+ }
402
+ const repairedKinds = result.repaired.map((d) => d.kind);
403
+ return {
404
+ ok: true,
405
+ reason: repairedKinds.length > 0
406
+ ? `repaired: ${repairedKinds.join(", ")}`
407
+ : "clean",
408
+ stateSnapshot: result.stateSnapshot,
409
+ };
410
+ }
411
+ // ── DispatchAdapter (folded) ─────────────────────────────────────────────
412
+ decideNextUnit(input) {
413
+ return decideOrchestratorDispatch(this.ctx, this.pi, this.dispatchBasePath, this.s, input);
414
+ }
415
+ // ── ToolContractAdapter (folded) ─────────────────────────────────────────
416
+ compileUnitToolContract(unitType) {
417
+ const result = compileUnitToolContract(unitType);
418
+ if (!result.ok)
419
+ return { ok: false, reason: result.detail };
420
+ return { ok: true, reason: result.contract.validationRules.join(", ") };
421
+ }
422
+ // ── WorktreeAdapter (folded) ─────────────────────────────────────────────
423
+ getEffectiveUnitIsolationMode(basePath) {
424
+ const configuredMode = getIsolationMode(basePath);
425
+ return configuredMode === "worktree" && this.s.isolationDegraded ? "branch" : configuredMode;
426
+ }
427
+ buildLifecycle() {
428
+ return new WorktreeLifecycle(this.s, {
429
+ gitServiceFactory: (basePath) => {
430
+ const gitConfig = loadEffectiveGSDPreferences()?.preferences?.git ?? {};
431
+ return new GitServiceImpl(basePath, gitConfig);
432
+ },
433
+ worktreeProjection: new WorktreeStateProjection(),
434
+ mergeMilestoneToMain,
435
+ });
436
+ }
437
+ rebuildScope(rawPath, milestoneId) {
438
+ if (!milestoneId) {
439
+ this.s.scope = null;
440
+ return;
441
+ }
442
+ try {
443
+ const workspace = createWorkspace(rawPath);
444
+ this.s.scope = scopeMilestone(workspace, milestoneId);
445
+ }
446
+ catch {
447
+ // Non-fatal — scope is additive. Existing readers still use basePath.
448
+ this.s.scope = null;
449
+ }
450
+ }
451
+ async prepareWorktreeForUnit(unitType, unitId) {
452
+ const isolationMode = this.getEffectiveUnitIsolationMode(this.runtimeBasePath);
453
+ const manifest = resolveManifest(unitType);
454
+ if (!manifest) {
455
+ return {
456
+ ok: false,
457
+ reason: `No Unit manifest is registered for ${unitType}`,
458
+ };
459
+ }
460
+ if (isolationMode !== "worktree") {
461
+ return { ok: true, reason: "not-required" };
462
+ }
463
+ const writeScope = manifest.tools.mode === "all" || manifest.tools.mode === "docs"
464
+ ? "source-writing"
465
+ : "planning-only";
466
+ const safety = createWorktreeSafetyModule();
467
+ const activeBasePath = this.getLiveDispatchBasePath();
468
+ const snapshot = await deriveState(activeBasePath);
469
+ const milestoneId = snapshot.activeMilestone?.id ?? null;
470
+ const expectedBranch = milestoneId ? autoWorktreeBranch(milestoneId) : null;
471
+ let result = safety.validateUnitRoot({
472
+ unitType,
473
+ unitId,
474
+ writeScope,
475
+ projectRoot: this.runtimeBasePath,
476
+ unitRoot: activeBasePath,
477
+ milestoneId,
478
+ isolationMode,
479
+ expectedBranch,
480
+ });
481
+ if (!result.ok) {
482
+ const repaired = await repairAutoWorktreeSafetyFailure({
483
+ safetyResult: result,
484
+ projectRoot: this.runtimeBasePath,
485
+ activeRoot: activeBasePath,
486
+ milestoneId,
487
+ enterMilestone: async (id) => {
488
+ this.buildLifecycle().adoptSessionRoot(this.runtimeBasePath, this.s.originalBasePath || this.runtimeBasePath);
489
+ const enterResult = this.buildLifecycle().enterMilestone(id, {
490
+ notify: this.ctx.ui.notify.bind(this.ctx.ui),
491
+ });
492
+ if (!enterResult.ok)
493
+ return { ok: false, reason: enterResult.reason };
494
+ this.rebuildScope(this.s.basePath, this.s.currentMilestoneId);
495
+ return { ok: true };
496
+ },
497
+ revalidate: () => safety.validateUnitRoot({
498
+ unitType,
499
+ unitId,
500
+ writeScope,
501
+ projectRoot: this.runtimeBasePath,
502
+ unitRoot: this.getLiveDispatchBasePath(),
503
+ milestoneId,
504
+ isolationMode: this.getEffectiveUnitIsolationMode(this.runtimeBasePath),
505
+ expectedBranch,
506
+ }),
507
+ });
508
+ result = repaired.result;
509
+ if (result.ok) {
510
+ return { ok: true, reason: repaired.repaired ? `repaired-${result.kind}` : result.kind };
511
+ }
512
+ const repairDetail = repaired.repairReason
513
+ ? ` (repair skipped: ${repaired.repairReason})`
514
+ : "";
515
+ return { ok: false, reason: `${result.kind}: ${result.reason}${repairDetail}` };
516
+ }
517
+ return { ok: true, reason: result.kind };
518
+ }
519
+ // ── RecoveryAdapter (folded) ─────────────────────────────────────────────
520
+ classifyAndRecover(input) {
521
+ const recovery = classifyFailure(input);
522
+ return { action: recovery.action, reason: recovery.reason };
523
+ }
524
+ // ── Lifecycle verbs ──────────────────────────────────────────────────────
525
+ /**
526
+ * #442: graduated stuck recovery, ported from the legacy
527
+ * auto/phases.ts:runDispatch path that Phase 3 retires. The ring-buffer
528
+ * hard-stops (stuck-loop saturation and finalized-repeat) would otherwise
529
+ * KILL a unit that actually completed on disk but whose DB row is still
530
+ * stale. Before hard-stopping, verify the expected artifact exists; if so,
531
+ * refresh the DB from it, invalidate caches and reset the dispatch ring so
532
+ * the next advance picks the correct next unit. Bounded to one attempt per
533
+ * stuck key per episode (reset on lifecycle + genuine finalize) to avoid an
534
+ * unbounded recover→re-saturate→recover loop — mirrors the legacy
535
+ * Level-1-recover-then-Level-2-hard-stop escalation.
536
+ *
537
+ * Returns true when recovery succeeded; the caller should re-loop (return a
538
+ * skipped result) instead of stopping.
539
+ */
540
+ tryStuckArtifactRecovery(unitType, unitId) {
541
+ const key = `${unitType}:${unitId}`;
542
+ if (this.lastStuckRecoveryKey === key)
543
+ return false; // already tried this episode
544
+ const basePath = this.getLiveDispatchBasePath();
545
+ if (!verifyExpectedArtifact(unitType, unitId, basePath))
546
+ return false;
547
+ const refreshed = refreshRecoveryDbForArtifact(unitType, unitId, basePath);
548
+ // Fatal failures cannot be recovered — hard-stop. Non-fatal (e.g. plan-slice
549
+ // DB refresh hiccup) still fall through: invalidating caches and resetting
550
+ // the ring gives the next advance a clean slate to pick up the correct state,
551
+ // mirroring the legacy Level-1 "continue" escalation path.
552
+ if (!refreshed.ok && refreshed.fatal)
553
+ return false;
554
+ this.lastStuckRecoveryKey = key;
555
+ invalidateAllCaches();
556
+ this.dispatchKeyWindow = [];
557
+ this.lastAdvanceKey = null;
558
+ this.lastFinalizedUnitKey = null;
559
+ return true;
560
+ }
561
+ stuckRecovered(decision, stateSnapshot) {
562
+ const recovered = {
563
+ kind: "skipped",
564
+ reason: `stuck-recovery: ${decision.unitType} ${decision.unitId} artifact found on disk; DB refreshed`,
565
+ stateSnapshot,
566
+ };
567
+ this.status.phase = "running";
568
+ this.status.activeUnit = undefined;
569
+ this.bumpTransition();
570
+ this.journalTransition({
571
+ name: "advance-skipped",
572
+ reason: recovered.reason,
573
+ unitType: decision.unitType,
574
+ unitId: decision.unitId,
575
+ });
576
+ this.postAdvanceRecord(recovered);
577
+ return recovered;
32
578
  }
33
579
  async start(_sessionContext) {
34
580
  this.lastAdvanceKey = null;
35
581
  this.lastFinalizedUnitKey = null;
36
582
  this.dispatchKeyWindow = [];
583
+ this.lastStuckRecoveryKey = null;
37
584
  this.status.phase = "running";
38
585
  this.bumpTransition();
39
- await this.deps.runtime.journalTransition({ name: "start" });
40
- await this.deps.notifications.notifyLifecycle({ name: "start" });
586
+ this.journalTransition({ name: "start" });
587
+ this.notifyLifecycle({ name: "start" });
41
588
  return { kind: "started" };
42
589
  }
43
590
  async advance() {
591
+ debugCount("dispatches");
592
+ const stopAdvanceTimer = debugTime("orchestrator-advance");
44
593
  try {
45
- await this.deps.runtime.ensureLockOwnership();
46
- const staleMsg = this.deps.health.checkResourcesStale();
594
+ this.ensureLockOwnership();
595
+ const staleMsg = this.checkResourcesStale();
47
596
  if (staleMsg) {
48
- await this.deps.uokGate.emit({
597
+ await this.emitUokGate({
49
598
  gateId: "resource-version-guard",
50
599
  gateType: "policy",
51
600
  outcome: "fail",
@@ -54,20 +603,20 @@ export class AutoOrchestrator {
54
603
  findings: staleMsg,
55
604
  });
56
605
  const blocked = { kind: "blocked", reason: staleMsg, action: "pause" };
57
- await this.deps.runtime.journalTransition({ name: "advance-blocked", reason: blocked.reason });
58
- await this.deps.health.postAdvanceRecord(blocked);
606
+ this.journalTransition({ name: "advance-blocked", reason: blocked.reason });
607
+ this.postAdvanceRecord(blocked);
59
608
  return blocked;
60
609
  }
61
- await this.deps.uokGate.emit({
610
+ await this.emitUokGate({
62
611
  gateId: "resource-version-guard",
63
612
  gateType: "policy",
64
613
  outcome: "pass",
65
614
  failureClass: "none",
66
615
  rationale: "resource version guard passed",
67
616
  });
68
- const gate = await this.deps.health.preAdvanceGate();
617
+ const gate = await this.preAdvanceGate();
69
618
  if (gate.kind === "fail") {
70
- await this.deps.uokGate.emit({
619
+ await this.emitUokGate({
71
620
  gateId: "pre-dispatch-health-gate",
72
621
  gateType: "execution",
73
622
  outcome: "manual-attention",
@@ -80,12 +629,12 @@ export class AutoOrchestrator {
80
629
  reason: gate.reason,
81
630
  action: gate.action ?? "pause",
82
631
  };
83
- await this.deps.runtime.journalTransition({ name: "advance-blocked", reason: blocked.reason });
84
- await this.deps.health.postAdvanceRecord(blocked);
632
+ this.journalTransition({ name: "advance-blocked", reason: blocked.reason });
633
+ this.postAdvanceRecord(blocked);
85
634
  return blocked;
86
635
  }
87
636
  if (gate.kind === "threw") {
88
- await this.deps.uokGate.emit({
637
+ await this.emitUokGate({
89
638
  gateId: "pre-dispatch-health-gate",
90
639
  gateType: "execution",
91
640
  outcome: "manual-attention",
@@ -96,7 +645,7 @@ export class AutoOrchestrator {
96
645
  // intentional fall-through: matches runPreDispatch behaviour
97
646
  }
98
647
  else {
99
- await this.deps.uokGate.emit({
648
+ await this.emitUokGate({
100
649
  gateId: "pre-dispatch-health-gate",
101
650
  gateType: "execution",
102
651
  outcome: "pass",
@@ -105,7 +654,7 @@ export class AutoOrchestrator {
105
654
  findings: gate.fixesApplied?.join(", ") ?? "",
106
655
  });
107
656
  }
108
- const reconciliation = await this.deps.stateReconciliation.reconcileBeforeDispatch();
657
+ const reconciliation = await this.reconcileBeforeDispatch();
109
658
  if (!reconciliation.ok || !reconciliation.stateSnapshot) {
110
659
  const blocked = {
111
660
  kind: "blocked",
@@ -113,11 +662,11 @@ export class AutoOrchestrator {
113
662
  action: "pause",
114
663
  stateSnapshot: reconciliation.stateSnapshot,
115
664
  };
116
- await this.deps.runtime.journalTransition({ name: "advance-blocked", reason: blocked.reason });
117
- await this.deps.health.postAdvanceRecord(blocked);
665
+ this.journalTransition({ name: "advance-blocked", reason: blocked.reason });
666
+ this.postAdvanceRecord(blocked);
118
667
  return blocked;
119
668
  }
120
- const decision = await this.deps.dispatch.decideNextUnit({ stateSnapshot: reconciliation.stateSnapshot });
669
+ const decision = await this.decideNextUnit({ stateSnapshot: reconciliation.stateSnapshot });
121
670
  if (!decision) {
122
671
  const stopped = {
123
672
  kind: "stopped",
@@ -129,8 +678,8 @@ export class AutoOrchestrator {
129
678
  this.lastAdvanceKey = null;
130
679
  this.dispatchKeyWindow = [];
131
680
  this.bumpTransition();
132
- await this.deps.runtime.journalTransition({ name: "advance-stopped", reason: stopped.reason });
133
- await this.deps.health.postAdvanceRecord(stopped);
681
+ this.journalTransition({ name: "advance-stopped", reason: stopped.reason });
682
+ this.postAdvanceRecord(stopped);
134
683
  return stopped;
135
684
  }
136
685
  if ("kind" in decision && decision.kind === "skipped") {
@@ -142,8 +691,8 @@ export class AutoOrchestrator {
142
691
  this.status.phase = "running";
143
692
  this.status.activeUnit = undefined;
144
693
  this.bumpTransition();
145
- await this.deps.runtime.journalTransition({ name: "advance-skipped", reason: skipped.reason });
146
- await this.deps.health.postAdvanceRecord(skipped);
694
+ this.journalTransition({ name: "advance-skipped", reason: skipped.reason });
695
+ this.postAdvanceRecord(skipped);
147
696
  return skipped;
148
697
  }
149
698
  if (!("unitType" in decision)) {
@@ -153,8 +702,8 @@ export class AutoOrchestrator {
153
702
  action: decision.action,
154
703
  stateSnapshot: reconciliation.stateSnapshot,
155
704
  };
156
- await this.deps.runtime.journalTransition({ name: "advance-blocked", reason: blocked.reason });
157
- await this.deps.health.postAdvanceRecord(blocked);
705
+ this.journalTransition({ name: "advance-blocked", reason: blocked.reason });
706
+ this.postAdvanceRecord(blocked);
158
707
  return blocked;
159
708
  }
160
709
  const nextKey = `${decision.unitType}:${decision.unitId}`;
@@ -168,19 +717,25 @@ export class AutoOrchestrator {
168
717
  }
169
718
  const matchingCount = this.dispatchKeyWindow.filter((k) => k === nextKey).length;
170
719
  if (this.lastFinalizedUnitKey === nextKey) {
720
+ // #442: the unit re-dispatched immediately after finalizing may have
721
+ // actually completed on disk with a stale DB. Verify + recover before
722
+ // hard-stopping (legacy graduated stuck-recovery parity).
723
+ if (this.tryStuckArtifactRecovery(decision.unitType, decision.unitId)) {
724
+ return this.stuckRecovered(decision, reconciliation.stateSnapshot);
725
+ }
171
726
  const blocked = {
172
727
  kind: "blocked",
173
728
  reason: `state did not advance after finalized ${decision.unitType} ${decision.unitId}`,
174
729
  action: "stop",
175
730
  stateSnapshot: reconciliation.stateSnapshot,
176
731
  };
177
- await this.deps.runtime.journalTransition({
732
+ this.journalTransition({
178
733
  name: "advance-blocked",
179
734
  reason: blocked.reason,
180
735
  unitType: decision.unitType,
181
736
  unitId: decision.unitId,
182
737
  });
183
- await this.deps.health.postAdvanceRecord(blocked);
738
+ this.postAdvanceRecord(blocked);
184
739
  return blocked;
185
740
  }
186
741
  // Idempotency: same key as immediately previous successful advance.
@@ -191,13 +746,13 @@ export class AutoOrchestrator {
191
746
  // stuck-loop for the saturated-window case.
192
747
  if (this.lastAdvanceKey === nextKey && matchingCount < STUCK_WINDOW_SIZE) {
193
748
  const blocked = { kind: "blocked", reason: "idempotent advance: unit already active", action: "pause" };
194
- await this.deps.runtime.journalTransition({
749
+ this.journalTransition({
195
750
  name: "advance-blocked",
196
751
  reason: blocked.reason,
197
752
  unitType: decision.unitType,
198
753
  unitId: decision.unitId,
199
754
  });
200
- await this.deps.health.postAdvanceRecord(blocked);
755
+ this.postAdvanceRecord(blocked);
201
756
  return blocked;
202
757
  }
203
758
  // Stuck-loop detection: when the ring is saturated with copies of
@@ -205,21 +760,27 @@ export class AutoOrchestrator {
205
760
  // picking the same unit across the whole window and must hard-stop with
206
761
  // a diagnosable reason.
207
762
  if (matchingCount >= STUCK_WINDOW_SIZE) {
763
+ // #442: before declaring a stuck loop, verify the unit didn't actually
764
+ // complete on disk (stale DB) and recover if so — legacy graduated
765
+ // stuck-recovery parity. Otherwise hard-stop with a diagnosable reason.
766
+ if (this.tryStuckArtifactRecovery(decision.unitType, decision.unitId)) {
767
+ return this.stuckRecovered(decision, reconciliation.stateSnapshot);
768
+ }
208
769
  const blocked = {
209
770
  kind: "blocked",
210
771
  reason: `stuck-loop: ${nextKey} picked ${matchingCount} times`,
211
772
  action: "stop",
212
773
  };
213
- await this.deps.runtime.journalTransition({
774
+ this.journalTransition({
214
775
  name: "advance-blocked",
215
776
  reason: blocked.reason,
216
777
  unitType: decision.unitType,
217
778
  unitId: decision.unitId,
218
779
  });
219
- await this.deps.health.postAdvanceRecord(blocked);
780
+ this.postAdvanceRecord(blocked);
220
781
  return blocked;
221
782
  }
222
- const contract = await this.deps.toolContract.compileUnitToolContract(decision.unitType, decision.unitId);
783
+ const contract = this.compileUnitToolContract(decision.unitType);
223
784
  if (!contract.ok) {
224
785
  const blocked = {
225
786
  kind: "blocked",
@@ -227,16 +788,16 @@ export class AutoOrchestrator {
227
788
  action: "pause",
228
789
  stateSnapshot: reconciliation.stateSnapshot,
229
790
  };
230
- await this.deps.runtime.journalTransition({
791
+ this.journalTransition({
231
792
  name: "advance-blocked",
232
793
  reason: blocked.reason,
233
794
  unitType: decision.unitType,
234
795
  unitId: decision.unitId,
235
796
  });
236
- await this.deps.health.postAdvanceRecord(blocked);
797
+ this.postAdvanceRecord(blocked);
237
798
  return blocked;
238
799
  }
239
- const worktree = await this.deps.worktree.prepareForUnit(decision.unitType, decision.unitId);
800
+ const worktree = await this.prepareWorktreeForUnit(decision.unitType, decision.unitId);
240
801
  if (!worktree.ok) {
241
802
  const blocked = {
242
803
  kind: "blocked",
@@ -244,36 +805,36 @@ export class AutoOrchestrator {
244
805
  action: "pause",
245
806
  stateSnapshot: reconciliation.stateSnapshot,
246
807
  };
247
- await this.deps.runtime.journalTransition({
808
+ this.journalTransition({
248
809
  name: "advance-blocked",
249
810
  reason: blocked.reason,
250
811
  unitType: decision.unitType,
251
812
  unitId: decision.unitId,
252
813
  });
253
- await this.deps.health.postAdvanceRecord(blocked);
814
+ this.postAdvanceRecord(blocked);
254
815
  return blocked;
255
816
  }
256
817
  this.status.activeUnit = { unitType: decision.unitType, unitId: decision.unitId };
257
818
  this.status.phase = "running";
258
819
  this.lastAdvanceKey = nextKey;
259
820
  this.bumpTransition();
260
- await this.deps.runtime.journalTransition({
821
+ this.journalTransition({
261
822
  name: "advance",
262
823
  reason: decision.reason,
263
824
  unitType: decision.unitType,
264
825
  unitId: decision.unitId,
265
826
  });
266
- await this.deps.worktree.syncAfterUnit(decision.unitType, decision.unitId);
827
+ // syncAfterUnit was a no-op in the wired WorktreeAdapter.
267
828
  const advanced = {
268
829
  kind: "advanced",
269
830
  unit: { unitType: decision.unitType, unitId: decision.unitId },
270
831
  stateSnapshot: reconciliation.stateSnapshot,
271
832
  };
272
- await this.deps.health.postAdvanceRecord(advanced);
833
+ this.postAdvanceRecord(advanced);
273
834
  return advanced;
274
835
  }
275
836
  catch (error) {
276
- const recovery = await this.deps.recovery.classifyAndRecover({
837
+ const recovery = this.classifyAndRecover({
277
838
  error,
278
839
  unitType: this.status.activeUnit?.unitType,
279
840
  unitId: this.status.activeUnit?.unitId,
@@ -304,43 +865,48 @@ export class AutoOrchestrator {
304
865
  : result.kind === "stopped"
305
866
  ? "advance-stopped"
306
867
  : "advance-error";
307
- await this.deps.runtime.journalTransition({ name: journalName, reason: recovery.reason });
868
+ this.journalTransition({ name: journalName, reason: recovery.reason });
308
869
  if (result.kind === "paused") {
309
- await this.deps.notifications.notifyLifecycle({ name: "pause", detail: recovery.reason });
870
+ this.notifyLifecycle({ name: "pause", detail: recovery.reason });
310
871
  }
311
872
  else if (result.kind === "stopped") {
312
- await this.deps.notifications.notifyLifecycle({ name: "stopped", detail: recovery.reason });
873
+ this.notifyLifecycle({ name: "stopped", detail: recovery.reason });
313
874
  }
314
875
  else if (result.kind === "error") {
315
- await this.deps.notifications.notifyLifecycle({ name: "error", detail: recovery.reason });
876
+ this.notifyLifecycle({ name: "error", detail: recovery.reason });
316
877
  }
317
- await this.deps.health.postAdvanceRecord(result);
878
+ this.postAdvanceRecord(result);
318
879
  return result;
319
880
  }
881
+ finally {
882
+ stopAdvanceTimer();
883
+ }
320
884
  }
321
885
  async resume() {
322
886
  this.lastAdvanceKey = null;
323
887
  this.lastFinalizedUnitKey = null;
324
888
  this.dispatchKeyWindow = [];
889
+ this.lastStuckRecoveryKey = null;
325
890
  this.status.phase = "running";
326
891
  this.bumpTransition();
327
- await this.deps.runtime.journalTransition({ name: "resume" });
328
- await this.deps.notifications.notifyLifecycle({ name: "resume" });
892
+ this.journalTransition({ name: "resume" });
893
+ this.notifyLifecycle({ name: "resume" });
329
894
  return { kind: "resumed" };
330
895
  }
331
896
  async stop(reason) {
332
897
  if (this.status.phase === "stopped") {
333
898
  return { kind: "stopped", reason };
334
899
  }
335
- await this.deps.worktree.cleanupOnStop(reason);
900
+ // cleanupOnStop was a no-op in the wired WorktreeAdapter.
336
901
  this.status.phase = "stopped";
337
902
  this.status.activeUnit = undefined;
338
903
  this.lastAdvanceKey = null;
339
904
  this.lastFinalizedUnitKey = null;
340
905
  this.dispatchKeyWindow = [];
906
+ this.lastStuckRecoveryKey = null;
341
907
  this.bumpTransition();
342
- await this.deps.runtime.journalTransition({ name: "stop", reason });
343
- await this.deps.notifications.notifyLifecycle({ name: "stop", detail: reason });
908
+ this.journalTransition({ name: "stop", reason });
909
+ this.notifyLifecycle({ name: "stop", detail: reason });
344
910
  return { kind: "stopped", reason };
345
911
  }
346
912
  getStatus() {
@@ -356,8 +922,10 @@ export class AutoOrchestrator {
356
922
  this.status.activeUnit = undefined;
357
923
  this.lastAdvanceKey = null;
358
924
  this.lastFinalizedUnitKey = unitKey;
925
+ // Genuine progress — re-enable graduated stuck recovery for future episodes.
926
+ this.lastStuckRecoveryKey = null;
359
927
  this.bumpTransition();
360
- await this.deps.runtime.journalTransition({
928
+ this.journalTransition({
361
929
  name: "unit-finalized",
362
930
  unitType: unit.unitType,
363
931
  unitId: unit.unitId,
@@ -376,7 +944,7 @@ export class AutoOrchestrator {
376
944
  this.lastAdvanceKey = null;
377
945
  this.lastFinalizedUnitKey = null;
378
946
  this.bumpTransition();
379
- await this.deps.runtime.journalTransition({
947
+ this.journalTransition({
380
948
  name: "unit-retry",
381
949
  reason: "finalize-retry",
382
950
  unitType: unit.unitType,
@@ -388,6 +956,40 @@ export class AutoOrchestrator {
388
956
  this.status.lastTransitionAt = now();
389
957
  }
390
958
  }
391
- export function createAutoOrchestrator(deps) {
392
- return new AutoOrchestrator(deps);
959
+ function isUsableLiveOrchestratorBasePath(basePath) {
960
+ if (!basePath || !existsSync(basePath))
961
+ return false;
962
+ if (!detectWorktreeName(basePath))
963
+ return true;
964
+ try {
965
+ return readFileSync(join(basePath, ".git"), "utf8").trim().startsWith("gitdir: ");
966
+ }
967
+ catch {
968
+ return false;
969
+ }
970
+ }
971
+ /**
972
+ * Resolve the base path the live orchestrator should dispatch from, falling
973
+ * back to the project root when the captured worktree path has been removed
974
+ * (e.g. after milestone-merge cleanup). Exported for the closeout-regression
975
+ * tests and reused by the orchestrator's getLiveDispatchBasePath.
976
+ */
977
+ export function resolveLiveOrchestratorBasePath(input) {
978
+ const primary = input.sessionBasePath || input.capturedBasePath;
979
+ if (isUsableLiveOrchestratorBasePath(primary))
980
+ return primary;
981
+ const fallbacks = [
982
+ input.originalBasePath,
983
+ input.runtimeBasePath,
984
+ resolveProjectRoot(input.capturedBasePath),
985
+ ];
986
+ for (const candidate of fallbacks) {
987
+ if (candidate && isUsableLiveOrchestratorBasePath(candidate)) {
988
+ return candidate;
989
+ }
990
+ }
991
+ return input.runtimeBasePath || input.capturedBasePath;
992
+ }
993
+ export function createAutoOrchestrator(context) {
994
+ return new AutoOrchestrator(context);
393
995
  }