@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,8 +1,61 @@
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.
3
12
 
4
- import type { AutoAdvanceResult, AutoOrchestrationModule, AutoOrchestratorDeps, AutoSessionContext, AutoStatus } from "./contracts.js";
13
+ import type { ExtensionAPI, ExtensionContext } from "@gsd/pi-coding-agent";
14
+
15
+ import type { AutoAdvanceResult, AutoOrchestrationModule, AutoSessionContext, AutoStatus } from "./contracts.js";
16
+ import type { AutoSession, PendingOrchestrationDispatch } from "./session.js";
5
17
  import type { GSDState } from "../types.js";
18
+ import type { MinimalModelRegistry } from "../context-budget.js";
19
+
20
+ import { debugCount, debugTime } from "../debug-logger.js";
21
+ import { reconcileBeforeDispatch } from "../state-reconciliation.js";
22
+ import { resolveDispatch } from "../auto-dispatch.js";
23
+ import { classifyFailure } from "../recovery-classification.js";
24
+ import { verifyExpectedArtifact, refreshRecoveryDbForArtifact } from "../auto-recovery.js";
25
+ import { invalidateAllCaches } from "../cache.js";
26
+ import { compileUnitToolContract } from "../tool-contract.js";
27
+ import { createWorktreeSafetyModule } from "../worktree-safety.js";
28
+ import { repairAutoWorktreeSafetyFailure } from "../auto-worktree-repair.js";
29
+ import { resolveManifest } from "../unit-context-manifest.js";
30
+ import {
31
+ preDispatchHealthGate,
32
+ recordHealthSnapshot,
33
+ } from "../doctor-proactive.js";
34
+ import { checkResourcesStale, autoWorktreeBranch, mergeMilestoneToMain } from "../auto-worktree.js";
35
+ import { getSessionLockStatus } from "../session-lock.js";
36
+ import { resolveUokFlags } from "../uok/flags.js";
37
+ import { emitJournalEvent as _emitJournalEvent } from "../journal.js";
38
+ import { loadEffectiveGSDPreferences, getIsolationMode } from "../preferences.js";
39
+ import { detectWorktreeName, resolveProjectRoot } from "../worktree.js";
40
+ import { GitServiceImpl } from "../git-service.js";
41
+ import { WorktreeStateProjection } from "../worktree-state-projection.js";
42
+ import { WorktreeLifecycle } from "../worktree-lifecycle.js";
43
+ import { createWorkspace, scopeMilestone } from "../workspace.js";
44
+ import { supportsStructuredQuestions } from "../workflow-mcp.js";
45
+ import { getToolBaselineSnapshot } from "../auto-model-selection.js";
46
+ import { deriveState } from "../state.js";
47
+ import { parseUnitId } from "../unit-id.js";
48
+ import { isClosedStatus } from "../status-guards.js";
49
+ import {
50
+ isDbAvailable,
51
+ getSlice,
52
+ getTask,
53
+ refreshOpenDatabaseFromDisk,
54
+ } from "../gsd-db.js";
55
+ import { getErrorMessage } from "../error-utils.js";
56
+ import { logWarning } from "../workflow-logger.js";
57
+ import { existsSync, readFileSync } from "node:fs";
58
+ import { join } from "node:path";
6
59
 
7
60
  function now(): number {
8
61
  return Date.now();
@@ -25,38 +78,662 @@ function noRemainingUnitsReason(stateSnapshot: GSDState): string {
25
78
  return "no remaining units";
26
79
  }
27
80
 
81
+ /**
82
+ * Concrete construction context for the Auto Orchestrator.
83
+ *
84
+ * Phase 2 of #442 replaced the nine adapter interfaces with this bundle of the
85
+ * real values the wiring factory used to close over: the extension context and
86
+ * API, the dispatch/runtime base paths, and the shared {@link AutoSession}
87
+ * singleton.
88
+ */
89
+ export interface OrchestratorContext {
90
+ ctx: ExtensionContext;
91
+ pi: ExtensionAPI;
92
+ dispatchBasePath: string;
93
+ runtimeBasePath: string;
94
+ session: AutoSession;
95
+ }
96
+
97
+ /** Result type of a single dispatch decision. */
98
+ export type DispatchDecision =
99
+ | { kind: "blocked"; reason: string; action: "pause" | "stop" }
100
+ | { kind: "skipped"; reason: string }
101
+ | { unitType: string; unitId: string; reason: string; preconditions: string[] }
102
+ | null;
103
+
104
+ /** Inputs to a dispatch decision. Caller-supplied fields override ctx-derived ones. */
105
+ export interface DispatchDecisionInput {
106
+ stateSnapshot: GSDState;
107
+ /** Optional live session context, forwarded to dispatch rules that need session-derived state. */
108
+ session?: AutoSession;
109
+ /** Mirrors `DispatchContext.structuredQuestionsAvailable` — "true"/"false" string per the dispatch contract. */
110
+ structuredQuestionsAvailable?: "true" | "false";
111
+ /** Session model context window in tokens, forwarded to the budget engine. */
112
+ sessionContextWindow?: number;
113
+ /** Session model provider, used for provider-specific effective context windows. */
114
+ sessionProvider?: string;
115
+ /** Model registry for executor-model lookups inside the budget engine. */
116
+ modelRegistry?: MinimalModelRegistry;
117
+ }
118
+
119
+ function getAlreadyClosedDispatchReason(unitType: string, unitId: string): string | null {
120
+ if (!isDbAvailable()) return null;
121
+ refreshOpenDatabaseFromDisk();
122
+ const { milestone, slice, task } = parseUnitId(unitId);
123
+ if (unitType === "execute-task" && milestone && slice && task) {
124
+ const row = getTask(milestone, slice, task);
125
+ return row && isClosedStatus(row.status)
126
+ ? `execute-task ${unitId} is already ${row.status}`
127
+ : null;
128
+ }
129
+ if (unitType === "complete-slice" && milestone && slice) {
130
+ const row = getSlice(milestone, slice);
131
+ return row && isClosedStatus(row.status)
132
+ ? `complete-slice ${unitId} is already ${row.status}`
133
+ : null;
134
+ }
135
+ return null;
136
+ }
137
+
138
+ function shouldAdoptActiveMilestone(
139
+ state: GSDState,
140
+ activeSession: AutoSession | undefined,
141
+ activeDispatchBasePath: string,
142
+ ): boolean {
143
+ const activeMilestoneId = state.activeMilestone?.id;
144
+ const currentMilestoneId = activeSession?.currentMilestoneId;
145
+ if (!activeSession || !activeMilestoneId || !currentMilestoneId || activeMilestoneId === currentMilestoneId) {
146
+ return false;
147
+ }
148
+
149
+ const scopedWorktreeMilestone =
150
+ (activeSession.basePath ? detectWorktreeName(activeSession.basePath) : null) ??
151
+ detectWorktreeName(activeDispatchBasePath);
152
+ if (scopedWorktreeMilestone && scopedWorktreeMilestone !== activeMilestoneId) {
153
+ return false;
154
+ }
155
+
156
+ const currentMilestone = state.registry.find((milestone) => milestone.id === currentMilestoneId);
157
+ return !!currentMilestone && isClosedStatus(currentMilestone.status);
158
+ }
159
+
160
+ /**
161
+ * Pure dispatch-decision function — formerly `createWiredDispatchAdapter`'s
162
+ * `decideNextUnit`. Folded out of the closure so the orchestrator can call it
163
+ * directly and tests can drive the exact dispatch decision logic against real
164
+ * fixtures without re-introducing an adapter seam.
165
+ *
166
+ * Derives session-derived dispatch inputs the same way phases.ts:runDispatch
167
+ * does (#5789): prefers caller-supplied values when present so test harnesses
168
+ * and alternative wirings can inject deterministic snapshots; otherwise pulls
169
+ * from the captured pi/ctx references.
170
+ */
171
+ export async function decideOrchestratorDispatch(
172
+ ctx: ExtensionContext,
173
+ pi: ExtensionAPI,
174
+ dispatchBasePath: string,
175
+ session: AutoSession | undefined,
176
+ input: DispatchDecisionInput,
177
+ ): Promise<DispatchDecision> {
178
+ const state = input.stateSnapshot;
179
+ const active = state.activeMilestone;
180
+ if (!active) return null;
181
+
182
+ const activeSession = input.session ?? session;
183
+ const activeDispatchBasePath = activeSession?.basePath || dispatchBasePath;
184
+ if (activeSession && shouldAdoptActiveMilestone(state, activeSession, activeDispatchBasePath)) {
185
+ activeSession.currentMilestoneId = active.id;
186
+ }
187
+ const prefs = loadEffectiveGSDPreferences(activeDispatchBasePath)?.preferences;
188
+
189
+ // Derive session-derived dispatch inputs the same way phases.ts:runDispatch does
190
+ // (#5789). Prefer caller-supplied values when present so test harnesses and
191
+ // alternative wirings can inject deterministic snapshots; otherwise pull from
192
+ // the captured pi/ctx references.
193
+ const sessionProvider = input.sessionProvider ?? ctx.model?.provider;
194
+ const sessionContextWindow = input.sessionContextWindow ?? ctx.model?.contextWindow;
195
+ const modelRegistry = input.modelRegistry ?? (ctx.modelRegistry as MinimalModelRegistry | undefined);
196
+ const authMode =
197
+ sessionProvider && typeof ctx.modelRegistry?.getProviderAuthMode === "function"
198
+ ? ctx.modelRegistry.getProviderAuthMode(sessionProvider)
199
+ : undefined;
200
+ // Use baseline snapshot — same reason as phases.ts:runDispatch: the live
201
+ // active set may be narrowed by the prior unit before selectAndApplyModel
202
+ // restores it, causing false transport-preflight failures (#477 follow-up).
203
+ const activeTools = getToolBaselineSnapshot(pi);
204
+ // Mirrors runDispatch: deep-planning keeps approval gates in plain chat
205
+ // because structured questions can be cancelled outside the chat turn on
206
+ // some transports.
207
+ const structuredQuestionsAvailable =
208
+ input.structuredQuestionsAvailable ??
209
+ (prefs?.planning_depth === "deep"
210
+ ? "false"
211
+ : supportsStructuredQuestions(activeTools, {
212
+ authMode,
213
+ baseUrl: ctx.model?.baseUrl,
214
+ })
215
+ ? "true"
216
+ : "false");
217
+
218
+ const pendingRetry = session?.pendingVerificationRetryDispatch;
219
+ if (session && pendingRetry) {
220
+ session.pendingVerificationRetryDispatch = null;
221
+ const alreadyClosedReason = getAlreadyClosedDispatchReason(
222
+ pendingRetry.unitType,
223
+ pendingRetry.unitId,
224
+ );
225
+ if (alreadyClosedReason) {
226
+ session.pendingOrchestrationDispatch = null;
227
+ session.pendingVerificationRetry = null;
228
+ return { kind: "skipped", reason: alreadyClosedReason };
229
+ }
230
+ session.pendingOrchestrationDispatch = pendingRetry;
231
+ return {
232
+ unitType: pendingRetry.unitType,
233
+ unitId: pendingRetry.unitId,
234
+ reason: "verification-retry",
235
+ preconditions: [],
236
+ };
237
+ }
238
+
239
+ const action = await resolveDispatch({
240
+ basePath: activeDispatchBasePath,
241
+ mid: active.id,
242
+ midTitle: active.title,
243
+ state,
244
+ prefs,
245
+ session: activeSession,
246
+ structuredQuestionsAvailable,
247
+ sessionContextWindow,
248
+ sessionProvider,
249
+ modelRegistry,
250
+ activeTools,
251
+ sessionAuthMode: authMode,
252
+ sessionBaseUrl: ctx.model?.baseUrl,
253
+ });
254
+
255
+ if (action.action === "stop") {
256
+ if (session) session.pendingOrchestrationDispatch = null;
257
+ return {
258
+ kind: "blocked",
259
+ reason: action.reason,
260
+ action: action.level === "warning" ? "pause" : "stop",
261
+ };
262
+ }
263
+ if (action.action !== "dispatch") {
264
+ if (session) session.pendingOrchestrationDispatch = null;
265
+ return {
266
+ kind: "skipped",
267
+ reason: action.matchedRule ?? "dispatch-skip",
268
+ };
269
+ }
270
+ const alreadyClosedReason = getAlreadyClosedDispatchReason(action.unitType, action.unitId);
271
+ if (alreadyClosedReason) {
272
+ if (session) {
273
+ session.pendingOrchestrationDispatch = null;
274
+ session.pendingVerificationRetry = null;
275
+ }
276
+ return { kind: "skipped", reason: alreadyClosedReason };
277
+ }
278
+ if (session) {
279
+ const pending: PendingOrchestrationDispatch = {
280
+ unitType: action.unitType,
281
+ unitId: action.unitId,
282
+ prompt: action.prompt,
283
+ pauseAfterUatDispatch: action.pauseAfterDispatch ?? false,
284
+ state,
285
+ mid: active.id,
286
+ midTitle: active.title,
287
+ };
288
+ session.pendingOrchestrationDispatch = pending;
289
+ }
290
+ return {
291
+ unitType: action.unitType,
292
+ unitId: action.unitId,
293
+ reason: action.matchedRule ?? "dispatch",
294
+ preconditions: [],
295
+ };
296
+ }
297
+
28
298
  export class AutoOrchestrator implements AutoOrchestrationModule {
29
299
  private status: AutoStatus = {
30
300
  phase: "idle",
31
301
  transitionCount: 0,
32
302
  };
33
- private readonly deps: AutoOrchestratorDeps;
303
+ private readonly ctx: ExtensionContext;
304
+ private readonly pi: ExtensionAPI;
305
+ private readonly dispatchBasePath: string;
306
+ private readonly runtimeBasePath: string;
307
+ private readonly s: AutoSession;
308
+ private readonly flowId: string;
309
+ private seq = 0;
34
310
  private lastAdvanceKey: string | null = null;
35
311
  private lastFinalizedUnitKey: string | null = null;
36
312
  private dispatchKeyWindow: string[] = [];
313
+ // #442: the unit key we last attempted graduated stuck-recovery for. Bounds
314
+ // recovery to one attempt per stuck episode per run (reset on start/resume/
315
+ // stop), mirroring the legacy Level-1-then-Level-2 escalation in phases.ts.
316
+ private lastStuckRecoveryKey: string | null = null;
317
+
318
+ public constructor(context: OrchestratorContext) {
319
+ this.ctx = context.ctx;
320
+ this.pi = context.pi;
321
+ this.dispatchBasePath = context.dispatchBasePath;
322
+ this.runtimeBasePath = context.runtimeBasePath;
323
+ this.s = context.session;
324
+ this.flowId = `auto-orchestrator-${Date.now()}`;
325
+ }
326
+
327
+ // ── Live base-path resolution (was the wiring factory's getLiveDispatchBasePath) ──
328
+
329
+ private getLiveDispatchBasePath(): string {
330
+ return resolveLiveOrchestratorBasePath({
331
+ capturedBasePath: this.dispatchBasePath,
332
+ runtimeBasePath: this.runtimeBasePath,
333
+ sessionBasePath: this.s.basePath,
334
+ originalBasePath: this.s.originalBasePath,
335
+ });
336
+ }
337
+
338
+ // ── RuntimePersistenceAdapter (folded) ───────────────────────────────────
339
+
340
+ private ensureLockOwnership(): void {
341
+ const status = getSessionLockStatus(this.runtimeBasePath);
342
+ if (!status.valid || status.failureReason === "pid-mismatch") {
343
+ throw new Error("session lock held by another process");
344
+ }
345
+ }
346
+
347
+ /**
348
+ * Map an orchestrator lifecycle event name to its journal eventType and emit
349
+ * it. The name→eventType ternary is preserved byte-for-byte from the legacy
350
+ * wired RuntimePersistenceAdapter.journalTransition.
351
+ */
352
+ private journalTransition(event: {
353
+ name: string;
354
+ reason?: string;
355
+ unitType?: string;
356
+ unitId?: string;
357
+ }): void {
358
+ const eventType = event.name === "start"
359
+ ? "orchestrator-iteration-start"
360
+ : event.name === "resume"
361
+ ? "orchestrator-iteration-start"
362
+ : event.name === "advance"
363
+ ? "orchestrator-dispatch-match"
364
+ : event.name === "advance-blocked"
365
+ ? "orchestrator-guard-block"
366
+ : event.name === "advance-stopped"
367
+ ? "orchestrator-dispatch-stop"
368
+ : event.name === "advance-error"
369
+ ? "orchestrator-iteration-end"
370
+ : event.name === "advance-paused" || event.name === "advance-retry"
371
+ ? "orchestrator-guard-block"
372
+ : event.name === "stop"
373
+ ? "orchestrator-terminal"
374
+ : "orchestrator-iteration-end";
375
+
376
+ _emitJournalEvent(this.runtimeBasePath, {
377
+ ts: new Date().toISOString(),
378
+ flowId: this.flowId,
379
+ seq: ++this.seq,
380
+ eventType,
381
+ data: {
382
+ source: "auto-orchestrator",
383
+ name: event.name,
384
+ reason: event.reason,
385
+ unitType: event.unitType,
386
+ unitId: event.unitId,
387
+ },
388
+ });
389
+ }
390
+
391
+ // ── NotificationAdapter (folded) ─────────────────────────────────────────
392
+
393
+ private notifyLifecycle(event: { name: string; detail?: string }): void {
394
+ if (event.name === "error") {
395
+ this.ctx.ui.notify(event.detail ?? "auto orchestration error", "error");
396
+ }
397
+ }
398
+
399
+ // ── HealthAdapter (folded) ───────────────────────────────────────────────
400
+
401
+ private checkResourcesStale(): string | null {
402
+ return checkResourcesStale(this.s.resourceVersionOnStart);
403
+ }
404
+
405
+ private async preAdvanceGate(): Promise<
406
+ | { kind: "pass"; fixesApplied?: readonly string[] }
407
+ | { kind: "fail"; reason: string; action?: "pause" | "stop" }
408
+ | { kind: "threw"; error: unknown }
409
+ > {
410
+ try {
411
+ const gate = await preDispatchHealthGate(this.getLiveDispatchBasePath());
412
+ if (gate.proceed) {
413
+ return {
414
+ kind: "pass",
415
+ fixesApplied: gate.fixesApplied,
416
+ };
417
+ }
418
+ return {
419
+ kind: "fail",
420
+ reason: gate.reason ?? "Pre-dispatch health check failed — run /gsd doctor for details.",
421
+ action: gate.severity ?? "pause",
422
+ };
423
+ } catch (error) {
424
+ return { kind: "threw", error };
425
+ }
426
+ }
427
+
428
+ private postAdvanceRecord(result: AutoAdvanceResult): void {
429
+ if (result.kind === "error") {
430
+ recordHealthSnapshot(1, 0, 0, [{
431
+ code: "orchestration-error",
432
+ message: result.reason ?? "orchestration error",
433
+ severity: "error",
434
+ unitId: "orchestration",
435
+ }], [], "orchestration");
436
+ } else if (result.kind === "blocked") {
437
+ recordHealthSnapshot(0, 1, 0, [{
438
+ code: "orchestration-blocked",
439
+ message: result.reason ?? "orchestration blocked",
440
+ severity: "warning",
441
+ unitId: "orchestration",
442
+ }], [], "orchestration");
443
+ }
444
+ }
445
+
446
+ // ── UokGateAdapter (folded) ──────────────────────────────────────────────
447
+
448
+ private async emitUokGate(input: {
449
+ gateId: string;
450
+ gateType: "policy" | "execution";
451
+ outcome: "pass" | "fail" | "manual-attention";
452
+ failureClass: "none" | "policy" | "manual-attention";
453
+ rationale: string;
454
+ findings?: string;
455
+ milestoneId?: string;
456
+ }): Promise<void> {
457
+ const activeBasePath = this.getLiveDispatchBasePath();
458
+ const prefs = loadEffectiveGSDPreferences(activeBasePath)?.preferences;
459
+ const uokFlags = resolveUokFlags(prefs);
460
+ if (!uokFlags.gates) return;
461
+ const milestoneId = input.milestoneId ?? this.s.currentMilestoneId ?? undefined;
462
+ try {
463
+ const { UokGateRunner } = await import("../uok/gate-runner.js");
464
+ const runner = new UokGateRunner();
465
+ runner.register({
466
+ id: input.gateId,
467
+ type: input.gateType,
468
+ execute: async () => ({
469
+ outcome: input.outcome,
470
+ failureClass: input.failureClass,
471
+ rationale: input.rationale,
472
+ findings: input.findings ?? "",
473
+ }),
474
+ });
475
+ await runner.run(input.gateId, {
476
+ basePath: activeBasePath,
477
+ traceId: `pre-dispatch:${this.flowId}`,
478
+ turnId: `orch-${this.seq}`,
479
+ milestoneId,
480
+ unitType: "pre-dispatch",
481
+ unitId: `orch-${this.seq}`,
482
+ });
483
+ } catch (err) {
484
+ logWarning("engine", `uok gate emit failed: ${getErrorMessage(err)}`, {
485
+ file: "orchestrator.ts",
486
+ gateId: input.gateId,
487
+ gateType: input.gateType,
488
+ ...(milestoneId ? { milestoneId } : {}),
489
+ });
490
+ }
491
+ }
492
+
493
+ // ── StateReconciliationAdapter (folded) ──────────────────────────────────
494
+
495
+ private async reconcileBeforeDispatch(): Promise<
496
+ { ok: true; reason: string; stateSnapshot?: GSDState }
497
+ | { ok: false; reason: string; stateSnapshot?: GSDState }
498
+ > {
499
+ const activeBasePath = this.getLiveDispatchBasePath();
500
+ const result = await reconcileBeforeDispatch(activeBasePath);
501
+ // Failure-path summaries written by gsd_summary_save create
502
+ // artifact-db-status-divergence blockers for tasks that are still
503
+ // pending (gsd_task_complete never ran). These tasks can still be
504
+ // dispatched and the drift self-heals once they complete successfully.
505
+ const hardBlockers = result.blockers.filter(
506
+ (b) =>
507
+ !b.includes("has SUMMARY artifact while DB status is") &&
508
+ !b.includes("has SUMMARY on disk while DB status is") &&
509
+ !b.includes("has task SUMMARY artifacts but no DB tasks"),
510
+ );
511
+ if (hardBlockers.length > 0) {
512
+ return {
513
+ ok: false,
514
+ reason: hardBlockers[0],
515
+ stateSnapshot: result.stateSnapshot,
516
+ };
517
+ }
518
+ const repairedKinds = result.repaired.map((d) => d.kind);
519
+ return {
520
+ ok: true,
521
+ reason:
522
+ repairedKinds.length > 0
523
+ ? `repaired: ${repairedKinds.join(", ")}`
524
+ : "clean",
525
+ stateSnapshot: result.stateSnapshot,
526
+ };
527
+ }
528
+
529
+ // ── DispatchAdapter (folded) ─────────────────────────────────────────────
37
530
 
38
- public constructor(deps: AutoOrchestratorDeps) {
39
- this.deps = deps;
531
+ private decideNextUnit(input: DispatchDecisionInput): Promise<DispatchDecision> {
532
+ return decideOrchestratorDispatch(this.ctx, this.pi, this.dispatchBasePath, this.s, input);
533
+ }
534
+
535
+ // ── ToolContractAdapter (folded) ─────────────────────────────────────────
536
+
537
+ private compileUnitToolContract(unitType: string): { ok: true; reason: string } | { ok: false; reason: string } {
538
+ const result = compileUnitToolContract(unitType);
539
+ if (!result.ok) return { ok: false, reason: result.detail };
540
+ return { ok: true, reason: result.contract.validationRules.join(", ") };
541
+ }
542
+
543
+ // ── WorktreeAdapter (folded) ─────────────────────────────────────────────
544
+
545
+ private getEffectiveUnitIsolationMode(basePath: string): ReturnType<typeof getIsolationMode> {
546
+ const configuredMode = getIsolationMode(basePath);
547
+ return configuredMode === "worktree" && this.s.isolationDegraded ? "branch" : configuredMode;
548
+ }
549
+
550
+ private buildLifecycle(): WorktreeLifecycle {
551
+ return new WorktreeLifecycle(this.s, {
552
+ gitServiceFactory: (basePath: string) => {
553
+ const gitConfig = loadEffectiveGSDPreferences()?.preferences?.git ?? {};
554
+ return new GitServiceImpl(basePath, gitConfig);
555
+ },
556
+ worktreeProjection: new WorktreeStateProjection(),
557
+ mergeMilestoneToMain,
558
+ });
559
+ }
560
+
561
+ private rebuildScope(rawPath: string, milestoneId: string | null): void {
562
+ if (!milestoneId) {
563
+ this.s.scope = null;
564
+ return;
565
+ }
566
+ try {
567
+ const workspace = createWorkspace(rawPath);
568
+ this.s.scope = scopeMilestone(workspace, milestoneId);
569
+ } catch {
570
+ // Non-fatal — scope is additive. Existing readers still use basePath.
571
+ this.s.scope = null;
572
+ }
573
+ }
574
+
575
+ private async prepareWorktreeForUnit(
576
+ unitType: string,
577
+ unitId: string,
578
+ ): Promise<{ ok: true; reason: string } | { ok: false; reason: string }> {
579
+ const isolationMode = this.getEffectiveUnitIsolationMode(this.runtimeBasePath);
580
+ const manifest = resolveManifest(unitType);
581
+ if (!manifest) {
582
+ return {
583
+ ok: false,
584
+ reason: `No Unit manifest is registered for ${unitType}`,
585
+ };
586
+ }
587
+ if (isolationMode !== "worktree") {
588
+ return { ok: true, reason: "not-required" };
589
+ }
590
+ const writeScope =
591
+ manifest.tools.mode === "all" || manifest.tools.mode === "docs"
592
+ ? "source-writing"
593
+ : "planning-only";
594
+ const safety = createWorktreeSafetyModule();
595
+ const activeBasePath = this.getLiveDispatchBasePath();
596
+ const snapshot = await deriveState(activeBasePath);
597
+ const milestoneId = snapshot.activeMilestone?.id ?? null;
598
+ const expectedBranch = milestoneId ? autoWorktreeBranch(milestoneId) : null;
599
+ let result = safety.validateUnitRoot({
600
+ unitType,
601
+ unitId,
602
+ writeScope,
603
+ projectRoot: this.runtimeBasePath,
604
+ unitRoot: activeBasePath,
605
+ milestoneId,
606
+ isolationMode,
607
+ expectedBranch,
608
+ });
609
+ if (!result.ok) {
610
+ const repaired = await repairAutoWorktreeSafetyFailure({
611
+ safetyResult: result,
612
+ projectRoot: this.runtimeBasePath,
613
+ activeRoot: activeBasePath,
614
+ milestoneId,
615
+ enterMilestone: async (id) => {
616
+ this.buildLifecycle().adoptSessionRoot(this.runtimeBasePath, this.s.originalBasePath || this.runtimeBasePath);
617
+ const enterResult = this.buildLifecycle().enterMilestone(id, {
618
+ notify: this.ctx.ui.notify.bind(this.ctx.ui),
619
+ });
620
+ if (!enterResult.ok) return { ok: false, reason: enterResult.reason };
621
+ this.rebuildScope(this.s.basePath, this.s.currentMilestoneId);
622
+ return { ok: true };
623
+ },
624
+ revalidate: () => safety.validateUnitRoot({
625
+ unitType,
626
+ unitId,
627
+ writeScope,
628
+ projectRoot: this.runtimeBasePath,
629
+ unitRoot: this.getLiveDispatchBasePath(),
630
+ milestoneId,
631
+ isolationMode: this.getEffectiveUnitIsolationMode(this.runtimeBasePath),
632
+ expectedBranch,
633
+ }),
634
+ });
635
+ result = repaired.result;
636
+ if (result.ok) {
637
+ return { ok: true, reason: repaired.repaired ? `repaired-${result.kind}` : result.kind };
638
+ }
639
+ const repairDetail = repaired.repairReason
640
+ ? ` (repair skipped: ${repaired.repairReason})`
641
+ : "";
642
+ return { ok: false, reason: `${result.kind}: ${result.reason}${repairDetail}` };
643
+ }
644
+ return { ok: true, reason: result.kind };
645
+ }
646
+
647
+ // ── RecoveryAdapter (folded) ─────────────────────────────────────────────
648
+
649
+ private classifyAndRecover(input: {
650
+ error: unknown;
651
+ unitType?: string;
652
+ unitId?: string;
653
+ }): { action: "retry" | "escalate" | "stop"; reason: string } {
654
+ const recovery = classifyFailure(input);
655
+ return { action: recovery.action, reason: recovery.reason };
656
+ }
657
+
658
+ // ── Lifecycle verbs ──────────────────────────────────────────────────────
659
+
660
+ /**
661
+ * #442: graduated stuck recovery, ported from the legacy
662
+ * auto/phases.ts:runDispatch path that Phase 3 retires. The ring-buffer
663
+ * hard-stops (stuck-loop saturation and finalized-repeat) would otherwise
664
+ * KILL a unit that actually completed on disk but whose DB row is still
665
+ * stale. Before hard-stopping, verify the expected artifact exists; if so,
666
+ * refresh the DB from it, invalidate caches and reset the dispatch ring so
667
+ * the next advance picks the correct next unit. Bounded to one attempt per
668
+ * stuck key per episode (reset on lifecycle + genuine finalize) to avoid an
669
+ * unbounded recover→re-saturate→recover loop — mirrors the legacy
670
+ * Level-1-recover-then-Level-2-hard-stop escalation.
671
+ *
672
+ * Returns true when recovery succeeded; the caller should re-loop (return a
673
+ * skipped result) instead of stopping.
674
+ */
675
+ private tryStuckArtifactRecovery(unitType: string, unitId: string): boolean {
676
+ const key = `${unitType}:${unitId}`;
677
+ if (this.lastStuckRecoveryKey === key) return false; // already tried this episode
678
+ const basePath = this.getLiveDispatchBasePath();
679
+ if (!verifyExpectedArtifact(unitType, unitId, basePath)) return false;
680
+ const refreshed = refreshRecoveryDbForArtifact(unitType, unitId, basePath);
681
+ // Fatal failures cannot be recovered — hard-stop. Non-fatal (e.g. plan-slice
682
+ // DB refresh hiccup) still fall through: invalidating caches and resetting
683
+ // the ring gives the next advance a clean slate to pick up the correct state,
684
+ // mirroring the legacy Level-1 "continue" escalation path.
685
+ if (!refreshed.ok && refreshed.fatal) return false;
686
+ this.lastStuckRecoveryKey = key;
687
+ invalidateAllCaches();
688
+ this.dispatchKeyWindow = [];
689
+ this.lastAdvanceKey = null;
690
+ this.lastFinalizedUnitKey = null;
691
+ return true;
692
+ }
693
+
694
+ private stuckRecovered(
695
+ decision: { unitType: string; unitId: string },
696
+ stateSnapshot: GSDState,
697
+ ): AutoAdvanceResult {
698
+ const recovered: AutoAdvanceResult = {
699
+ kind: "skipped",
700
+ reason: `stuck-recovery: ${decision.unitType} ${decision.unitId} artifact found on disk; DB refreshed`,
701
+ stateSnapshot,
702
+ };
703
+ this.status.phase = "running";
704
+ this.status.activeUnit = undefined;
705
+ this.bumpTransition();
706
+ this.journalTransition({
707
+ name: "advance-skipped",
708
+ reason: recovered.reason,
709
+ unitType: decision.unitType,
710
+ unitId: decision.unitId,
711
+ });
712
+ this.postAdvanceRecord(recovered);
713
+ return recovered;
40
714
  }
41
715
 
42
716
  public async start(_sessionContext: AutoSessionContext): Promise<AutoAdvanceResult> {
43
717
  this.lastAdvanceKey = null;
44
718
  this.lastFinalizedUnitKey = null;
45
719
  this.dispatchKeyWindow = [];
720
+ this.lastStuckRecoveryKey = null;
46
721
  this.status.phase = "running";
47
722
  this.bumpTransition();
48
- await this.deps.runtime.journalTransition({ name: "start" });
49
- await this.deps.notifications.notifyLifecycle({ name: "start" });
723
+ this.journalTransition({ name: "start" });
724
+ this.notifyLifecycle({ name: "start" });
50
725
  return { kind: "started" };
51
726
  }
52
727
 
53
728
  public async advance(): Promise<AutoAdvanceResult> {
729
+ debugCount("dispatches");
730
+ const stopAdvanceTimer = debugTime("orchestrator-advance");
54
731
  try {
55
- await this.deps.runtime.ensureLockOwnership();
732
+ this.ensureLockOwnership();
56
733
 
57
- const staleMsg = this.deps.health.checkResourcesStale();
734
+ const staleMsg = this.checkResourcesStale();
58
735
  if (staleMsg) {
59
- await this.deps.uokGate.emit({
736
+ await this.emitUokGate({
60
737
  gateId: "resource-version-guard",
61
738
  gateType: "policy",
62
739
  outcome: "fail",
@@ -65,11 +742,11 @@ export class AutoOrchestrator implements AutoOrchestrationModule {
65
742
  findings: staleMsg,
66
743
  });
67
744
  const blocked: AutoAdvanceResult = { kind: "blocked", reason: staleMsg, action: "pause" };
68
- await this.deps.runtime.journalTransition({ name: "advance-blocked", reason: blocked.reason });
69
- await this.deps.health.postAdvanceRecord(blocked);
745
+ this.journalTransition({ name: "advance-blocked", reason: blocked.reason });
746
+ this.postAdvanceRecord(blocked);
70
747
  return blocked;
71
748
  }
72
- await this.deps.uokGate.emit({
749
+ await this.emitUokGate({
73
750
  gateId: "resource-version-guard",
74
751
  gateType: "policy",
75
752
  outcome: "pass",
@@ -77,9 +754,9 @@ export class AutoOrchestrator implements AutoOrchestrationModule {
77
754
  rationale: "resource version guard passed",
78
755
  });
79
756
 
80
- const gate = await this.deps.health.preAdvanceGate();
757
+ const gate = await this.preAdvanceGate();
81
758
  if (gate.kind === "fail") {
82
- await this.deps.uokGate.emit({
759
+ await this.emitUokGate({
83
760
  gateId: "pre-dispatch-health-gate",
84
761
  gateType: "execution",
85
762
  outcome: "manual-attention",
@@ -92,12 +769,12 @@ export class AutoOrchestrator implements AutoOrchestrationModule {
92
769
  reason: gate.reason,
93
770
  action: gate.action ?? "pause",
94
771
  };
95
- await this.deps.runtime.journalTransition({ name: "advance-blocked", reason: blocked.reason });
96
- await this.deps.health.postAdvanceRecord(blocked);
772
+ this.journalTransition({ name: "advance-blocked", reason: blocked.reason });
773
+ this.postAdvanceRecord(blocked);
97
774
  return blocked;
98
775
  }
99
776
  if (gate.kind === "threw") {
100
- await this.deps.uokGate.emit({
777
+ await this.emitUokGate({
101
778
  gateId: "pre-dispatch-health-gate",
102
779
  gateType: "execution",
103
780
  outcome: "manual-attention",
@@ -107,7 +784,7 @@ export class AutoOrchestrator implements AutoOrchestrationModule {
107
784
  });
108
785
  // intentional fall-through: matches runPreDispatch behaviour
109
786
  } else {
110
- await this.deps.uokGate.emit({
787
+ await this.emitUokGate({
111
788
  gateId: "pre-dispatch-health-gate",
112
789
  gateType: "execution",
113
790
  outcome: "pass",
@@ -117,7 +794,7 @@ export class AutoOrchestrator implements AutoOrchestrationModule {
117
794
  });
118
795
  }
119
796
 
120
- const reconciliation = await this.deps.stateReconciliation.reconcileBeforeDispatch();
797
+ const reconciliation = await this.reconcileBeforeDispatch();
121
798
  if (!reconciliation.ok || !reconciliation.stateSnapshot) {
122
799
  const blocked: AutoAdvanceResult = {
123
800
  kind: "blocked",
@@ -125,12 +802,12 @@ export class AutoOrchestrator implements AutoOrchestrationModule {
125
802
  action: "pause",
126
803
  stateSnapshot: reconciliation.stateSnapshot,
127
804
  };
128
- await this.deps.runtime.journalTransition({ name: "advance-blocked", reason: blocked.reason });
129
- await this.deps.health.postAdvanceRecord(blocked);
805
+ this.journalTransition({ name: "advance-blocked", reason: blocked.reason });
806
+ this.postAdvanceRecord(blocked);
130
807
  return blocked;
131
808
  }
132
809
 
133
- const decision = await this.deps.dispatch.decideNextUnit({ stateSnapshot: reconciliation.stateSnapshot });
810
+ const decision = await this.decideNextUnit({ stateSnapshot: reconciliation.stateSnapshot });
134
811
  if (!decision) {
135
812
  const stopped: AutoAdvanceResult = {
136
813
  kind: "stopped",
@@ -142,8 +819,8 @@ export class AutoOrchestrator implements AutoOrchestrationModule {
142
819
  this.lastAdvanceKey = null;
143
820
  this.dispatchKeyWindow = [];
144
821
  this.bumpTransition();
145
- await this.deps.runtime.journalTransition({ name: "advance-stopped", reason: stopped.reason });
146
- await this.deps.health.postAdvanceRecord(stopped);
822
+ this.journalTransition({ name: "advance-stopped", reason: stopped.reason });
823
+ this.postAdvanceRecord(stopped);
147
824
  return stopped;
148
825
  }
149
826
  if ("kind" in decision && decision.kind === "skipped") {
@@ -155,8 +832,8 @@ export class AutoOrchestrator implements AutoOrchestrationModule {
155
832
  this.status.phase = "running";
156
833
  this.status.activeUnit = undefined;
157
834
  this.bumpTransition();
158
- await this.deps.runtime.journalTransition({ name: "advance-skipped", reason: skipped.reason });
159
- await this.deps.health.postAdvanceRecord(skipped);
835
+ this.journalTransition({ name: "advance-skipped", reason: skipped.reason });
836
+ this.postAdvanceRecord(skipped);
160
837
  return skipped;
161
838
  }
162
839
  if (!("unitType" in decision)) {
@@ -166,8 +843,8 @@ export class AutoOrchestrator implements AutoOrchestrationModule {
166
843
  action: decision.action,
167
844
  stateSnapshot: reconciliation.stateSnapshot,
168
845
  };
169
- await this.deps.runtime.journalTransition({ name: "advance-blocked", reason: blocked.reason });
170
- await this.deps.health.postAdvanceRecord(blocked);
846
+ this.journalTransition({ name: "advance-blocked", reason: blocked.reason });
847
+ this.postAdvanceRecord(blocked);
171
848
  return blocked;
172
849
  }
173
850
 
@@ -184,19 +861,25 @@ export class AutoOrchestrator implements AutoOrchestrationModule {
184
861
 
185
862
  const matchingCount = this.dispatchKeyWindow.filter((k) => k === nextKey).length;
186
863
  if (this.lastFinalizedUnitKey === nextKey) {
864
+ // #442: the unit re-dispatched immediately after finalizing may have
865
+ // actually completed on disk with a stale DB. Verify + recover before
866
+ // hard-stopping (legacy graduated stuck-recovery parity).
867
+ if (this.tryStuckArtifactRecovery(decision.unitType, decision.unitId)) {
868
+ return this.stuckRecovered(decision, reconciliation.stateSnapshot);
869
+ }
187
870
  const blocked: AutoAdvanceResult = {
188
871
  kind: "blocked",
189
872
  reason: `state did not advance after finalized ${decision.unitType} ${decision.unitId}`,
190
873
  action: "stop",
191
874
  stateSnapshot: reconciliation.stateSnapshot,
192
875
  };
193
- await this.deps.runtime.journalTransition({
876
+ this.journalTransition({
194
877
  name: "advance-blocked",
195
878
  reason: blocked.reason,
196
879
  unitType: decision.unitType,
197
880
  unitId: decision.unitId,
198
881
  });
199
- await this.deps.health.postAdvanceRecord(blocked);
882
+ this.postAdvanceRecord(blocked);
200
883
  return blocked;
201
884
  }
202
885
 
@@ -208,13 +891,13 @@ export class AutoOrchestrator implements AutoOrchestrationModule {
208
891
  // stuck-loop for the saturated-window case.
209
892
  if (this.lastAdvanceKey === nextKey && matchingCount < STUCK_WINDOW_SIZE) {
210
893
  const blocked: AutoAdvanceResult = { kind: "blocked", reason: "idempotent advance: unit already active", action: "pause" };
211
- await this.deps.runtime.journalTransition({
894
+ this.journalTransition({
212
895
  name: "advance-blocked",
213
896
  reason: blocked.reason,
214
897
  unitType: decision.unitType,
215
898
  unitId: decision.unitId,
216
899
  });
217
- await this.deps.health.postAdvanceRecord(blocked);
900
+ this.postAdvanceRecord(blocked);
218
901
  return blocked;
219
902
  }
220
903
 
@@ -223,22 +906,28 @@ export class AutoOrchestrator implements AutoOrchestrationModule {
223
906
  // picking the same unit across the whole window and must hard-stop with
224
907
  // a diagnosable reason.
225
908
  if (matchingCount >= STUCK_WINDOW_SIZE) {
909
+ // #442: before declaring a stuck loop, verify the unit didn't actually
910
+ // complete on disk (stale DB) and recover if so — legacy graduated
911
+ // stuck-recovery parity. Otherwise hard-stop with a diagnosable reason.
912
+ if (this.tryStuckArtifactRecovery(decision.unitType, decision.unitId)) {
913
+ return this.stuckRecovered(decision, reconciliation.stateSnapshot);
914
+ }
226
915
  const blocked: AutoAdvanceResult = {
227
916
  kind: "blocked",
228
917
  reason: `stuck-loop: ${nextKey} picked ${matchingCount} times`,
229
918
  action: "stop",
230
919
  };
231
- await this.deps.runtime.journalTransition({
920
+ this.journalTransition({
232
921
  name: "advance-blocked",
233
922
  reason: blocked.reason,
234
923
  unitType: decision.unitType,
235
924
  unitId: decision.unitId,
236
925
  });
237
- await this.deps.health.postAdvanceRecord(blocked);
926
+ this.postAdvanceRecord(blocked);
238
927
  return blocked;
239
928
  }
240
929
 
241
- const contract = await this.deps.toolContract.compileUnitToolContract(decision.unitType, decision.unitId);
930
+ const contract = this.compileUnitToolContract(decision.unitType);
242
931
  if (!contract.ok) {
243
932
  const blocked: AutoAdvanceResult = {
244
933
  kind: "blocked",
@@ -246,17 +935,17 @@ export class AutoOrchestrator implements AutoOrchestrationModule {
246
935
  action: "pause",
247
936
  stateSnapshot: reconciliation.stateSnapshot,
248
937
  };
249
- await this.deps.runtime.journalTransition({
938
+ this.journalTransition({
250
939
  name: "advance-blocked",
251
940
  reason: blocked.reason,
252
941
  unitType: decision.unitType,
253
942
  unitId: decision.unitId,
254
943
  });
255
- await this.deps.health.postAdvanceRecord(blocked);
944
+ this.postAdvanceRecord(blocked);
256
945
  return blocked;
257
946
  }
258
947
 
259
- const worktree = await this.deps.worktree.prepareForUnit(decision.unitType, decision.unitId);
948
+ const worktree = await this.prepareWorktreeForUnit(decision.unitType, decision.unitId);
260
949
  if (!worktree.ok) {
261
950
  const blocked: AutoAdvanceResult = {
262
951
  kind: "blocked",
@@ -264,13 +953,13 @@ export class AutoOrchestrator implements AutoOrchestrationModule {
264
953
  action: "pause",
265
954
  stateSnapshot: reconciliation.stateSnapshot,
266
955
  };
267
- await this.deps.runtime.journalTransition({
956
+ this.journalTransition({
268
957
  name: "advance-blocked",
269
958
  reason: blocked.reason,
270
959
  unitType: decision.unitType,
271
960
  unitId: decision.unitId,
272
961
  });
273
- await this.deps.health.postAdvanceRecord(blocked);
962
+ this.postAdvanceRecord(blocked);
274
963
  return blocked;
275
964
  }
276
965
 
@@ -279,23 +968,23 @@ export class AutoOrchestrator implements AutoOrchestrationModule {
279
968
  this.lastAdvanceKey = nextKey;
280
969
  this.bumpTransition();
281
970
 
282
- await this.deps.runtime.journalTransition({
971
+ this.journalTransition({
283
972
  name: "advance",
284
973
  reason: decision.reason,
285
974
  unitType: decision.unitType,
286
975
  unitId: decision.unitId,
287
976
  });
288
- await this.deps.worktree.syncAfterUnit(decision.unitType, decision.unitId);
977
+ // syncAfterUnit was a no-op in the wired WorktreeAdapter.
289
978
 
290
979
  const advanced: AutoAdvanceResult = {
291
980
  kind: "advanced",
292
981
  unit: { unitType: decision.unitType, unitId: decision.unitId },
293
982
  stateSnapshot: reconciliation.stateSnapshot,
294
983
  };
295
- await this.deps.health.postAdvanceRecord(advanced);
984
+ this.postAdvanceRecord(advanced);
296
985
  return advanced;
297
986
  } catch (error) {
298
- const recovery = await this.deps.recovery.classifyAndRecover({
987
+ const recovery = this.classifyAndRecover({
299
988
  error,
300
989
  unitType: this.status.activeUnit?.unitType,
301
990
  unitId: this.status.activeUnit?.unitId,
@@ -327,17 +1016,19 @@ export class AutoOrchestrator implements AutoOrchestrationModule {
327
1016
  : result.kind === "stopped"
328
1017
  ? "advance-stopped"
329
1018
  : "advance-error";
330
- await this.deps.runtime.journalTransition({ name: journalName, reason: recovery.reason });
1019
+ this.journalTransition({ name: journalName, reason: recovery.reason });
331
1020
 
332
1021
  if (result.kind === "paused") {
333
- await this.deps.notifications.notifyLifecycle({ name: "pause", detail: recovery.reason });
1022
+ this.notifyLifecycle({ name: "pause", detail: recovery.reason });
334
1023
  } else if (result.kind === "stopped") {
335
- await this.deps.notifications.notifyLifecycle({ name: "stopped", detail: recovery.reason });
1024
+ this.notifyLifecycle({ name: "stopped", detail: recovery.reason });
336
1025
  } else if (result.kind === "error") {
337
- await this.deps.notifications.notifyLifecycle({ name: "error", detail: recovery.reason });
1026
+ this.notifyLifecycle({ name: "error", detail: recovery.reason });
338
1027
  }
339
- await this.deps.health.postAdvanceRecord(result);
1028
+ this.postAdvanceRecord(result);
340
1029
  return result;
1030
+ } finally {
1031
+ stopAdvanceTimer();
341
1032
  }
342
1033
  }
343
1034
 
@@ -345,10 +1036,11 @@ export class AutoOrchestrator implements AutoOrchestrationModule {
345
1036
  this.lastAdvanceKey = null;
346
1037
  this.lastFinalizedUnitKey = null;
347
1038
  this.dispatchKeyWindow = [];
1039
+ this.lastStuckRecoveryKey = null;
348
1040
  this.status.phase = "running";
349
1041
  this.bumpTransition();
350
- await this.deps.runtime.journalTransition({ name: "resume" });
351
- await this.deps.notifications.notifyLifecycle({ name: "resume" });
1042
+ this.journalTransition({ name: "resume" });
1043
+ this.notifyLifecycle({ name: "resume" });
352
1044
  return { kind: "resumed" };
353
1045
  }
354
1046
 
@@ -356,15 +1048,16 @@ export class AutoOrchestrator implements AutoOrchestrationModule {
356
1048
  if (this.status.phase === "stopped") {
357
1049
  return { kind: "stopped", reason };
358
1050
  }
359
- await this.deps.worktree.cleanupOnStop(reason);
1051
+ // cleanupOnStop was a no-op in the wired WorktreeAdapter.
360
1052
  this.status.phase = "stopped";
361
1053
  this.status.activeUnit = undefined;
362
1054
  this.lastAdvanceKey = null;
363
1055
  this.lastFinalizedUnitKey = null;
364
1056
  this.dispatchKeyWindow = [];
1057
+ this.lastStuckRecoveryKey = null;
365
1058
  this.bumpTransition();
366
- await this.deps.runtime.journalTransition({ name: "stop", reason });
367
- await this.deps.notifications.notifyLifecycle({ name: "stop", detail: reason });
1059
+ this.journalTransition({ name: "stop", reason });
1060
+ this.notifyLifecycle({ name: "stop", detail: reason });
368
1061
  return { kind: "stopped", reason };
369
1062
  }
370
1063
 
@@ -382,8 +1075,10 @@ export class AutoOrchestrator implements AutoOrchestrationModule {
382
1075
  this.status.activeUnit = undefined;
383
1076
  this.lastAdvanceKey = null;
384
1077
  this.lastFinalizedUnitKey = unitKey;
1078
+ // Genuine progress — re-enable graduated stuck recovery for future episodes.
1079
+ this.lastStuckRecoveryKey = null;
385
1080
  this.bumpTransition();
386
- await this.deps.runtime.journalTransition({
1081
+ this.journalTransition({
387
1082
  name: "unit-finalized",
388
1083
  unitType: unit.unitType,
389
1084
  unitId: unit.unitId,
@@ -403,7 +1098,7 @@ export class AutoOrchestrator implements AutoOrchestrationModule {
403
1098
  this.lastAdvanceKey = null;
404
1099
  this.lastFinalizedUnitKey = null;
405
1100
  this.bumpTransition();
406
- await this.deps.runtime.journalTransition({
1101
+ this.journalTransition({
407
1102
  name: "unit-retry",
408
1103
  reason: "finalize-retry",
409
1104
  unitType: unit.unitType,
@@ -417,6 +1112,47 @@ export class AutoOrchestrator implements AutoOrchestrationModule {
417
1112
  }
418
1113
  }
419
1114
 
420
- export function createAutoOrchestrator(deps: AutoOrchestratorDeps): AutoOrchestrationModule {
421
- return new AutoOrchestrator(deps);
1115
+ function isUsableLiveOrchestratorBasePath(basePath: string): boolean {
1116
+ if (!basePath || !existsSync(basePath)) return false;
1117
+ if (!detectWorktreeName(basePath)) return true;
1118
+
1119
+ try {
1120
+ return readFileSync(join(basePath, ".git"), "utf8").trim().startsWith("gitdir: ");
1121
+ } catch {
1122
+ return false;
1123
+ }
1124
+ }
1125
+
1126
+ /**
1127
+ * Resolve the base path the live orchestrator should dispatch from, falling
1128
+ * back to the project root when the captured worktree path has been removed
1129
+ * (e.g. after milestone-merge cleanup). Exported for the closeout-regression
1130
+ * tests and reused by the orchestrator's getLiveDispatchBasePath.
1131
+ */
1132
+ export function resolveLiveOrchestratorBasePath(input: {
1133
+ capturedBasePath: string;
1134
+ runtimeBasePath: string;
1135
+ sessionBasePath?: string | null;
1136
+ originalBasePath?: string | null;
1137
+ }): string {
1138
+ const primary = input.sessionBasePath || input.capturedBasePath;
1139
+ if (isUsableLiveOrchestratorBasePath(primary)) return primary;
1140
+
1141
+ const fallbacks = [
1142
+ input.originalBasePath,
1143
+ input.runtimeBasePath,
1144
+ resolveProjectRoot(input.capturedBasePath),
1145
+ ];
1146
+
1147
+ for (const candidate of fallbacks) {
1148
+ if (candidate && isUsableLiveOrchestratorBasePath(candidate)) {
1149
+ return candidate;
1150
+ }
1151
+ }
1152
+
1153
+ return input.runtimeBasePath || input.capturedBasePath;
1154
+ }
1155
+
1156
+ export function createAutoOrchestrator(context: OrchestratorContext): AutoOrchestrationModule {
1157
+ return new AutoOrchestrator(context);
422
1158
  }