@opengsd/gsd-pi 1.1.1-dev.74e8dd1 → 1.1.1-dev.a5a2de8

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 (329) hide show
  1. package/dist/cli.js +3 -2
  2. package/dist/help-text.js +10 -6
  3. package/dist/resources/.managed-resources-content-hash +1 -1
  4. package/dist/resources/extensions/browser-tools/engine/managed-gsd-browser.js +495 -0
  5. package/dist/resources/extensions/browser-tools/engine/selection.js +16 -0
  6. package/dist/resources/extensions/browser-tools/extension-manifest.json +2 -2
  7. package/dist/resources/extensions/browser-tools/index.js +57 -9
  8. package/dist/resources/extensions/browser-tools/package.json +5 -1
  9. package/dist/resources/extensions/gsd/auto/orchestrator.js +0 -1
  10. package/dist/resources/extensions/gsd/auto-dashboard.js +77 -13
  11. package/dist/resources/extensions/gsd/auto-dispatch.js +16 -0
  12. package/dist/resources/extensions/gsd/auto-post-unit.js +21 -3
  13. package/dist/resources/extensions/gsd/auto-prompts.js +63 -22
  14. package/dist/resources/extensions/gsd/auto-recovery.js +3 -4
  15. package/dist/resources/extensions/gsd/auto-runtime-state.js +3 -0
  16. package/dist/resources/extensions/gsd/auto-tool-tracking.js +1 -1
  17. package/dist/resources/extensions/gsd/auto-unit-tool-scope.js +18 -66
  18. package/dist/resources/extensions/gsd/auto-worktree.js +18 -5
  19. package/dist/resources/extensions/gsd/auto.js +9 -2
  20. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +20 -14
  21. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +28 -13
  22. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +18 -29
  23. package/dist/resources/extensions/gsd/browser-evidence.js +29 -2
  24. package/dist/resources/extensions/gsd/closeout-consistency-gate.js +61 -0
  25. package/dist/resources/extensions/gsd/commands/handlers/ops.js +2 -2
  26. package/dist/resources/extensions/gsd/commands-handlers.js +76 -11
  27. package/dist/resources/extensions/gsd/commands-mcp-status.js +2 -1
  28. package/dist/resources/extensions/gsd/dashboard-overlay.js +21 -7
  29. package/dist/resources/extensions/gsd/docs/preferences-reference.md +8 -0
  30. package/dist/resources/extensions/gsd/doctor-runtime-checks.js +2 -2
  31. package/dist/resources/extensions/gsd/escalation.js +4 -4
  32. package/dist/resources/extensions/gsd/forensics.js +74 -2
  33. package/dist/resources/extensions/gsd/gsd-db.js +5 -2
  34. package/dist/resources/extensions/gsd/guided-flow.js +118 -175
  35. package/dist/resources/extensions/gsd/mcp-project-config.js +9 -76
  36. package/dist/resources/extensions/gsd/memory-store.js +4 -1
  37. package/dist/resources/extensions/gsd/milestone-closeout.js +3 -1
  38. package/dist/resources/extensions/gsd/pending-auto-start.js +0 -1
  39. package/dist/resources/extensions/gsd/post-unit-hooks.js +9 -0
  40. package/dist/resources/extensions/gsd/preferences-validation.js +39 -0
  41. package/dist/resources/extensions/gsd/prompt-loader.js +7 -0
  42. package/dist/resources/extensions/gsd/prompts/forensics.md +61 -1
  43. package/dist/resources/extensions/gsd/prompts/gate-evaluate.md +3 -1
  44. package/dist/resources/extensions/gsd/prompts/parallel-research-slices.md +3 -1
  45. package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  46. package/dist/resources/extensions/gsd/prompts/reactive-execute.md +3 -1
  47. package/dist/resources/extensions/gsd/prompts/run-uat.md +25 -21
  48. package/dist/resources/extensions/gsd/prompts/validate-milestone.md +3 -3
  49. package/dist/resources/extensions/gsd/recovery-classification.js +20 -0
  50. package/dist/resources/extensions/gsd/rule-registry.js +428 -52
  51. package/dist/resources/extensions/gsd/state.js +2 -2
  52. package/dist/resources/extensions/gsd/templates/plan.md +3 -1
  53. package/dist/resources/extensions/gsd/tool-contract.js +5 -0
  54. package/dist/resources/extensions/gsd/tool-presentation-plan.js +17 -7
  55. package/dist/resources/extensions/gsd/tools/complete-slice.js +15 -1
  56. package/dist/resources/extensions/gsd/tools/complete-task.js +11 -1
  57. package/dist/resources/extensions/gsd/tools/validate-milestone.js +46 -16
  58. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +132 -18
  59. package/dist/resources/extensions/gsd/unit-tool-contracts.js +169 -0
  60. package/dist/resources/extensions/gsd/verdict-parser.js +59 -15
  61. package/dist/resources/extensions/gsd/verification-gate.js +72 -1
  62. package/dist/resources/extensions/gsd/workflow-mcp.js +3 -75
  63. package/dist/resources/extensions/shared/gsd-browser-cli.js +145 -0
  64. package/dist/rtk.d.ts +7 -1
  65. package/dist/rtk.js +27 -11
  66. package/dist/update-check.d.ts +15 -1
  67. package/dist/update-check.js +87 -12
  68. package/dist/update-cmd.d.ts +1 -0
  69. package/dist/update-cmd.js +53 -2
  70. package/dist/web/standalone/.next/BUILD_ID +1 -1
  71. package/dist/web/standalone/.next/app-path-routes-manifest.json +7 -7
  72. package/dist/web/standalone/.next/build-manifest.json +2 -2
  73. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  74. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  75. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  76. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  77. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  78. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  79. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  80. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  81. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  82. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  83. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  84. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  85. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  86. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  87. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  88. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  89. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  90. package/dist/web/standalone/.next/server/app/api/update/route.js +1 -1
  91. package/dist/web/standalone/.next/server/app/index.html +1 -1
  92. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  93. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  94. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  95. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  96. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  97. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  98. package/dist/web/standalone/.next/server/app-paths-manifest.json +7 -7
  99. package/dist/web/standalone/.next/server/chunks/8357.js +1 -1
  100. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  101. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  102. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  103. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  104. package/package.json +4 -2
  105. package/packages/cloud-mcp-gateway/package.json +2 -2
  106. package/packages/contracts/package.json +1 -1
  107. package/packages/daemon/package.json +4 -4
  108. package/packages/gsd-agent-core/dist/agent-session.d.ts +9 -0
  109. package/packages/gsd-agent-core/dist/agent-session.d.ts.map +1 -1
  110. package/packages/gsd-agent-core/dist/agent-session.js +32 -0
  111. package/packages/gsd-agent-core/dist/agent-session.js.map +1 -1
  112. package/packages/gsd-agent-core/dist/index.d.ts +1 -0
  113. package/packages/gsd-agent-core/dist/index.d.ts.map +1 -1
  114. package/packages/gsd-agent-core/dist/index.js +1 -0
  115. package/packages/gsd-agent-core/dist/index.js.map +1 -1
  116. package/packages/gsd-agent-core/dist/session/agent-session-compaction.d.ts +2 -0
  117. package/packages/gsd-agent-core/dist/session/agent-session-compaction.d.ts.map +1 -1
  118. package/packages/gsd-agent-core/dist/session/agent-session-compaction.js +8 -2
  119. package/packages/gsd-agent-core/dist/session/agent-session-compaction.js.map +1 -1
  120. package/packages/gsd-agent-core/dist/session/agent-session-host.d.ts +7 -0
  121. package/packages/gsd-agent-core/dist/session/agent-session-host.d.ts.map +1 -1
  122. package/packages/gsd-agent-core/dist/session/agent-session-host.js.map +1 -1
  123. package/packages/gsd-agent-core/dist/session/agent-session-prompt.d.ts.map +1 -1
  124. package/packages/gsd-agent-core/dist/session/agent-session-prompt.js +69 -1
  125. package/packages/gsd-agent-core/dist/session/agent-session-prompt.js.map +1 -1
  126. package/packages/gsd-agent-core/dist/turn-latency.d.ts +47 -0
  127. package/packages/gsd-agent-core/dist/turn-latency.d.ts.map +1 -0
  128. package/packages/gsd-agent-core/dist/turn-latency.js +123 -0
  129. package/packages/gsd-agent-core/dist/turn-latency.js.map +1 -0
  130. package/packages/gsd-agent-core/package.json +6 -6
  131. package/packages/gsd-agent-modes/dist/modes/interactive/components/__prototype__/gsd-widget-prototype.d.ts +21 -0
  132. package/packages/gsd-agent-modes/dist/modes/interactive/components/__prototype__/gsd-widget-prototype.d.ts.map +1 -0
  133. package/packages/gsd-agent-modes/dist/modes/interactive/components/__prototype__/gsd-widget-prototype.js +213 -0
  134. package/packages/gsd-agent-modes/dist/modes/interactive/components/__prototype__/gsd-widget-prototype.js.map +1 -0
  135. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  136. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.js +5 -0
  137. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.js.map +1 -1
  138. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  139. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.js +20 -0
  140. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  141. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/input-controller.d.ts.map +1 -1
  142. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/input-controller.js +7 -1
  143. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/input-controller.js.map +1 -1
  144. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-command-handlers.d.ts.map +1 -1
  145. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-command-handlers.js +6 -0
  146. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-command-handlers.js.map +1 -1
  147. package/packages/gsd-agent-modes/package.json +7 -7
  148. package/packages/mcp-server/dist/remote-questions.d.ts.map +1 -1
  149. package/packages/mcp-server/dist/remote-questions.js +23 -9
  150. package/packages/mcp-server/dist/remote-questions.js.map +1 -1
  151. package/packages/mcp-server/dist/workflow-tools.js +2 -2
  152. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  153. package/packages/mcp-server/package.json +3 -3
  154. package/packages/native/package.json +1 -1
  155. package/packages/pi-agent-core/dist/agent-loop.js +42 -3
  156. package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
  157. package/packages/pi-agent-core/dist/agent.d.ts +5 -1
  158. package/packages/pi-agent-core/dist/agent.d.ts.map +1 -1
  159. package/packages/pi-agent-core/dist/agent.js +2 -0
  160. package/packages/pi-agent-core/dist/agent.js.map +1 -1
  161. package/packages/pi-agent-core/dist/harness/agent-harness.d.ts.map +1 -1
  162. package/packages/pi-agent-core/dist/harness/agent-harness.js +3 -1
  163. package/packages/pi-agent-core/dist/harness/agent-harness.js.map +1 -1
  164. package/packages/pi-agent-core/dist/harness/types.d.ts +1 -0
  165. package/packages/pi-agent-core/dist/harness/types.d.ts.map +1 -1
  166. package/packages/pi-agent-core/dist/harness/types.js.map +1 -1
  167. package/packages/pi-agent-core/dist/types.d.ts +6 -1
  168. package/packages/pi-agent-core/dist/types.d.ts.map +1 -1
  169. package/packages/pi-agent-core/dist/types.js.map +1 -1
  170. package/packages/pi-agent-core/package.json +1 -1
  171. package/packages/pi-ai/dist/api-registry.d.ts +2 -0
  172. package/packages/pi-ai/dist/api-registry.d.ts.map +1 -1
  173. package/packages/pi-ai/dist/api-registry.js +23 -0
  174. package/packages/pi-ai/dist/api-registry.js.map +1 -1
  175. package/packages/pi-ai/dist/models.generated.d.ts +74 -6
  176. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  177. package/packages/pi-ai/dist/models.generated.js +78 -10
  178. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  179. package/packages/pi-ai/dist/stream.js +6 -6
  180. package/packages/pi-ai/dist/stream.js.map +1 -1
  181. package/packages/pi-ai/package.json +1 -1
  182. package/packages/pi-coding-agent/dist/core/extensions/extension-upstream-types.d.ts +3 -0
  183. package/packages/pi-coding-agent/dist/core/extensions/extension-upstream-types.d.ts.map +1 -1
  184. package/packages/pi-coding-agent/dist/core/extensions/extension-upstream-types.js.map +1 -1
  185. package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
  186. package/packages/pi-coding-agent/dist/core/model-registry.js +2 -2
  187. package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
  188. package/packages/pi-coding-agent/dist/core/tools/bash.js +2 -2
  189. package/packages/pi-coding-agent/dist/core/tools/bash.js.map +1 -1
  190. package/packages/pi-coding-agent/dist/core/tools/edit.d.ts.map +1 -1
  191. package/packages/pi-coding-agent/dist/core/tools/edit.js +3 -2
  192. package/packages/pi-coding-agent/dist/core/tools/edit.js.map +1 -1
  193. package/packages/pi-coding-agent/dist/core/tools/render-utils.d.ts +1 -0
  194. package/packages/pi-coding-agent/dist/core/tools/render-utils.d.ts.map +1 -1
  195. package/packages/pi-coding-agent/dist/core/tools/render-utils.js +6 -0
  196. package/packages/pi-coding-agent/dist/core/tools/render-utils.js.map +1 -1
  197. package/packages/pi-coding-agent/dist/core/tools/write.d.ts.map +1 -1
  198. package/packages/pi-coding-agent/dist/core/tools/write.js +3 -2
  199. package/packages/pi-coding-agent/dist/core/tools/write.js.map +1 -1
  200. package/packages/pi-coding-agent/package.json +7 -7
  201. package/packages/pi-tui/package.json +1 -1
  202. package/packages/rpc-client/package.json +2 -2
  203. package/pkg/package.json +1 -1
  204. package/src/resources/extensions/browser-tools/engine/managed-gsd-browser.ts +579 -0
  205. package/src/resources/extensions/browser-tools/engine/selection.ts +19 -0
  206. package/src/resources/extensions/browser-tools/extension-manifest.json +2 -2
  207. package/src/resources/extensions/browser-tools/index.ts +60 -9
  208. package/src/resources/extensions/browser-tools/package.json +5 -1
  209. package/src/resources/extensions/browser-tools/tests/browser-engine-selection.test.mjs +35 -0
  210. package/src/resources/extensions/browser-tools/tests/managed-gsd-browser-tools.test.mjs +33 -0
  211. package/src/resources/extensions/gsd/auto/orchestrator.ts +0 -1
  212. package/src/resources/extensions/gsd/auto-dashboard.ts +82 -14
  213. package/src/resources/extensions/gsd/auto-dispatch.ts +19 -0
  214. package/src/resources/extensions/gsd/auto-post-unit.ts +28 -2
  215. package/src/resources/extensions/gsd/auto-prompts.ts +97 -15
  216. package/src/resources/extensions/gsd/auto-recovery.ts +3 -3
  217. package/src/resources/extensions/gsd/auto-runtime-state.ts +4 -0
  218. package/src/resources/extensions/gsd/auto-tool-tracking.ts +1 -1
  219. package/src/resources/extensions/gsd/auto-unit-tool-scope.ts +43 -74
  220. package/src/resources/extensions/gsd/auto-worktree.ts +23 -5
  221. package/src/resources/extensions/gsd/auto.ts +12 -2
  222. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +20 -14
  223. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +32 -13
  224. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +50 -54
  225. package/src/resources/extensions/gsd/browser-evidence.ts +26 -2
  226. package/src/resources/extensions/gsd/closeout-consistency-gate.ts +137 -0
  227. package/src/resources/extensions/gsd/commands/handlers/ops.ts +2 -2
  228. package/src/resources/extensions/gsd/commands-handlers.ts +76 -11
  229. package/src/resources/extensions/gsd/commands-mcp-status.ts +2 -1
  230. package/src/resources/extensions/gsd/dashboard-overlay.ts +28 -7
  231. package/src/resources/extensions/gsd/docs/preferences-reference.md +8 -0
  232. package/src/resources/extensions/gsd/doctor-runtime-checks.ts +2 -2
  233. package/src/resources/extensions/gsd/escalation.ts +4 -4
  234. package/src/resources/extensions/gsd/forensics.ts +99 -5
  235. package/src/resources/extensions/gsd/gsd-db.ts +5 -2
  236. package/src/resources/extensions/gsd/guided-flow.ts +214 -216
  237. package/src/resources/extensions/gsd/mcp-project-config.ts +13 -78
  238. package/src/resources/extensions/gsd/memory-store.ts +4 -1
  239. package/src/resources/extensions/gsd/milestone-closeout.ts +3 -1
  240. package/src/resources/extensions/gsd/pending-auto-start.ts +0 -2
  241. package/src/resources/extensions/gsd/post-unit-hooks.ts +14 -1
  242. package/src/resources/extensions/gsd/preferences-validation.ts +36 -0
  243. package/src/resources/extensions/gsd/prompt-loader.ts +8 -0
  244. package/src/resources/extensions/gsd/prompts/forensics.md +61 -1
  245. package/src/resources/extensions/gsd/prompts/gate-evaluate.md +3 -1
  246. package/src/resources/extensions/gsd/prompts/parallel-research-slices.md +3 -1
  247. package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  248. package/src/resources/extensions/gsd/prompts/reactive-execute.md +3 -1
  249. package/src/resources/extensions/gsd/prompts/run-uat.md +25 -21
  250. package/src/resources/extensions/gsd/prompts/validate-milestone.md +3 -3
  251. package/src/resources/extensions/gsd/recovery-classification.ts +20 -0
  252. package/src/resources/extensions/gsd/rule-registry.ts +558 -58
  253. package/src/resources/extensions/gsd/rule-types.ts +2 -0
  254. package/src/resources/extensions/gsd/state.ts +2 -2
  255. package/src/resources/extensions/gsd/templates/plan.md +3 -1
  256. package/src/resources/extensions/gsd/tests/auto-dashboard.test.ts +105 -4
  257. package/src/resources/extensions/gsd/tests/auto-orchestrator.test.ts +37 -0
  258. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +10 -2
  259. package/src/resources/extensions/gsd/tests/auto-start-bootstrap-await-3420.test.ts +4 -1
  260. package/src/resources/extensions/gsd/tests/auto-warning-noise-regression.test.ts +12 -2
  261. package/src/resources/extensions/gsd/tests/browser-evidence.test.ts +142 -0
  262. package/src/resources/extensions/gsd/tests/check-auto-start-pending-gate.test.ts +9 -15
  263. package/src/resources/extensions/gsd/tests/check-auto-start-ready-guard.test.ts +26 -16
  264. package/src/resources/extensions/gsd/tests/commands-dispatcher-unmerged-milestone.test.ts +21 -0
  265. package/src/resources/extensions/gsd/tests/complete-milestone-excerpt.test.ts +30 -0
  266. package/src/resources/extensions/gsd/tests/complete-slice-verification-gate.test.ts +42 -0
  267. package/src/resources/extensions/gsd/tests/dashboard-overlay.test.ts +45 -0
  268. package/src/resources/extensions/gsd/tests/deep-planning-mode-dispatch.test.ts +53 -0
  269. package/src/resources/extensions/gsd/tests/discuss-milestone-structured-questions.test.ts +31 -0
  270. package/src/resources/extensions/gsd/tests/dispatch-complete-milestone-guard.test.ts +40 -1
  271. package/src/resources/extensions/gsd/tests/doctor-runtime-checks.test.ts +27 -0
  272. package/src/resources/extensions/gsd/tests/escalation.test.ts +16 -27
  273. package/src/resources/extensions/gsd/tests/forensics-issue-routing.test.ts +20 -0
  274. package/src/resources/extensions/gsd/tests/forensics-prompt-rendering.test.ts +3 -0
  275. package/src/resources/extensions/gsd/tests/forensics-tool-scope.test.ts +69 -0
  276. package/src/resources/extensions/gsd/tests/gate-1b-orphan-discrimination.test.ts +31 -79
  277. package/src/resources/extensions/gsd/tests/guided-discuss-milestone-prompt-rendering.test.ts +40 -1
  278. package/src/resources/extensions/gsd/tests/guided-dispatch-root.test.ts +86 -0
  279. package/src/resources/extensions/gsd/tests/guided-flow-session-isolation.test.ts +5 -3
  280. package/src/resources/extensions/gsd/tests/guided-flow-state-rebuild.test.ts +40 -4
  281. package/src/resources/extensions/gsd/tests/guided-flow.test.ts +12 -9
  282. package/src/resources/extensions/gsd/tests/integration/auto-recovery.test.ts +4 -4
  283. package/src/resources/extensions/gsd/tests/integration/auto-worktree-milestone-merge.test.ts +8 -0
  284. package/src/resources/extensions/gsd/tests/integration/parallel-merge.test.ts +16 -0
  285. package/src/resources/extensions/gsd/tests/integration/run-uat.test.ts +69 -10
  286. package/src/resources/extensions/gsd/tests/mcp-project-config.test.ts +32 -0
  287. package/src/resources/extensions/gsd/tests/mcp-status.test.ts +2 -0
  288. package/src/resources/extensions/gsd/tests/memory-maintenance.test.ts +39 -8
  289. package/src/resources/extensions/gsd/tests/merge-closeout-consistency-gate.test.ts +63 -0
  290. package/src/resources/extensions/gsd/tests/merge-db-cycle.test.ts +10 -1
  291. package/src/resources/extensions/gsd/tests/milestone-closeout.test.ts +9 -1
  292. package/src/resources/extensions/gsd/tests/new-milestone-discuss-routing.test.ts +3 -3
  293. package/src/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +9 -0
  294. package/src/resources/extensions/gsd/tests/post-unit-hooks.test.ts +157 -0
  295. package/src/resources/extensions/gsd/tests/post-unit-retry-on-orchestrator-bridge.test.ts +179 -0
  296. package/src/resources/extensions/gsd/tests/preferences.test.ts +29 -0
  297. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +61 -1
  298. package/src/resources/extensions/gsd/tests/prompt-loader-extension-dir.test.ts +14 -0
  299. package/src/resources/extensions/gsd/tests/queued-discuss-fast-path.test.ts +7 -8
  300. package/src/resources/extensions/gsd/tests/register-hooks-depth-verification.test.ts +44 -0
  301. package/src/resources/extensions/gsd/tests/rule-registry.test.ts +75 -0
  302. package/src/resources/extensions/gsd/tests/run-uat-composer.test.ts +4 -0
  303. package/src/resources/extensions/gsd/tests/runtime-invariant-modules.test.ts +36 -0
  304. package/src/resources/extensions/gsd/tests/session-start-footer.test.ts +100 -0
  305. package/src/resources/extensions/gsd/tests/state-reconciliation-drift.test.ts +139 -0
  306. package/src/resources/extensions/gsd/tests/token-tool-gating.test.ts +4 -4
  307. package/src/resources/extensions/gsd/tests/tool-invocation-error-loop-break.test.ts +19 -0
  308. package/src/resources/extensions/gsd/tests/tool-param-optionality.test.ts +7 -1
  309. package/src/resources/extensions/gsd/tests/validate-milestone-prompt-verification-classes.test.ts +6 -3
  310. package/src/resources/extensions/gsd/tests/validate-milestone-write-order.test.ts +133 -0
  311. package/src/resources/extensions/gsd/tests/verification-gate.test.ts +51 -0
  312. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +351 -0
  313. package/src/resources/extensions/gsd/tests/write-gate-planning-unit.test.ts +15 -0
  314. package/src/resources/extensions/gsd/tool-contract.ts +6 -0
  315. package/src/resources/extensions/gsd/tool-presentation-plan.ts +38 -8
  316. package/src/resources/extensions/gsd/tools/complete-slice.ts +14 -1
  317. package/src/resources/extensions/gsd/tools/complete-task.ts +20 -2
  318. package/src/resources/extensions/gsd/tools/validate-milestone.ts +46 -15
  319. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +163 -20
  320. package/src/resources/extensions/gsd/types.ts +69 -5
  321. package/src/resources/extensions/gsd/unit-tool-contracts.ts +186 -0
  322. package/src/resources/extensions/gsd/verdict-parser.ts +54 -13
  323. package/src/resources/extensions/gsd/verification-gate.ts +87 -1
  324. package/src/resources/extensions/gsd/workflow-mcp.ts +3 -75
  325. package/src/resources/extensions/shared/gsd-browser-cli.ts +172 -0
  326. package/src/resources/extensions/gsd/tests/gate-1b-recovery-bound-corrections.test.ts +0 -246
  327. package/src/resources/extensions/gsd/tests/gate-1b-recovery-bound.test.ts +0 -218
  328. /package/dist/web/standalone/.next/static/{eRWf-RI9bzbrwEurm_3uI → 9y3LeeR2uGr2yRj9RjY3D}/_buildManifest.js +0 -0
  329. /package/dist/web/standalone/.next/static/{eRWf-RI9bzbrwEurm_3uI → 9y3LeeR2uGr2yRj9RjY3D}/_ssgManifest.js +0 -0
@@ -17,11 +17,16 @@ import type {
17
17
  HookExecutionState,
18
18
  PersistedHookState,
19
19
  HookStatusEntry,
20
+ PostUnitGateBlock,
21
+ PostUnitHookOutcomeVerdict,
20
22
  } from "./types.js";
21
23
  import { resolvePostUnitHooks, resolvePreDispatchHooks } from "./preferences.js";
22
24
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
23
25
  import { join } from "node:path";
24
26
  import { parseUnitId } from "./unit-id.js";
27
+ import { queryJournal, type JournalEntry } from "./journal.js";
28
+ import { readUnitRuntimeRecord, type UnitRuntimePhase } from "./unit-runtime.js";
29
+ import { extractFrontmatterVerdict } from "./verdict-parser.js";
25
30
 
26
31
  // ─── Artifact Path Resolution ──────────────────────────────────────────────
27
32
 
@@ -56,6 +61,56 @@ export function convertDispatchRules(rules: DispatchRule[]): UnifiedRule[] {
56
61
  // ─── RuleRegistry ─────────────────────────────────────────────────────────
57
62
 
58
63
  const HOOK_STATE_FILE = "hook-state.json";
64
+ const FAILED_HOOK_RUNTIME_PHASES: ReadonlySet<UnitRuntimePhase> = new Set([
65
+ "timeout",
66
+ "finalize-timeout",
67
+ "crashed",
68
+ "paused",
69
+ ]);
70
+
71
+ interface HookFailureState {
72
+ hookName: string;
73
+ unitType: string;
74
+ unitId: string;
75
+ reason: string;
76
+ }
77
+
78
+ type HookCompletionAssessment =
79
+ | { outcome: "success" }
80
+ | { outcome: "failed"; reason: string }
81
+ | { outcome: "unknown" };
82
+
83
+ const HOOK_OUTCOME_VERDICTS = new Set<PostUnitHookOutcomeVerdict>([
84
+ "pass",
85
+ "advisory",
86
+ "needs-rework",
87
+ "needs-remediation",
88
+ "needs-attention",
89
+ ]);
90
+
91
+ interface HookTriggerRef {
92
+ triggerUnitType: string;
93
+ triggerUnitId: string;
94
+ }
95
+
96
+ interface GateOutcome {
97
+ verdict?: PostUnitHookOutcomeVerdict | "failed";
98
+ artifact?: string;
99
+ artifactPath?: string;
100
+ reason?: string;
101
+ }
102
+
103
+ function isBlockingHook(config: PostUnitHookConfig | undefined): boolean {
104
+ return config?.criticality === "blocking";
105
+ }
106
+
107
+ function hookMaxCycles(config: PostUnitHookConfig): number {
108
+ return config.max_cycles ?? 1;
109
+ }
110
+
111
+ function hookCycleKey(config: PostUnitHookConfig, trigger: HookTriggerRef): string {
112
+ return `${config.name}/${trigger.triggerUnitType}/${trigger.triggerUnitId}`;
113
+ }
59
114
 
60
115
  export class RuleRegistry {
61
116
  /** Static dispatch rules provided at construction time. */
@@ -68,10 +123,13 @@ export class RuleRegistry {
68
123
  config: PostUnitHookConfig;
69
124
  triggerUnitType: string;
70
125
  triggerUnitId: string;
126
+ forceRun?: boolean;
71
127
  }> = [];
72
128
  cycleCounts: Map<string, number> = new Map();
73
129
  retryPending: boolean = false;
74
- retryTrigger: { unitType: string; unitId: string; retryArtifact: string } | null = null;
130
+ retryTrigger: { unitType: string; unitId: string; retryArtifact?: string } | null = null;
131
+ hookFailure: HookFailureState | null = null;
132
+ gateBlockPending: PostUnitGateBlock | null = null;
75
133
 
76
134
  constructor(dispatchRules: UnifiedRule[]) {
77
135
  this.dispatchRules = dispatchRules;
@@ -100,6 +158,7 @@ export class RuleRegistry {
100
158
  artifact: hook.artifact,
101
159
  retry_on: hook.retry_on,
102
160
  max_cycles: hook.max_cycles,
161
+ criticality: hook.criticality,
103
162
  },
104
163
  });
105
164
  }
@@ -155,7 +214,10 @@ export class RuleRegistry {
155
214
  ): HookDispatchResult | null {
156
215
  // If we just completed a hook unit, handle its result
157
216
  if (this.activeHook) {
158
- return this._handleHookCompletion(basePath);
217
+ const observedCleanExecution =
218
+ completedUnitType === `hook/${this.activeHook.hookName}` &&
219
+ completedUnitId === this.activeHook.triggerUnitId;
220
+ return this._handleHookCompletion(basePath, observedCleanExecution);
159
221
  }
160
222
 
161
223
  // Don't trigger hooks for other hook units (prevent hook-on-hook chains)
@@ -187,47 +249,51 @@ export class RuleRegistry {
187
249
  private _dequeueNextHook(basePath: string): HookDispatchResult | null {
188
250
  while (this.hookQueue.length > 0) {
189
251
  const entry = this.hookQueue.shift()!;
190
- const { config, triggerUnitType, triggerUnitId } = entry;
252
+ const { config, triggerUnitType, triggerUnitId, forceRun } = entry;
191
253
 
192
- // Check idempotency if artifact already exists, skip
193
- if (config.artifact) {
254
+ // Advisory hooks preserve existing idempotency: any configured artifact
255
+ // means the hook already ran. Blocking gates must verify outcome first.
256
+ if (config.artifact && !forceRun) {
194
257
  const artifactPath = resolveHookArtifactPath(basePath, triggerUnitId, config.artifact);
195
- if (existsSync(artifactPath)) continue;
258
+ if (existsSync(artifactPath)) {
259
+ const completion = this._assessConfiguredHookCompletion(basePath, config.name, triggerUnitId);
260
+ if (completion.outcome === "failed") {
261
+ return this._handleFailedHookCompletion(
262
+ basePath,
263
+ {
264
+ hookName: config.name,
265
+ triggerUnitType,
266
+ triggerUnitId,
267
+ cycle: this.cycleCounts.get(hookCycleKey(config, { triggerUnitType, triggerUnitId })) ?? 0,
268
+ pendingRetry: false,
269
+ },
270
+ config,
271
+ completion.reason,
272
+ );
273
+ }
274
+ if (!isBlockingHook(config)) continue;
275
+ const decision = this._handleExistingBlockingArtifact(config, { triggerUnitType, triggerUnitId }, basePath);
276
+ if (decision === "skip") continue;
277
+ return decision;
278
+ }
196
279
  }
197
280
 
198
- // Check cycle limit
199
- const cycleKey = `${config.name}/${triggerUnitType}/${triggerUnitId}`;
200
- const currentCycle = (this.cycleCounts.get(cycleKey) ?? 0) + 1;
201
- const maxCycles = config.max_cycles ?? 1;
202
- if (currentCycle > maxCycles) continue;
203
-
204
- this.cycleCounts.set(cycleKey, currentCycle);
205
-
206
- this.activeHook = {
207
- hookName: config.name,
208
- triggerUnitType,
209
- triggerUnitId,
210
- cycle: currentCycle,
211
- pendingRetry: false,
212
- };
213
-
214
- // Build prompt with variable substitution
215
- const { milestone: mid, slice: sid, task: tid } = parseUnitId(triggerUnitId);
216
- let prompt = config.prompt
217
- .replace(/\{milestoneId\}/g, mid ?? "")
218
- .replace(/\{sliceId\}/g, sid ?? "")
219
- .replace(/\{taskId\}/g, tid ?? "");
220
-
221
- // Inject browser safety instruction
222
- prompt += "\n\n**Browser tool safety:** Do NOT use `browser_wait_for` with `condition: \"network_idle\"` — it hangs indefinitely when dev servers keep persistent connections (Vite HMR, WebSocket). Use `selector_visible`, `text_visible`, or `delay` instead.";
223
-
224
- return {
225
- hookName: config.name,
226
- prompt,
227
- model: config.model,
228
- unitType: `hook/${config.name}`,
229
- unitId: triggerUnitId,
230
- };
281
+ const dispatch = this._startHook(config, triggerUnitType, triggerUnitId);
282
+ if (dispatch) return dispatch;
283
+ if (isBlockingHook(config)) {
284
+ const cycleKey = hookCycleKey(config, { triggerUnitType, triggerUnitId });
285
+ const maxCycles = hookMaxCycles(config);
286
+ const currentCycle = this.cycleCounts.get(cycleKey) ?? 0;
287
+ if (currentCycle >= maxCycles) {
288
+ this._setGateBlock(config, { triggerUnitType, triggerUnitId }, {
289
+ action: "pause",
290
+ reason: `gate cycle budget exhausted before ${config.name} produced a passing outcome`,
291
+ cycle: currentCycle,
292
+ maxCycles,
293
+ });
294
+ return null;
295
+ }
296
+ }
231
297
  }
232
298
 
233
299
  // No more hooks — clear active state
@@ -235,38 +301,421 @@ export class RuleRegistry {
235
301
  return null;
236
302
  }
237
303
 
238
- private _handleHookCompletion(basePath: string): HookDispatchResult | null {
304
+ private _handleHookCompletion(basePath: string, observedCleanExecution: boolean): HookDispatchResult | null {
239
305
  const hook = this.activeHook!;
240
306
  const hooks = resolvePostUnitHooks(basePath);
241
307
  const config = hooks.find(h => h.name === hook.hookName);
308
+ if (!config) {
309
+ this.activeHook = null;
310
+ return this._dequeueNextHook(basePath);
311
+ }
312
+
313
+ const completion = this._assessHookCompletion(basePath, hook);
314
+ if (completion.outcome === "failed") {
315
+ return this._handleFailedHookCompletion(basePath, hook, config, completion.reason);
316
+ }
242
317
 
243
318
  // Check if retry was requested via retry_on artifact
244
- if (config?.retry_on) {
319
+ if (config.retry_on) {
245
320
  const retryArtifactPath = resolveHookArtifactPath(basePath, hook.triggerUnitId, config.retry_on);
246
321
  if (existsSync(retryArtifactPath)) {
247
- const cycleKey = `${config.name}/${hook.triggerUnitType}/${hook.triggerUnitId}`;
248
- const currentCycle = this.cycleCounts.get(cycleKey) ?? 1;
249
- const maxCycles = config.max_cycles ?? 1;
250
-
251
- if (currentCycle < maxCycles) {
322
+ if (this._requestTriggerRetry(config, hook, config.retry_on)) {
323
+ return null;
324
+ }
325
+ if (isBlockingHook(config)) {
326
+ this._setGateBlock(config, hook, {
327
+ action: "pause",
328
+ reason: `gate cycle budget exhausted after ${config.retry_on} requested rework`,
329
+ retryArtifact: config.retry_on,
330
+ });
252
331
  this.activeHook = null;
253
332
  this.hookQueue = [];
254
- this.retryPending = true;
255
- this.retryTrigger = {
256
- unitType: hook.triggerUnitType,
257
- unitId: hook.triggerUnitId,
258
- retryArtifact: config.retry_on,
259
- };
260
333
  return null;
261
334
  }
262
335
  }
263
336
  }
264
337
 
338
+ if (isBlockingHook(config)) {
339
+ return this._handleBlockingGateCompletion(config, hook, basePath, observedCleanExecution);
340
+ }
341
+
265
342
  // Hook completed normally — try next hook in queue
266
343
  this.activeHook = null;
267
344
  return this._dequeueNextHook(basePath);
268
345
  }
269
346
 
347
+ private _startHook(
348
+ config: PostUnitHookConfig,
349
+ triggerUnitType: string,
350
+ triggerUnitId: string,
351
+ ): HookDispatchResult | null {
352
+ const cycleKey = `${config.name}/${triggerUnitType}/${triggerUnitId}`;
353
+ const currentCycle = (this.cycleCounts.get(cycleKey) ?? 0) + 1;
354
+ const maxCycles = config.max_cycles ?? 1;
355
+ if (currentCycle > maxCycles) return null;
356
+
357
+ this.cycleCounts.set(cycleKey, currentCycle);
358
+
359
+ this.activeHook = {
360
+ hookName: config.name,
361
+ triggerUnitType,
362
+ triggerUnitId,
363
+ cycle: currentCycle,
364
+ pendingRetry: false,
365
+ };
366
+
367
+ const { milestone: mid, slice: sid, task: tid } = parseUnitId(triggerUnitId);
368
+ let prompt = config.prompt
369
+ .replace(/\{milestoneId\}/g, mid ?? "")
370
+ .replace(/\{sliceId\}/g, sid ?? "")
371
+ .replace(/\{taskId\}/g, tid ?? "");
372
+
373
+ prompt += "\n\n**Browser tool safety:** Do NOT use `browser_wait_for` with `condition: \"network_idle\"` — it hangs indefinitely when dev servers keep persistent connections (Vite HMR, WebSocket). Use `selector_visible`, `text_visible`, or `delay` instead.";
374
+
375
+ return {
376
+ hookName: config.name,
377
+ prompt,
378
+ model: config.model,
379
+ unitType: `hook/${config.name}`,
380
+ unitId: triggerUnitId,
381
+ };
382
+ }
383
+
384
+ private _assessHookCompletion(
385
+ basePath: string,
386
+ hook: HookExecutionState,
387
+ ): HookCompletionAssessment {
388
+ return this._assessConfiguredHookCompletion(basePath, hook.hookName, hook.triggerUnitId);
389
+ }
390
+
391
+ private _assessConfiguredHookCompletion(
392
+ basePath: string,
393
+ hookName: string,
394
+ unitId: string,
395
+ ): HookCompletionAssessment {
396
+ const unitType = `hook/${hookName}`;
397
+ const latestUnitEnd = this._latestHookUnitEnd(basePath, unitType, unitId);
398
+ if (latestUnitEnd) {
399
+ const data = latestUnitEnd.data ?? {};
400
+ const status = data.status;
401
+ const artifactVerified = data.artifactVerified;
402
+ if (status === "completed" && artifactVerified !== false) {
403
+ return { outcome: "success" };
404
+ }
405
+ return {
406
+ outcome: "failed",
407
+ reason: this._formatHookFailureReason(status, artifactVerified, data.errorContext),
408
+ };
409
+ }
410
+
411
+ const runtime = readUnitRuntimeRecord(basePath, unitType, unitId);
412
+ if (runtime && FAILED_HOOK_RUNTIME_PHASES.has(runtime.phase)) {
413
+ return { outcome: "failed", reason: `runtime phase ${runtime.phase}` };
414
+ }
415
+
416
+ return { outcome: "unknown" };
417
+ }
418
+
419
+ private _latestHookUnitEnd(
420
+ basePath: string,
421
+ unitType: string,
422
+ unitId: string,
423
+ ): JournalEntry | null {
424
+ const unitEnds = queryJournal(basePath, { eventType: "unit-end", unitId })
425
+ .filter(entry => entry.data?.unitType === unitType);
426
+ return unitEnds[unitEnds.length - 1] ?? null;
427
+ }
428
+
429
+ private _formatHookFailureReason(
430
+ status: unknown,
431
+ artifactVerified: unknown,
432
+ errorContext: unknown,
433
+ ): string {
434
+ const parts = [`status ${typeof status === "string" ? status : "unknown"}`];
435
+ if (artifactVerified === false) {
436
+ parts.push("artifact not verified");
437
+ }
438
+ if (typeof errorContext === "object" && errorContext !== null && "message" in errorContext) {
439
+ const message = (errorContext as { message?: unknown }).message;
440
+ if (typeof message === "string" && message.length > 0) {
441
+ parts.push(message);
442
+ }
443
+ }
444
+ return parts.join("; ");
445
+ }
446
+
447
+ private _handleFailedHookCompletion(
448
+ basePath: string,
449
+ hook: HookExecutionState,
450
+ config: PostUnitHookConfig | undefined,
451
+ reason: string,
452
+ ): HookDispatchResult | null {
453
+ if (config) {
454
+ const retry = this._startHook(config, hook.triggerUnitType, hook.triggerUnitId);
455
+ if (retry) return retry;
456
+ }
457
+
458
+ this.hookFailure = {
459
+ hookName: hook.hookName,
460
+ unitType: `hook/${hook.hookName}`,
461
+ unitId: hook.triggerUnitId,
462
+ reason,
463
+ };
464
+ this.activeHook = null;
465
+ this.hookQueue = [];
466
+ this.persistState(basePath);
467
+ return null;
468
+ }
469
+
470
+ private _handleExistingBlockingArtifact(
471
+ config: PostUnitHookConfig,
472
+ trigger: HookTriggerRef,
473
+ basePath: string,
474
+ ): "skip" | HookDispatchResult | null {
475
+ const outcome = this._readGateOutcome(config, trigger, basePath);
476
+ switch (outcome.verdict) {
477
+ case "pass":
478
+ case "advisory":
479
+ return "skip";
480
+ case "needs-rework":
481
+ return this._routeNeedsRework(config, trigger, outcome);
482
+ case "needs-remediation":
483
+ case "needs-attention":
484
+ this._pauseForGate(config, trigger, outcome, `gate reported ${outcome.verdict}`);
485
+ return null;
486
+ case "failed":
487
+ case undefined:
488
+ return this._rerunGateOrBlock(config, trigger, basePath, {
489
+ reason: outcome.reason ?? `gate artifact reported verdict=${outcome.verdict}`,
490
+ outcome,
491
+ });
492
+ }
493
+ return this._rerunGateOrBlock(config, trigger, basePath, {
494
+ reason: `gate artifact reported unsupported verdict=${String(outcome.verdict)}`,
495
+ outcome,
496
+ });
497
+ }
498
+
499
+ private _handleBlockingGateCompletion(
500
+ config: PostUnitHookConfig,
501
+ hook: HookExecutionState,
502
+ basePath: string,
503
+ observedCleanExecution: boolean,
504
+ ): HookDispatchResult | null {
505
+ if (!observedCleanExecution) {
506
+ return this._rerunGateOrBlock(config, hook, basePath, {
507
+ reason: `hook/${config.name} did not complete cleanly before the trigger unit resumed`,
508
+ });
509
+ }
510
+
511
+ const outcome = this._readGateOutcome(config, hook, basePath);
512
+ switch (outcome.verdict) {
513
+ case "pass":
514
+ case "advisory":
515
+ this.activeHook = null;
516
+ return this._dequeueNextHook(basePath);
517
+ case "needs-rework":
518
+ return this._routeNeedsRework(config, hook, outcome);
519
+ case "needs-remediation":
520
+ case "needs-attention":
521
+ return this._pauseForGate(config, hook, outcome, `gate reported ${outcome.verdict}`);
522
+ case "failed":
523
+ case undefined:
524
+ return this._rerunGateOrBlock(config, hook, basePath, {
525
+ reason: outcome.reason ?? `gate artifact reported verdict=${outcome.verdict}`,
526
+ outcome,
527
+ });
528
+ }
529
+ return this._rerunGateOrBlock(config, hook, basePath, {
530
+ reason: `gate artifact reported unsupported verdict=${String(outcome.verdict)}`,
531
+ outcome,
532
+ });
533
+ }
534
+
535
+ private _routeNeedsRework(
536
+ config: PostUnitHookConfig,
537
+ trigger: HookTriggerRef,
538
+ outcome: GateOutcome,
539
+ ): null {
540
+ const action = config.on_block?.action ?? "retry-unit";
541
+ if (action === "retry-task" || action === "retry-unit") {
542
+ if (this._requestTriggerRetry(config, trigger, config.on_block?.artifact)) {
543
+ return null;
544
+ }
545
+ this._setGateBlock(config, trigger, {
546
+ action: "pause",
547
+ reason: "gate cycle budget exhausted after needs-rework",
548
+ outcome,
549
+ retryArtifact: config.on_block?.artifact,
550
+ });
551
+ this.activeHook = null;
552
+ this.hookQueue = [];
553
+ return null;
554
+ }
555
+ return this._pauseForGate(config, trigger, outcome, `gate reported needs-rework; configured on_block action is ${action}`);
556
+ }
557
+
558
+ private _pauseForGate(
559
+ config: PostUnitHookConfig,
560
+ trigger: HookTriggerRef,
561
+ outcome: GateOutcome,
562
+ reason: string,
563
+ ): null {
564
+ this._setGateBlock(config, trigger, {
565
+ action: config.on_block?.action ?? "pause",
566
+ reason,
567
+ outcome,
568
+ retryArtifact: config.on_block?.artifact,
569
+ });
570
+ this.activeHook = null;
571
+ this.hookQueue = [];
572
+ return null;
573
+ }
574
+
575
+ private _requestTriggerRetry(
576
+ config: PostUnitHookConfig,
577
+ hook: HookTriggerRef,
578
+ retryArtifact?: string,
579
+ ): boolean {
580
+ const cycleKey = hookCycleKey(config, hook);
581
+ const currentCycle = this.cycleCounts.get(cycleKey) ?? 1;
582
+ const maxCycles = hookMaxCycles(config);
583
+ if (currentCycle >= maxCycles) return false;
584
+
585
+ this.activeHook = null;
586
+ this.hookQueue = [];
587
+ this.retryPending = true;
588
+ this.retryTrigger = {
589
+ unitType: hook.triggerUnitType,
590
+ unitId: hook.triggerUnitId,
591
+ };
592
+ if (retryArtifact !== undefined) {
593
+ this.retryTrigger.retryArtifact = retryArtifact;
594
+ }
595
+ return true;
596
+ }
597
+
598
+ private _rerunGateOrBlock(
599
+ config: PostUnitHookConfig,
600
+ trigger: HookTriggerRef,
601
+ basePath: string,
602
+ opts: {
603
+ reason: string;
604
+ outcome?: GateOutcome;
605
+ },
606
+ ): HookDispatchResult | null {
607
+ const cycleKey = hookCycleKey(config, trigger);
608
+ const currentCycle = this.cycleCounts.get(cycleKey) ?? 0;
609
+ const maxCycles = hookMaxCycles(config);
610
+ if (currentCycle < maxCycles) {
611
+ this.activeHook = null;
612
+ this.hookQueue.unshift({
613
+ config,
614
+ triggerUnitType: trigger.triggerUnitType,
615
+ triggerUnitId: trigger.triggerUnitId,
616
+ forceRun: true,
617
+ });
618
+ return this._dequeueNextHook(basePath);
619
+ }
620
+
621
+ this._setGateBlock(config, trigger, {
622
+ action: "pause",
623
+ reason: `${opts.reason}; gate cycle budget exhausted`,
624
+ outcome: opts.outcome,
625
+ cycle: currentCycle,
626
+ maxCycles,
627
+ });
628
+ this.activeHook = null;
629
+ this.hookQueue = [];
630
+ return null;
631
+ }
632
+
633
+ private _readGateOutcome(
634
+ config: PostUnitHookConfig,
635
+ trigger: HookTriggerRef,
636
+ basePath: string,
637
+ ): GateOutcome {
638
+ if (!config.artifact) {
639
+ return { reason: "blocking gate has no configured artifact" };
640
+ }
641
+ const artifactPath = resolveHookArtifactPath(basePath, trigger.triggerUnitId, config.artifact);
642
+ if (!existsSync(artifactPath)) {
643
+ return {
644
+ artifact: config.artifact,
645
+ artifactPath,
646
+ reason: `missing required gate artifact ${config.artifact}`,
647
+ };
648
+ }
649
+ let content = "";
650
+ try {
651
+ content = readFileSync(artifactPath, "utf-8");
652
+ } catch (e) {
653
+ return {
654
+ artifact: config.artifact,
655
+ artifactPath,
656
+ reason: `could not read gate artifact ${config.artifact}: ${(e as Error).message}`,
657
+ };
658
+ }
659
+
660
+ const rawVerdict = extractFrontmatterVerdict(content);
661
+ if (!rawVerdict) {
662
+ return {
663
+ artifact: config.artifact,
664
+ artifactPath,
665
+ reason: `gate artifact ${config.artifact} is missing frontmatter verdict`,
666
+ };
667
+ }
668
+ if (rawVerdict === "failed") {
669
+ return {
670
+ artifact: config.artifact,
671
+ artifactPath,
672
+ verdict: "failed",
673
+ reason: `gate artifact ${config.artifact} reported verdict=failed`,
674
+ };
675
+ }
676
+ if (!HOOK_OUTCOME_VERDICTS.has(rawVerdict as PostUnitHookOutcomeVerdict)) {
677
+ return {
678
+ artifact: config.artifact,
679
+ artifactPath,
680
+ reason: `gate artifact ${config.artifact} has unsupported verdict=${rawVerdict}`,
681
+ };
682
+ }
683
+ return {
684
+ artifact: config.artifact,
685
+ artifactPath,
686
+ verdict: rawVerdict as PostUnitHookOutcomeVerdict,
687
+ };
688
+ }
689
+
690
+ private _setGateBlock(
691
+ config: PostUnitHookConfig,
692
+ trigger: HookTriggerRef,
693
+ opts: {
694
+ action: PostUnitGateBlock["action"];
695
+ reason: string;
696
+ outcome?: GateOutcome;
697
+ cycle?: number;
698
+ maxCycles?: number;
699
+ retryArtifact?: string;
700
+ },
701
+ ): void {
702
+ const cycleKey = hookCycleKey(config, trigger);
703
+ const cycle = opts.cycle ?? this.cycleCounts.get(cycleKey) ?? 0;
704
+ this.gateBlockPending = {
705
+ hookName: config.name,
706
+ triggerUnitType: trigger.triggerUnitType,
707
+ triggerUnitId: trigger.triggerUnitId,
708
+ artifact: opts.outcome?.artifact ?? config.artifact,
709
+ artifactPath: opts.outcome?.artifactPath,
710
+ verdict: opts.outcome?.verdict,
711
+ action: opts.action,
712
+ reason: opts.reason,
713
+ cycle,
714
+ maxCycles: opts.maxCycles ?? hookMaxCycles(config),
715
+ retryArtifact: opts.retryArtifact,
716
+ };
717
+ }
718
+
270
719
  // ── Pre-dispatch hook evaluation (sync, all-matching with compose) ──
271
720
 
272
721
  /**
@@ -351,11 +800,22 @@ export class RuleRegistry {
351
800
  return this.retryPending;
352
801
  }
353
802
 
803
+ consumeHookFailure(): HookFailureState | null {
804
+ if (!this.hookFailure) return null;
805
+ const failure = { ...this.hookFailure };
806
+ this.hookFailure = null;
807
+ return failure;
808
+ }
809
+
810
+ isGateBlockPending(): boolean {
811
+ return this.gateBlockPending !== null;
812
+ }
813
+
354
814
  /**
355
815
  * Returns the trigger unit info for a pending retry, or null.
356
816
  * Clears the retry state after reading.
357
817
  */
358
- consumeRetryTrigger(): { unitType: string; unitId: string; retryArtifact: string } | null {
818
+ consumeRetryTrigger(): { unitType: string; unitId: string; retryArtifact?: string } | null {
359
819
  if (!this.retryPending || !this.retryTrigger) return null;
360
820
  const trigger = { ...this.retryTrigger };
361
821
  this.retryPending = false;
@@ -363,13 +823,26 @@ export class RuleRegistry {
363
823
  return trigger;
364
824
  }
365
825
 
366
- /** Clear all mutable state (activeHook, hookQueue, cycleCounts, retryPending, retryTrigger). */
826
+ /**
827
+ * Returns a pending post-unit gate block, or null.
828
+ * Clears the block state after reading.
829
+ */
830
+ consumeGateBlock(): PostUnitGateBlock | null {
831
+ if (!this.gateBlockPending) return null;
832
+ const block = { ...this.gateBlockPending };
833
+ this.gateBlockPending = null;
834
+ return block;
835
+ }
836
+
837
+ /** Clear all mutable hook lifecycle state. */
367
838
  resetState(): void {
368
839
  this.activeHook = null;
369
840
  this.hookQueue = [];
370
841
  this.cycleCounts.clear();
371
842
  this.retryPending = false;
372
843
  this.retryTrigger = null;
844
+ this.hookFailure = null;
845
+ this.gateBlockPending = null;
373
846
  }
374
847
 
375
848
  // ── Persistence ─────────────────────────────────────────────────────
@@ -378,10 +851,17 @@ export class RuleRegistry {
378
851
  return join(basePath, ".gsd", HOOK_STATE_FILE);
379
852
  }
380
853
 
381
- /** Persist current hook cycle counts to disk. */
854
+ /** Persist current hook state to disk. */
382
855
  persistState(basePath: string): void {
383
856
  const state: PersistedHookState = {
384
857
  cycleCounts: Object.fromEntries(this.cycleCounts),
858
+ activeHook: this.activeHook ? { ...this.activeHook } : null,
859
+ hookQueue: this.hookQueue.map(entry => ({
860
+ hookName: entry.config.name,
861
+ triggerUnitType: entry.triggerUnitType,
862
+ triggerUnitId: entry.triggerUnitId,
863
+ forceRun: entry.forceRun,
864
+ })),
385
865
  savedAt: new Date().toISOString(),
386
866
  };
387
867
  try {
@@ -393,7 +873,7 @@ export class RuleRegistry {
393
873
  }
394
874
  }
395
875
 
396
- /** Restore hook cycle counts from disk after a crash/restart. */
876
+ /** Restore hook state from disk after a crash/restart. */
397
877
  restoreState(basePath: string): void {
398
878
  try {
399
879
  const filePath = this._hookStatePath(basePath);
@@ -408,6 +888,24 @@ export class RuleRegistry {
408
888
  }
409
889
  }
410
890
  }
891
+ this.activeHook = state.activeHook && typeof state.activeHook === "object"
892
+ ? { ...state.activeHook }
893
+ : null;
894
+ this.hookQueue = [];
895
+ if (Array.isArray(state.hookQueue)) {
896
+ const hooks = resolvePostUnitHooks(basePath);
897
+ for (const entry of state.hookQueue) {
898
+ const config = hooks.find(h => h.name === entry.hookName);
899
+ if (config) {
900
+ this.hookQueue.push({
901
+ config,
902
+ triggerUnitType: entry.triggerUnitType,
903
+ triggerUnitId: entry.triggerUnitId,
904
+ forceRun: entry.forceRun,
905
+ });
906
+ }
907
+ }
908
+ }
411
909
  } catch (e) {
412
910
  logWarning("registry", `failed to restore hook state: ${(e as Error).message}`);
413
911
  }
@@ -420,7 +918,7 @@ export class RuleRegistry {
420
918
  if (existsSync(filePath)) {
421
919
  writeFileSync(
422
920
  filePath,
423
- JSON.stringify({ cycleCounts: {}, savedAt: new Date().toISOString() }, null, 2),
921
+ JSON.stringify({ cycleCounts: {}, activeHook: null, hookQueue: [], savedAt: new Date().toISOString() }, null, 2),
424
922
  "utf-8",
425
923
  );
426
924
  }
@@ -448,6 +946,7 @@ export class RuleRegistry {
448
946
  type: "post",
449
947
  enabled: hook.enabled !== false,
450
948
  targets: hook.after,
949
+ criticality: hook.criticality ?? "advisory",
451
950
  activeCycles,
452
951
  });
453
952
  }
@@ -537,9 +1036,10 @@ export class RuleRegistry {
537
1036
  lines.push("Post-Unit Hooks (run after unit completes):");
538
1037
  for (const hook of postHooks) {
539
1038
  const status = hook.enabled ? "enabled" : "disabled";
1039
+ const criticality = hook.criticality ?? "advisory";
540
1040
  const cycles = Object.keys(hook.activeCycles).length;
541
1041
  const cycleInfo = cycles > 0 ? ` (${cycles} active cycle${cycles === 1 ? "" : "s"})` : "";
542
- lines.push(` ${hook.name} [${status}] → after: ${hook.targets.join(", ")}${cycleInfo}`);
1042
+ lines.push(` ${hook.name} [${status}, ${criticality}] → after: ${hook.targets.join(", ")}${cycleInfo}`);
543
1043
  }
544
1044
  lines.push("");
545
1045
  }