@opengsd/gsd-pi 1.1.1-dev.b2556262 → 1.2.0-dev.0b870afa

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 (934) hide show
  1. package/dist/cli-style.d.ts +17 -0
  2. package/dist/cli-style.js +28 -0
  3. package/dist/cli-web-branch.d.ts +2 -0
  4. package/dist/cli-web-branch.js +9 -2
  5. package/dist/cli.js +1 -1
  6. package/dist/headless-events.d.ts +4 -2
  7. package/dist/headless-events.js +14 -34
  8. package/dist/help-text.js +5 -0
  9. package/dist/mcp-server.js +2 -1
  10. package/dist/models-resolver.d.ts +3 -13
  11. package/dist/models-resolver.js +3 -22
  12. package/dist/project-sessions.js +4 -2
  13. package/dist/resource-loader.js +2 -14
  14. package/dist/resources/.managed-resources-content-hash +1 -1
  15. package/dist/resources/GSD-WORKFLOW.md +5 -4
  16. package/dist/resources/extensions/ask-user-questions.js +78 -23
  17. package/dist/resources/extensions/async-jobs/async-bash-tool.js +30 -64
  18. package/dist/resources/extensions/async-jobs/await-tool.js +80 -12
  19. package/dist/resources/extensions/async-jobs/index.js +65 -0
  20. package/dist/resources/extensions/async-jobs/job-manager.js +12 -1
  21. package/dist/resources/extensions/bg-shell/bg-shell-command.js +6 -6
  22. package/dist/resources/extensions/bg-shell/bg-shell-tool.js +10 -7
  23. package/dist/resources/extensions/bg-shell/overlay.js +9 -6
  24. package/dist/resources/extensions/bg-shell/process-manager.js +54 -25
  25. package/dist/resources/extensions/bg-shell/readiness-detector.js +11 -0
  26. package/dist/resources/extensions/bg-shell/utilities.js +5 -2
  27. package/dist/resources/extensions/browser-tools/engine/managed-gsd-browser.js +209 -88
  28. package/dist/resources/extensions/browser-tools/engine/selection.js +73 -5
  29. package/dist/resources/extensions/browser-tools/index.js +69 -12
  30. package/dist/resources/extensions/claude-code-cli/models.js +9 -0
  31. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +139 -243
  32. package/dist/resources/extensions/claude-code-cli/turn-assembler.js +224 -0
  33. package/dist/resources/extensions/github-sync/templates.js +3 -3
  34. package/dist/resources/extensions/gsd/artifact-projection.js +14 -0
  35. package/dist/resources/extensions/gsd/auto/contracts.js +8 -1
  36. package/dist/resources/extensions/gsd/auto/custom-verify-retry-store.js +17 -2
  37. package/dist/resources/extensions/gsd/auto/detect-stuck.js +33 -13
  38. package/dist/resources/extensions/gsd/auto/dispatch-history.js +105 -0
  39. package/dist/resources/extensions/gsd/auto/dispatch-key.js +37 -0
  40. package/dist/resources/extensions/gsd/auto/loop.js +77 -56
  41. package/dist/resources/extensions/gsd/auto/orchestrator.js +860 -96
  42. package/dist/resources/extensions/gsd/auto/phases.js +81 -8
  43. package/dist/resources/extensions/gsd/auto/run-unit.js +2 -1
  44. package/dist/resources/extensions/gsd/auto/session.js +6 -0
  45. package/dist/resources/extensions/gsd/auto-dashboard.js +16 -4
  46. package/dist/resources/extensions/gsd/auto-direct-dispatch.js +11 -34
  47. package/dist/resources/extensions/gsd/auto-dispatch.js +56 -63
  48. package/dist/resources/extensions/gsd/auto-model-selection.js +44 -13
  49. package/dist/resources/extensions/gsd/auto-post-unit.js +47 -17
  50. package/dist/resources/extensions/gsd/auto-prompts.js +271 -27
  51. package/dist/resources/extensions/gsd/auto-recovery.js +48 -49
  52. package/dist/resources/extensions/gsd/auto-runtime-state.js +17 -0
  53. package/dist/resources/extensions/gsd/auto-start.js +36 -49
  54. package/dist/resources/extensions/gsd/auto-timers.js +16 -2
  55. package/dist/resources/extensions/gsd/auto-tool-tracking.js +55 -0
  56. package/dist/resources/extensions/gsd/auto-unit-closeout.js +45 -21
  57. package/dist/resources/extensions/gsd/auto-unit-tool-scope.js +33 -37
  58. package/dist/resources/extensions/gsd/auto-verification.js +30 -37
  59. package/dist/resources/extensions/gsd/auto-worktree-repair.js +10 -2
  60. package/dist/resources/extensions/gsd/auto-worktree.js +67 -375
  61. package/dist/resources/extensions/gsd/auto.js +112 -486
  62. package/dist/resources/extensions/gsd/blocked-models.js +28 -0
  63. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +29 -8
  64. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +28 -37
  65. package/dist/resources/extensions/gsd/bootstrap/dynamic-tools.js +43 -49
  66. package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +2 -2
  67. package/dist/resources/extensions/gsd/bootstrap/query-tools.js +2 -2
  68. package/dist/resources/extensions/gsd/bootstrap/register-extension.js +19 -0
  69. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +319 -161
  70. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +365 -58
  71. package/dist/resources/extensions/gsd/branch-patterns.js +2 -0
  72. package/dist/resources/extensions/gsd/browser-daemon-auto-prep.js +83 -0
  73. package/dist/resources/extensions/gsd/browser-evidence.js +8 -2
  74. package/dist/resources/extensions/gsd/captures.js +5 -15
  75. package/dist/resources/extensions/gsd/closeout-consistency-gate.js +21 -4
  76. package/dist/resources/extensions/gsd/closeout-recovery.js +3 -2
  77. package/dist/resources/extensions/gsd/closeout-wizard.js +92 -0
  78. package/dist/resources/extensions/gsd/codebase-generator.js +8 -4
  79. package/dist/resources/extensions/gsd/commands/catalog.js +6 -62
  80. package/dist/resources/extensions/gsd/commands/context.js +16 -2
  81. package/dist/resources/extensions/gsd/commands/handlers/auto.js +3 -0
  82. package/dist/resources/extensions/gsd/commands-handlers.js +66 -3
  83. package/dist/resources/extensions/gsd/commands-inspect.js +4 -8
  84. package/dist/resources/extensions/gsd/commands-maintenance.js +61 -41
  85. package/dist/resources/extensions/gsd/commands-ship.js +2 -2
  86. package/dist/resources/extensions/gsd/commands-verdict.js +12 -2
  87. package/dist/resources/extensions/gsd/consent-question.js +353 -0
  88. package/dist/resources/extensions/gsd/consent-verdict.js +63 -0
  89. package/dist/resources/extensions/gsd/constants.js +0 -2
  90. package/dist/resources/extensions/gsd/crash-recovery.js +12 -15
  91. package/dist/resources/extensions/gsd/db/engine.js +755 -0
  92. package/dist/resources/extensions/gsd/db/queries.js +398 -0
  93. package/dist/resources/extensions/gsd/db/sql-constants.js +11 -0
  94. package/dist/resources/extensions/gsd/db/writers/cascades.js +194 -0
  95. package/dist/resources/extensions/gsd/db/writers/import-restore.js +182 -0
  96. package/dist/resources/extensions/gsd/db/writers/memory.js +149 -0
  97. package/dist/resources/extensions/gsd/db/writers/reconcile.js +458 -0
  98. package/dist/resources/extensions/gsd/db/writers/status.js +70 -0
  99. package/dist/resources/extensions/gsd/db-workspace.js +103 -0
  100. package/dist/resources/extensions/gsd/debug-logger.js +10 -0
  101. package/dist/resources/extensions/gsd/delegation-policy.js +2 -10
  102. package/dist/resources/extensions/gsd/discussion-handoff.js +218 -0
  103. package/dist/resources/extensions/gsd/dispatch-guard.js +10 -35
  104. package/dist/resources/extensions/gsd/docs/preferences-reference.md +9 -0
  105. package/dist/resources/extensions/gsd/doctor-engine-checks.js +5 -5
  106. package/dist/resources/extensions/gsd/doctor-environment.js +5 -11
  107. package/dist/resources/extensions/gsd/doctor-format.js +9 -6
  108. package/dist/resources/extensions/gsd/doctor-git-checks.js +6 -21
  109. package/dist/resources/extensions/gsd/doctor-proactive.js +7 -2
  110. package/dist/resources/extensions/gsd/doctor-runtime-checks.js +21 -16
  111. package/dist/resources/extensions/gsd/doctor.js +16 -9
  112. package/dist/resources/extensions/gsd/engine-hook-contract.js +70 -0
  113. package/dist/resources/extensions/gsd/error-classifier.js +10 -1
  114. package/dist/resources/extensions/gsd/exec-sandbox.js +30 -10
  115. package/dist/resources/extensions/gsd/files.js +33 -19
  116. package/dist/resources/extensions/gsd/git-conflict-state.js +16 -1
  117. package/dist/resources/extensions/gsd/git-service.js +1 -0
  118. package/dist/resources/extensions/gsd/gitignore.js +3 -0
  119. package/dist/resources/extensions/gsd/gsd-command-home.js +22 -12
  120. package/dist/resources/extensions/gsd/gsd-db.js +184 -2048
  121. package/dist/resources/extensions/gsd/guidance.js +158 -0
  122. package/dist/resources/extensions/gsd/guided-flow.js +91 -476
  123. package/dist/resources/extensions/gsd/guided-unit-completion.js +225 -0
  124. package/dist/resources/extensions/gsd/markdown-renderer.js +43 -33
  125. package/dist/resources/extensions/gsd/mcp-filter.js +10 -20
  126. package/dist/resources/extensions/gsd/mcp-tool-name.js +18 -0
  127. package/dist/resources/extensions/gsd/md-importer.js +4 -3
  128. package/dist/resources/extensions/gsd/memory-consolidation-scanner.js +1 -1
  129. package/dist/resources/extensions/gsd/migrate/safety.js +22 -11
  130. package/dist/resources/extensions/gsd/migration-auto-check.js +27 -5
  131. package/dist/resources/extensions/gsd/milestone-closeout-proof.js +72 -0
  132. package/dist/resources/extensions/gsd/milestone-closeout.js +97 -28
  133. package/dist/resources/extensions/gsd/milestone-merge-transaction.js +10 -0
  134. package/dist/resources/extensions/gsd/milestone-planning-persistence.js +156 -0
  135. package/dist/resources/extensions/gsd/milestone-readiness.js +77 -0
  136. package/dist/resources/extensions/gsd/milestone-reopen-events.js +3 -5
  137. package/dist/resources/extensions/gsd/milestone-settlement.js +50 -0
  138. package/dist/resources/extensions/gsd/milestone-validation-evidence.js +73 -0
  139. package/dist/resources/extensions/gsd/milestone-validation-verdict.js +57 -0
  140. package/dist/resources/extensions/gsd/model-cost-table.js +1 -0
  141. package/dist/resources/extensions/gsd/model-router.js +3 -0
  142. package/dist/resources/extensions/gsd/native-git-bridge.js +45 -0
  143. package/dist/resources/extensions/gsd/notification-store.js +11 -4
  144. package/dist/resources/extensions/gsd/parallel-eligibility.js +3 -6
  145. package/dist/resources/extensions/gsd/parallel-merge.js +14 -11
  146. package/dist/resources/extensions/gsd/parallel-monitor-overlay.js +11 -7
  147. package/dist/resources/extensions/gsd/parallel-orchestrator.js +3 -2
  148. package/dist/resources/extensions/gsd/parsers-legacy.js +16 -4
  149. package/dist/resources/extensions/gsd/paths.js +37 -24
  150. package/dist/resources/extensions/gsd/pre-execution-checks.js +91 -3
  151. package/dist/resources/extensions/gsd/preferences-diagnostics.js +67 -0
  152. package/dist/resources/extensions/gsd/preferences-models.js +14 -48
  153. package/dist/resources/extensions/gsd/preferences.js +161 -29
  154. package/dist/resources/extensions/gsd/projection-flush.js +7 -0
  155. package/dist/resources/extensions/gsd/prompts/complete-slice.md +4 -3
  156. package/dist/resources/extensions/gsd/prompts/discuss.md +6 -7
  157. package/dist/resources/extensions/gsd/prompts/execute-task.md +3 -1
  158. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +5 -7
  159. package/dist/resources/extensions/gsd/prompts/guided-discuss-project.md +6 -6
  160. package/dist/resources/extensions/gsd/prompts/guided-discuss-requirements.md +1 -2
  161. package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +5 -6
  162. package/dist/resources/extensions/gsd/prompts/plan-milestone.md +3 -1
  163. package/dist/resources/extensions/gsd/prompts/plan-slice.md +4 -3
  164. package/dist/resources/extensions/gsd/prompts/quick-task.md +1 -1
  165. package/dist/resources/extensions/gsd/prompts/reassess-roadmap.md +1 -1
  166. package/dist/resources/extensions/gsd/prompts/refine-slice.md +3 -2
  167. package/dist/resources/extensions/gsd/prompts/replan-slice.md +2 -2
  168. package/dist/resources/extensions/gsd/prompts/research-milestone.md +3 -3
  169. package/dist/resources/extensions/gsd/prompts/research-slice.md +1 -1
  170. package/dist/resources/extensions/gsd/prompts/rewrite-docs.md +1 -1
  171. package/dist/resources/extensions/gsd/prompts/run-uat.md +7 -5
  172. package/dist/resources/extensions/gsd/prompts/system.md +6 -3
  173. package/dist/resources/extensions/gsd/prompts/triage-captures.md +1 -1
  174. package/dist/resources/extensions/gsd/prompts/validate-milestone.md +6 -4
  175. package/dist/resources/extensions/gsd/provider-error-guidance.js +1 -5
  176. package/dist/resources/extensions/gsd/provider-payload-policy.js +83 -0
  177. package/dist/resources/extensions/gsd/provider-switch-observer.js +1 -1
  178. package/dist/resources/extensions/gsd/publication.js +87 -0
  179. package/dist/resources/extensions/gsd/pull-request-process.js +13 -0
  180. package/dist/resources/extensions/gsd/quality-gate-closure.js +109 -0
  181. package/dist/resources/extensions/gsd/question-transport.js +86 -0
  182. package/dist/resources/extensions/gsd/reactive-graph.js +8 -1
  183. package/dist/resources/extensions/gsd/recovery-classification.js +41 -87
  184. package/dist/resources/extensions/gsd/roadmap-slices.js +33 -5
  185. package/dist/resources/extensions/gsd/safety/destructive-confirmation.js +108 -0
  186. package/dist/resources/extensions/gsd/safety/evidence-collector.js +37 -4
  187. package/dist/resources/extensions/gsd/safety/evidence-cross-ref.js +7 -2
  188. package/dist/resources/extensions/gsd/safety/file-change-validator.js +10 -0
  189. package/dist/resources/extensions/gsd/schemas/parsers.js +6 -1
  190. package/dist/resources/extensions/gsd/session-lock.js +1 -1
  191. package/dist/resources/extensions/gsd/slice-parallel-orchestrator.js +3 -2
  192. package/dist/resources/extensions/gsd/state-reconciliation/drift/artifact-db.js +21 -1
  193. package/dist/resources/extensions/gsd/state-transition-matrix.js +38 -0
  194. package/dist/resources/extensions/gsd/state.js +19 -25
  195. package/dist/resources/extensions/gsd/status-guards.js +56 -8
  196. package/dist/resources/extensions/gsd/stop-notice.js +57 -0
  197. package/dist/resources/extensions/gsd/templates/plan.md +7 -0
  198. package/dist/resources/extensions/gsd/templates/project.md +1 -0
  199. package/dist/resources/extensions/gsd/templates/roadmap.md +1 -1
  200. package/dist/resources/extensions/gsd/templates/uat.md +5 -1
  201. package/dist/resources/extensions/gsd/tool-contract.js +66 -11
  202. package/dist/resources/extensions/gsd/tool-presentation-plan.js +17 -36
  203. package/dist/resources/extensions/gsd/tool-surface-readiness.js +56 -0
  204. package/dist/resources/extensions/gsd/tool-surface-snapshot.js +17 -0
  205. package/dist/resources/extensions/gsd/tools/complete-milestone.js +3 -2
  206. package/dist/resources/extensions/gsd/tools/complete-slice.js +46 -55
  207. package/dist/resources/extensions/gsd/tools/complete-task.js +3 -2
  208. package/dist/resources/extensions/gsd/tools/exec-tool.js +10 -8
  209. package/dist/resources/extensions/gsd/tools/plan-milestone.js +15 -143
  210. package/dist/resources/extensions/gsd/tools/plan-slice.js +14 -8
  211. package/dist/resources/extensions/gsd/tools/plan-task.js +2 -2
  212. package/dist/resources/extensions/gsd/tools/reassess-roadmap.js +41 -2
  213. package/dist/resources/extensions/gsd/tools/reopen-milestone.js +13 -31
  214. package/dist/resources/extensions/gsd/tools/reopen-slice.js +16 -35
  215. package/dist/resources/extensions/gsd/tools/reopen-task.js +2 -2
  216. package/dist/resources/extensions/gsd/tools/replan-slice.js +2 -2
  217. package/dist/resources/extensions/gsd/tools/skip-slice.js +18 -36
  218. package/dist/resources/extensions/gsd/tools/validate-milestone.js +15 -78
  219. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +236 -22
  220. package/dist/resources/extensions/gsd/uat-policy.js +57 -25
  221. package/dist/resources/extensions/gsd/uat-run.js +9 -14
  222. package/dist/resources/extensions/gsd/undo.js +8 -7
  223. package/dist/resources/extensions/gsd/unit-closeout.js +138 -0
  224. package/dist/resources/extensions/gsd/unit-context-composer.js +114 -21
  225. package/dist/resources/extensions/gsd/unit-context-manifest.js +4 -27
  226. package/dist/resources/extensions/gsd/unit-registry.js +337 -0
  227. package/dist/resources/extensions/gsd/unit-runtime.js +3 -2
  228. package/dist/resources/extensions/gsd/unit-tool-contracts.js +9 -181
  229. package/dist/resources/extensions/gsd/validation-block-guard.js +2 -0
  230. package/dist/resources/extensions/gsd/verdict-parser.js +1 -1
  231. package/dist/resources/extensions/gsd/verification-verdict.js +2 -1
  232. package/dist/resources/extensions/gsd/web-app-uat.js +117 -0
  233. package/dist/resources/extensions/gsd/workflow-event-ledger.js +91 -0
  234. package/dist/resources/extensions/gsd/workflow-event-vocabulary.js +46 -0
  235. package/dist/resources/extensions/gsd/workflow-events.js +6 -18
  236. package/dist/resources/extensions/gsd/workflow-mcp.js +15 -102
  237. package/dist/resources/extensions/gsd/workflow-reconcile.js +25 -59
  238. package/dist/resources/extensions/gsd/workflow-tool-surface.js +46 -0
  239. package/dist/resources/extensions/gsd/workspace-git-guard.js +2 -0
  240. package/dist/resources/extensions/gsd/worktree-git-recovery.js +293 -0
  241. package/dist/resources/extensions/gsd/worktree-lifecycle.js +12 -3
  242. package/dist/resources/extensions/gsd/worktree-manager.js +45 -28
  243. package/dist/resources/extensions/gsd/worktree-placement.js +59 -0
  244. package/dist/resources/extensions/gsd/worktree-reentry.js +12 -8
  245. package/dist/resources/extensions/gsd/worktree-root.js +28 -6
  246. package/dist/resources/extensions/gsd/worktree-safety.js +8 -5
  247. package/dist/resources/extensions/gsd/worktree-session-state.js +12 -11
  248. package/dist/resources/extensions/gsd/worktree-state-projection.js +33 -4
  249. package/dist/resources/extensions/gsd/worktree-telemetry.js +12 -0
  250. package/dist/resources/extensions/search-the-web/native-search.js +5 -3
  251. package/dist/resources/extensions/shared/browser-contract.js +59 -0
  252. package/dist/resources/extensions/shared/gsd-browser-cli.js +116 -6
  253. package/dist/resources/extensions/shared/interview-ui.js +2 -2
  254. package/dist/resources/shared/claude-runtime-floor.js +182 -0
  255. package/dist/resources/shared/gsd-browser-path-sync.js +214 -0
  256. package/dist/resources/shared/package-manager-detection.js +1 -1
  257. package/dist/resources/shared/package.json +3 -0
  258. package/dist/resources/skills/create-skill/references/executable-code.md +1 -1
  259. package/dist/resources/skills/create-skill/workflows/add-reference.md +8 -3
  260. package/dist/resources/skills/create-skill/workflows/add-script.md +4 -2
  261. package/dist/resources/skills/create-skill/workflows/add-template.md +3 -1
  262. package/dist/resources/skills/create-skill/workflows/add-workflow.md +8 -3
  263. package/dist/resources/skills/create-skill/workflows/upgrade-to-router.md +10 -5
  264. package/dist/resources/skills/create-skill/workflows/verify-skill.md +9 -4
  265. package/dist/resources/skills/gsd-browser/SKILL.md +1 -1
  266. package/dist/resources/skills/spike-wrap-up/SKILL.md +9 -9
  267. package/dist/tsconfig.extensions.tsbuildinfo +1 -0
  268. package/dist/update-check.d.ts +2 -0
  269. package/dist/update-check.js +24 -1
  270. package/dist/update-cmd.js +40 -3
  271. package/dist/web/standalone/.next/BUILD_ID +1 -1
  272. package/dist/web/standalone/.next/app-path-routes-manifest.json +5 -5
  273. package/dist/web/standalone/.next/build-manifest.json +3 -3
  274. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  275. package/dist/web/standalone/.next/react-loadable-manifest.json +8 -8
  276. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  277. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  278. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  279. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  280. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  281. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  282. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  283. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  284. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  285. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  286. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  287. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  288. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  289. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  290. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  291. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  292. package/dist/web/standalone/.next/server/app/api/boot/route.js.nft.json +1 -1
  293. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js.nft.json +1 -1
  294. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js.nft.json +1 -1
  295. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js.nft.json +1 -1
  296. package/dist/web/standalone/.next/server/app/api/captures/route.js.nft.json +1 -1
  297. package/dist/web/standalone/.next/server/app/api/cleanup/route.js.nft.json +1 -1
  298. package/dist/web/standalone/.next/server/app/api/doctor/route.js.nft.json +1 -1
  299. package/dist/web/standalone/.next/server/app/api/export-data/route.js.nft.json +1 -1
  300. package/dist/web/standalone/.next/server/app/api/files/route.js.nft.json +1 -1
  301. package/dist/web/standalone/.next/server/app/api/forensics/route.js.nft.json +1 -1
  302. package/dist/web/standalone/.next/server/app/api/git/route.js +1 -1
  303. package/dist/web/standalone/.next/server/app/api/git/route.js.nft.json +1 -1
  304. package/dist/web/standalone/.next/server/app/api/history/route.js.nft.json +1 -1
  305. package/dist/web/standalone/.next/server/app/api/hooks/route.js.nft.json +1 -1
  306. package/dist/web/standalone/.next/server/app/api/inspect/route.js.nft.json +1 -1
  307. package/dist/web/standalone/.next/server/app/api/knowledge/route.js.nft.json +1 -1
  308. package/dist/web/standalone/.next/server/app/api/live-state/route.js.nft.json +1 -1
  309. package/dist/web/standalone/.next/server/app/api/mcp-connections/route.js.nft.json +1 -1
  310. package/dist/web/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
  311. package/dist/web/standalone/.next/server/app/api/onboarding/route.js.nft.json +1 -1
  312. package/dist/web/standalone/.next/server/app/api/projects/route.js.nft.json +1 -1
  313. package/dist/web/standalone/.next/server/app/api/recovery/route.js.nft.json +1 -1
  314. package/dist/web/standalone/.next/server/app/api/session/browser/route.js.nft.json +1 -1
  315. package/dist/web/standalone/.next/server/app/api/session/command/route.js.nft.json +1 -1
  316. package/dist/web/standalone/.next/server/app/api/session/events/route.js.nft.json +1 -1
  317. package/dist/web/standalone/.next/server/app/api/session/manage/route.js.nft.json +1 -1
  318. package/dist/web/standalone/.next/server/app/api/settings-data/route.js.nft.json +1 -1
  319. package/dist/web/standalone/.next/server/app/api/shutdown/route.js.nft.json +1 -1
  320. package/dist/web/standalone/.next/server/app/api/skill-health/route.js.nft.json +1 -1
  321. package/dist/web/standalone/.next/server/app/api/steer/route.js.nft.json +1 -1
  322. package/dist/web/standalone/.next/server/app/api/switch-root/route.js.nft.json +1 -1
  323. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js.nft.json +1 -1
  324. package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js.nft.json +1 -1
  325. package/dist/web/standalone/.next/server/app/api/undo/route.js.nft.json +1 -1
  326. package/dist/web/standalone/.next/server/app/api/update/route.js +1 -1
  327. package/dist/web/standalone/.next/server/app/api/update/route.js.nft.json +1 -1
  328. package/dist/web/standalone/.next/server/app/api/visualizer/route.js.nft.json +1 -1
  329. package/dist/web/standalone/.next/server/app/index.html +1 -1
  330. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  331. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  332. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  333. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  334. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  335. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  336. package/dist/web/standalone/.next/server/app-paths-manifest.json +5 -5
  337. package/dist/web/standalone/.next/server/chunks/5124.js +1 -0
  338. package/dist/web/standalone/.next/server/chunks/5942.js +2 -0
  339. package/dist/web/standalone/.next/server/chunks/8357.js +3 -3
  340. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  341. package/dist/web/standalone/.next/server/middleware-react-loadable-manifest.js +1 -1
  342. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  343. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  344. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  345. package/dist/web/standalone/.next/static/chunks/2659.b7b129ee6a769448.js +1 -0
  346. package/dist/web/standalone/.next/static/chunks/2772.bfa657f49f955239.js +1 -0
  347. package/dist/web/standalone/.next/static/chunks/{3616.4113d484a994e411.js → 3616.3c60753b8ffcbd2e.js} +1 -1
  348. package/dist/web/standalone/.next/static/chunks/4283.e4873b058df143a1.js +2 -0
  349. package/dist/web/standalone/.next/static/chunks/5826.a46ecdd1cfe8dabc.js +1 -0
  350. package/dist/web/standalone/.next/static/chunks/796.e0bdc932325d7e03.js +10 -0
  351. package/dist/web/standalone/.next/static/chunks/8785.2e5a118797fb2dd2.js +1 -0
  352. package/dist/web/standalone/.next/static/chunks/{webpack-dda80a1ef5587410.js → webpack-f0285ce91d4ec9ef.js} +1 -1
  353. package/dist/web/standalone/node_modules/@gsd/native/package.json +1 -1
  354. package/dist/web/standalone/node_modules/node-pty/build/Makefile +1 -1
  355. package/dist/web/standalone/node_modules/postcss/lib/container.js +26 -18
  356. package/dist/web/standalone/node_modules/postcss/lib/css-syntax-error.js +47 -14
  357. package/dist/web/standalone/node_modules/postcss/lib/declaration.js +4 -4
  358. package/dist/web/standalone/node_modules/postcss/lib/fromJSON.js +3 -3
  359. package/dist/web/standalone/node_modules/postcss/lib/input.js +54 -29
  360. package/dist/web/standalone/node_modules/postcss/lib/lazy-result.js +47 -37
  361. package/dist/web/standalone/node_modules/postcss/lib/map-generator.js +26 -9
  362. package/dist/web/standalone/node_modules/postcss/lib/no-work-result.js +57 -55
  363. package/dist/web/standalone/node_modules/postcss/lib/node.js +99 -31
  364. package/dist/web/standalone/node_modules/postcss/lib/parse.js +1 -1
  365. package/dist/web/standalone/node_modules/postcss/lib/parser.js +10 -9
  366. package/dist/web/standalone/node_modules/postcss/lib/postcss.js +12 -12
  367. package/dist/web/standalone/node_modules/postcss/lib/previous-map.js +30 -11
  368. package/dist/web/standalone/node_modules/postcss/lib/processor.js +7 -7
  369. package/dist/web/standalone/node_modules/postcss/lib/result.js +5 -5
  370. package/dist/web/standalone/node_modules/postcss/lib/rule.js +6 -6
  371. package/dist/web/standalone/node_modules/postcss/lib/stringifier.js +69 -28
  372. package/dist/web/standalone/node_modules/postcss/lib/tokenize.js +6 -2
  373. package/dist/web/standalone/node_modules/postcss/package.json +48 -48
  374. package/dist/web/standalone/package.json +1 -1
  375. package/dist/web-mode.d.ts +2 -0
  376. package/dist/web-mode.js +20 -8
  377. package/dist/worktree-cli.js +3 -6
  378. package/dist/worktree-status-banner.js +7 -11
  379. package/package.json +17 -11
  380. package/packages/cloud-mcp-gateway/package.json +2 -2
  381. package/packages/contracts/dist/rpc.d.ts +1 -0
  382. package/packages/contracts/dist/rpc.d.ts.map +1 -1
  383. package/packages/contracts/dist/rpc.js.map +1 -1
  384. package/packages/contracts/dist/workflow.d.ts +4 -0
  385. package/packages/contracts/dist/workflow.d.ts.map +1 -1
  386. package/packages/contracts/dist/workflow.js.map +1 -1
  387. package/packages/contracts/package.json +1 -1
  388. package/packages/daemon/package.json +4 -4
  389. package/packages/gsd-agent-core/dist/session/agent-session-extensions.d.ts +2 -0
  390. package/packages/gsd-agent-core/dist/session/agent-session-extensions.d.ts.map +1 -1
  391. package/packages/gsd-agent-core/dist/session/agent-session-extensions.js +14 -0
  392. package/packages/gsd-agent-core/dist/session/agent-session-extensions.js.map +1 -1
  393. package/packages/gsd-agent-core/package.json +5 -5
  394. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.d.ts +5 -0
  395. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  396. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.js +8 -0
  397. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.js.map +1 -1
  398. package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.js +1 -1
  399. package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.js.map +1 -1
  400. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  401. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.js +113 -40
  402. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  403. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/input-controller.d.ts.map +1 -1
  404. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/input-controller.js +8 -1
  405. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/input-controller.js.map +1 -1
  406. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-chat-render.d.ts.map +1 -1
  407. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-chat-render.js +11 -1
  408. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-chat-render.js.map +1 -1
  409. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-extension-widgets.d.ts.map +1 -1
  410. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-extension-widgets.js +6 -0
  411. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-extension-widgets.js.map +1 -1
  412. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-selectors-auth.d.ts.map +1 -1
  413. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-selectors-auth.js +4 -4
  414. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-selectors-auth.js.map +1 -1
  415. package/packages/gsd-agent-modes/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  416. package/packages/gsd-agent-modes/dist/modes/rpc/rpc-mode.js +3 -1
  417. package/packages/gsd-agent-modes/dist/modes/rpc/rpc-mode.js.map +1 -1
  418. package/packages/gsd-agent-modes/package.json +7 -7
  419. package/packages/mcp-server/dist/cli.js +9 -1
  420. package/packages/mcp-server/dist/cli.js.map +1 -1
  421. package/packages/mcp-server/dist/moonshot-tool-schema.d.ts +29 -0
  422. package/packages/mcp-server/dist/moonshot-tool-schema.d.ts.map +1 -0
  423. package/packages/mcp-server/dist/moonshot-tool-schema.js +50 -0
  424. package/packages/mcp-server/dist/moonshot-tool-schema.js.map +1 -0
  425. package/packages/mcp-server/dist/server.d.ts +10 -0
  426. package/packages/mcp-server/dist/server.d.ts.map +1 -1
  427. package/packages/mcp-server/dist/server.js +12 -0
  428. package/packages/mcp-server/dist/server.js.map +1 -1
  429. package/packages/mcp-server/dist/workflow-tools.d.ts +49 -0
  430. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  431. package/packages/mcp-server/dist/workflow-tools.js +147 -60
  432. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  433. package/packages/mcp-server/package.json +5 -4
  434. package/packages/native/package.json +1 -1
  435. package/packages/pi-agent-core/dist/harness/env/nodejs.d.ts +1 -0
  436. package/packages/pi-agent-core/dist/harness/env/nodejs.d.ts.map +1 -1
  437. package/packages/pi-agent-core/dist/harness/env/nodejs.js +34 -3
  438. package/packages/pi-agent-core/dist/harness/env/nodejs.js.map +1 -1
  439. package/packages/pi-agent-core/dist/index.d.ts +1 -0
  440. package/packages/pi-agent-core/dist/index.d.ts.map +1 -1
  441. package/packages/pi-agent-core/dist/index.js +3 -0
  442. package/packages/pi-agent-core/dist/index.js.map +1 -1
  443. package/packages/pi-agent-core/package.json +1 -1
  444. package/packages/pi-ai/README.md +1 -0
  445. package/packages/pi-ai/dist/index.d.ts +2 -0
  446. package/packages/pi-ai/dist/index.d.ts.map +1 -1
  447. package/packages/pi-ai/dist/index.js +2 -0
  448. package/packages/pi-ai/dist/index.js.map +1 -1
  449. package/packages/pi-ai/dist/models.generated.d.ts +183 -110
  450. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  451. package/packages/pi-ai/dist/models.generated.js +205 -158
  452. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  453. package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
  454. package/packages/pi-ai/dist/providers/anthropic.js +12 -7
  455. package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
  456. package/packages/pi-ai/dist/providers/google-shared.d.ts +5 -0
  457. package/packages/pi-ai/dist/providers/google-shared.d.ts.map +1 -1
  458. package/packages/pi-ai/dist/providers/google-shared.js +12 -3
  459. package/packages/pi-ai/dist/providers/google-shared.js.map +1 -1
  460. package/packages/pi-ai/dist/providers/openai-completions.d.ts.map +1 -1
  461. package/packages/pi-ai/dist/providers/openai-completions.js +7 -3
  462. package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
  463. package/packages/pi-ai/dist/utils/moonshot-tool-schema.d.ts +9 -0
  464. package/packages/pi-ai/dist/utils/moonshot-tool-schema.d.ts.map +1 -0
  465. package/packages/pi-ai/dist/utils/moonshot-tool-schema.js +34 -0
  466. package/packages/pi-ai/dist/utils/moonshot-tool-schema.js.map +1 -0
  467. package/packages/pi-ai/dist/utils/oauth/github-copilot.d.ts.map +1 -1
  468. package/packages/pi-ai/dist/utils/oauth/github-copilot.js +6 -2
  469. package/packages/pi-ai/dist/utils/oauth/github-copilot.js.map +1 -1
  470. package/packages/pi-ai/package.json +3 -2
  471. package/packages/pi-coding-agent/dist/core/auth-storage.d.ts +2 -2
  472. package/packages/pi-coding-agent/dist/core/auth-storage.d.ts.map +1 -1
  473. package/packages/pi-coding-agent/dist/core/auth-storage.js +29 -15
  474. package/packages/pi-coding-agent/dist/core/auth-storage.js.map +1 -1
  475. package/packages/pi-coding-agent/dist/core/capability-patches.d.ts.map +1 -1
  476. package/packages/pi-coding-agent/dist/core/capability-patches.js +3 -1
  477. package/packages/pi-coding-agent/dist/core/capability-patches.js.map +1 -1
  478. package/packages/pi-coding-agent/dist/core/provider-readiness.d.ts.map +1 -1
  479. package/packages/pi-coding-agent/dist/core/provider-readiness.js +13 -6
  480. package/packages/pi-coding-agent/dist/core/provider-readiness.js.map +1 -1
  481. package/packages/pi-coding-agent/dist/core/tools/bash.d.ts +11 -0
  482. package/packages/pi-coding-agent/dist/core/tools/bash.d.ts.map +1 -1
  483. package/packages/pi-coding-agent/dist/core/tools/bash.js +53 -11
  484. package/packages/pi-coding-agent/dist/core/tools/bash.js.map +1 -1
  485. package/packages/pi-coding-agent/dist/index.d.ts +1 -1
  486. package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
  487. package/packages/pi-coding-agent/dist/index.js +1 -1
  488. package/packages/pi-coding-agent/dist/index.js.map +1 -1
  489. package/packages/pi-coding-agent/dist/utils/shell.d.ts +28 -2
  490. package/packages/pi-coding-agent/dist/utils/shell.d.ts.map +1 -1
  491. package/packages/pi-coding-agent/dist/utils/shell.js +56 -10
  492. package/packages/pi-coding-agent/dist/utils/shell.js.map +1 -1
  493. package/packages/pi-coding-agent/package.json +7 -7
  494. package/packages/pi-tui/dist/components/input.js +1 -1
  495. package/packages/pi-tui/dist/components/input.js.map +1 -1
  496. package/packages/pi-tui/dist/keys.d.ts.map +1 -1
  497. package/packages/pi-tui/dist/keys.js +39 -30
  498. package/packages/pi-tui/dist/keys.js.map +1 -1
  499. package/packages/pi-tui/dist/stdin-buffer.d.ts.map +1 -1
  500. package/packages/pi-tui/dist/stdin-buffer.js +22 -0
  501. package/packages/pi-tui/dist/stdin-buffer.js.map +1 -1
  502. package/packages/pi-tui/dist/tui.d.ts.map +1 -1
  503. package/packages/pi-tui/dist/tui.js +9 -0
  504. package/packages/pi-tui/dist/tui.js.map +1 -1
  505. package/packages/pi-tui/package.json +2 -2
  506. package/packages/rpc-client/package.json +2 -2
  507. package/pkg/package.json +1 -1
  508. package/scripts/install/deps.js +10 -0
  509. package/scripts/link-workspace-packages.cjs +7 -40
  510. package/src/resources/GSD-WORKFLOW.md +5 -4
  511. package/src/resources/extensions/ask-user-questions.ts +87 -24
  512. package/src/resources/extensions/async-jobs/async-bash-cancel.test.ts +360 -0
  513. package/src/resources/extensions/async-jobs/async-bash-tool.ts +33 -56
  514. package/src/resources/extensions/async-jobs/await-tool.test.ts +139 -0
  515. package/src/resources/extensions/async-jobs/await-tool.ts +82 -12
  516. package/src/resources/extensions/async-jobs/index.ts +79 -0
  517. package/src/resources/extensions/async-jobs/job-manager.ts +21 -1
  518. package/src/resources/extensions/bg-shell/bg-shell-command.ts +6 -6
  519. package/src/resources/extensions/bg-shell/bg-shell-tool.ts +10 -6
  520. package/src/resources/extensions/bg-shell/overlay.ts +9 -5
  521. package/src/resources/extensions/bg-shell/process-manager.ts +50 -25
  522. package/src/resources/extensions/bg-shell/readiness-detector.ts +12 -0
  523. package/src/resources/extensions/bg-shell/tests/lifecycle-and-utilities.test.ts +48 -1
  524. package/src/resources/extensions/bg-shell/utilities.ts +5 -2
  525. package/src/resources/extensions/browser-tools/engine/managed-gsd-browser.ts +265 -98
  526. package/src/resources/extensions/browser-tools/engine/selection.ts +90 -4
  527. package/src/resources/extensions/browser-tools/index.ts +71 -13
  528. package/src/resources/extensions/browser-tools/tests/browser-engine-selection.test.mjs +83 -13
  529. package/src/resources/extensions/browser-tools/tests/gsd-browser-launch-config.test.mjs +40 -1
  530. package/src/resources/extensions/browser-tools/tests/managed-gsd-browser-tools.test.mjs +136 -0
  531. package/src/resources/extensions/claude-code-cli/models.ts +9 -0
  532. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +166 -293
  533. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +270 -2
  534. package/src/resources/extensions/claude-code-cli/turn-assembler.ts +287 -0
  535. package/src/resources/extensions/github-sync/templates.ts +3 -3
  536. package/src/resources/extensions/github-sync/tests/templates.test.ts +2 -2
  537. package/src/resources/extensions/gsd/artifact-projection.ts +31 -0
  538. package/src/resources/extensions/gsd/auto/contracts.ts +40 -121
  539. package/src/resources/extensions/gsd/auto/custom-verify-retry-store.ts +21 -3
  540. package/src/resources/extensions/gsd/auto/detect-stuck.ts +32 -9
  541. package/src/resources/extensions/gsd/auto/dispatch-history.ts +152 -0
  542. package/src/resources/extensions/gsd/auto/dispatch-key.ts +39 -0
  543. package/src/resources/extensions/gsd/auto/loop-deps.ts +3 -1
  544. package/src/resources/extensions/gsd/auto/loop.ts +86 -61
  545. package/src/resources/extensions/gsd/auto/orchestrator.ts +1023 -98
  546. package/src/resources/extensions/gsd/auto/phases.ts +108 -28
  547. package/src/resources/extensions/gsd/auto/run-unit.ts +2 -1
  548. package/src/resources/extensions/gsd/auto/session.ts +7 -0
  549. package/src/resources/extensions/gsd/auto-dashboard.ts +18 -4
  550. package/src/resources/extensions/gsd/auto-direct-dispatch.ts +18 -48
  551. package/src/resources/extensions/gsd/auto-dispatch.ts +68 -68
  552. package/src/resources/extensions/gsd/auto-model-selection.ts +49 -12
  553. package/src/resources/extensions/gsd/auto-post-unit.ts +56 -16
  554. package/src/resources/extensions/gsd/auto-prompts.ts +338 -44
  555. package/src/resources/extensions/gsd/auto-recovery.ts +50 -50
  556. package/src/resources/extensions/gsd/auto-runtime-state.ts +30 -0
  557. package/src/resources/extensions/gsd/auto-start.ts +41 -49
  558. package/src/resources/extensions/gsd/auto-timers.ts +16 -2
  559. package/src/resources/extensions/gsd/auto-tool-tracking.ts +59 -0
  560. package/src/resources/extensions/gsd/auto-unit-closeout.ts +83 -28
  561. package/src/resources/extensions/gsd/auto-unit-tool-scope.ts +43 -38
  562. package/src/resources/extensions/gsd/auto-verification.ts +33 -36
  563. package/src/resources/extensions/gsd/auto-worktree-repair.ts +13 -2
  564. package/src/resources/extensions/gsd/auto-worktree.ts +83 -391
  565. package/src/resources/extensions/gsd/auto.ts +151 -524
  566. package/src/resources/extensions/gsd/blocked-models.ts +49 -0
  567. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +37 -10
  568. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +29 -37
  569. package/src/resources/extensions/gsd/bootstrap/dynamic-tools.ts +43 -49
  570. package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +2 -2
  571. package/src/resources/extensions/gsd/bootstrap/query-tools.ts +2 -2
  572. package/src/resources/extensions/gsd/bootstrap/register-extension.ts +24 -0
  573. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +379 -177
  574. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +452 -58
  575. package/src/resources/extensions/gsd/branch-patterns.ts +3 -0
  576. package/src/resources/extensions/gsd/browser-daemon-auto-prep.ts +108 -0
  577. package/src/resources/extensions/gsd/browser-evidence.ts +18 -2
  578. package/src/resources/extensions/gsd/captures.ts +5 -16
  579. package/src/resources/extensions/gsd/closeout-consistency-gate.ts +27 -5
  580. package/src/resources/extensions/gsd/closeout-recovery.ts +2 -1
  581. package/src/resources/extensions/gsd/closeout-wizard.ts +102 -0
  582. package/src/resources/extensions/gsd/codebase-generator.ts +9 -5
  583. package/src/resources/extensions/gsd/commands/catalog.ts +6 -68
  584. package/src/resources/extensions/gsd/commands/context.ts +16 -2
  585. package/src/resources/extensions/gsd/commands/handlers/auto.ts +3 -0
  586. package/src/resources/extensions/gsd/commands-handlers.ts +64 -3
  587. package/src/resources/extensions/gsd/commands-inspect.ts +7 -8
  588. package/src/resources/extensions/gsd/commands-maintenance.ts +74 -40
  589. package/src/resources/extensions/gsd/commands-ship.ts +2 -2
  590. package/src/resources/extensions/gsd/commands-verdict.ts +19 -2
  591. package/src/resources/extensions/gsd/consent-question.ts +431 -0
  592. package/src/resources/extensions/gsd/consent-verdict.ts +86 -0
  593. package/src/resources/extensions/gsd/constants.ts +0 -3
  594. package/src/resources/extensions/gsd/crash-recovery.ts +13 -11
  595. package/src/resources/extensions/gsd/db/engine.ts +809 -0
  596. package/src/resources/extensions/gsd/db/queries.ts +490 -0
  597. package/src/resources/extensions/gsd/db/sql-constants.ts +12 -0
  598. package/src/resources/extensions/gsd/db/writers/cascades.ts +237 -0
  599. package/src/resources/extensions/gsd/db/writers/import-restore.ts +310 -0
  600. package/src/resources/extensions/gsd/db/writers/memory.ts +220 -0
  601. package/src/resources/extensions/gsd/db/writers/reconcile.ts +500 -0
  602. package/src/resources/extensions/gsd/db/writers/status.ts +88 -0
  603. package/src/resources/extensions/gsd/db-workspace.ts +170 -0
  604. package/src/resources/extensions/gsd/debug-logger.ts +11 -0
  605. package/src/resources/extensions/gsd/delegation-policy.ts +3 -11
  606. package/src/resources/extensions/gsd/discussion-handoff.ts +276 -0
  607. package/src/resources/extensions/gsd/dispatch-guard.ts +8 -31
  608. package/src/resources/extensions/gsd/docs/preferences-reference.md +9 -0
  609. package/src/resources/extensions/gsd/doctor-engine-checks.ts +5 -4
  610. package/src/resources/extensions/gsd/doctor-environment.ts +5 -13
  611. package/src/resources/extensions/gsd/doctor-format.ts +12 -7
  612. package/src/resources/extensions/gsd/doctor-git-checks.ts +5 -22
  613. package/src/resources/extensions/gsd/doctor-proactive.ts +8 -2
  614. package/src/resources/extensions/gsd/doctor-runtime-checks.ts +22 -17
  615. package/src/resources/extensions/gsd/doctor.ts +15 -5
  616. package/src/resources/extensions/gsd/engine-hook-contract.ts +79 -0
  617. package/src/resources/extensions/gsd/error-classifier.ts +12 -1
  618. package/src/resources/extensions/gsd/exec-sandbox.ts +49 -9
  619. package/src/resources/extensions/gsd/files.ts +33 -12
  620. package/src/resources/extensions/gsd/git-conflict-state.ts +17 -1
  621. package/src/resources/extensions/gsd/git-service.ts +1 -0
  622. package/src/resources/extensions/gsd/gitignore.ts +3 -0
  623. package/src/resources/extensions/gsd/gsd-command-home.ts +13 -3
  624. package/src/resources/extensions/gsd/gsd-db.ts +188 -2375
  625. package/src/resources/extensions/gsd/guidance.ts +217 -0
  626. package/src/resources/extensions/gsd/guided-flow.ts +118 -589
  627. package/src/resources/extensions/gsd/guided-unit-completion.ts +275 -0
  628. package/src/resources/extensions/gsd/markdown-renderer.ts +51 -20
  629. package/src/resources/extensions/gsd/mcp-filter.ts +11 -24
  630. package/src/resources/extensions/gsd/mcp-tool-name.ts +30 -0
  631. package/src/resources/extensions/gsd/md-importer.ts +3 -3
  632. package/src/resources/extensions/gsd/memory-consolidation-scanner.ts +1 -1
  633. package/src/resources/extensions/gsd/migrate/safety.ts +20 -9
  634. package/src/resources/extensions/gsd/migration-auto-check.ts +30 -5
  635. package/src/resources/extensions/gsd/milestone-closeout-proof.ts +131 -0
  636. package/src/resources/extensions/gsd/milestone-closeout.ts +121 -28
  637. package/src/resources/extensions/gsd/milestone-merge-transaction.ts +47 -0
  638. package/src/resources/extensions/gsd/milestone-planning-persistence.ts +224 -0
  639. package/src/resources/extensions/gsd/milestone-readiness.ts +125 -0
  640. package/src/resources/extensions/gsd/milestone-reopen-events.ts +3 -6
  641. package/src/resources/extensions/gsd/milestone-settlement.ts +81 -0
  642. package/src/resources/extensions/gsd/milestone-validation-evidence.ts +95 -0
  643. package/src/resources/extensions/gsd/milestone-validation-verdict.ts +80 -0
  644. package/src/resources/extensions/gsd/model-cost-table.ts +1 -0
  645. package/src/resources/extensions/gsd/model-router.ts +3 -0
  646. package/src/resources/extensions/gsd/native-git-bridge.ts +48 -0
  647. package/src/resources/extensions/gsd/notification-store.ts +26 -3
  648. package/src/resources/extensions/gsd/parallel-eligibility.ts +4 -5
  649. package/src/resources/extensions/gsd/parallel-merge.ts +12 -9
  650. package/src/resources/extensions/gsd/parallel-monitor-overlay.ts +10 -7
  651. package/src/resources/extensions/gsd/parallel-orchestrator.ts +6 -2
  652. package/src/resources/extensions/gsd/parsers-legacy.ts +16 -4
  653. package/src/resources/extensions/gsd/paths.ts +42 -22
  654. package/src/resources/extensions/gsd/pre-execution-checks.ts +109 -3
  655. package/src/resources/extensions/gsd/preferences-diagnostics.ts +98 -0
  656. package/src/resources/extensions/gsd/preferences-models.ts +12 -47
  657. package/src/resources/extensions/gsd/preferences-types.ts +16 -0
  658. package/src/resources/extensions/gsd/preferences.ts +191 -28
  659. package/src/resources/extensions/gsd/projection-flush.ts +20 -0
  660. package/src/resources/extensions/gsd/prompts/complete-slice.md +4 -3
  661. package/src/resources/extensions/gsd/prompts/discuss.md +6 -7
  662. package/src/resources/extensions/gsd/prompts/execute-task.md +3 -1
  663. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +5 -7
  664. package/src/resources/extensions/gsd/prompts/guided-discuss-project.md +6 -6
  665. package/src/resources/extensions/gsd/prompts/guided-discuss-requirements.md +1 -2
  666. package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +5 -6
  667. package/src/resources/extensions/gsd/prompts/plan-milestone.md +3 -1
  668. package/src/resources/extensions/gsd/prompts/plan-slice.md +4 -3
  669. package/src/resources/extensions/gsd/prompts/quick-task.md +1 -1
  670. package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +1 -1
  671. package/src/resources/extensions/gsd/prompts/refine-slice.md +3 -2
  672. package/src/resources/extensions/gsd/prompts/replan-slice.md +2 -2
  673. package/src/resources/extensions/gsd/prompts/research-milestone.md +3 -3
  674. package/src/resources/extensions/gsd/prompts/research-slice.md +1 -1
  675. package/src/resources/extensions/gsd/prompts/rewrite-docs.md +1 -1
  676. package/src/resources/extensions/gsd/prompts/run-uat.md +7 -5
  677. package/src/resources/extensions/gsd/prompts/system.md +6 -3
  678. package/src/resources/extensions/gsd/prompts/triage-captures.md +1 -1
  679. package/src/resources/extensions/gsd/prompts/validate-milestone.md +6 -4
  680. package/src/resources/extensions/gsd/provider-error-guidance.ts +4 -9
  681. package/src/resources/extensions/gsd/provider-payload-policy.ts +140 -0
  682. package/src/resources/extensions/gsd/provider-switch-observer.ts +1 -1
  683. package/src/resources/extensions/gsd/publication.ts +122 -0
  684. package/src/resources/extensions/gsd/pull-request-process.ts +41 -0
  685. package/src/resources/extensions/gsd/quality-gate-closure.ts +140 -0
  686. package/src/resources/extensions/gsd/question-transport.ts +138 -0
  687. package/src/resources/extensions/gsd/reactive-graph.ts +11 -1
  688. package/src/resources/extensions/gsd/recovery-classification.ts +47 -88
  689. package/src/resources/extensions/gsd/roadmap-slices.ts +36 -5
  690. package/src/resources/extensions/gsd/safety/destructive-confirmation.ts +134 -0
  691. package/src/resources/extensions/gsd/safety/evidence-collector.ts +36 -4
  692. package/src/resources/extensions/gsd/safety/evidence-cross-ref.ts +7 -2
  693. package/src/resources/extensions/gsd/safety/file-change-validator.ts +14 -0
  694. package/src/resources/extensions/gsd/schemas/parsers.ts +6 -1
  695. package/src/resources/extensions/gsd/session-lock.ts +1 -1
  696. package/src/resources/extensions/gsd/slice-parallel-orchestrator.ts +6 -2
  697. package/src/resources/extensions/gsd/state-reconciliation/drift/artifact-db.ts +31 -10
  698. package/src/resources/extensions/gsd/state-transition-matrix.ts +42 -0
  699. package/src/resources/extensions/gsd/state.ts +24 -26
  700. package/src/resources/extensions/gsd/status-guards.ts +59 -8
  701. package/src/resources/extensions/gsd/stop-notice.ts +75 -0
  702. package/src/resources/extensions/gsd/templates/plan.md +7 -0
  703. package/src/resources/extensions/gsd/templates/project.md +1 -0
  704. package/src/resources/extensions/gsd/templates/roadmap.md +1 -1
  705. package/src/resources/extensions/gsd/templates/uat.md +5 -1
  706. package/src/resources/extensions/gsd/tests/artifact-db-drift-memo.test.ts +66 -0
  707. package/src/resources/extensions/gsd/tests/ask-user-questions-render.test.ts +92 -0
  708. package/src/resources/extensions/gsd/tests/auto-dashboard.test.ts +29 -1
  709. package/src/resources/extensions/gsd/tests/auto-dispatch-baseline-harness.test.ts +53 -0
  710. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +444 -5
  711. package/src/resources/extensions/gsd/tests/auto-milestone-target.test.ts +23 -0
  712. package/src/resources/extensions/gsd/tests/auto-model-selection-tool-poisoning.test.ts +18 -0
  713. package/src/resources/extensions/gsd/tests/auto-model-selection.test.ts +91 -0
  714. package/src/resources/extensions/gsd/tests/auto-orchestrator.test.ts +894 -858
  715. package/src/resources/extensions/gsd/tests/auto-paused-ui-cleanup.test.ts +41 -11
  716. package/src/resources/extensions/gsd/tests/auto-post-unit-evidence-crossref-4909.test.ts +46 -0
  717. package/src/resources/extensions/gsd/tests/auto-remote-session-lock-cleanup.test.ts +65 -3
  718. package/src/resources/extensions/gsd/tests/auto-runtime-state.test.ts +34 -0
  719. package/src/resources/extensions/gsd/tests/auto-worktree-registry.test.ts +2 -2
  720. package/src/resources/extensions/gsd/tests/auto-worktree-repair.test.ts +4 -2
  721. package/src/resources/extensions/gsd/tests/blocked-models.test.ts +19 -0
  722. package/src/resources/extensions/gsd/tests/browser-automation-contract-fixture.ts +39 -0
  723. package/src/resources/extensions/gsd/tests/browser-contract.test.ts +44 -0
  724. package/src/resources/extensions/gsd/tests/browser-daemon-auto-prep.test.ts +144 -0
  725. package/src/resources/extensions/gsd/tests/canonical-milestone-root.test.ts +20 -0
  726. package/src/resources/extensions/gsd/tests/checkout-branch-stash-guard.test.ts +66 -1
  727. package/src/resources/extensions/gsd/tests/clear-stale-autostart.test.ts +44 -0
  728. package/src/resources/extensions/gsd/tests/codebase-generator.test.ts +22 -0
  729. package/src/resources/extensions/gsd/tests/commands-dispatcher-workspace-git.test.ts +11 -0
  730. package/src/resources/extensions/gsd/tests/commands-verdict.test.ts +46 -8
  731. package/src/resources/extensions/gsd/tests/complete-slice-verification-gate.test.ts +42 -0
  732. package/src/resources/extensions/gsd/tests/consent-question.test.ts +351 -0
  733. package/src/resources/extensions/gsd/tests/custom-verify-retry-store.test.ts +67 -0
  734. package/src/resources/extensions/gsd/tests/debug-logger.test.ts +15 -0
  735. package/src/resources/extensions/gsd/tests/deep-project-auto-loop.test.ts +10 -10
  736. package/src/resources/extensions/gsd/tests/destructive-confirmation.test.ts +303 -0
  737. package/src/resources/extensions/gsd/tests/dispatch-complete-milestone-guard.test.ts +34 -3
  738. package/src/resources/extensions/gsd/tests/dispatch-history.test.ts +273 -0
  739. package/src/resources/extensions/gsd/tests/dispatch-run-uat-browser-tools.test.ts +89 -0
  740. package/src/resources/extensions/gsd/tests/doctor-git-checks-terminal.test.ts +73 -0
  741. package/src/resources/extensions/gsd/tests/doctor-scope-db-unavailable.test.ts +18 -0
  742. package/src/resources/extensions/gsd/tests/dynamic-bash-no-cap.test.ts +132 -0
  743. package/src/resources/extensions/gsd/tests/engine-hook-contract.test.ts +148 -0
  744. package/src/resources/extensions/gsd/tests/evidence-xref-gsd-exec.test.ts +157 -0
  745. package/src/resources/extensions/gsd/tests/exec-graceful-kill.test.ts +193 -0
  746. package/src/resources/extensions/gsd/tests/exec-tool.test.ts +29 -1
  747. package/src/resources/extensions/gsd/tests/execute-summary-save-empty-project.test.ts +64 -1
  748. package/src/resources/extensions/gsd/tests/execute-task-rendering.test.ts +1 -0
  749. package/src/resources/extensions/gsd/tests/extension-bootstrap-isolation.test.ts +35 -1
  750. package/src/resources/extensions/gsd/tests/file-change-validator.test.ts +33 -1
  751. package/src/resources/extensions/gsd/tests/fixtures/pr-body/swarm-lane-no-blockers.md +1 -5
  752. package/src/resources/extensions/gsd/tests/fixtures/pr-body/swarm-lane-with-blockers.md +1 -5
  753. package/src/resources/extensions/gsd/tests/gate-state-canonicalization.test.ts +48 -1
  754. package/src/resources/extensions/gsd/tests/gsd-command-home.test.ts +120 -0
  755. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +27 -0
  756. package/src/resources/extensions/gsd/tests/guidance.test.ts +148 -0
  757. package/src/resources/extensions/gsd/tests/guided-dispatch-root.test.ts +2 -6
  758. package/src/resources/extensions/gsd/tests/integration/auto-worktree-milestone-merge.test.ts +58 -15
  759. package/src/resources/extensions/gsd/tests/integration/auto-worktree.test.ts +74 -59
  760. package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +3 -2
  761. package/src/resources/extensions/gsd/tests/integration/gsd-integration-fixture.ts +80 -0
  762. package/src/resources/extensions/gsd/tests/integration/merge-strategy-regular.test.ts +157 -0
  763. package/src/resources/extensions/gsd/tests/integration/run-uat.test.ts +199 -0
  764. package/src/resources/extensions/gsd/tests/markdown-renderer-parse-cache.test.ts +75 -0
  765. package/src/resources/extensions/gsd/tests/mcp-project-config.test.ts +3 -1
  766. package/src/resources/extensions/gsd/tests/mcp-tool-name.test.ts +34 -0
  767. package/src/resources/extensions/gsd/tests/migration-auto-check.test.ts +143 -1
  768. package/src/resources/extensions/gsd/tests/milestone-closeout-proof.test.ts +99 -0
  769. package/src/resources/extensions/gsd/tests/milestone-closeout.test.ts +120 -4
  770. package/src/resources/extensions/gsd/tests/milestone-merge-transaction.test.ts +46 -0
  771. package/src/resources/extensions/gsd/tests/milestone-readiness.test.ts +65 -0
  772. package/src/resources/extensions/gsd/tests/milestone-validation-evidence.test.ts +41 -0
  773. package/src/resources/extensions/gsd/tests/milestone-validation-verdict.test.ts +55 -0
  774. package/src/resources/extensions/gsd/tests/model-unittype-mapping.test.ts +32 -1
  775. package/src/resources/extensions/gsd/tests/native-merge-regular.test.ts +139 -0
  776. package/src/resources/extensions/gsd/tests/notification-store.test.ts +32 -0
  777. package/src/resources/extensions/gsd/tests/oauth-api-model-routing.test.ts +167 -0
  778. package/src/resources/extensions/gsd/tests/orchestrator-legacy-parity.test.ts +127 -0
  779. package/src/resources/extensions/gsd/tests/parallel-research-dispatch.test.ts +18 -0
  780. package/src/resources/extensions/gsd/tests/parse-project-milestone-bridge.test.ts +77 -0
  781. package/src/resources/extensions/gsd/tests/parsers-legacy-importers.test.ts +138 -0
  782. package/src/resources/extensions/gsd/tests/phases-terminal-complete-idempotent.test.ts +242 -0
  783. package/src/resources/extensions/gsd/tests/plan-milestone.test.ts +45 -0
  784. package/src/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +6 -2
  785. package/src/resources/extensions/gsd/tests/planning-crossval.test.ts +45 -0
  786. package/src/resources/extensions/gsd/tests/post-exec-retry-bypass.test.ts +63 -2
  787. package/src/resources/extensions/gsd/tests/pre-execution-checks.test.ts +193 -1
  788. package/src/resources/extensions/gsd/tests/preferences-diagnostics.test.ts +67 -0
  789. package/src/resources/extensions/gsd/tests/preferences.test.ts +183 -0
  790. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +75 -2
  791. package/src/resources/extensions/gsd/tests/prompt-db.test.ts +124 -6
  792. package/src/resources/extensions/gsd/tests/provider-error-guidance.test.ts +3 -3
  793. package/src/resources/extensions/gsd/tests/provider-errors.test.ts +9 -0
  794. package/src/resources/extensions/gsd/tests/provider-payload-policy.test.ts +165 -0
  795. package/src/resources/extensions/gsd/tests/publication.test.ts +120 -0
  796. package/src/resources/extensions/gsd/tests/pull-request-process.test.ts +47 -0
  797. package/src/resources/extensions/gsd/tests/recovery-classification-illegal-transition.test.ts +30 -0
  798. package/src/resources/extensions/gsd/tests/register-hooks-depth-verification.test.ts +342 -1
  799. package/src/resources/extensions/gsd/tests/research-milestone-composer.test.ts +65 -0
  800. package/src/resources/extensions/gsd/tests/roadmap-parse-regression.test.ts +40 -0
  801. package/src/resources/extensions/gsd/tests/roadmap-slices.test.ts +68 -0
  802. package/src/resources/extensions/gsd/tests/runtime-invariant-modules.test.ts +44 -1
  803. package/src/resources/extensions/gsd/tests/safety-harness-false-positives.test.ts +38 -0
  804. package/src/resources/extensions/gsd/tests/session-start-footer.test.ts +80 -0
  805. package/src/resources/extensions/gsd/tests/session-switch-clears-pending-autostart.test.ts +108 -0
  806. package/src/resources/extensions/gsd/tests/single-writer-invariant.test.ts +144 -7
  807. package/src/resources/extensions/gsd/tests/stale-queued-milestone.test.ts +27 -0
  808. package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +21 -6
  809. package/src/resources/extensions/gsd/tests/state-transition-matrix.test.ts +36 -0
  810. package/src/resources/extensions/gsd/tests/status-guards.test.ts +38 -0
  811. package/src/resources/extensions/gsd/tests/stop-notice.test.ts +70 -0
  812. package/src/resources/extensions/gsd/tests/token-tool-gating.test.ts +114 -0
  813. package/src/resources/extensions/gsd/tests/tool-availability-audit.test.ts +35 -0
  814. package/src/resources/extensions/gsd/tests/tool-invocation-error-loop-break.test.ts +8 -0
  815. package/src/resources/extensions/gsd/tests/tool-naming.test.ts +35 -42
  816. package/src/resources/extensions/gsd/tests/tool-surface-readiness.test.ts +155 -0
  817. package/src/resources/extensions/gsd/tests/tool-unavailable-retry.test.ts +33 -0
  818. package/src/resources/extensions/gsd/tests/transport-gate-double-complete.test.ts +139 -0
  819. package/src/resources/extensions/gsd/tests/uat-policy.test.ts +112 -6
  820. package/src/resources/extensions/gsd/tests/unit-closeout.test.ts +209 -0
  821. package/src/resources/extensions/gsd/tests/unit-context-composer.test.ts +114 -2
  822. package/src/resources/extensions/gsd/tests/unit-registry.test.ts +163 -0
  823. package/src/resources/extensions/gsd/tests/uok-audit-unified.test.ts +8 -0
  824. package/src/resources/extensions/gsd/tests/validate-milestone-stuck-guard.test.ts +39 -0
  825. package/src/resources/extensions/gsd/tests/verification-verdict.test.ts +2 -0
  826. package/src/resources/extensions/gsd/tests/web-app-uat.test.ts +193 -0
  827. package/src/resources/extensions/gsd/tests/workflow-events.test.ts +19 -0
  828. package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +128 -11
  829. package/src/resources/extensions/gsd/tests/workflow-reconcile.test.ts +20 -0
  830. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +275 -40
  831. package/src/resources/extensions/gsd/tests/workspace-git-preflight.test.ts +15 -0
  832. package/src/resources/extensions/gsd/tests/worktree-lifecycle.test.ts +41 -4
  833. package/src/resources/extensions/gsd/tests/worktree-manager.test.ts +43 -1
  834. package/src/resources/extensions/gsd/tests/worktree-placement.test.ts +113 -0
  835. package/src/resources/extensions/gsd/tests/worktree-projection-writers.test.ts +1 -1
  836. package/src/resources/extensions/gsd/tests/worktree-reentry.test.ts +1 -1
  837. package/src/resources/extensions/gsd/tests/worktree-safety.test.ts +27 -1
  838. package/src/resources/extensions/gsd/tests/worktree-symlink-removal.test.ts +12 -6
  839. package/src/resources/extensions/gsd/tests/worktree-teardown-safety.test.ts +2 -2
  840. package/src/resources/extensions/gsd/tests/worktree-telemetry.test.ts +22 -0
  841. package/src/resources/extensions/gsd/tests/write-gate-planning-unit.test.ts +15 -3
  842. package/src/resources/extensions/gsd/tests/write-gate-seam.test.ts +358 -0
  843. package/src/resources/extensions/gsd/tests/write-gate.test.ts +188 -1
  844. package/src/resources/extensions/gsd/tool-contract.ts +124 -11
  845. package/src/resources/extensions/gsd/tool-presentation-plan.ts +18 -35
  846. package/src/resources/extensions/gsd/tool-surface-readiness.ts +76 -0
  847. package/src/resources/extensions/gsd/tool-surface-snapshot.ts +47 -0
  848. package/src/resources/extensions/gsd/tools/complete-milestone.ts +3 -2
  849. package/src/resources/extensions/gsd/tools/complete-slice.ts +45 -70
  850. package/src/resources/extensions/gsd/tools/complete-task.ts +3 -2
  851. package/src/resources/extensions/gsd/tools/exec-tool.ts +9 -8
  852. package/src/resources/extensions/gsd/tools/plan-milestone.ts +19 -160
  853. package/src/resources/extensions/gsd/tools/plan-slice.ts +14 -8
  854. package/src/resources/extensions/gsd/tools/plan-task.ts +2 -2
  855. package/src/resources/extensions/gsd/tools/reassess-roadmap.ts +45 -2
  856. package/src/resources/extensions/gsd/tools/reopen-milestone.ts +13 -40
  857. package/src/resources/extensions/gsd/tools/reopen-slice.ts +16 -44
  858. package/src/resources/extensions/gsd/tools/reopen-task.ts +2 -2
  859. package/src/resources/extensions/gsd/tools/replan-slice.ts +2 -2
  860. package/src/resources/extensions/gsd/tools/skip-slice.ts +18 -44
  861. package/src/resources/extensions/gsd/tools/validate-milestone.ts +25 -84
  862. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +264 -23
  863. package/src/resources/extensions/gsd/uat-policy.ts +80 -25
  864. package/src/resources/extensions/gsd/uat-run.ts +10 -14
  865. package/src/resources/extensions/gsd/undo.ts +9 -8
  866. package/src/resources/extensions/gsd/unit-closeout.ts +201 -0
  867. package/src/resources/extensions/gsd/unit-context-composer.ts +196 -21
  868. package/src/resources/extensions/gsd/unit-context-manifest.ts +4 -28
  869. package/src/resources/extensions/gsd/unit-registry.ts +412 -0
  870. package/src/resources/extensions/gsd/unit-runtime.ts +3 -2
  871. package/src/resources/extensions/gsd/unit-tool-contracts.ts +27 -191
  872. package/src/resources/extensions/gsd/validation-block-guard.ts +2 -0
  873. package/src/resources/extensions/gsd/verdict-parser.ts +1 -1
  874. package/src/resources/extensions/gsd/verification-verdict.ts +4 -2
  875. package/src/resources/extensions/gsd/web-app-uat.ts +144 -0
  876. package/src/resources/extensions/gsd/workflow-event-ledger.ts +131 -0
  877. package/src/resources/extensions/gsd/workflow-event-vocabulary.ts +59 -0
  878. package/src/resources/extensions/gsd/workflow-events.ts +12 -20
  879. package/src/resources/extensions/gsd/workflow-mcp.ts +22 -110
  880. package/src/resources/extensions/gsd/workflow-reconcile.ts +32 -65
  881. package/src/resources/extensions/gsd/workflow-tool-surface.ts +76 -0
  882. package/src/resources/extensions/gsd/workspace-git-guard.ts +1 -0
  883. package/src/resources/extensions/gsd/worktree-git-recovery.ts +314 -0
  884. package/src/resources/extensions/gsd/worktree-lifecycle.ts +20 -25
  885. package/src/resources/extensions/gsd/worktree-manager.ts +47 -28
  886. package/src/resources/extensions/gsd/worktree-placement.ts +63 -0
  887. package/src/resources/extensions/gsd/worktree-reentry.ts +10 -7
  888. package/src/resources/extensions/gsd/worktree-root.ts +29 -6
  889. package/src/resources/extensions/gsd/worktree-safety.ts +8 -5
  890. package/src/resources/extensions/gsd/worktree-session-state.ts +11 -11
  891. package/src/resources/extensions/gsd/worktree-state-projection.ts +55 -7
  892. package/src/resources/extensions/gsd/worktree-telemetry.ts +16 -0
  893. package/src/resources/extensions/search-the-web/native-search.ts +5 -3
  894. package/src/resources/extensions/shared/browser-contract.ts +66 -0
  895. package/src/resources/extensions/shared/gsd-browser-cli.ts +141 -6
  896. package/src/resources/extensions/shared/interview-ui.ts +15 -2
  897. package/src/resources/shared/claude-runtime-floor.ts +248 -0
  898. package/src/resources/shared/gsd-browser-path-sync.ts +273 -0
  899. package/src/resources/shared/package-manager-detection.ts +1 -1
  900. package/src/resources/shared/package.json +3 -0
  901. package/src/resources/skills/create-skill/references/executable-code.md +1 -1
  902. package/src/resources/skills/create-skill/workflows/add-reference.md +8 -3
  903. package/src/resources/skills/create-skill/workflows/add-script.md +4 -2
  904. package/src/resources/skills/create-skill/workflows/add-template.md +3 -1
  905. package/src/resources/skills/create-skill/workflows/add-workflow.md +8 -3
  906. package/src/resources/skills/create-skill/workflows/upgrade-to-router.md +10 -5
  907. package/src/resources/skills/create-skill/workflows/verify-skill.md +9 -4
  908. package/src/resources/skills/gsd-browser/SKILL.md +1 -1
  909. package/src/resources/skills/spike-wrap-up/SKILL.md +9 -9
  910. package/dist/resources/extensions/gsd/user-input-boundary.js +0 -157
  911. package/dist/web/standalone/.next/server/chunks/678.js +0 -2
  912. package/dist/web/standalone/.next/static/chunks/2659.feb6499ca863ebfc.js +0 -1
  913. package/dist/web/standalone/.next/static/chunks/2772.151789db0edea835.js +0 -1
  914. package/dist/web/standalone/.next/static/chunks/4283.10a065467b5340d8.js +0 -2
  915. package/dist/web/standalone/.next/static/chunks/5826.960dc4634cc9b0d3.js +0 -1
  916. package/dist/web/standalone/.next/static/chunks/796.46f811c0fac23aab.js +0 -10
  917. package/dist/web/standalone/.next/static/chunks/8785.d32f7a61f55c1600.js +0 -1
  918. package/packages/gsd-agent-modes/dist/modes/interactive/components/__prototype__/gsd-widget-prototype.d.ts +0 -21
  919. package/packages/gsd-agent-modes/dist/modes/interactive/components/__prototype__/gsd-widget-prototype.d.ts.map +0 -1
  920. package/packages/gsd-agent-modes/dist/modes/interactive/components/__prototype__/gsd-widget-prototype.js +0 -213
  921. package/packages/gsd-agent-modes/dist/modes/interactive/components/__prototype__/gsd-widget-prototype.js.map +0 -1
  922. package/packages/gsd-agent-modes/dist/modes/interactive/components/__prototype__/transcript-density-prototype.d.ts +0 -28
  923. package/packages/gsd-agent-modes/dist/modes/interactive/components/__prototype__/transcript-density-prototype.d.ts.map +0 -1
  924. package/packages/gsd-agent-modes/dist/modes/interactive/components/__prototype__/transcript-density-prototype.js +0 -249
  925. package/packages/gsd-agent-modes/dist/modes/interactive/components/__prototype__/transcript-density-prototype.js.map +0 -1
  926. package/packages/gsd-agent-modes/dist/modes/interactive/components/__prototype__/transcript-design-prototype.d.ts +0 -19
  927. package/packages/gsd-agent-modes/dist/modes/interactive/components/__prototype__/transcript-design-prototype.d.ts.map +0 -1
  928. package/packages/gsd-agent-modes/dist/modes/interactive/components/__prototype__/transcript-design-prototype.js +0 -797
  929. package/packages/gsd-agent-modes/dist/modes/interactive/components/__prototype__/transcript-design-prototype.js.map +0 -1
  930. package/scripts/ensure-workspace-builds.cjs +0 -129
  931. package/src/resources/extensions/gsd/tests/user-input-boundary.test.ts +0 -26
  932. package/src/resources/extensions/gsd/user-input-boundary.ts +0 -166
  933. /package/dist/web/standalone/.next/static/{tJOKQbQRO-9MiFDO8DIDS → T-LTxEw5wir5Lm5T3qEVd}/_buildManifest.js +0 -0
  934. /package/dist/web/standalone/.next/static/{tJOKQbQRO-9MiFDO8DIDS → T-LTxEw5wir5Lm5T3qEVd}/_ssgManifest.js +0 -0
@@ -1,26 +1,207 @@
1
1
  // Project/App: gsd-pi
2
2
  // File Purpose: Auto Orchestration module contract and ADR-015 invariant sequence tests.
3
+ //
4
+ // Phase 2 of #442 collapsed the nine adapter seams into AutoOrchestrator. These
5
+ // tests therefore drive the REAL collapsed orchestrator against real temp
6
+ // SQLite + git fixtures (fixture builder modelled on
7
+ // state-reconciliation-drift.test.ts) and inject dispatch decisions through the
8
+ // real unified rule registry (setRegistry) rather than mock adapters. Decision
9
+ // logic is asserted on observable advance() outcomes and journal events instead
10
+ // of an internal calls[] array. Dispatch-decision parity (formerly the
11
+ // createWiredDispatchAdapter tests) is asserted against the exported pure
12
+ // decideOrchestratorDispatch helper.
3
13
 
4
14
  import test from "node:test";
5
15
  import assert from "node:assert/strict";
16
+ import { execFileSync } from "node:child_process";
6
17
  import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
7
18
  import { tmpdir } from "node:os";
8
19
  import { join } from "node:path";
9
20
 
10
- import { createAutoOrchestrator, STUCK_WINDOW_SIZE } from "../auto/orchestrator.js";
11
- import type { AutoOrchestratorDeps } from "../auto/contracts.js";
21
+ import {
22
+ createAutoOrchestrator,
23
+ decideOrchestratorDispatch,
24
+ resolveLiveOrchestratorBasePath,
25
+ } from "../auto/orchestrator.js";
26
+ import { STUCK_WINDOW_SIZE } from "../auto/dispatch-history.js";
27
+ import type { OrchestratorContext } from "../auto/orchestrator.js";
28
+ import type { AutoOrchestrationModule, AutoSessionContext } from "../auto/contracts.js";
12
29
  import type { GSDState } from "../types.js";
13
- import { createWiredDispatchAdapter, resolveLiveOrchestratorBasePath } from "../auto.js";
14
30
  import { resolveDispatch, type DispatchContext } from "../auto-dispatch.js";
15
31
  import { RuleRegistry, setRegistry, resetRegistry } from "../rule-registry.js";
16
32
  import type { UnifiedRule } from "../rule-types.js";
17
33
  import { supportsStructuredQuestions } from "../workflow-mcp.js";
18
- import { closeDatabase, insertMilestone, insertSlice, insertTask, openDatabase } from "../gsd-db.js";
34
+ import {
35
+ closeDatabase,
36
+ insertAssessment,
37
+ insertGateRow,
38
+ insertMilestone,
39
+ insertSlice,
40
+ insertTask,
41
+ openDatabase,
42
+ } from "../gsd-db.js";
43
+ import { AutoSession } from "../auto/session.js";
44
+ import { registerAutoWorker } from "../db/auto-workers.js";
45
+ import { claimMilestoneLease } from "../db/milestone-leases.js";
46
+ import { recordDispatchClaim, markFailed } from "../db/unit-dispatches.js";
47
+ import { normalizeRealPath } from "../paths.js";
48
+ import { acquireSessionLock, releaseSessionLock } from "../session-lock.js";
49
+ import { queryJournal } from "../journal.js";
50
+ import { invalidateAllCaches } from "../cache.js";
51
+ import { invalidateStateCache } from "../state.js";
52
+
53
+ // ─────────────────────────────────────────────────────────────────────────────
54
+ // Fixture builder
55
+ //
56
+ // Builds a real, isolated project: a git repo (so the pre-dispatch health gate
57
+ // and merge-state reconciliation have something real to probe), a SQLite DB
58
+ // seeded with one active milestone/slice/task, and the matching ROADMAP/PLAN
59
+ // markdown projection. A real session lock is acquired so the orchestrator's
60
+ // ensureLockOwnership passes. A fresh AutoSession is wired to the base path. A
61
+ // dispatch rule is installed in the real unified registry so resolveDispatch
62
+ // yields a deterministic decision — this is the only "injection", and it is the
63
+ // same public seam (setRegistry) the dispatch engine already exposes.
64
+ // ─────────────────────────────────────────────────────────────────────────────
65
+
66
+ type DispatchRuleResult =
67
+ | { action: "dispatch"; unitType: string; unitId: string; prompt: string; pauseAfterDispatch?: boolean }
68
+ | { action: "stop"; reason: string; level: "info" | "warning" | "error" }
69
+ | { action: "skip"; matchedRule?: string };
70
+
71
+ interface FixtureOptions {
72
+ /** When provided, the rule returns this result. Defaults to dispatching M001/S01/T01. */
73
+ dispatch?: () => DispatchRuleResult | Promise<DispatchRuleResult>;
74
+ /** Rule name (becomes the dispatch `reason`/`matchedRule`). */
75
+ ruleName?: string;
76
+ /** Skip seeding a ready task (used for the "no remaining units" / complete scenarios). */
77
+ noTask?: boolean;
78
+ /** Mark the seeded milestone complete (drives the completion → stopped path). */
79
+ complete?: boolean;
80
+ }
19
81
 
20
- function assertBlockedResult(
21
- result: Awaited<ReturnType<ReturnType<typeof createAutoOrchestrator>["advance"]>>,
22
- ): asserts result is Extract<typeof result, { kind: "blocked" }> {
23
- assert.equal(result.kind, "blocked");
82
+ interface Fixture {
83
+ base: string;
84
+ session: AutoSession;
85
+ ctx: OrchestratorContext;
86
+ orchestrator: AutoOrchestrationModule;
87
+ /** Names emitted to the journal by the orchestrator (data.name), in order. */
88
+ journalNames(): string[];
89
+ cleanup(): void;
90
+ }
91
+
92
+ const DEFAULT_DISPATCH: DispatchRuleResult = {
93
+ action: "dispatch",
94
+ unitType: "execute-task",
95
+ unitId: "M001/S01/T01",
96
+ prompt: "fixture-prompt",
97
+ };
98
+
99
+ function gitInit(base: string): void {
100
+ execFileSync("git", ["init", "--initial-branch=main"], { cwd: base, stdio: "ignore" });
101
+ execFileSync("git", ["config", "user.email", "test@test.com"], { cwd: base, stdio: "ignore" });
102
+ execFileSync("git", ["config", "user.name", "Test"], { cwd: base, stdio: "ignore" });
103
+ writeFileSync(join(base, ".gitkeep"), "");
104
+ execFileSync("git", ["add", "."], { cwd: base, stdio: "ignore" });
105
+ execFileSync("git", ["commit", "-m", "initial"], { cwd: base, stdio: "ignore" });
106
+ }
107
+
108
+ function makeFixture(opts: FixtureOptions = {}): Fixture {
109
+ const base = mkdtempSync(join(tmpdir(), "gsd-orchestrator-"));
110
+ gitInit(base);
111
+
112
+ const milestoneDir = join(base, ".gsd", "milestones", "M001");
113
+ const sliceDir = join(milestoneDir, "slices", "S01");
114
+ mkdirSync(join(sliceDir, "tasks"), { recursive: true });
115
+
116
+ invalidateAllCaches();
117
+ invalidateStateCache();
118
+ openDatabase(join(base, ".gsd", "gsd.db"));
119
+ insertMilestone({ id: "M001", title: "Milestone", status: opts.complete ? "complete" : "active" });
120
+ if (!opts.noTask && !opts.complete) {
121
+ insertSlice({ id: "S01", milestoneId: "M001", title: "Slice", status: "active", risk: "low", depends: [], demo: "", sequence: 1 });
122
+ insertTask({ id: "T01", sliceId: "S01", milestoneId: "M001", title: "Task", status: "active" });
123
+ }
124
+
125
+ writeFileSync(
126
+ join(milestoneDir, "M001-ROADMAP.md"),
127
+ [
128
+ "# M001: Milestone",
129
+ "",
130
+ "**Vision:** Fixture milestone",
131
+ "",
132
+ "## Slices",
133
+ "",
134
+ "- [ ] **S01: Slice** `risk:low` `depends:[]`",
135
+ "",
136
+ ].join("\n"),
137
+ );
138
+ if (!opts.noTask && !opts.complete) {
139
+ writeFileSync(
140
+ join(sliceDir, "S01-PLAN.md"),
141
+ [
142
+ "# S01: Slice",
143
+ "",
144
+ "**Goal:** Fixture goal",
145
+ "**Demo:** Fixture demo",
146
+ "",
147
+ "## Must-Haves",
148
+ "",
149
+ "- Everything works",
150
+ "",
151
+ "## Tasks",
152
+ "",
153
+ "- [ ] **T01: Task** `est:1h`",
154
+ "",
155
+ ].join("\n"),
156
+ );
157
+ }
158
+
159
+ acquireSessionLock(base);
160
+
161
+ const session = new AutoSession();
162
+ session.basePath = base;
163
+ session.originalBasePath = base;
164
+ session.currentMilestoneId = "M001";
165
+ session.resourceVersionOnStart = null;
166
+
167
+ const ctx: OrchestratorContext = {
168
+ ctx: { model: {}, modelRegistry: { getAll: () => [] }, ui: { notify() {} } } as never,
169
+ pi: { getActiveTools: () => [] } as never,
170
+ dispatchBasePath: base,
171
+ runtimeBasePath: base,
172
+ session,
173
+ };
174
+
175
+ const ruleName = opts.ruleName ?? "fixture-dispatch";
176
+ const decide = opts.dispatch ?? (() => DEFAULT_DISPATCH);
177
+ const rule: UnifiedRule = {
178
+ name: ruleName,
179
+ when: "dispatch",
180
+ evaluation: "first-match",
181
+ where: async () => decide(),
182
+ then: (r: unknown) => r,
183
+ };
184
+ setRegistry(new RuleRegistry([rule]));
185
+
186
+ const orchestrator = createAutoOrchestrator(ctx);
187
+
188
+ return {
189
+ base,
190
+ session,
191
+ ctx,
192
+ orchestrator,
193
+ journalNames() {
194
+ return queryJournal(base)
195
+ .map((e) => (e.data as Record<string, unknown> | undefined)?.name)
196
+ .filter((n): n is string => typeof n === "string");
197
+ },
198
+ cleanup() {
199
+ resetRegistry();
200
+ try { releaseSessionLock(base); } catch { /* */ }
201
+ try { closeDatabase(); } catch { /* */ }
202
+ try { rmSync(base, { recursive: true, force: true }); } catch { /* */ }
203
+ },
204
+ };
24
205
  }
25
206
 
26
207
  function makeState(): GSDState {
@@ -38,1002 +219,771 @@ function makeState(): GSDState {
38
219
  };
39
220
  }
40
221
 
41
- function makeDeps(overrides: Partial<AutoOrchestratorDeps> = {}): { deps: AutoOrchestratorDeps; calls: string[] } {
42
- const calls: string[] = [];
43
- const stateSnapshot = makeState();
222
+ const SESSION_CONTEXT: AutoSessionContext = { basePath: "/tmp/project", trigger: "manual" };
44
223
 
45
- const deps: AutoOrchestratorDeps = {
46
- stateReconciliation: {
47
- async reconcileBeforeDispatch() {
48
- calls.push("state.reconcile");
49
- return { ok: true, stateSnapshot };
50
- },
51
- },
52
- dispatch: {
53
- async decideNextUnit(input) {
54
- calls.push("dispatch.decide");
55
- assert.equal(input.stateSnapshot, stateSnapshot);
56
- return { unitType: "execute-task", unitId: "T01", reason: "ready", preconditions: [] };
57
- },
58
- },
59
- toolContract: {
60
- async compileUnitToolContract() {
61
- calls.push("tool.compile");
62
- return { ok: true };
63
- },
64
- },
65
- recovery: {
66
- async classifyAndRecover() {
67
- calls.push("recovery.classify");
68
- return { action: "stop", reason: "fatal" };
69
- },
70
- },
71
- worktree: {
72
- async prepareForUnit() {
73
- calls.push("worktree.prepare");
74
- return { ok: true };
75
- },
76
- async syncAfterUnit() { calls.push("worktree.sync"); },
77
- async cleanupOnStop() { calls.push("worktree.cleanup"); },
78
- },
79
- health: {
80
- checkResourcesStale() {
81
- calls.push("health.stale");
82
- return null;
83
- },
84
- async preAdvanceGate() {
85
- calls.push("health.pre");
86
- return { kind: "pass" };
87
- },
88
- async postAdvanceRecord() { calls.push("health.post"); },
89
- },
90
- runtime: {
91
- async ensureLockOwnership() { calls.push("runtime.lock"); },
92
- async journalTransition(event) { calls.push(`journal:${event.name}`); },
93
- },
94
- notifications: {
95
- async notifyLifecycle(event) { calls.push(`notify:${event.name}`); },
96
- },
97
- uokGate: {
98
- async emit(input) { calls.push(`gate:${input.gateId}:${input.outcome}`); },
99
- },
100
- };
101
-
102
- return { deps: { ...deps, ...overrides }, calls };
103
- }
224
+ // ─────────────────────────────────────────────────────────────────────────────
225
+ // Lifecycle: start / resume / stop
226
+ // ─────────────────────────────────────────────────────────────────────────────
104
227
 
105
- test("start() enters running phase without dispatching", async () => {
106
- const { deps, calls } = makeDeps();
107
- const orchestrator = createAutoOrchestrator(deps);
228
+ test("start() enters running phase without dispatching", async (t) => {
229
+ const f = makeFixture();
230
+ t.after(() => f.cleanup());
108
231
 
109
- const result = await orchestrator.start({ basePath: "/tmp/project", trigger: "manual" });
232
+ const result = await f.orchestrator.start(SESSION_CONTEXT);
110
233
 
111
234
  assert.equal(result.kind, "started");
112
- const status = orchestrator.getStatus();
235
+ const status = f.orchestrator.getStatus();
113
236
  assert.equal(status.phase, "running");
114
237
  assert.equal(status.activeUnit, undefined);
115
- assert.ok(calls.includes("journal:start"));
116
- assert.ok(!calls.includes("journal:advance"));
238
+ assert.ok(f.journalNames().includes("start"));
239
+ assert.ok(!f.journalNames().includes("advance"));
117
240
  });
118
241
 
119
- test("advance() returns blocked when health gate denies", async () => {
120
- const { deps, calls } = makeDeps({
121
- health: {
122
- checkResourcesStale: () => null,
123
- async preAdvanceGate() { return { kind: "fail", reason: "doctor-block" }; },
124
- async postAdvanceRecord() {},
125
- },
126
- });
127
- const orchestrator = createAutoOrchestrator(deps);
242
+ test("resume() enters running phase without dispatching", async (t) => {
243
+ const f = makeFixture();
244
+ t.after(() => f.cleanup());
128
245
 
129
- const result = await orchestrator.advance();
246
+ const result = await f.orchestrator.resume();
130
247
 
131
- assertBlockedResult(result);
132
- assert.equal(result.reason, "doctor-block");
133
- assert.equal(result.action, "pause");
134
- assert.ok(calls.includes("gate:pre-dispatch-health-gate:manual-attention"));
248
+ assert.equal(result.kind, "resumed");
249
+ assert.equal(f.orchestrator.getStatus().phase, "running");
250
+ assert.ok(!f.journalNames().includes("advance"));
135
251
  });
136
252
 
137
- test("advance() stops auto when health gate reports unrecoverable git probe", async () => {
138
- const { deps } = makeDeps({
139
- health: {
140
- checkResourcesStale: () => null,
141
- async preAdvanceGate() {
142
- return { kind: "fail", reason: "Could not verify git conflict state", action: "stop" };
143
- },
144
- async postAdvanceRecord() {},
145
- },
146
- });
147
- const orchestrator = createAutoOrchestrator(deps);
253
+ test("transitionCount increases across lifecycle transitions", async (t) => {
254
+ const f = makeFixture();
255
+ t.after(() => f.cleanup());
148
256
 
149
- const result = await orchestrator.advance();
257
+ const before = f.orchestrator.getStatus().transitionCount;
258
+ await f.orchestrator.start(SESSION_CONTEXT);
259
+ const afterStart = f.orchestrator.getStatus().transitionCount;
260
+ await f.orchestrator.stop("done");
261
+ const afterStop = f.orchestrator.getStatus().transitionCount;
150
262
 
151
- assertBlockedResult(result);
152
- assert.equal(result.action, "stop");
263
+ assert.ok(afterStart > before);
264
+ assert.ok(afterStop > afterStart);
153
265
  });
154
266
 
155
- test("advance() returns blocked pause when resources are stale", async () => {
156
- const { deps, calls } = makeDeps({
157
- health: {
158
- checkResourcesStale: () => "resources changed since session start",
159
- async preAdvanceGate() { return { kind: "pass" }; },
160
- async postAdvanceRecord() {},
161
- },
162
- });
163
- const orchestrator = createAutoOrchestrator(deps);
267
+ test("stop() transitions to stopped and journals stop", async (t) => {
268
+ const f = makeFixture();
269
+ t.after(() => f.cleanup());
164
270
 
165
- const result = await orchestrator.advance();
271
+ const result = await f.orchestrator.stop("user-request");
166
272
 
167
- assertBlockedResult(result);
168
- assert.equal(result.reason, "resources changed since session start");
169
- assert.equal(result.action, "pause");
170
- assert.ok(calls.includes("gate:resource-version-guard:fail"));
171
- assert.ok(!calls.includes("health.pre"));
172
- assert.ok(!calls.includes("state.reconcile"));
273
+ assert.equal(result.kind, "stopped");
274
+ assert.equal(f.orchestrator.getStatus().phase, "stopped");
275
+ assert.ok(f.journalNames().includes("stop"));
173
276
  });
174
277
 
175
- test("advance() pre-dispatch parity: gate emissions and control-flow action match legacy branches", async () => {
176
- type Scenario = {
177
- name: string;
178
- staleMsg: string | null;
179
- gateResult: Awaited<ReturnType<AutoOrchestratorDeps["health"]["preAdvanceGate"]>>;
180
- expectedKind: "advanced" | "blocked";
181
- expectedAction?: "pause" | "stop";
182
- expectedReason?: string;
183
- expectedGates: string[];
184
- };
185
- const scenarios: Scenario[] = [
186
- {
187
- name: "pass",
188
- staleMsg: null,
189
- gateResult: { kind: "pass" },
190
- expectedKind: "advanced",
191
- expectedGates: [
192
- "resource-version-guard:policy:pass:none:resource version guard passed:",
193
- "pre-dispatch-health-gate:execution:pass:none:pre-dispatch health gate passed:",
194
- ],
195
- },
196
- {
197
- name: "resource-stale",
198
- staleMsg: "resources changed since session start",
199
- gateResult: { kind: "pass" },
200
- expectedKind: "blocked",
201
- expectedAction: "pause",
202
- expectedReason: "resources changed since session start",
203
- expectedGates: [
204
- "resource-version-guard:policy:fail:policy:resource version guard blocked dispatch:resources changed since session start",
205
- ],
206
- },
207
- {
208
- name: "health-gate-fail",
209
- staleMsg: null,
210
- gateResult: { kind: "fail", reason: "doctor-block" },
211
- expectedKind: "blocked",
212
- expectedAction: "pause",
213
- expectedReason: "doctor-block",
214
- expectedGates: [
215
- "resource-version-guard:policy:pass:none:resource version guard passed:",
216
- "pre-dispatch-health-gate:execution:manual-attention:manual-attention:pre-dispatch health gate blocked dispatch:doctor-block",
217
- ],
218
- },
219
- ];
220
-
221
- for (const scenario of scenarios) {
222
- const gateEvents: string[] = [];
223
- const { deps } = makeDeps({
224
- health: {
225
- checkResourcesStale: () => scenario.staleMsg,
226
- async preAdvanceGate() { return scenario.gateResult; },
227
- async postAdvanceRecord() {},
228
- },
229
- uokGate: {
230
- async emit(input) {
231
- gateEvents.push(
232
- `${input.gateId}:${input.gateType}:${input.outcome}:${input.failureClass}:${input.rationale}:${input.findings ?? ""}`,
233
- );
234
- },
235
- },
236
- });
237
- const orchestrator = createAutoOrchestrator(deps);
238
- const result = await orchestrator.advance();
239
-
240
- assert.equal(result.kind, scenario.expectedKind, `${scenario.name} result kind`);
241
- if (scenario.expectedKind === "blocked") {
242
- assertBlockedResult(result);
243
- assert.equal(result.action, scenario.expectedAction, `${scenario.name} blocked action`);
244
- assert.equal(result.reason, scenario.expectedReason, `${scenario.name} blocked reason`);
245
- }
246
- assert.deepEqual(gateEvents, scenario.expectedGates, `${scenario.name} gate parity`);
247
- }
248
- });
278
+ // ─────────────────────────────────────────────────────────────────────────────
279
+ // advance(): happy path + ADR-015 invariant sequence
280
+ // ─────────────────────────────────────────────────────────────────────────────
249
281
 
250
- test("advance() continues past pre-dispatch health gate when it throws", async () => {
251
- const { deps, calls } = makeDeps({
252
- health: {
253
- checkResourcesStale: () => null,
254
- async preAdvanceGate() { return { kind: "threw", error: new Error("boom") }; },
255
- async postAdvanceRecord() {},
256
- },
257
- });
258
- const orchestrator = createAutoOrchestrator(deps);
282
+ test("advance() dispatches the resolved unit and journals advance", async (t) => {
283
+ const f = makeFixture();
284
+ t.after(() => f.cleanup());
259
285
 
260
- const result = await orchestrator.advance();
286
+ const result = await f.orchestrator.advance();
261
287
 
262
288
  assert.equal(result.kind, "advanced");
263
- assert.ok(calls.includes("gate:pre-dispatch-health-gate:manual-attention"));
264
- assert.ok(calls.includes("state.reconcile"));
265
- assert.ok(calls.includes("dispatch.decide"));
289
+ if (result.kind !== "advanced") return;
290
+ assert.deepEqual(result.unit, { unitType: "execute-task", unitId: "M001/S01/T01" });
291
+ assert.equal(f.orchestrator.getStatus().phase, "running");
292
+ // Journal records the advance AFTER the invariant gates (lock, health,
293
+ // reconcile, dispatch, tool-contract, worktree) — i.e. no advance-blocked.
294
+ const names = f.journalNames();
295
+ assert.ok(names.includes("advance"));
296
+ assert.ok(!names.includes("advance-blocked"));
266
297
  });
267
298
 
268
- test("advance() forwards fixesApplied into pre-dispatch-health-gate pass findings", async () => {
269
- let observed = "";
270
- const { deps } = makeDeps({
271
- health: {
272
- checkResourcesStale: () => null,
273
- async preAdvanceGate() { return { kind: "pass", fixesApplied: ["fix-a", "fix-b"] }; },
274
- async postAdvanceRecord() {},
275
- },
276
- uokGate: {
277
- async emit(input) {
278
- if (input.gateId === "pre-dispatch-health-gate" && input.outcome === "pass") {
279
- observed = input.findings ?? "";
280
- }
281
- },
282
- },
283
- });
284
- const orchestrator = createAutoOrchestrator(deps);
299
+ test("advance() sets active unit and is reflected in status", async (t) => {
300
+ const f = makeFixture();
301
+ t.after(() => f.cleanup());
285
302
 
286
- await orchestrator.advance();
303
+ await f.orchestrator.advance();
287
304
 
288
- assert.equal(observed, "fix-a, fix-b");
305
+ assert.deepEqual(f.orchestrator.getStatus().activeUnit, {
306
+ unitType: "execute-task",
307
+ unitId: "M001/S01/T01",
308
+ });
289
309
  });
290
310
 
291
- test("advance() follows the ADR-015 invariant sequence before journaling advance", async () => {
292
- const { deps, calls } = makeDeps();
293
- const orchestrator = createAutoOrchestrator(deps);
311
+ test("advance() blocks source dispatch when an earlier slice is incomplete", async (t) => {
312
+ const f = makeFixture({
313
+ dispatch: () => ({
314
+ action: "dispatch",
315
+ unitType: "execute-task",
316
+ unitId: "M001/S02/T01",
317
+ prompt: "fixture-prompt",
318
+ }),
319
+ });
320
+ t.after(() => f.cleanup());
321
+
322
+ insertSlice({
323
+ id: "S02",
324
+ milestoneId: "M001",
325
+ title: "Second slice",
326
+ status: "active",
327
+ risk: "low",
328
+ depends: [],
329
+ demo: "",
330
+ sequence: 2,
331
+ });
332
+ insertTask({
333
+ id: "T01",
334
+ sliceId: "S02",
335
+ milestoneId: "M001",
336
+ title: "Second task",
337
+ status: "active",
338
+ });
294
339
 
295
- const result = await orchestrator.advance();
340
+ const result = await f.orchestrator.advance();
296
341
 
297
- assert.equal(result.kind, "advanced");
298
- assert.deepEqual(result.unit, { unitType: "execute-task", unitId: "T01" });
299
- assert.deepEqual(calls, [
300
- "runtime.lock",
301
- "health.stale",
302
- "gate:resource-version-guard:pass",
303
- "health.pre",
304
- "gate:pre-dispatch-health-gate:pass",
305
- "state.reconcile",
306
- "dispatch.decide",
307
- "tool.compile",
308
- "worktree.prepare",
309
- "journal:advance",
310
- "worktree.sync",
311
- "health.post",
312
- ]);
342
+ assert.equal(result.kind, "blocked");
343
+ if (result.kind !== "blocked") return;
344
+ assert.equal(result.action, "stop");
345
+ assert.match(result.reason, /earlier slice M001\/S01 is not complete/);
346
+ assert.equal(f.session.pendingOrchestrationDispatch, null);
347
+ assert.deepEqual(f.orchestrator.getStatus().activeUnit, undefined);
348
+ assert.ok(f.journalNames().includes("advance-blocked"));
313
349
  });
314
350
 
315
- test("advance() blocks before dispatch when State Reconciliation blocks", async () => {
316
- const { deps, calls } = makeDeps({
317
- stateReconciliation: {
318
- async reconcileBeforeDispatch() {
319
- calls.push("state.reconcile");
320
- return { ok: false, reason: "state drift blocked", stateSnapshot: makeState() };
321
- },
322
- },
323
- });
324
- const orchestrator = createAutoOrchestrator(deps);
351
+ test("getStatus() returns defensive copy of activeUnit", async (t) => {
352
+ const f = makeFixture();
353
+ t.after(() => f.cleanup());
325
354
 
326
- const result = await orchestrator.advance();
355
+ await f.orchestrator.advance();
356
+ const snap1 = f.orchestrator.getStatus();
357
+ if (snap1.activeUnit) snap1.activeUnit.unitId = "MUTATED";
358
+ const snap2 = f.orchestrator.getStatus();
327
359
 
328
- assertBlockedResult(result);
329
- assert.equal(result.reason, "state drift blocked");
330
- assert.equal(result.action, "pause");
331
- assert.ok(!calls.includes("dispatch.decide"));
332
- assert.ok(calls.includes("journal:advance-blocked"));
360
+ assert.equal(snap2.activeUnit?.unitId, "M001/S01/T01");
333
361
  });
334
362
 
335
- test("advance() blocks before Runtime persistence when Tool Contract fails", async () => {
336
- const { deps, calls } = makeDeps({
337
- toolContract: {
338
- async compileUnitToolContract() {
339
- calls.push("tool.compile");
340
- return { ok: false, reason: "unknown Unit" };
341
- },
342
- },
363
+ // ─────────────────────────────────────────────────────────────────────────────
364
+ // Dispatch passthrough decisions (skip / blocked / no-remaining-units)
365
+ // ─────────────────────────────────────────────────────────────────────────────
366
+
367
+ test("advance() keeps running when dispatch intentionally skips a phase", async (t) => {
368
+ const f = makeFixture({
369
+ dispatch: () => ({ action: "skip", matchedRule: "evaluating-gates skipped after marking gates omitted" }),
343
370
  });
344
- const orchestrator = createAutoOrchestrator(deps);
371
+ t.after(() => f.cleanup());
345
372
 
346
- const result = await orchestrator.advance();
373
+ const result = await f.orchestrator.advance();
347
374
 
348
- assertBlockedResult(result);
349
- assert.equal(result.reason, "unknown Unit");
350
- assert.equal(result.action, "pause");
351
- assert.ok(!calls.includes("worktree.prepare"));
352
- assert.ok(!calls.includes("journal:advance"));
353
- assert.ok(calls.includes("journal:advance-blocked"));
375
+ assert.equal(result.kind, "skipped");
376
+ if (result.kind !== "skipped") return;
377
+ assert.equal(result.reason, "evaluating-gates skipped after marking gates omitted");
378
+ assert.equal(f.orchestrator.getStatus().phase, "running");
379
+ const names = f.journalNames();
380
+ assert.ok(names.includes("advance-skipped"));
381
+ assert.ok(!names.includes("advance-stopped"));
354
382
  });
355
383
 
356
- test("advance() blocks before Runtime persistence when Worktree Safety fails", async () => {
357
- const { deps, calls } = makeDeps({
358
- worktree: {
359
- async prepareForUnit() {
360
- calls.push("worktree.prepare");
361
- return { ok: false, reason: "worktree invalid" };
362
- },
363
- async syncAfterUnit() { calls.push("worktree.sync"); },
364
- async cleanupOnStop() { calls.push("worktree.cleanup"); },
365
- },
384
+ test("advance() surfaces dispatch blocker reason instead of generic no remaining units", async (t) => {
385
+ const reason = "Milestone M001 validation verdict is needs-remediation but all slices are complete.";
386
+ const f = makeFixture({
387
+ dispatch: () => ({ action: "stop", reason, level: "warning" }),
366
388
  });
367
- const orchestrator = createAutoOrchestrator(deps);
389
+ t.after(() => f.cleanup());
368
390
 
369
- const result = await orchestrator.advance();
391
+ const result = await f.orchestrator.advance();
370
392
 
371
- assertBlockedResult(result);
372
- assert.equal(result.reason, "worktree invalid");
393
+ assert.equal(result.kind, "blocked");
394
+ if (result.kind !== "blocked") return;
395
+ assert.equal(result.reason, reason);
373
396
  assert.equal(result.action, "pause");
374
- assert.ok(!calls.includes("journal:advance"));
375
- assert.ok(!calls.includes("worktree.sync"));
376
- assert.ok(calls.includes("journal:advance-blocked"));
397
+ const names = f.journalNames();
398
+ assert.ok(names.includes("advance-blocked"));
399
+ assert.ok(!names.includes("advance-stopped"));
377
400
  });
378
401
 
379
- test("advance() allows non-worktree isolation prepare result", async () => {
380
- const { deps, calls } = makeDeps({
381
- worktree: {
382
- async prepareForUnit() {
383
- calls.push("worktree.prepare");
384
- return { ok: true, reason: "isolation-not-worktree" };
385
- },
386
- async syncAfterUnit() { calls.push("worktree.sync"); },
387
- async cleanupOnStop() { calls.push("worktree.cleanup"); },
388
- },
402
+ test("advance() stop level=error blocks with action stop", async (t) => {
403
+ const f = makeFixture({
404
+ dispatch: () => ({ action: "stop", reason: "hard blocker", level: "error" }),
389
405
  });
390
- const orchestrator = createAutoOrchestrator(deps);
406
+ t.after(() => f.cleanup());
391
407
 
392
- const result = await orchestrator.advance();
408
+ const result = await f.orchestrator.advance();
393
409
 
394
- assert.equal(result.kind, "advanced");
395
- assert.ok(calls.includes("journal:advance"));
396
- assert.ok(calls.includes("worktree.sync"));
410
+ assert.equal(result.kind, "blocked");
411
+ if (result.kind !== "blocked") return;
412
+ assert.equal(result.action, "stop");
397
413
  });
398
414
 
399
- test("advance() stops when dispatch has no next unit", async () => {
400
- const { deps } = makeDeps({
401
- dispatch: {
402
- async decideNextUnit() { return null; },
403
- },
404
- });
405
- const orchestrator = createAutoOrchestrator(deps);
415
+ test("advance() reports completion when complete state has no next unit", async (t) => {
416
+ const f = makeFixture({ complete: true, noTask: true });
417
+ t.after(() => f.cleanup());
406
418
 
407
- const result = await orchestrator.advance();
419
+ const result = await f.orchestrator.advance();
408
420
 
409
421
  assert.equal(result.kind, "stopped");
410
- assert.equal(orchestrator.getStatus().phase, "stopped");
422
+ if (result.kind !== "stopped") return;
423
+ assert.equal(result.reason, "All milestones complete");
424
+ assert.equal(result.terminalOutcome?.code, "all-complete");
425
+ assert.equal(f.orchestrator.getStatus().phase, "stopped");
411
426
  });
412
427
 
413
- test("advance() reports completion when complete state has no next unit", async () => {
414
- const completeState: GSDState = {
415
- ...makeState(),
416
- activeMilestone: null,
417
- phase: "complete",
418
- lastCompletedMilestone: { id: "M001", title: "Milestone" },
419
- nextAction: "All milestones complete.",
420
- };
421
- const { deps } = makeDeps({
422
- stateReconciliation: {
423
- async reconcileBeforeDispatch() {
424
- return { ok: true, stateSnapshot: completeState };
425
- },
426
- },
427
- dispatch: {
428
- async decideNextUnit() { return null; },
429
- },
428
+ test("advance() blocks all-complete stop when completed milestone is still unmerged in a worktree", async (t) => {
429
+ const f = makeFixture({ complete: true, noTask: true });
430
+ t.after(() => f.cleanup());
431
+
432
+ insertSlice({
433
+ id: "S01",
434
+ milestoneId: "M001",
435
+ title: "Slice",
436
+ status: "complete",
437
+ risk: "low",
438
+ depends: [],
439
+ demo: "",
440
+ sequence: 1,
430
441
  });
431
- const orchestrator = createAutoOrchestrator(deps);
432
-
433
- const result = await orchestrator.advance();
434
-
435
- assert.equal(result.kind, "stopped");
436
- assert.equal(result.reason, "all milestones complete");
437
- });
438
-
439
- test("advance() keeps running when dispatch intentionally skips a phase", async () => {
440
- const { deps, calls } = makeDeps({
441
- dispatch: {
442
- async decideNextUnit() {
443
- return { kind: "skipped", reason: "evaluating-gates skipped after marking gates omitted" };
444
- },
445
- },
442
+ insertAssessment({
443
+ path: "milestones/M001/M001-VALIDATION.md",
444
+ milestoneId: "M001",
445
+ status: "pass",
446
+ scope: "milestone-validation",
447
+ fullContent: "verdict: pass",
446
448
  });
447
- const orchestrator = createAutoOrchestrator(deps);
448
-
449
- const result = await orchestrator.advance();
450
-
451
- assert.equal(result.kind, "skipped");
452
- if (result.kind !== "skipped") return;
453
- assert.equal(result.reason, "evaluating-gates skipped after marking gates omitted");
454
- assert.equal(orchestrator.getStatus().phase, "running");
455
- assert.ok(calls.includes("journal:advance-skipped"));
456
- assert.ok(!calls.includes("journal:advance-stopped"));
457
- });
458
-
459
- test("advance() surfaces dispatch blocker reason instead of generic no remaining units", async () => {
460
- const { deps, calls } = makeDeps({
461
- dispatch: {
462
- async decideNextUnit() {
463
- return {
464
- kind: "blocked",
465
- reason: "Milestone M001 validation verdict is needs-remediation but all slices are complete.",
466
- action: "pause",
467
- };
468
- },
469
- },
449
+ insertGateRow({
450
+ milestoneId: "M001",
451
+ sliceId: "S01",
452
+ gateId: "Q3",
453
+ scope: "slice",
454
+ status: "pending",
470
455
  });
471
- const orchestrator = createAutoOrchestrator(deps);
472
456
 
473
- const result = await orchestrator.advance();
457
+ const worktreePath = join(f.base, ".gsd", "worktrees", "M001");
458
+ mkdirSync(join(f.base, ".gsd", "worktrees"), { recursive: true });
459
+ execFileSync("git", ["worktree", "add", "-b", "milestone/M001", worktreePath], { cwd: f.base, stdio: "ignore" });
460
+ mkdirSync(join(worktreePath, ".gsd", "milestones", "M001"), { recursive: true });
461
+ writeFileSync(join(worktreePath, ".gsd", "milestones", "M001", "M001-SUMMARY.md"), "# Milestone Summary\n");
462
+ f.session.basePath = worktreePath;
463
+ f.session.originalBasePath = f.base;
464
+ f.session.currentMilestoneId = "M001";
465
+ f.session.milestoneMergedInPhases = false;
466
+
467
+ const result = await f.orchestrator.advance();
474
468
 
475
469
  assert.equal(result.kind, "blocked");
476
470
  if (result.kind !== "blocked") return;
477
- assert.equal(result.reason, "Milestone M001 validation verdict is needs-remediation but all slices are complete.");
478
471
  assert.equal(result.action, "pause");
479
- assert.ok(calls.includes("journal:advance-blocked"));
480
- assert.ok(!calls.includes("journal:advance-stopped"));
472
+ assert.equal(result.terminalOutcome?.code, "settlement-blocked");
473
+ assert.match(result.reason, /worktree branch has not been merged to main/);
474
+ assert.doesNotMatch(result.reason, /quality gate Q3 is still pending/);
475
+ assert.equal(f.orchestrator.getStatus().phase, "paused");
476
+ assert.equal(f.session.milestoneSettlement?.ok, false);
477
+ const names = f.journalNames();
478
+ assert.ok(names.includes("advance-blocked"));
479
+ assert.ok(!names.includes("advance-stopped"));
481
480
  });
482
481
 
483
- test("resume() enters running phase without dispatching", async () => {
484
- const { deps, calls } = makeDeps();
485
- const orchestrator = createAutoOrchestrator(deps);
482
+ test("advance() stopped clears previous activeUnit and resets idempotent lock", async (t) => {
483
+ // First advance dispatches; then we make the milestone resolve to no unit by
484
+ // closing it on disk + DB and re-deriving. Simpler: drive a fixture that
485
+ // dispatches once, finalize externally, then the next decision is complete.
486
+ let dispatchOnce = true;
487
+ const f = makeFixture({
488
+ dispatch: () => {
489
+ if (dispatchOnce) {
490
+ dispatchOnce = false;
491
+ return DEFAULT_DISPATCH;
492
+ }
493
+ // After the first advance, signal completion via a benign skip → still
494
+ // exercises the running/active-unit transition. For the stopped path we
495
+ // rely on the complete-state test above.
496
+ return { action: "skip", matchedRule: "done" };
497
+ },
498
+ });
499
+ t.after(() => f.cleanup());
486
500
 
487
- const result = await orchestrator.resume();
501
+ const first = await f.orchestrator.advance();
502
+ assert.equal(first.kind, "advanced");
488
503
 
489
- assert.equal(result.kind, "resumed");
490
- assert.equal(orchestrator.getStatus().phase, "running");
491
- assert.ok(!calls.includes("journal:advance"));
492
- assert.ok(!calls.includes("dispatch.decide"));
504
+ const second = await f.orchestrator.advance();
505
+ assert.equal(second.kind, "skipped");
506
+ // skip clears activeUnit
507
+ assert.equal(f.orchestrator.getStatus().activeUnit, undefined);
493
508
  });
494
509
 
495
- test("advance() uses recovery on error", async () => {
496
- const { deps, calls } = makeDeps({
497
- runtime: {
498
- async ensureLockOwnership() { throw new Error("lock lost"); },
499
- async journalTransition(event) { calls.push(`journal:${event.name}`); },
500
- },
501
- recovery: {
502
- async classifyAndRecover() { return { action: "escalate", reason: "needs manual" }; },
503
- },
504
- });
505
- const orchestrator = createAutoOrchestrator(deps);
510
+ // ─────────────────────────────────────────────────────────────────────────────
511
+ // Idempotency + finalized guard + stuck-loop ring (issues #5786 / #5787 / #415)
512
+ // ─────────────────────────────────────────────────────────────────────────────
513
+
514
+ test("advance() is idempotent for the same active unit", async (t) => {
515
+ const f = makeFixture();
516
+ t.after(() => f.cleanup());
506
517
 
507
- const result = await orchestrator.advance();
518
+ const first = await f.orchestrator.advance();
519
+ const second = await f.orchestrator.advance();
508
520
 
509
- assert.equal(result.kind, "error");
510
- assert.equal(result.reason, "needs manual");
511
- assert.equal(orchestrator.getStatus().phase, "error");
512
- assert.ok(calls.includes("journal:advance-error"));
521
+ assert.equal(first.kind, "advanced");
522
+ if (first.kind === "advanced") {
523
+ assert.deepEqual(first.unit, { unitType: "execute-task", unitId: "M001/S01/T01" });
524
+ }
525
+ assert.equal(second.kind, "skipped");
526
+ if (second.kind !== "skipped") return;
527
+ assert.equal(second.reason, "idempotent advance: unit already active");
513
528
  });
514
529
 
515
- test("advance() is idempotent for the same active unit", async () => {
516
- const { deps, calls } = makeDeps();
517
- const orchestrator = createAutoOrchestrator(deps);
530
+ test("idempotency skip fires with its own reason before saturation", async (t) => {
531
+ const f = makeFixture();
532
+ t.after(() => f.cleanup());
518
533
 
519
- const first = await orchestrator.advance();
520
- const second = await orchestrator.advance();
534
+ const first = await f.orchestrator.advance();
535
+ const second = await f.orchestrator.advance();
521
536
 
522
537
  assert.equal(first.kind, "advanced");
523
- assert.deepEqual(first.unit, { unitType: "execute-task", unitId: "T01" });
524
- assert.equal(second.kind, "blocked");
538
+ assert.equal(second.kind, "skipped");
539
+ if (second.kind !== "skipped") return;
525
540
  assert.equal(second.reason, "idempotent advance: unit already active");
526
- assert.equal(second.action, "pause");
527
-
528
- const prepareCalls = calls.filter((c) => c === "worktree.prepare").length;
529
- assert.equal(prepareCalls, 1);
530
541
  });
531
542
 
532
- test("completeActiveUnit clears in-flight idempotency and stops stale same-unit advance", async () => {
533
- const { deps, calls } = makeDeps();
534
- const orchestrator = createAutoOrchestrator(deps);
543
+ test("completeActiveUnit clears in-flight idempotency and stops stale same-unit advance", async (t) => {
544
+ const f = makeFixture();
545
+ t.after(() => f.cleanup());
535
546
 
536
- const first = await orchestrator.advance();
547
+ const first = await f.orchestrator.advance();
537
548
  assert.equal(first.kind, "advanced");
538
549
  if (first.kind !== "advanced") throw new Error("expected first advance");
539
550
 
540
- await orchestrator.completeActiveUnit(first.unit);
541
- const second = await orchestrator.advance();
551
+ await f.orchestrator.completeActiveUnit(first.unit);
552
+ const second = await f.orchestrator.advance();
542
553
 
543
- assert.equal(orchestrator.getStatus().activeUnit, undefined);
554
+ assert.equal(f.orchestrator.getStatus().activeUnit, undefined);
544
555
  assert.equal(second.kind, "blocked");
545
556
  if (second.kind !== "blocked") throw new Error("expected stale same-unit block");
546
557
  assert.equal(second.action, "stop");
547
- assert.equal(second.reason, "state did not advance after finalized execute-task T01");
548
- assert.ok(calls.includes("journal:unit-finalized"));
549
- const prepareCalls = calls.filter((c) => c === "worktree.prepare").length;
550
- assert.equal(prepareCalls, 1, "stale same-unit advance must not prepare or redispatch");
558
+ assert.equal(second.reason, "state did not advance after finalized execute-task M001/S01/T01");
559
+ assert.ok(f.journalNames().includes("unit-finalized"));
551
560
  });
552
561
 
553
- test("completeActiveUnit allows a different next unit to advance", async () => {
554
- let nextTaskId = "T01";
555
- const { deps } = makeDeps({
556
- dispatch: {
557
- async decideNextUnit() {
558
- return { unitType: "execute-task", unitId: nextTaskId, reason: "ready", preconditions: [] };
559
- },
560
- },
562
+ test("#442: finalized-repeat recovers (skipped) when the unit's artifact already exists on disk", async (t) => {
563
+ // plan-milestone's expected artifact is the ROADMAP, which the fixture
564
+ // already writes so verifyExpectedArtifact returns true. This is the legacy
565
+ // stuck-recovery scenario (unit completed on disk, DB row stale): instead of
566
+ // the finalized-repeat HARD-STOP, #442 verify-and-recover should refresh +
567
+ // skip so the loop can progress. plan-milestone is deliberately NOT one of
568
+ // the DB-refreshing unit types, so the recovery stays side-effect-light.
569
+ const f = makeFixture({
570
+ dispatch: () => ({ action: "dispatch", unitType: "plan-milestone", unitId: "M001", prompt: "p" }),
561
571
  });
562
- const orchestrator = createAutoOrchestrator(deps);
572
+ t.after(() => f.cleanup());
563
573
 
564
- const first = await orchestrator.advance();
574
+ const first = await f.orchestrator.advance();
575
+ if (first.kind !== "advanced") {
576
+ throw new Error(`expected advanced, got ${first.kind}: ${(first as { reason?: string }).reason ?? ""}`);
577
+ }
578
+ await f.orchestrator.completeActiveUnit(first.unit);
579
+
580
+ const second = await f.orchestrator.advance();
581
+ assert.equal(second.kind, "skipped", "should recover via artifact verification, not hard-stop");
582
+ if (second.kind !== "skipped") throw new Error("expected skipped recovery");
583
+ assert.match(second.reason, /stuck-recovery/);
584
+ assert.ok(f.journalNames().includes("advance-skipped"));
585
+ });
586
+
587
+ test("completeActiveUnit allows a different next unit to advance", async (t) => {
588
+ let nextTaskId = "M001/S01/T01";
589
+ const f = makeFixture({
590
+ dispatch: () => ({ action: "dispatch", unitType: "execute-task", unitId: nextTaskId, prompt: "p" }),
591
+ });
592
+ t.after(() => f.cleanup());
593
+
594
+ const first = await f.orchestrator.advance();
565
595
  assert.equal(first.kind, "advanced");
566
596
  if (first.kind !== "advanced") throw new Error("expected first advance");
567
597
 
568
- await orchestrator.completeActiveUnit(first.unit);
569
- nextTaskId = "T02";
570
- const second = await orchestrator.advance();
598
+ await f.orchestrator.completeActiveUnit(first.unit);
599
+ nextTaskId = "M001/S01/T02";
600
+ const second = await f.orchestrator.advance();
571
601
 
572
602
  assert.equal(second.kind, "advanced");
573
603
  if (second.kind !== "advanced") throw new Error("expected second advance");
574
- assert.deepEqual(second.unit, { unitType: "execute-task", unitId: "T02" });
604
+ assert.deepEqual(second.unit, { unitType: "execute-task", unitId: "M001/S01/T02" });
575
605
  });
576
606
 
577
- test("completeActiveUnit guard survives an intervening advance and blocks X→Y→X re-dispatch", async () => {
578
- // Regression test for issue #415: lastFinalizedUnitKey was wiped on every advance(),
579
- // allowing completed units to be re-dispatched after any interleaving unit (X→Y→X).
580
- let nextTaskId = "T01";
581
- const { deps } = makeDeps({
582
- dispatch: {
583
- async decideNextUnit() {
584
- return { unitType: "execute-task", unitId: nextTaskId, reason: "ready", preconditions: [] };
585
- },
586
- },
607
+ test("completeActiveUnit guard survives an intervening advance and blocks X→Y→X re-dispatch (#415)", async (t) => {
608
+ let nextTaskId = "M001/S01/T01";
609
+ const f = makeFixture({
610
+ dispatch: () => ({ action: "dispatch", unitType: "execute-task", unitId: nextTaskId, prompt: "p" }),
587
611
  });
588
- const orchestrator = createAutoOrchestrator(deps);
612
+ t.after(() => f.cleanup());
589
613
 
590
- // Step 1: advance X (T01)
591
- const first = await orchestrator.advance();
614
+ const first = await f.orchestrator.advance();
592
615
  assert.equal(first.kind, "advanced");
593
616
  if (first.kind !== "advanced") throw new Error("expected first advance");
594
617
 
595
- // Step 2: complete X (T01) — sets lastFinalizedUnitKey = 'execute-task:T01'
596
- await orchestrator.completeActiveUnit(first.unit);
618
+ await f.orchestrator.completeActiveUnit(first.unit);
597
619
 
598
- // Step 3: advance Y (T02) — must NOT clear lastFinalizedUnitKey
599
- nextTaskId = "T02";
600
- const second = await orchestrator.advance();
620
+ nextTaskId = "M001/S01/T02";
621
+ const second = await f.orchestrator.advance();
601
622
  assert.equal(second.kind, "advanced");
602
623
  if (second.kind !== "advanced") throw new Error("expected second advance (T02)");
603
- assert.deepEqual(second.unit, { unitType: "execute-task", unitId: "T02" });
624
+ assert.deepEqual(second.unit, { unitType: "execute-task", unitId: "M001/S01/T02" });
604
625
 
605
- // Step 4: re-select X (T01) — must be blocked because T01 was finalized
606
- nextTaskId = "T01";
607
- const third = await orchestrator.advance();
626
+ nextTaskId = "M001/S01/T01";
627
+ const third = await f.orchestrator.advance();
608
628
  assert.equal(third.kind, "blocked");
609
629
  if (third.kind !== "blocked") throw new Error("expected X→Y→X re-dispatch to be blocked");
610
630
  assert.equal(third.action, "stop");
611
- assert.equal(third.reason, "state did not advance after finalized execute-task T01");
631
+ assert.equal(third.reason, "state did not advance after finalized execute-task M001/S01/T01");
612
632
  });
613
633
 
614
- test("retryActiveUnit clears in-flight idempotency without marking the unit finalized", async () => {
615
- const { deps, calls } = makeDeps();
616
- const orchestrator = createAutoOrchestrator(deps);
634
+ test("retryActiveUnit clears in-flight idempotency without marking the unit finalized", async (t) => {
635
+ const f = makeFixture();
636
+ t.after(() => f.cleanup());
617
637
 
618
- const first = await orchestrator.advance();
638
+ const first = await f.orchestrator.advance();
619
639
  assert.equal(first.kind, "advanced");
620
640
  if (first.kind !== "advanced") throw new Error("expected first advance");
621
641
 
622
- await orchestrator.retryActiveUnit(first.unit);
623
- const second = await orchestrator.advance();
642
+ await f.orchestrator.retryActiveUnit(first.unit);
643
+ const second = await f.orchestrator.advance();
624
644
 
625
645
  assert.equal(second.kind, "advanced");
626
646
  if (second.kind !== "advanced") throw new Error("expected retry advance");
627
647
  assert.deepEqual(second.unit, first.unit);
628
- assert.ok(calls.includes("journal:unit-retry"));
629
- const prepareCalls = calls.filter((c) => c === "worktree.prepare").length;
630
- assert.equal(prepareCalls, 2, "retry should intentionally redispatch the same unit");
648
+ assert.ok(f.journalNames().includes("unit-retry"));
631
649
  });
632
650
 
633
- test("retryActiveUnit clears finalized same-unit guard for post-hook retries", async () => {
634
- const { deps, calls } = makeDeps();
635
- const orchestrator = createAutoOrchestrator(deps);
651
+ test("retryActiveUnit clears finalized same-unit guard for post-hook retries", async (t) => {
652
+ const f = makeFixture();
653
+ t.after(() => f.cleanup());
636
654
 
637
- const first = await orchestrator.advance();
655
+ const first = await f.orchestrator.advance();
638
656
  assert.equal(first.kind, "advanced");
639
657
  if (first.kind !== "advanced") throw new Error("expected first advance");
640
658
 
641
- await orchestrator.completeActiveUnit(first.unit);
642
- await orchestrator.retryActiveUnit(first.unit);
643
- const second = await orchestrator.advance();
659
+ await f.orchestrator.completeActiveUnit(first.unit);
660
+ await f.orchestrator.retryActiveUnit(first.unit);
661
+ const second = await f.orchestrator.advance();
644
662
 
645
663
  assert.equal(second.kind, "advanced");
646
664
  if (second.kind !== "advanced") throw new Error("expected retry advance");
647
665
  assert.deepEqual(second.unit, first.unit);
648
- assert.ok(calls.includes("journal:unit-finalized"));
649
- assert.ok(calls.includes("journal:unit-retry"));
650
- const prepareCalls = calls.filter((c) => c === "worktree.prepare").length;
651
- assert.equal(prepareCalls, 2, "post-hook retry should redispatch the finalized unit");
652
- });
653
-
654
- test("resume() re-enters running phase", async () => {
655
- const { deps } = makeDeps();
656
- const orchestrator = createAutoOrchestrator(deps);
657
-
658
- const result = await orchestrator.resume();
659
-
660
- assert.equal(result.kind, "resumed");
661
- assert.equal(orchestrator.getStatus().phase, "running");
666
+ const names = f.journalNames();
667
+ assert.ok(names.includes("unit-finalized"));
668
+ assert.ok(names.includes("unit-retry"));
662
669
  });
663
670
 
664
- test("resume() clears idempotent lock and allows re-advance", async () => {
665
- const { deps } = makeDeps();
666
- const orchestrator = createAutoOrchestrator(deps);
671
+ test("resume() clears idempotent lock and allows re-advance", async (t) => {
672
+ const f = makeFixture();
673
+ t.after(() => f.cleanup());
667
674
 
668
- const first = await orchestrator.advance();
669
- const blocked = await orchestrator.advance();
670
- const resumed = await orchestrator.resume();
671
- const next = await orchestrator.advance();
675
+ const first = await f.orchestrator.advance();
676
+ const idempotent = await f.orchestrator.advance();
677
+ const resumed = await f.orchestrator.resume();
678
+ const next = await f.orchestrator.advance();
672
679
 
673
680
  assert.equal(first.kind, "advanced");
674
- assert.equal(blocked.kind, "blocked");
681
+ assert.equal(idempotent.kind, "skipped");
675
682
  assert.equal(resumed.kind, "resumed");
676
683
  assert.equal(next.kind, "advanced");
677
684
  });
678
685
 
679
- test("transitionCount increases across lifecycle transitions", async () => {
680
- const { deps } = makeDeps();
681
- const orchestrator = createAutoOrchestrator(deps);
682
-
683
- const before = orchestrator.getStatus().transitionCount;
684
- await orchestrator.start({ basePath: "/tmp/project", trigger: "manual" });
685
- const afterStart = orchestrator.getStatus().transitionCount;
686
- await orchestrator.stop("done");
687
- const afterStop = orchestrator.getStatus().transitionCount;
688
-
689
- assert.ok(afterStart > before);
690
- assert.ok(afterStop > afterStart);
691
- });
692
-
693
- test("stop() clears idempotent unit lock so advance can run again", async () => {
694
- const { deps } = makeDeps();
695
- const orchestrator = createAutoOrchestrator(deps);
696
-
697
- const first = await orchestrator.advance();
698
- const blocked = await orchestrator.advance();
699
- const stopped = await orchestrator.stop("reset");
700
- const second = await orchestrator.advance();
686
+ test("start() clears prior idempotent lock", async (t) => {
687
+ const f = makeFixture();
688
+ t.after(() => f.cleanup());
701
689
 
702
- assert.equal(first.kind, "advanced");
703
- assert.equal(blocked.kind, "blocked");
704
- assert.equal(stopped.kind, "stopped");
705
- assert.equal(second.kind, "advanced");
706
- });
690
+ await f.orchestrator.advance();
691
+ const idempotent = await f.orchestrator.advance();
692
+ const restarted = await f.orchestrator.start(SESSION_CONTEXT);
693
+ const next = await f.orchestrator.advance();
707
694
 
708
- test("advance() stopped clears previous activeUnit", async () => {
709
- let first = true;
710
- const { deps } = makeDeps({
711
- dispatch: {
712
- async decideNextUnit() {
713
- if (first) {
714
- first = false;
715
- return { unitType: "execute-task", unitId: "T01", reason: "ready", preconditions: [] };
716
- }
717
- return null;
718
- },
719
- },
720
- });
721
- const orchestrator = createAutoOrchestrator(deps);
722
-
723
- await orchestrator.advance();
724
- const stopped = await orchestrator.advance();
725
-
726
- assert.equal(stopped.kind, "stopped");
727
- assert.equal(orchestrator.getStatus().activeUnit, undefined);
728
- });
729
-
730
- test("recovery stop clears activeUnit", async () => {
731
- const { deps, calls } = makeDeps({
732
- runtime: {
733
- async ensureLockOwnership() { throw new Error("boom"); },
734
- async journalTransition(event) { calls.push(`journal:${event.name}`); },
735
- },
736
- recovery: {
737
- async classifyAndRecover() { return { action: "stop", reason: "fatal" }; },
738
- },
739
- });
740
- const orchestrator = createAutoOrchestrator(deps);
741
-
742
- const result = await orchestrator.advance();
743
-
744
- assert.equal(result.kind, "stopped");
745
- assert.equal(orchestrator.getStatus().activeUnit, undefined);
746
- assert.ok(calls.includes("journal:advance-stopped"));
747
- assert.ok(calls.includes("notify:stopped"));
748
- assert.ok(!calls.includes("notify:error"));
749
- });
750
-
751
- test("recovery retry maps to paused result", async () => {
752
- const { deps, calls } = makeDeps({
753
- runtime: {
754
- async ensureLockOwnership() { throw new Error("boom"); },
755
- async journalTransition(event) { calls.push(`journal:${event.name}`); },
756
- },
757
- recovery: {
758
- async classifyAndRecover() { return { action: "retry", reason: "transient" }; },
759
- },
760
- });
761
- const orchestrator = createAutoOrchestrator(deps);
762
-
763
- const result = await orchestrator.advance();
764
-
765
- assert.equal(result.kind, "paused");
766
- assert.equal(result.reason, "transient");
767
- assert.equal(orchestrator.getStatus().phase, "paused");
768
- assert.ok(calls.includes("journal:advance-paused"));
769
- assert.ok(calls.includes("notify:pause"));
770
- });
771
-
772
- test("getStatus() returns defensive copy of activeUnit", async () => {
773
- const { deps } = makeDeps();
774
- const orchestrator = createAutoOrchestrator(deps);
775
-
776
- await orchestrator.advance();
777
- const snap1 = orchestrator.getStatus();
778
- if (snap1.activeUnit) snap1.activeUnit.unitId = "MUTATED";
779
- const snap2 = orchestrator.getStatus();
780
-
781
- assert.equal(snap2.activeUnit?.unitId, "T01");
782
- });
783
-
784
- test("start() clears prior idempotent lock", async () => {
785
- const { deps } = makeDeps();
786
- const orchestrator = createAutoOrchestrator(deps);
787
-
788
- await orchestrator.advance();
789
- const blocked = await orchestrator.advance();
790
- const restarted = await orchestrator.start({ basePath: "/tmp/project", trigger: "manual" });
791
- const next = await orchestrator.advance();
792
-
793
- assert.equal(blocked.kind, "blocked");
695
+ assert.equal(idempotent.kind, "skipped");
794
696
  assert.equal(restarted.kind, "started");
795
697
  assert.equal(next.kind, "advanced");
796
698
  });
797
699
 
798
- test("error path emits error notification", async () => {
799
- const { deps, calls } = makeDeps({
800
- runtime: {
801
- async ensureLockOwnership() { throw new Error("boom"); },
802
- async journalTransition(event) { calls.push(`journal:${event.name}`); },
803
- },
804
- recovery: {
805
- async classifyAndRecover() { return { action: "escalate", reason: "needs manual" }; },
806
- },
807
- });
808
- const orchestrator = createAutoOrchestrator(deps);
809
-
810
- await orchestrator.advance();
811
-
812
- assert.ok(calls.includes("notify:error"));
813
- });
814
-
815
- test("blocked path journals advance-blocked", async () => {
816
- const { deps, calls } = makeDeps();
817
- const orchestrator = createAutoOrchestrator(deps);
818
-
819
- await orchestrator.advance();
820
- await orchestrator.advance();
821
-
822
- assert.ok(calls.includes("journal:advance-blocked"));
823
- });
824
-
825
- test("health post hook runs on blocked result", async () => {
826
- const { deps, calls } = makeDeps();
827
- const orchestrator = createAutoOrchestrator(deps);
700
+ test("stop() clears idempotent unit lock so advance can run again", async (t) => {
701
+ const f = makeFixture();
702
+ t.after(() => f.cleanup());
828
703
 
829
- await orchestrator.advance();
830
- await orchestrator.advance();
831
-
832
- assert.ok(calls.includes("health.post"));
833
- });
834
-
835
- test("start() emits start notification", async () => {
836
- const { deps, calls } = makeDeps();
837
- const orchestrator = createAutoOrchestrator(deps);
838
-
839
- await orchestrator.start({ basePath: "/tmp/project", trigger: "manual" });
840
-
841
- assert.ok(calls.includes("notify:start"));
842
- });
843
-
844
- test("resume() emits resume notification", async () => {
845
- const { deps, calls } = makeDeps();
846
- const orchestrator = createAutoOrchestrator(deps);
847
-
848
- await orchestrator.resume();
849
-
850
- assert.ok(calls.includes("notify:resume"));
851
- });
852
-
853
- test("stopped with no remaining units clears idempotent lock for next advance", async () => {
854
- let callCount = 0;
855
- const { deps } = makeDeps({
856
- dispatch: {
857
- async decideNextUnit() {
858
- callCount += 1;
859
- if (callCount === 2) return null;
860
- return { unitType: "execute-task", unitId: "T01", reason: "ready", preconditions: [] };
861
- },
862
- },
863
- });
864
- const orchestrator = createAutoOrchestrator(deps);
865
-
866
- const first = await orchestrator.advance();
867
- const stopped = await orchestrator.advance();
868
- const after = await orchestrator.advance();
704
+ const first = await f.orchestrator.advance();
705
+ const idempotent = await f.orchestrator.advance();
706
+ const stopped = await f.orchestrator.stop("reset");
707
+ const second = await f.orchestrator.advance();
869
708
 
870
709
  assert.equal(first.kind, "advanced");
710
+ assert.equal(idempotent.kind, "skipped");
871
711
  assert.equal(stopped.kind, "stopped");
872
- assert.equal(after.kind, "advanced");
712
+ assert.equal(second.kind, "advanced");
873
713
  });
874
714
 
875
- test("stop() cleans up worktree and transitions to stopped", async () => {
876
- const { deps, calls } = makeDeps();
877
- const orchestrator = createAutoOrchestrator(deps);
715
+ test("idempotent path journals advance-skipped and records a health snapshot", async (t) => {
716
+ const f = makeFixture();
717
+ t.after(() => f.cleanup());
878
718
 
879
- const result = await orchestrator.stop("user-request");
719
+ await f.orchestrator.advance();
720
+ await f.orchestrator.advance();
880
721
 
881
- assert.equal(result.kind, "stopped");
882
- assert.equal(orchestrator.getStatus().phase, "stopped");
883
- assert.ok(calls.includes("worktree.cleanup"));
884
- assert.ok(calls.includes("journal:stop"));
885
- assert.ok(calls.includes("notify:stop"));
722
+ assert.ok(f.journalNames().includes("advance-skipped"));
886
723
  });
887
724
 
888
- // ────────────────────────────────────────────────────────────────────────
889
- // Stuck-loop ring buffer (issue #5787)
890
- // ────────────────────────────────────────────────────────────────────────
891
-
892
- test("STUCK_WINDOW_SIZE matches the legacy auto/phases.ts constant", () => {
893
- assert.equal(STUCK_WINDOW_SIZE, 6);
894
- });
725
+ // ─── Stuck-loop ring buffer (issue #5787) ──────────────────────────────────
895
726
 
896
- test("stuck-loop: empty ring on a freshly constructed orchestrator advances normally", async () => {
897
- const { deps } = makeDeps();
898
- const orchestrator = createAutoOrchestrator(deps);
727
+ test("stuck-loop: empty ring on a freshly constructed orchestrator advances normally", async (t) => {
728
+ const f = makeFixture();
729
+ t.after(() => f.cleanup());
899
730
 
900
- const result = await orchestrator.advance();
731
+ const result = await f.orchestrator.advance();
901
732
 
902
733
  assert.equal(result.kind, "advanced");
903
734
  });
904
735
 
905
- test("stuck-loop: partial fill of mixed units does not block", async () => {
906
- // Alternate A/B for STUCK_WINDOW_SIZE rounds. No single key saturates the
907
- // window, so neither idempotency nor stuck-loop should fire.
736
+ test("stuck-loop: partial fill of mixed units does not block", async (t) => {
908
737
  let i = 0;
909
- const sequence = ["A", "B", "A", "B", "A", "B"];
910
- const { deps } = makeDeps({
911
- dispatch: {
912
- async decideNextUnit() {
913
- const id = sequence[i++ % sequence.length];
914
- return { unitType: "execute-task", unitId: id, reason: "ready", preconditions: [] };
915
- },
916
- },
738
+ const sequence = ["M001/S01/A", "M001/S01/B", "M001/S01/A", "M001/S01/B", "M001/S01/A", "M001/S01/B"];
739
+ const f = makeFixture({
740
+ dispatch: () => ({ action: "dispatch", unitType: "execute-task", unitId: sequence[i++ % sequence.length], prompt: "p" }),
917
741
  });
918
- const orchestrator = createAutoOrchestrator(deps);
742
+ t.after(() => f.cleanup());
919
743
 
920
744
  for (let round = 0; round < STUCK_WINDOW_SIZE; round++) {
921
- const result = await orchestrator.advance();
745
+ const result = await f.orchestrator.advance();
922
746
  assert.equal(result.kind, "advanced", `round ${round} should advance, got ${result.kind}`);
923
747
  }
924
748
  });
925
749
 
926
- test("stuck-loop: ring saturated with same unit blocks with action 'stop' and stuck-loop reason", async () => {
927
- // Dispatch picks the same unit every time. The first advance succeeds.
928
- // Calls 2..STUCK_WINDOW_SIZE-1 are idempotency-blocked while the ring fills.
929
- // The STUCK_WINDOW_SIZE'th call sees a saturated ring and returns stuck-loop.
930
- const { deps } = makeDeps();
931
- const orchestrator = createAutoOrchestrator(deps);
750
+ test("stuck-loop: ring saturated with same unit blocks with action 'stop' and stuck-loop reason", async (t) => {
751
+ const f = makeFixture();
752
+ t.after(() => f.cleanup());
932
753
 
933
- const results: Awaited<ReturnType<typeof orchestrator.advance>>[] = [];
754
+ const results: Awaited<ReturnType<typeof f.orchestrator.advance>>[] = [];
934
755
  for (let i = 0; i < STUCK_WINDOW_SIZE; i++) {
935
- results.push(await orchestrator.advance());
756
+ results.push(await f.orchestrator.advance());
936
757
  }
937
758
 
938
759
  // First call advances.
939
760
  assert.equal(results[0].kind, "advanced");
940
761
 
941
- // Intermediate calls are blocked by idempotency (not stuck-loop yet).
762
+ // Intermediate calls are skipped by idempotency (not stuck-loop yet).
942
763
  for (let i = 1; i < STUCK_WINDOW_SIZE - 1; i++) {
943
764
  const r = results[i];
944
- assert.equal(r.kind, "blocked", `round ${i} should be blocked`);
945
- if (r.kind !== "blocked") return;
765
+ assert.equal(r.kind, "skipped", `round ${i} should be skipped`);
766
+ if (r.kind !== "skipped") return;
946
767
  assert.equal(r.reason, "idempotent advance: unit already active");
947
- assert.equal(r.action, "pause");
948
768
  }
949
769
 
950
- // The final call (ring now holds STUCK_WINDOW_SIZE copies) returns stuck-loop.
770
+ // The final call (ring now holds STUCK_WINDOW_SIZE copies) returns stuck-loop
771
+ // with the detect-stuck rule verdict in the reason.
951
772
  const last = results[STUCK_WINDOW_SIZE - 1];
952
773
  assert.equal(last.kind, "blocked");
953
774
  if (last.kind !== "blocked") return;
954
775
  assert.equal(last.action, "stop");
955
- assert.equal(last.reason, `stuck-loop: execute-task:T01 picked ${STUCK_WINDOW_SIZE} times`);
776
+ assert.ok(
777
+ last.reason.startsWith("stuck-loop: execute-task:M001/S01/T01 derived"),
778
+ `expected detect-stuck verdict reason, got: ${last.reason}`,
779
+ );
956
780
  });
957
781
 
958
- test("stuck-loop: idempotency block continues to fire with its own reason before saturation", async () => {
959
- // Two identical calls should produce idempotent (not stuck-loop). Ensures the
960
- // existing idempotency block is not absorbed by the new check.
961
- const { deps } = makeDeps();
962
- const orchestrator = createAutoOrchestrator(deps);
782
+ test("stuck-loop: start() resets the ring so a fresh saturation cycle is required", async (t) => {
783
+ const f = makeFixture();
784
+ t.after(() => f.cleanup());
785
+
786
+ for (let i = 0; i < STUCK_WINDOW_SIZE - 1; i++) {
787
+ await f.orchestrator.advance();
788
+ }
963
789
 
964
- const first = await orchestrator.advance();
965
- const second = await orchestrator.advance();
790
+ const restarted = await f.orchestrator.start(SESSION_CONTEXT);
791
+ assert.equal(restarted.kind, "started");
966
792
 
967
- assert.equal(first.kind, "advanced");
968
- assert.equal(second.kind, "blocked");
969
- assert.equal(second.reason, "idempotent advance: unit already active");
970
- assert.equal(second.action, "pause");
793
+ const next = await f.orchestrator.advance();
794
+ assert.equal(next.kind, "advanced");
971
795
  });
972
796
 
973
- test("stuck-loop: start() resets the ring so a fresh saturation cycle is required", async () => {
974
- // Fill the ring to one short of saturation, then start() — the ring should
975
- // be cleared, and the next advance must succeed instead of going stuck.
976
- const { deps } = makeDeps();
977
- const orchestrator = createAutoOrchestrator(deps);
797
+ test("stuck-loop: resume() preserves ring so detection accumulates across pause/resume", async (t) => {
798
+ // Regression for #572: resume() must NOT reset dispatchKeyWindow. Before the
799
+ // fix, a pause/resume cycle cleared the window, letting a stuck loop silently
800
+ // re-accumulate STUCK_WINDOW_SIZE dispatches before being detected again.
801
+ const f = makeFixture();
802
+ t.after(() => f.cleanup());
978
803
 
979
804
  for (let i = 0; i < STUCK_WINDOW_SIZE - 1; i++) {
980
- await orchestrator.advance();
805
+ await f.orchestrator.advance();
981
806
  }
982
807
 
983
- const restarted = await orchestrator.start({ basePath: "/tmp/project", trigger: "manual" });
984
- assert.equal(restarted.kind, "started");
808
+ const resumed = await f.orchestrator.resume();
809
+ assert.equal(resumed.kind, "resumed");
985
810
 
986
- // Immediately after start(), the next advance should succeed because start()
987
- // no longer pre-dispatches and the ring was reset.
988
- const next = await orchestrator.advance();
989
- assert.equal(next.kind, "advanced");
811
+ // The ring is preserved, so the next advance pushes it to STUCK_WINDOW_SIZE
812
+ // and triggers stuck-loop detection not a fresh dispatch.
813
+ const next = await f.orchestrator.advance();
814
+ assert.equal(next.kind, "blocked");
815
+ if (next.kind !== "blocked") return;
816
+ assert.equal(next.action, "stop");
817
+ assert.ok(next.reason.startsWith("stuck-loop:"), `expected stuck-loop reason, got: ${next.reason}`);
990
818
  });
991
819
 
992
- test("stuck-loop: resume() resets the ring", async () => {
993
- const { deps } = makeDeps();
994
- const orchestrator = createAutoOrchestrator(deps);
820
+ test("stuck-loop: stop('pause') preserves ring across the stop/resume cycle", async (t) => {
821
+ // Regression for #572: stop("pause") must behave the same as resume()
822
+ // the window must survive so detection accumulates across pause/resume pairs.
823
+ const f = makeFixture();
824
+ t.after(() => f.cleanup());
995
825
 
996
826
  for (let i = 0; i < STUCK_WINDOW_SIZE - 1; i++) {
997
- await orchestrator.advance();
827
+ await f.orchestrator.advance();
998
828
  }
999
829
 
1000
- const resumed = await orchestrator.resume();
830
+ const stopped = await f.orchestrator.stop("pause");
831
+ assert.equal(stopped.kind, "stopped");
832
+
833
+ const resumed = await f.orchestrator.resume();
1001
834
  assert.equal(resumed.kind, "resumed");
1002
835
 
1003
- const next = await orchestrator.advance();
1004
- assert.equal(next.kind, "advanced");
836
+ const next = await f.orchestrator.advance();
837
+ assert.equal(next.kind, "blocked");
838
+ if (next.kind !== "blocked") return;
839
+ assert.equal(next.action, "stop");
840
+ assert.ok(next.reason.startsWith("stuck-loop:"), `expected stuck-loop reason, got: ${next.reason}`);
1005
841
  });
1006
842
 
1007
- test("stuck-loop: stop() resets the ring", async () => {
1008
- const { deps } = makeDeps();
1009
- const orchestrator = createAutoOrchestrator(deps);
843
+ test("stuck-loop: stop('user-request') resets the ring (hard stop)", async (t) => {
844
+ const f = makeFixture();
845
+ t.after(() => f.cleanup());
1010
846
 
1011
847
  for (let i = 0; i < STUCK_WINDOW_SIZE - 1; i++) {
1012
- await orchestrator.advance();
848
+ await f.orchestrator.advance();
1013
849
  }
1014
850
 
1015
- const stopped = await orchestrator.stop("user-request");
851
+ const stopped = await f.orchestrator.stop("user-request");
1016
852
  assert.equal(stopped.kind, "stopped");
1017
853
 
1018
- // Ring is cleared by stop(). A subsequent advance is a fresh first-touch.
1019
- const next = await orchestrator.advance();
854
+ // Hard stop clears the ring, so the next advance dispatches fresh.
855
+ const next = await f.orchestrator.advance();
1020
856
  assert.equal(next.kind, "advanced");
1021
857
  });
1022
858
 
1023
- test("stuck-loop: journal records the stuck-loop reason on advance-blocked", async () => {
1024
- const { deps, calls } = makeDeps();
1025
- const orchestrator = createAutoOrchestrator(deps);
859
+ test("stuck-loop #482 regression: start() rehydrates the window from the dispatch ledger so cross-session re-dispatch loops are detected", async (t) => {
860
+ const f = makeFixture();
861
+ t.after(() => f.cleanup());
862
+
863
+ // Simulate a PRIOR session: the dispatch ledger recorded the same unit
864
+ // being re-dispatched repeatedly without progress. The orchestrator under test is a
865
+ // fresh instance (as it would be after a session restart) — before the
866
+ // Dispatch History module, start() reset the window to [] and the loop
867
+ // would silently re-dispatch the unit forever (#482: 146 re-dispatches).
868
+ const worker = registerAutoWorker({ projectRootRealpath: normalizeRealPath(f.base) });
869
+ const lease = claimMilestoneLease(worker, "M001");
870
+ assert.equal(lease.ok, true);
871
+ if (!lease.ok) return;
872
+ for (let i = 0; i < STUCK_WINDOW_SIZE - 1; i++) {
873
+ const claim = recordDispatchClaim({
874
+ traceId: `prior-session-${i}`,
875
+ workerId: worker,
876
+ milestoneLeaseToken: lease.token,
877
+ milestoneId: "M001",
878
+ unitType: "execute-task",
879
+ unitId: "M001/S01/T01",
880
+ });
881
+ assert.equal(claim.ok, true);
882
+ if (!claim.ok) return;
883
+ markFailed(claim.dispatchId, { errorSummary: "" });
884
+ }
885
+
886
+ const started = await f.orchestrator.start(SESSION_CONTEXT);
887
+ assert.equal(started.kind, "started");
888
+
889
+ // The very next decision for the same unit must trip the stuck verdict
890
+ // instead of advancing.
891
+ const result = await f.orchestrator.advance();
892
+ assert.equal(result.kind, "blocked");
893
+ if (result.kind !== "blocked") return;
894
+ assert.equal(result.action, "stop");
895
+ assert.ok(result.reason.startsWith("stuck-loop:"), `expected stuck-loop reason, got: ${result.reason}`);
896
+ });
897
+
898
+ test("stuck-loop #482: resume() with an empty window rehydrates from the dispatch ledger", async (t) => {
899
+ const f = makeFixture();
900
+ t.after(() => f.cleanup());
901
+
902
+ const worker = registerAutoWorker({ projectRootRealpath: normalizeRealPath(f.base) });
903
+ const lease = claimMilestoneLease(worker, "M001");
904
+ assert.equal(lease.ok, true);
905
+ if (!lease.ok) return;
906
+ for (let i = 0; i < STUCK_WINDOW_SIZE - 1; i++) {
907
+ const claim = recordDispatchClaim({
908
+ traceId: `prior-session-resume-${i}`,
909
+ workerId: worker,
910
+ milestoneLeaseToken: lease.token,
911
+ milestoneId: "M001",
912
+ unitType: "execute-task",
913
+ unitId: "M001/S01/T01",
914
+ });
915
+ assert.equal(claim.ok, true);
916
+ if (!claim.ok) return;
917
+ markFailed(claim.dispatchId, { errorSummary: "" });
918
+ }
919
+
920
+ // Fresh orchestrator resuming a prior session: window starts empty, so
921
+ // resume() must rehydrate (while in-process resume keeps the live window —
922
+ // see the #572 preservation tests above).
923
+ const resumed = await f.orchestrator.resume();
924
+ assert.equal(resumed.kind, "resumed");
925
+
926
+ const result = await f.orchestrator.advance();
927
+ assert.equal(result.kind, "blocked");
928
+ if (result.kind !== "blocked") return;
929
+ assert.equal(result.action, "stop");
930
+ assert.ok(result.reason.startsWith("stuck-loop:"), `expected stuck-loop reason, got: ${result.reason}`);
931
+ });
932
+
933
+ test("stuck-loop: journal records the stuck-loop reason on advance-blocked", async (t) => {
934
+ const f = makeFixture();
935
+ t.after(() => f.cleanup());
1026
936
 
1027
937
  for (let i = 0; i < STUCK_WINDOW_SIZE; i++) {
1028
- await orchestrator.advance();
938
+ await f.orchestrator.advance();
1029
939
  }
1030
940
 
1031
- assert.ok(calls.includes("journal:advance-blocked"));
941
+ const stuckEntry = queryJournal(f.base).find(
942
+ (e) => {
943
+ const reason = (e.data as Record<string, unknown> | undefined)?.reason;
944
+ return typeof reason === "string" && reason.startsWith("stuck-loop:");
945
+ },
946
+ );
947
+ assert.ok(stuckEntry, "journal must record an advance-blocked entry with the stuck-loop reason");
948
+ assert.ok(f.journalNames().includes("advance-blocked"));
949
+ });
950
+
951
+ // ─────────────────────────────────────────────────────────────────────────────
952
+ // Recovery path: a lock held by another process throws inside advance() and is
953
+ // routed through the REAL classifyFailure → result mapping + notifications.
954
+ // We force the throw by acquiring the lock under a different PID (writing a
955
+ // foreign-PID lockfile is not portable, so we drive the deterministic-stop
956
+ // classification via a fixture whose runtimeBasePath has no valid lock).
957
+ // ─────────────────────────────────────────────────────────────────────────────
958
+
959
+ test("advance() routes a lost-lock error through recovery and journals an outcome", async (t) => {
960
+ const f = makeFixture();
961
+ t.after(() => f.cleanup());
962
+
963
+ // Release the lock so ensureLockOwnership() sees missing-metadata and throws,
964
+ // exercising the catch → classifyAndRecover → result-mapping branch.
965
+ releaseSessionLock(f.base);
966
+ // Remove the lockfile artifact so getSessionLockStatus returns !valid.
967
+ try { rmSync(join(f.base, ".gsd", "auto.lock"), { force: true }); } catch { /* */ }
968
+ try { rmSync(join(f.base, ".gsd.lock"), { recursive: true, force: true }); } catch { /* */ }
969
+
970
+ const result = await f.orchestrator.advance();
971
+
972
+ // classifyFailure maps a generic Error to a recovery action; the orchestrator
973
+ // surfaces it as paused/stopped/error and journals the corresponding event.
974
+ assert.ok(["paused", "stopped", "error"].includes(result.kind), `unexpected kind ${result.kind}`);
975
+ const names = f.journalNames();
976
+ assert.ok(
977
+ names.includes("advance-paused") || names.includes("advance-stopped") || names.includes("advance-error"),
978
+ "recovery must journal an advance-paused/stopped/error event",
979
+ );
1032
980
  });
1033
981
 
1034
- // ─── closeout regression: wired orchestrator must not dispatch from a removed worktree ───
982
+ // ─────────────────────────────────────────────────────────────────────────────
983
+ // closeout regression: live-base resolver after worktree cleanup
984
+ // ─────────────────────────────────────────────────────────────────────────────
1035
985
 
1036
- test("wired orchestrator base resolver prefers live project root after worktree cleanup", (t) => {
986
+ test("live orchestrator base resolver prefers live project root after worktree cleanup", (t) => {
1037
987
  const projectRoot = mkdtempSync(join(tmpdir(), "gsd-orch-root-"));
1038
988
  const staleWorktreeRoot = join(projectRoot, ".gsd", "worktrees", "M002");
1039
989
  mkdirSync(join(staleWorktreeRoot, ".bg-shell"), { recursive: true });
@@ -1050,7 +1000,7 @@ test("wired orchestrator base resolver prefers live project root after worktree
1050
1000
  );
1051
1001
  });
1052
1002
 
1053
- test("wired orchestrator base resolver keeps a captured active git worktree", (t) => {
1003
+ test("live orchestrator base resolver keeps a captured active git worktree", (t) => {
1054
1004
  const projectRoot = mkdtempSync(join(tmpdir(), "gsd-orch-worktree-"));
1055
1005
  const worktreeRoot = join(projectRoot, ".gsd", "worktrees", "M003");
1056
1006
  mkdirSync(worktreeRoot, { recursive: true });
@@ -1066,14 +1016,14 @@ test("wired orchestrator base resolver keeps a captured active git worktree", (t
1066
1016
  );
1067
1017
  });
1068
1018
 
1069
- // ─── #5789 parity: wired dispatch adapter mirrors runDispatch's resolveDispatch call ───
1019
+ // ─────────────────────────────────────────────────────────────────────────────
1020
+ // Dispatch-decision parity (#5789) — formerly the createWiredDispatchAdapter
1021
+ // tests. These exercise the exported pure decideOrchestratorDispatch helper.
1022
+ // ─────────────────────────────────────────────────────────────────────────────
1070
1023
 
1071
- test("wired DispatchAdapter forwards session-derived dispatch inputs identically to runDispatch", async () => {
1024
+ test("decideOrchestratorDispatch forwards session-derived dispatch inputs identically to runDispatch", async () => {
1072
1025
  const stateSnapshot = makeState();
1073
1026
 
1074
- // Install a capturing registry so we observe the DispatchContext both code paths
1075
- // build, and force a deterministic dispatch action so the parity assertion is
1076
- // about *inputs*, not rule evaluation.
1077
1027
  const captured: DispatchContext[] = [];
1078
1028
  const captureRule: UnifiedRule = {
1079
1029
  name: "test-capture",
@@ -1093,7 +1043,6 @@ test("wired DispatchAdapter forwards session-derived dispatch inputs identically
1093
1043
  setRegistry(new RuleRegistry([captureRule]));
1094
1044
 
1095
1045
  try {
1096
- // Mock ExtensionContext + ExtensionAPI with the surface the wired adapter touches.
1097
1046
  const fakeModelRegistry = {
1098
1047
  getAll: () => [],
1099
1048
  getProviderAuthMode: (_provider: string) => "apiKey" as const,
@@ -1105,30 +1054,28 @@ test("wired DispatchAdapter forwards session-derived dispatch inputs identically
1105
1054
  contextWindow: 200_000,
1106
1055
  },
1107
1056
  modelRegistry: fakeModelRegistry,
1108
- } as any;
1057
+ } as never;
1109
1058
  const pi = {
1110
1059
  getActiveTools: () => ["read_file", "write_file"],
1111
- } as any;
1060
+ } as never;
1112
1061
  const basePath = "/tmp/parity-fixture";
1113
1062
 
1114
- // Path A — wired adapter (what createWiredAutoOrchestrationModule uses).
1115
- const adapter = createWiredDispatchAdapter(ctx, pi, basePath);
1116
- const adapterResult = await adapter.decideNextUnit({ stateSnapshot });
1063
+ // Path A — the orchestrator's pure dispatch decision.
1064
+ const adapterResult = await decideOrchestratorDispatch(ctx, pi, basePath, undefined, { stateSnapshot });
1117
1065
 
1118
1066
  // Path B — direct resolveDispatch call mirroring phases.ts:runDispatch.
1119
- // Inline the same derivations runDispatch uses so any drift here is a parity break.
1120
- const prefs = undefined; // loadEffectiveGSDPreferences returns null for /tmp/parity-fixture.
1121
- const provider = ctx.model?.provider;
1122
- const authMode = provider && typeof ctx.modelRegistry?.getProviderAuthMode === "function"
1123
- ? ctx.modelRegistry.getProviderAuthMode(provider)
1067
+ const prefs = undefined;
1068
+ const provider = (ctx as { model?: { provider?: string } }).model?.provider;
1069
+ const authMode = provider && typeof fakeModelRegistry.getProviderAuthMode === "function"
1070
+ ? fakeModelRegistry.getProviderAuthMode(provider)
1124
1071
  : undefined;
1125
- const activeTools = typeof pi.getActiveTools === "function" ? pi.getActiveTools() : [];
1072
+ const activeTools = ["read_file", "write_file"];
1126
1073
  const structuredQuestionsAvailable: "true" | "false" =
1127
1074
  prefs !== undefined && (prefs as { planning_depth?: string }).planning_depth === "deep"
1128
1075
  ? "false"
1129
1076
  : supportsStructuredQuestions(activeTools, {
1130
1077
  authMode,
1131
- baseUrl: ctx.model?.baseUrl,
1078
+ baseUrl: (ctx as { model?: { baseUrl?: string } }).model?.baseUrl,
1132
1079
  })
1133
1080
  ? "true"
1134
1081
  : "false";
@@ -1140,17 +1087,15 @@ test("wired DispatchAdapter forwards session-derived dispatch inputs identically
1140
1087
  state: stateSnapshot,
1141
1088
  prefs,
1142
1089
  structuredQuestionsAvailable,
1143
- sessionContextWindow: ctx.model?.contextWindow,
1144
- sessionProvider: ctx.model?.provider,
1145
- modelRegistry: ctx.modelRegistry,
1090
+ sessionContextWindow: 200_000,
1091
+ sessionProvider: "anthropic",
1092
+ modelRegistry: fakeModelRegistry,
1146
1093
  };
1147
1094
  const directAction = await resolveDispatch(builtDirectCtx);
1148
1095
 
1149
- // Two contexts captured: one per resolveDispatch call.
1150
1096
  assert.equal(captured.length, 2, "expected two captured dispatch contexts");
1151
1097
  const [adapterCtx, directCtx] = captured;
1152
1098
 
1153
- // Parity assertion: session-derived fields are identical.
1154
1099
  assert.equal(adapterCtx.structuredQuestionsAvailable, directCtx.structuredQuestionsAvailable);
1155
1100
  assert.equal(adapterCtx.sessionContextWindow, directCtx.sessionContextWindow);
1156
1101
  assert.equal(adapterCtx.sessionProvider, directCtx.sessionProvider);
@@ -1159,7 +1104,6 @@ test("wired DispatchAdapter forwards session-derived dispatch inputs identically
1159
1104
  assert.equal(adapterCtx.mid, directCtx.mid);
1160
1105
  assert.equal(adapterCtx.midTitle, directCtx.midTitle);
1161
1106
 
1162
- // Dispatch action equality: both flows reach the same dispatch decision.
1163
1107
  if (!adapterResult || !("unitType" in adapterResult)) {
1164
1108
  assert.fail("expected adapter result to be a dispatch decision");
1165
1109
  }
@@ -1177,7 +1121,7 @@ test("wired DispatchAdapter forwards session-derived dispatch inputs identically
1177
1121
  }
1178
1122
  });
1179
1123
 
1180
- test("wired DispatchAdapter prefers caller-supplied dispatch inputs over ctx-derived values", async () => {
1124
+ test("decideOrchestratorDispatch prefers caller-supplied dispatch inputs over ctx-derived values", async () => {
1181
1125
  const stateSnapshot = makeState();
1182
1126
  const captured: DispatchContext[] = [];
1183
1127
  const captureRule: UnifiedRule = {
@@ -1213,14 +1157,11 @@ test("wired DispatchAdapter prefers caller-supplied dispatch inputs over ctx-der
1213
1157
  contextWindow: 200_000,
1214
1158
  },
1215
1159
  modelRegistry: ctxModelRegistry,
1216
- } as any;
1217
- const pi = {
1218
- getActiveTools: () => [],
1219
- } as any;
1220
- const adapter = createWiredDispatchAdapter(ctx, pi, "/tmp/parity-fixture");
1221
- const session = { basePath: "/tmp/session-fixture" } as any;
1160
+ } as never;
1161
+ const pi = { getActiveTools: () => [] } as never;
1162
+ const session = { basePath: "/tmp/session-fixture" } as never;
1222
1163
 
1223
- const result = await adapter.decideNextUnit({
1164
+ const result = await decideOrchestratorDispatch(ctx, pi, "/tmp/parity-fixture", undefined, {
1224
1165
  stateSnapshot,
1225
1166
  session,
1226
1167
  structuredQuestionsAvailable: "true",
@@ -1242,7 +1183,7 @@ test("wired DispatchAdapter prefers caller-supplied dispatch inputs over ctx-der
1242
1183
  }
1243
1184
  });
1244
1185
 
1245
- test("wired DispatchAdapter forwards constructor session when advance input omits session", async () => {
1186
+ test("decideOrchestratorDispatch forwards constructor session when advance input omits session", async () => {
1246
1187
  const stateSnapshot = makeState();
1247
1188
  const captured: DispatchContext[] = [];
1248
1189
  const captureRule: UnifiedRule = {
@@ -1263,16 +1204,15 @@ test("wired DispatchAdapter forwards constructor session when advance input omit
1263
1204
  setRegistry(new RuleRegistry([captureRule]));
1264
1205
 
1265
1206
  try {
1266
- const ctx = { model: {}, modelRegistry: { getAll: () => [] } } as any;
1267
- const pi = { getActiveTools: () => [] } as any;
1207
+ const ctx = { model: {}, modelRegistry: { getAll: () => [] } } as never;
1208
+ const pi = { getActiveTools: () => [] } as never;
1268
1209
  const session = {
1269
1210
  basePath: "/tmp/worktree-fixture",
1270
1211
  originalBasePath: "/tmp/project-fixture",
1271
1212
  currentMilestoneId: "M001",
1272
- } as any;
1273
- const adapter = createWiredDispatchAdapter(ctx, pi, "/tmp/project-fixture", session);
1213
+ } as never;
1274
1214
 
1275
- const result = await adapter.decideNextUnit({ stateSnapshot });
1215
+ const result = await decideOrchestratorDispatch(ctx, pi, "/tmp/project-fixture", session, { stateSnapshot });
1276
1216
 
1277
1217
  assert.ok(result);
1278
1218
  assert.equal(captured.length, 1, "expected one captured dispatch context");
@@ -1283,7 +1223,104 @@ test("wired DispatchAdapter forwards constructor session when advance input omit
1283
1223
  }
1284
1224
  });
1285
1225
 
1286
- test("wired DispatchAdapter adopts next active milestone after the session milestone is closed", async (t) => {
1226
+ test("decideOrchestratorDispatch evaluates deep pre-planning rules without an active milestone", async (t) => {
1227
+ const base = mkdtempSync(join(tmpdir(), "gsd-orchestrator-no-active-"));
1228
+ t.after(() => {
1229
+ resetRegistry();
1230
+ rmSync(base, { recursive: true, force: true });
1231
+ });
1232
+ resetRegistry();
1233
+ mkdirSync(join(base, ".gsd"), { recursive: true });
1234
+ writeFileSync(
1235
+ join(base, ".gsd", "PREFERENCES.md"),
1236
+ [
1237
+ "---",
1238
+ "planning_depth: deep",
1239
+ "workflow_prefs_captured: true",
1240
+ "---",
1241
+ "",
1242
+ ].join("\n"),
1243
+ );
1244
+
1245
+ const stateSnapshot: GSDState = {
1246
+ ...makeState(),
1247
+ activeMilestone: null,
1248
+ phase: "pre-planning",
1249
+ nextAction: "All remaining milestones are parked (M027). Run /gsd unpark M027 or create a new milestone.",
1250
+ registry: [{ id: "M027", title: "Parked", status: "parked" }],
1251
+ };
1252
+ const ctx = { model: {}, modelRegistry: { getAll: () => [] } } as never;
1253
+ const pi = { getActiveTools: () => [] } as never;
1254
+ const session = {
1255
+ basePath: base,
1256
+ originalBasePath: base,
1257
+ currentMilestoneId: "M027",
1258
+ } as never;
1259
+
1260
+ const result = await decideOrchestratorDispatch(ctx, pi, base, session, { stateSnapshot });
1261
+
1262
+ assert.ok(result && "unitType" in result, `expected project-level dispatch, got ${JSON.stringify(result)}`);
1263
+ assert.equal(result.unitType, "discuss-project");
1264
+ assert.equal(result.unitId, "PROJECT");
1265
+ });
1266
+
1267
+ test("decideOrchestratorDispatch does not replay milestone-scoped verification retry when no milestone is active", async (t) => {
1268
+ const base = mkdtempSync(join(tmpdir(), "gsd-orchestrator-no-active-retry-"));
1269
+ t.after(() => {
1270
+ resetRegistry();
1271
+ rmSync(base, { recursive: true, force: true });
1272
+ });
1273
+ resetRegistry();
1274
+ mkdirSync(join(base, ".gsd"), { recursive: true });
1275
+ writeFileSync(
1276
+ join(base, ".gsd", "PREFERENCES.md"),
1277
+ [
1278
+ "---",
1279
+ "planning_depth: deep",
1280
+ "workflow_prefs_captured: true",
1281
+ "---",
1282
+ "",
1283
+ ].join("\n"),
1284
+ );
1285
+
1286
+ const stateSnapshot: GSDState = {
1287
+ ...makeState(),
1288
+ activeMilestone: null,
1289
+ phase: "pre-planning",
1290
+ nextAction: "All remaining milestones are parked (M027). Run /gsd unpark M027 or create a new milestone.",
1291
+ registry: [{ id: "M027", title: "Parked", status: "parked" }],
1292
+ };
1293
+ const ctx = { model: {}, modelRegistry: { getAll: () => [] } } as never;
1294
+ const pi = { getActiveTools: () => [] } as never;
1295
+ const stalePendingRetry = {
1296
+ unitType: "execute-task",
1297
+ unitId: "M027.S1.T1",
1298
+ prompt: "stale retry prompt",
1299
+ pauseAfterUatDispatch: false,
1300
+ state: stateSnapshot,
1301
+ mid: "M027",
1302
+ midTitle: "Parked",
1303
+ };
1304
+ const session = {
1305
+ basePath: base,
1306
+ originalBasePath: base,
1307
+ currentMilestoneId: "M027",
1308
+ pendingVerificationRetryDispatch: stalePendingRetry,
1309
+ } as never;
1310
+
1311
+ const result = await decideOrchestratorDispatch(ctx, pi, base, session, { stateSnapshot });
1312
+
1313
+ assert.ok(result && "unitType" in result, `expected project-level dispatch, got ${JSON.stringify(result)}`);
1314
+ assert.equal(result.unitType, "discuss-project");
1315
+ assert.equal(result.unitId, "PROJECT");
1316
+ // The stale retry must be preserved for a future tick, not consumed by this
1317
+ // no-active-milestone path (mirrors pre-#712-fix behavior where !active
1318
+ // returned null before touching the retry).
1319
+ const sess = session as unknown as { pendingVerificationRetryDispatch: unknown };
1320
+ assert.equal(sess.pendingVerificationRetryDispatch, stalePendingRetry);
1321
+ });
1322
+
1323
+ test("decideOrchestratorDispatch adopts next active milestone after the session milestone is closed", async (t) => {
1287
1324
  const base = mkdtempSync(join(tmpdir(), "gsd-orchestrator-milestone-adopt-"));
1288
1325
  t.after(() => rmSync(base, { recursive: true, force: true }));
1289
1326
 
@@ -1314,28 +1351,27 @@ test("wired DispatchAdapter adopts next active milestone after the session miles
1314
1351
  setRegistry(new RuleRegistry([captureRule]));
1315
1352
 
1316
1353
  try {
1317
- const ctx = { model: {}, modelRegistry: { getAll: () => [] } } as any;
1318
- const pi = { getActiveTools: () => [] } as any;
1354
+ const ctx = { model: {}, modelRegistry: { getAll: () => [] } } as never;
1355
+ const pi = { getActiveTools: () => [] } as never;
1319
1356
  const session = {
1320
1357
  basePath: base,
1321
1358
  originalBasePath: base,
1322
1359
  currentMilestoneId: "M001",
1323
- } as any;
1324
- const adapter = createWiredDispatchAdapter(ctx, pi, base, session);
1360
+ } as never;
1325
1361
 
1326
- const result = await adapter.decideNextUnit({ stateSnapshot });
1362
+ const result = await decideOrchestratorDispatch(ctx, pi, base, session, { stateSnapshot });
1327
1363
 
1328
1364
  assert.ok(result);
1329
- if (!("unitType" in result)) assert.fail(`expected dispatch decision, got ${JSON.stringify(result)}`);
1365
+ if (!result || !("unitType" in result)) assert.fail(`expected dispatch decision, got ${JSON.stringify(result)}`);
1330
1366
  assert.equal(result.unitId, "M002/S01/T01");
1331
- assert.equal(session.currentMilestoneId, "M002");
1367
+ assert.equal((session as { currentMilestoneId: string }).currentMilestoneId, "M002");
1332
1368
  assert.equal(captured[0]?.session?.currentMilestoneId, "M002");
1333
1369
  } finally {
1334
1370
  resetRegistry();
1335
1371
  }
1336
1372
  });
1337
1373
 
1338
- test("wired DispatchAdapter keeps blocking stale milestone worktree scope", async (t) => {
1374
+ test("decideOrchestratorDispatch keeps blocking stale milestone worktree scope", async (t) => {
1339
1375
  const base = mkdtempSync(join(tmpdir(), "gsd-orchestrator-worktree-block-"));
1340
1376
  t.after(() => rmSync(base, { recursive: true, force: true }));
1341
1377
 
@@ -1349,16 +1385,15 @@ test("wired DispatchAdapter keeps blocking stale milestone worktree scope", asyn
1349
1385
  };
1350
1386
  const worktreePath = join(base, ".gsd", "worktrees", "M001");
1351
1387
  mkdirSync(worktreePath, { recursive: true });
1352
- const ctx = { model: {}, modelRegistry: { getAll: () => [] } } as any;
1353
- const pi = { getActiveTools: () => [] } as any;
1388
+ const ctx = { model: {}, modelRegistry: { getAll: () => [] } } as never;
1389
+ const pi = { getActiveTools: () => [] } as never;
1354
1390
  const session = {
1355
1391
  basePath: worktreePath,
1356
1392
  originalBasePath: base,
1357
1393
  currentMilestoneId: "M001",
1358
- } as any;
1359
- const adapter = createWiredDispatchAdapter(ctx, pi, base, session);
1394
+ } as never;
1360
1395
 
1361
- const result = await adapter.decideNextUnit({ stateSnapshot });
1396
+ const result = await decideOrchestratorDispatch(ctx, pi, base, session, { stateSnapshot });
1362
1397
 
1363
1398
  assert.deepEqual(result, {
1364
1399
  kind: "blocked",
@@ -1366,13 +1401,13 @@ test("wired DispatchAdapter keeps blocking stale milestone worktree scope", asyn
1366
1401
  'Dispatch milestone mismatch: context mid "M002" does not match session.currentMilestoneId "M001". The active worktree/session and derived project state disagree; recover, park, or discard the stranded milestone before continuing.',
1367
1402
  action: "pause",
1368
1403
  });
1369
- assert.equal(session.currentMilestoneId, "M001");
1404
+ assert.equal((session as { currentMilestoneId: string }).currentMilestoneId, "M001");
1370
1405
  });
1371
1406
 
1372
- test("wired DispatchAdapter replays pending verification retry dispatch", async () => {
1407
+ test("decideOrchestratorDispatch replays pending verification retry dispatch", async () => {
1373
1408
  const stateSnapshot = makeState();
1374
- const ctx = { model: {}, modelRegistry: { getAll: () => [] } } as any;
1375
- const pi = { getActiveTools: () => [] } as any;
1409
+ const ctx = { model: {}, modelRegistry: { getAll: () => [] } } as never;
1410
+ const pi = { getActiveTools: () => [] } as never;
1376
1411
  const session = {
1377
1412
  basePath: "/tmp/worktree-fixture",
1378
1413
  pendingOrchestrationDispatch: null,
@@ -1385,22 +1420,25 @@ test("wired DispatchAdapter replays pending verification retry dispatch", async
1385
1420
  mid: "M004",
1386
1421
  midTitle: "Milestone 4",
1387
1422
  },
1388
- } as any;
1389
- const adapter = createWiredDispatchAdapter(ctx, pi, "/tmp/project-fixture", session);
1423
+ } as never;
1390
1424
 
1391
- const result = await adapter.decideNextUnit({ stateSnapshot });
1425
+ const result = await decideOrchestratorDispatch(ctx, pi, "/tmp/project-fixture", session, { stateSnapshot });
1392
1426
 
1393
1427
  assert.ok(result);
1394
- if (!("unitType" in result)) assert.fail("expected dispatch decision");
1428
+ if (!result || !("unitType" in result)) assert.fail("expected dispatch decision");
1395
1429
  assert.equal(result.unitType, "complete-slice");
1396
1430
  assert.equal(result.unitId, "M004/S01");
1397
1431
  assert.equal(result.reason, "verification-retry");
1398
- assert.equal(session.pendingVerificationRetryDispatch, null);
1399
- assert.equal(session.pendingOrchestrationDispatch?.prompt, "repair slice closeout");
1400
- assert.equal(session.pendingOrchestrationDispatch?.state, stateSnapshot);
1432
+ const sess = session as {
1433
+ pendingVerificationRetryDispatch: unknown;
1434
+ pendingOrchestrationDispatch: { prompt?: string; state?: unknown } | null;
1435
+ };
1436
+ assert.equal(sess.pendingVerificationRetryDispatch, null);
1437
+ assert.equal(sess.pendingOrchestrationDispatch?.prompt, "repair slice closeout");
1438
+ assert.equal(sess.pendingOrchestrationDispatch?.state, stateSnapshot);
1401
1439
  });
1402
1440
 
1403
- test("wired DispatchAdapter clears verification retry state when skipping an already closed retry dispatch", async () => {
1441
+ test("decideOrchestratorDispatch clears verification retry state when skipping an already closed retry dispatch", async () => {
1404
1442
  const stateSnapshot = makeState();
1405
1443
  const base = mkdtempSync(join(tmpdir(), "gsd-orchestrator-closed-retry-"));
1406
1444
 
@@ -1425,8 +1463,8 @@ test("wired DispatchAdapter clears verification retry state when skipping an alr
1425
1463
  };
1426
1464
  setRegistry(new RuleRegistry([retryRule]));
1427
1465
 
1428
- const ctx = { model: {}, modelRegistry: { getAll: () => [] } } as any;
1429
- const pi = { getActiveTools: () => [] } as any;
1466
+ const ctx = { model: {}, modelRegistry: { getAll: () => [] } } as never;
1467
+ const pi = { getActiveTools: () => [] } as never;
1430
1468
  const session = {
1431
1469
  basePath: base,
1432
1470
  pendingOrchestrationDispatch: { stale: true },
@@ -1435,17 +1473,17 @@ test("wired DispatchAdapter clears verification retry state when skipping an alr
1435
1473
  failureContext: "artifact missing",
1436
1474
  attempt: 1,
1437
1475
  },
1438
- } as any;
1439
- const adapter = createWiredDispatchAdapter(ctx, pi, base, session);
1476
+ } as never;
1440
1477
 
1441
- const result = await adapter.decideNextUnit({ stateSnapshot });
1478
+ const result = await decideOrchestratorDispatch(ctx, pi, base, session, { stateSnapshot });
1442
1479
 
1443
1480
  assert.deepEqual(result, {
1444
1481
  kind: "skipped",
1445
1482
  reason: "execute-task M001/S01/T01 is already complete",
1446
1483
  });
1447
- assert.equal(session.pendingVerificationRetry, null);
1448
- assert.equal(session.pendingOrchestrationDispatch, null);
1484
+ const sess = session as { pendingVerificationRetry: unknown; pendingOrchestrationDispatch: unknown };
1485
+ assert.equal(sess.pendingVerificationRetry, null);
1486
+ assert.equal(sess.pendingOrchestrationDispatch, null);
1449
1487
  } finally {
1450
1488
  resetRegistry();
1451
1489
  closeDatabase();
@@ -1453,7 +1491,7 @@ test("wired DispatchAdapter clears verification retry state when skipping an alr
1453
1491
  }
1454
1492
  });
1455
1493
 
1456
- test("wired DispatchAdapter preserves stop reason as a blocked decision", async () => {
1494
+ test("decideOrchestratorDispatch preserves stop reason as a blocked decision", async () => {
1457
1495
  const stateSnapshot = makeState();
1458
1496
  const stopRule: UnifiedRule = {
1459
1497
  name: "test-stop",
@@ -1469,11 +1507,10 @@ test("wired DispatchAdapter preserves stop reason as a blocked decision", async
1469
1507
  setRegistry(new RuleRegistry([stopRule]));
1470
1508
 
1471
1509
  try {
1472
- const ctx = { model: {}, modelRegistry: { getAll: () => [] } } as any;
1473
- const pi = { getActiveTools: () => [] } as any;
1474
- const adapter = createWiredDispatchAdapter(ctx, pi, "/tmp/parity-fixture");
1510
+ const ctx = { model: {}, modelRegistry: { getAll: () => [] } } as never;
1511
+ const pi = { getActiveTools: () => [] } as never;
1475
1512
 
1476
- const result = await adapter.decideNextUnit({ stateSnapshot });
1513
+ const result = await decideOrchestratorDispatch(ctx, pi, "/tmp/parity-fixture", undefined, { stateSnapshot });
1477
1514
 
1478
1515
  assert.deepEqual(result, {
1479
1516
  kind: "blocked",
@@ -1485,7 +1522,7 @@ test("wired DispatchAdapter preserves stop reason as a blocked decision", async
1485
1522
  }
1486
1523
  });
1487
1524
 
1488
- test("wired DispatchAdapter preserves dispatch skip instead of collapsing it to no remaining units", async () => {
1525
+ test("decideOrchestratorDispatch preserves dispatch skip instead of collapsing it to no remaining units", async () => {
1489
1526
  const stateSnapshot = makeState();
1490
1527
  const skipRule: UnifiedRule = {
1491
1528
  name: "test-skip-gate",
@@ -1500,11 +1537,10 @@ test("wired DispatchAdapter preserves dispatch skip instead of collapsing it to
1500
1537
  setRegistry(new RuleRegistry([skipRule]));
1501
1538
 
1502
1539
  try {
1503
- const ctx = { model: {}, modelRegistry: { getAll: () => [] } } as any;
1504
- const pi = { getActiveTools: () => [] } as any;
1505
- const adapter = createWiredDispatchAdapter(ctx, pi, "/tmp/parity-fixture");
1540
+ const ctx = { model: {}, modelRegistry: { getAll: () => [] } } as never;
1541
+ const pi = { getActiveTools: () => [] } as never;
1506
1542
 
1507
- const result = await adapter.decideNextUnit({ stateSnapshot });
1543
+ const result = await decideOrchestratorDispatch(ctx, pi, "/tmp/parity-fixture", undefined, { stateSnapshot });
1508
1544
 
1509
1545
  assert.deepEqual(result, {
1510
1546
  kind: "skipped",