clinkx 0.2.3 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (574) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/LICENSE +10 -0
  3. package/README.md +113 -18
  4. package/clinkx-workflows/dist/allowed-roots.d.ts +1 -0
  5. package/clinkx-workflows/dist/allowed-roots.js +5 -0
  6. package/clinkx-workflows/dist/allowed-roots.js.map +1 -0
  7. package/clinkx-workflows/dist/artifacts.d.ts +20 -1
  8. package/clinkx-workflows/dist/artifacts.js +38 -9
  9. package/clinkx-workflows/dist/artifacts.js.map +1 -1
  10. package/clinkx-workflows/dist/backend.d.ts +4 -0
  11. package/clinkx-workflows/dist/citation-check.d.ts +40 -0
  12. package/clinkx-workflows/dist/citation-check.js +348 -0
  13. package/clinkx-workflows/dist/citation-check.js.map +1 -0
  14. package/clinkx-workflows/dist/clink-client.d.ts +2 -1
  15. package/clinkx-workflows/dist/clink-client.js +17 -4
  16. package/clinkx-workflows/dist/clink-client.js.map +1 -1
  17. package/clinkx-workflows/dist/config.d.ts +145 -16
  18. package/clinkx-workflows/dist/config.js +384 -26
  19. package/clinkx-workflows/dist/config.js.map +1 -1
  20. package/clinkx-workflows/dist/contained-file.d.ts +172 -0
  21. package/clinkx-workflows/dist/contained-file.js +483 -0
  22. package/clinkx-workflows/dist/contained-file.js.map +1 -0
  23. package/clinkx-workflows/dist/context-builder-lifecycle-contract.d.ts +416 -0
  24. package/clinkx-workflows/dist/context-builder-lifecycle-contract.js +328 -0
  25. package/clinkx-workflows/dist/context-builder-lifecycle-contract.js.map +1 -0
  26. package/clinkx-workflows/dist/context-builder-lifecycle-router.d.ts +24 -0
  27. package/clinkx-workflows/dist/context-builder-lifecycle-router.js +39 -0
  28. package/clinkx-workflows/dist/context-builder-lifecycle-router.js.map +1 -0
  29. package/clinkx-workflows/dist/context-cache-key.d.ts +53 -0
  30. package/clinkx-workflows/dist/context-cache-key.js +91 -0
  31. package/clinkx-workflows/dist/context-cache-key.js.map +1 -0
  32. package/clinkx-workflows/dist/context-mode-resolver.d.ts +27 -0
  33. package/clinkx-workflows/dist/context-mode-resolver.js +126 -0
  34. package/clinkx-workflows/dist/context-mode-resolver.js.map +1 -0
  35. package/clinkx-workflows/dist/context-preflight.d.ts +184 -0
  36. package/clinkx-workflows/dist/context-preflight.js +91 -0
  37. package/clinkx-workflows/dist/context-preflight.js.map +1 -0
  38. package/clinkx-workflows/dist/council-expander.d.ts +1 -1
  39. package/clinkx-workflows/dist/council-expander.js +2 -1
  40. package/clinkx-workflows/dist/council-expander.js.map +1 -1
  41. package/clinkx-workflows/dist/dashboard/activity-contract.d.ts +165 -0
  42. package/clinkx-workflows/dist/dashboard/activity-contract.js +105 -0
  43. package/clinkx-workflows/dist/dashboard/activity-contract.js.map +1 -0
  44. package/clinkx-workflows/dist/dashboard/activity-lease.d.ts +25 -0
  45. package/clinkx-workflows/dist/dashboard/activity-lease.js +34 -0
  46. package/clinkx-workflows/dist/dashboard/activity-lease.js.map +1 -0
  47. package/clinkx-workflows/dist/dashboard/activity-reader.d.ts +86 -0
  48. package/clinkx-workflows/dist/dashboard/activity-reader.js +480 -0
  49. package/clinkx-workflows/dist/dashboard/activity-reader.js.map +1 -0
  50. package/clinkx-workflows/dist/dashboard/api-routes.d.ts +36 -0
  51. package/clinkx-workflows/dist/dashboard/api-routes.js +1047 -0
  52. package/clinkx-workflows/dist/dashboard/api-routes.js.map +1 -0
  53. package/clinkx-workflows/dist/dashboard/artifact-index.d.ts +37 -0
  54. package/clinkx-workflows/dist/dashboard/artifact-index.js +145 -0
  55. package/clinkx-workflows/dist/dashboard/artifact-index.js.map +1 -0
  56. package/clinkx-workflows/dist/dashboard/auth/challenge-store.d.ts +31 -0
  57. package/clinkx-workflows/dist/dashboard/auth/challenge-store.js +65 -0
  58. package/clinkx-workflows/dist/dashboard/auth/challenge-store.js.map +1 -0
  59. package/clinkx-workflows/dist/dashboard/auth/controller.d.ts +26 -0
  60. package/clinkx-workflows/dist/dashboard/auth/controller.js +596 -0
  61. package/clinkx-workflows/dist/dashboard/auth/controller.js.map +1 -0
  62. package/clinkx-workflows/dist/dashboard/auth/credential-store.d.ts +60 -0
  63. package/clinkx-workflows/dist/dashboard/auth/credential-store.js +776 -0
  64. package/clinkx-workflows/dist/dashboard/auth/credential-store.js.map +1 -0
  65. package/clinkx-workflows/dist/dashboard/auth/enrollment-gate.d.ts +9 -0
  66. package/clinkx-workflows/dist/dashboard/auth/enrollment-gate.js +44 -0
  67. package/clinkx-workflows/dist/dashboard/auth/enrollment-gate.js.map +1 -0
  68. package/clinkx-workflows/dist/dashboard/auth/session-store.d.ts +17 -0
  69. package/clinkx-workflows/dist/dashboard/auth/session-store.js +77 -0
  70. package/clinkx-workflows/dist/dashboard/auth/session-store.js.map +1 -0
  71. package/clinkx-workflows/dist/dashboard/auth/webauthn-service.d.ts +25 -0
  72. package/clinkx-workflows/dist/dashboard/auth/webauthn-service.js +71 -0
  73. package/clinkx-workflows/dist/dashboard/auth/webauthn-service.js.map +1 -0
  74. package/clinkx-workflows/dist/dashboard/context-run-liveness.d.ts +37 -0
  75. package/clinkx-workflows/dist/dashboard/context-run-liveness.js +49 -0
  76. package/clinkx-workflows/dist/dashboard/context-run-liveness.js.map +1 -0
  77. package/clinkx-workflows/dist/dashboard/context-run-reader.d.ts +222 -0
  78. package/clinkx-workflows/dist/dashboard/context-run-reader.js +1178 -0
  79. package/clinkx-workflows/dist/dashboard/context-run-reader.js.map +1 -0
  80. package/clinkx-workflows/dist/dashboard/contracts.d.ts +8936 -0
  81. package/clinkx-workflows/dist/dashboard/contracts.js +1157 -0
  82. package/clinkx-workflows/dist/dashboard/contracts.js.map +1 -0
  83. package/clinkx-workflows/dist/dashboard/generation.d.ts +14 -0
  84. package/clinkx-workflows/dist/dashboard/generation.js +41 -0
  85. package/clinkx-workflows/dist/dashboard/generation.js.map +1 -0
  86. package/clinkx-workflows/dist/dashboard/index.d.ts +28 -0
  87. package/clinkx-workflows/dist/dashboard/index.js +16 -0
  88. package/clinkx-workflows/dist/dashboard/index.js.map +1 -0
  89. package/clinkx-workflows/dist/dashboard/lazy-content.d.ts +101 -0
  90. package/clinkx-workflows/dist/dashboard/lazy-content.js +748 -0
  91. package/clinkx-workflows/dist/dashboard/lazy-content.js.map +1 -0
  92. package/clinkx-workflows/dist/dashboard/progress-report-contract.d.ts +303 -0
  93. package/clinkx-workflows/dist/dashboard/progress-report-contract.js +221 -0
  94. package/clinkx-workflows/dist/dashboard/progress-report-contract.js.map +1 -0
  95. package/clinkx-workflows/dist/dashboard/progress-report-reader.d.ts +50 -0
  96. package/clinkx-workflows/dist/dashboard/progress-report-reader.js +581 -0
  97. package/clinkx-workflows/dist/dashboard/progress-report-reader.js.map +1 -0
  98. package/clinkx-workflows/dist/dashboard/progress-report-status-overlay.d.ts +89 -0
  99. package/clinkx-workflows/dist/dashboard/progress-report-status-overlay.js +71 -0
  100. package/clinkx-workflows/dist/dashboard/progress-report-status-overlay.js.map +1 -0
  101. package/clinkx-workflows/dist/dashboard/progress-report-status-store.d.ts +28 -0
  102. package/clinkx-workflows/dist/dashboard/progress-report-status-store.js +110 -0
  103. package/clinkx-workflows/dist/dashboard/progress-report-status-store.js.map +1 -0
  104. package/clinkx-workflows/dist/dashboard/read-model.d.ts +86 -0
  105. package/clinkx-workflows/dist/dashboard/read-model.js +440 -0
  106. package/clinkx-workflows/dist/dashboard/read-model.js.map +1 -0
  107. package/clinkx-workflows/dist/dashboard/request-admission.d.ts +31 -0
  108. package/clinkx-workflows/dist/dashboard/request-admission.js +126 -0
  109. package/clinkx-workflows/dist/dashboard/request-admission.js.map +1 -0
  110. package/clinkx-workflows/dist/dashboard/scanner.d.ts +75 -0
  111. package/clinkx-workflows/dist/dashboard/scanner.js +1110 -0
  112. package/clinkx-workflows/dist/dashboard/scanner.js.map +1 -0
  113. package/clinkx-workflows/dist/dashboard/server-factory.d.ts +50 -0
  114. package/clinkx-workflows/dist/dashboard/server-factory.js +431 -0
  115. package/clinkx-workflows/dist/dashboard/server-factory.js.map +1 -0
  116. package/clinkx-workflows/dist/dashboard/session-log-contract.d.ts +2611 -0
  117. package/clinkx-workflows/dist/dashboard/session-log-contract.js +480 -0
  118. package/clinkx-workflows/dist/dashboard/session-log-contract.js.map +1 -0
  119. package/clinkx-workflows/dist/dashboard/session-log-identity.d.ts +21 -0
  120. package/clinkx-workflows/dist/dashboard/session-log-identity.js +101 -0
  121. package/clinkx-workflows/dist/dashboard/session-log-identity.js.map +1 -0
  122. package/clinkx-workflows/dist/dashboard/session-log-journal.d.ts +43 -0
  123. package/clinkx-workflows/dist/dashboard/session-log-journal.js +522 -0
  124. package/clinkx-workflows/dist/dashboard/session-log-journal.js.map +1 -0
  125. package/clinkx-workflows/dist/dashboard/session-log-reader.d.ts +61 -0
  126. package/clinkx-workflows/dist/dashboard/session-log-reader.js +378 -0
  127. package/clinkx-workflows/dist/dashboard/session-log-reader.js.map +1 -0
  128. package/clinkx-workflows/dist/dashboard/session-log-redaction.d.ts +6 -0
  129. package/clinkx-workflows/dist/dashboard/session-log-redaction.js +367 -0
  130. package/clinkx-workflows/dist/dashboard/session-log-redaction.js.map +1 -0
  131. package/clinkx-workflows/dist/dashboard/session-log-retention.d.ts +23 -0
  132. package/clinkx-workflows/dist/dashboard/session-log-retention.js +212 -0
  133. package/clinkx-workflows/dist/dashboard/session-log-retention.js.map +1 -0
  134. package/clinkx-workflows/dist/dashboard/static-responses.d.ts +10 -0
  135. package/clinkx-workflows/dist/dashboard/static-responses.js +94 -0
  136. package/clinkx-workflows/dist/dashboard/static-responses.js.map +1 -0
  137. package/clinkx-workflows/dist/dashboard/worker-lock-observer.d.ts +28 -0
  138. package/clinkx-workflows/dist/dashboard/worker-lock-observer.js +241 -0
  139. package/clinkx-workflows/dist/dashboard/worker-lock-observer.js.map +1 -0
  140. package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
  141. package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
  142. package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
  143. package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
  144. package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
  145. package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
  146. package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2 +0 -0
  147. package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff +0 -0
  148. package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
  149. package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
  150. package/clinkx-workflows/dist/dashboard-ui/assets/index-DOkMXTfB.js +25 -0
  151. package/clinkx-workflows/dist/dashboard-ui/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  152. package/clinkx-workflows/dist/dashboard-ui/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  153. package/clinkx-workflows/dist/dashboard-ui/assets/jetbrains-mono-latin-500-normal-BWZEU5yA.woff2 +0 -0
  154. package/clinkx-workflows/dist/dashboard-ui/assets/jetbrains-mono-latin-500-normal-CJOVTJB7.woff +0 -0
  155. package/clinkx-workflows/dist/dashboard-ui/assets/style-ChJsn0KD.css +1 -0
  156. package/clinkx-workflows/dist/dashboard-ui/contracts/council-plan-review-input-v1.json +158 -0
  157. package/clinkx-workflows/dist/dashboard-ui/index.html +15 -0
  158. package/clinkx-workflows/dist/dashboard-ui/licenses/IBM-Plex-Mono-OFL.txt +93 -0
  159. package/clinkx-workflows/dist/dashboard-ui/licenses/IBM-Plex-Sans-OFL.txt +93 -0
  160. package/clinkx-workflows/dist/dashboard-ui/licenses/JetBrains-Mono-OFL.txt +93 -0
  161. package/clinkx-workflows/dist/dashboard-ui/licenses/NOTICE.md +32 -0
  162. package/clinkx-workflows/dist/dashboard-ui/theme-init.js +8 -0
  163. package/clinkx-workflows/dist/definition-normalizer.d.ts +19 -0
  164. package/clinkx-workflows/dist/definition-normalizer.js.map +1 -1
  165. package/clinkx-workflows/dist/delegate-validate.d.ts +78 -0
  166. package/clinkx-workflows/dist/delegate-validate.js +97 -0
  167. package/clinkx-workflows/dist/delegate-validate.js.map +1 -0
  168. package/clinkx-workflows/dist/engine.d.ts +141 -0
  169. package/clinkx-workflows/dist/engine.js +882 -28
  170. package/clinkx-workflows/dist/engine.js.map +1 -1
  171. package/clinkx-workflows/dist/errors.d.ts +2 -0
  172. package/clinkx-workflows/dist/errors.js.map +1 -1
  173. package/clinkx-workflows/dist/fingerprint.d.ts +1 -1
  174. package/clinkx-workflows/dist/fingerprint.js +1 -1
  175. package/clinkx-workflows/dist/index.d.ts +19 -0
  176. package/clinkx-workflows/dist/index.js +15 -0
  177. package/clinkx-workflows/dist/index.js.map +1 -1
  178. package/clinkx-workflows/dist/loader.d.ts +9 -3
  179. package/clinkx-workflows/dist/loader.js +92 -21
  180. package/clinkx-workflows/dist/loader.js.map +1 -1
  181. package/clinkx-workflows/dist/plan-review/admission.d.ts +160 -0
  182. package/clinkx-workflows/dist/plan-review/admission.js +290 -0
  183. package/clinkx-workflows/dist/plan-review/admission.js.map +1 -0
  184. package/clinkx-workflows/dist/plan-review/context-policy.d.ts +55 -0
  185. package/clinkx-workflows/dist/plan-review/context-policy.js +146 -0
  186. package/clinkx-workflows/dist/plan-review/context-policy.js.map +1 -0
  187. package/clinkx-workflows/dist/plan-review/contract.d.ts +763 -0
  188. package/clinkx-workflows/dist/plan-review/contract.js +289 -0
  189. package/clinkx-workflows/dist/plan-review/contract.js.map +1 -0
  190. package/clinkx-workflows/dist/plan-review/digest.d.ts +68 -0
  191. package/clinkx-workflows/dist/plan-review/digest.js +241 -0
  192. package/clinkx-workflows/dist/plan-review/digest.js.map +1 -0
  193. package/clinkx-workflows/dist/plan-review/discovery.d.ts +45 -0
  194. package/clinkx-workflows/dist/plan-review/discovery.js +36 -0
  195. package/clinkx-workflows/dist/plan-review/discovery.js.map +1 -0
  196. package/clinkx-workflows/dist/plan-review/execution-contract.d.ts +298 -0
  197. package/clinkx-workflows/dist/plan-review/execution-contract.js +194 -0
  198. package/clinkx-workflows/dist/plan-review/execution-contract.js.map +1 -0
  199. package/clinkx-workflows/dist/plan-review/expander.d.ts +48 -0
  200. package/clinkx-workflows/dist/plan-review/expander.js +243 -0
  201. package/clinkx-workflows/dist/plan-review/expander.js.map +1 -0
  202. package/clinkx-workflows/dist/plan-review/projector.d.ts +126 -0
  203. package/clinkx-workflows/dist/plan-review/projector.js +437 -0
  204. package/clinkx-workflows/dist/plan-review/projector.js.map +1 -0
  205. package/clinkx-workflows/dist/plan-review/publication.d.ts +95 -0
  206. package/clinkx-workflows/dist/plan-review/publication.js +182 -0
  207. package/clinkx-workflows/dist/plan-review/publication.js.map +1 -0
  208. package/clinkx-workflows/dist/plan-review/ranking-reducer.d.ts +34 -0
  209. package/clinkx-workflows/dist/plan-review/ranking-reducer.js +139 -0
  210. package/clinkx-workflows/dist/plan-review/ranking-reducer.js.map +1 -0
  211. package/clinkx-workflows/dist/plan-review/resume.d.ts +38 -0
  212. package/clinkx-workflows/dist/plan-review/resume.js +122 -0
  213. package/clinkx-workflows/dist/plan-review/resume.js.map +1 -0
  214. package/clinkx-workflows/dist/plan-review/rubric.d.ts +7 -0
  215. package/clinkx-workflows/dist/plan-review/rubric.js +24 -0
  216. package/clinkx-workflows/dist/plan-review/rubric.js.map +1 -0
  217. package/clinkx-workflows/dist/plan-review/run-state.d.ts +398 -0
  218. package/clinkx-workflows/dist/plan-review/run-state.js +183 -0
  219. package/clinkx-workflows/dist/plan-review/run-state.js.map +1 -0
  220. package/clinkx-workflows/dist/plan-review/runtime-policy.d.ts +74 -0
  221. package/clinkx-workflows/dist/plan-review/runtime-policy.js +186 -0
  222. package/clinkx-workflows/dist/plan-review/runtime-policy.js.map +1 -0
  223. package/clinkx-workflows/dist/plan-review/snapshot.d.ts +81 -0
  224. package/clinkx-workflows/dist/plan-review/snapshot.js +170 -0
  225. package/clinkx-workflows/dist/plan-review/snapshot.js.map +1 -0
  226. package/clinkx-workflows/dist/plan-review/stage-error.d.ts +19 -0
  227. package/clinkx-workflows/dist/plan-review/stage-error.js +20 -0
  228. package/clinkx-workflows/dist/plan-review/stage-error.js.map +1 -0
  229. package/clinkx-workflows/dist/post-processors/index.d.ts +20 -0
  230. package/clinkx-workflows/dist/post-processors/index.js +21 -0
  231. package/clinkx-workflows/dist/post-processors/index.js.map +1 -0
  232. package/clinkx-workflows/dist/prompt-budget.d.ts +7 -0
  233. package/clinkx-workflows/dist/prompt-budget.js +7 -1
  234. package/clinkx-workflows/dist/prompt-budget.js.map +1 -1
  235. package/clinkx-workflows/dist/reducers/index.js +2 -0
  236. package/clinkx-workflows/dist/reducers/index.js.map +1 -1
  237. package/clinkx-workflows/dist/run-lock-heartbeat.d.ts +2 -0
  238. package/clinkx-workflows/dist/run-lock-heartbeat.js +3 -0
  239. package/clinkx-workflows/dist/run-lock-heartbeat.js.map +1 -0
  240. package/clinkx-workflows/dist/run-summary/cards/council-plan-review.d.ts +34 -0
  241. package/clinkx-workflows/dist/run-summary/cards/council-plan-review.js +74 -0
  242. package/clinkx-workflows/dist/run-summary/cards/council-plan-review.js.map +1 -0
  243. package/clinkx-workflows/dist/run-summary/cards/index.js +5 -0
  244. package/clinkx-workflows/dist/run-summary/cards/index.js.map +1 -1
  245. package/clinkx-workflows/dist/run-summary.d.ts +13 -0
  246. package/clinkx-workflows/dist/run-summary.js +29 -1
  247. package/clinkx-workflows/dist/run-summary.js.map +1 -1
  248. package/clinkx-workflows/dist/schema.d.ts +76 -21
  249. package/clinkx-workflows/dist/schema.js +10 -1
  250. package/clinkx-workflows/dist/schema.js.map +1 -1
  251. package/clinkx-workflows/dist/seed-terms.d.ts +30 -0
  252. package/clinkx-workflows/dist/seed-terms.js +176 -0
  253. package/clinkx-workflows/dist/seed-terms.js.map +1 -0
  254. package/clinkx-workflows/dist/server.d.ts +5 -2
  255. package/clinkx-workflows/dist/server.js +82 -52
  256. package/clinkx-workflows/dist/server.js.map +1 -1
  257. package/clinkx-workflows/dist/shutdown.d.ts +18 -20
  258. package/clinkx-workflows/dist/shutdown.js +43 -61
  259. package/clinkx-workflows/dist/shutdown.js.map +1 -1
  260. package/clinkx-workflows/dist/state-schema.d.ts +38 -0
  261. package/clinkx-workflows/dist/state.d.ts +170 -3
  262. package/clinkx-workflows/dist/state.js +1275 -28
  263. package/clinkx-workflows/dist/state.js.map +1 -1
  264. package/clinkx-workflows/dist/transport.d.ts +15 -0
  265. package/clinkx-workflows/dist/transport.js +19 -10
  266. package/clinkx-workflows/dist/transport.js.map +1 -1
  267. package/clinkx-workflows/dist/types.d.ts +14 -2
  268. package/clinkx-workflows/dist/validators/council.d.ts +318 -85
  269. package/clinkx-workflows/dist/validators/council.js +52 -13
  270. package/clinkx-workflows/dist/validators/council.js.map +1 -1
  271. package/clinkx-workflows/dist/validators/index.d.ts +1 -0
  272. package/clinkx-workflows/dist/validators/index.js +3 -0
  273. package/clinkx-workflows/dist/validators/index.js.map +1 -1
  274. package/clinkx-workflows/dist/validators/plan-review.d.ts +398 -0
  275. package/clinkx-workflows/dist/validators/plan-review.js +316 -0
  276. package/clinkx-workflows/dist/validators/plan-review.js.map +1 -0
  277. package/clinkx-workflows/dist/workflow-receipt.js +188 -4
  278. package/clinkx-workflows/dist/workflow-receipt.js.map +1 -1
  279. package/clinkx-workflows/dist/workflow-tools.d.ts +48 -3
  280. package/clinkx-workflows/dist/workflow-tools.js +1402 -59
  281. package/clinkx-workflows/dist/workflow-tools.js.map +1 -1
  282. package/clinkx-workflows/dist/workspace-signature.d.ts +16 -0
  283. package/clinkx-workflows/dist/workspace-signature.js +388 -0
  284. package/clinkx-workflows/dist/workspace-signature.js.map +1 -0
  285. package/clinkx-workflows/templates/council/answer-specialist.txt +2 -1
  286. package/clinkx-workflows/templates/council/code-review-chairman.txt +13 -0
  287. package/clinkx-workflows/templates/council/code-review-cross-reviewer.txt +4 -1
  288. package/clinkx-workflows/templates/council/code-review-reviewer.txt +19 -3
  289. package/clinkx-workflows/templates/council/debug-debugger.txt +2 -1
  290. package/clinkx-workflows/templates/council/discover-analyst.txt +2 -1
  291. package/clinkx-workflows/templates/council/member.txt +2 -1
  292. package/clinkx-workflows/templates/council/plan-review-chairman.txt +68 -0
  293. package/clinkx-workflows/templates/council/plan-review-cross-reviewer.txt +51 -0
  294. package/clinkx-workflows/templates/council/plan-review-reviewer.txt +60 -0
  295. package/clinkx-workflows/templates/plan-review/chairman.txt +48 -0
  296. package/clinkx-workflows/templates/plan-review/cross-review.txt +52 -0
  297. package/clinkx-workflows/templates/plan-review/ranking.txt +16 -0
  298. package/clinkx-workflows/templates/plan-review/reviewer.txt +57 -0
  299. package/clinkx-workflows/workflows/council-answer.yaml +97 -8
  300. package/clinkx-workflows/workflows/council-code-review.yaml +103 -8
  301. package/clinkx-workflows/workflows/council-debug.yaml +97 -8
  302. package/clinkx-workflows/workflows/council-default.yaml +97 -8
  303. package/clinkx-workflows/workflows/council-discover.yaml +97 -8
  304. package/clinkx-workflows/workflows/council-plan-review.yaml +250 -0
  305. package/clinkx-workflows/workflows/plan-review-receipts.yaml +230 -0
  306. package/conf/adapters/agy.json +49 -0
  307. package/conf/adapters/claude.json +5 -0
  308. package/conf/adapters/codex-app-server.json +44 -0
  309. package/conf/adapters/codex.json +7 -2
  310. package/conf/adapters/glm.json +5 -0
  311. package/conf/adapters/grok.json +60 -0
  312. package/conf/adapters/hapi/agy.json +52 -0
  313. package/conf/adapters/hapi/claude.json +5 -0
  314. package/conf/adapters/hapi/codex.json +7 -2
  315. package/conf/adapters/hapi/glm.json +5 -0
  316. package/conf/prompts/json-codereviewer.txt +1 -0
  317. package/conf/prompts/json.txt +1 -0
  318. package/dist/activity-journal.d.ts +42 -0
  319. package/dist/activity-journal.js +381 -0
  320. package/dist/activity-journal.js.map +1 -0
  321. package/dist/allowed-roots.d.ts +53 -0
  322. package/dist/allowed-roots.js +216 -0
  323. package/dist/allowed-roots.js.map +1 -0
  324. package/dist/codex/activity-session-log.d.ts +14 -0
  325. package/dist/codex/activity-session-log.js +206 -0
  326. package/dist/codex/activity-session-log.js.map +1 -0
  327. package/dist/codex/agent-tools.d.ts +23 -0
  328. package/dist/codex/agent-tools.js +1270 -0
  329. package/dist/codex/agent-tools.js.map +1 -0
  330. package/dist/codex/app-server-client.d.ts +76 -0
  331. package/dist/codex/app-server-client.js +582 -0
  332. package/dist/codex/app-server-client.js.map +1 -0
  333. package/dist/codex/app-server-registry.d.ts +389 -0
  334. package/dist/codex/app-server-registry.js +1407 -0
  335. package/dist/codex/app-server-registry.js.map +1 -0
  336. package/dist/codex/client-types.d.ts +109 -0
  337. package/dist/codex/client-types.js +2 -0
  338. package/dist/codex/client-types.js.map +1 -0
  339. package/dist/codex/protocol.d.ts +5375 -0
  340. package/dist/codex/protocol.js +614 -0
  341. package/dist/codex/protocol.js.map +1 -0
  342. package/dist/codex/session.d.ts +263 -0
  343. package/dist/codex/session.js +466 -0
  344. package/dist/codex/session.js.map +1 -0
  345. package/dist/config.d.ts +185 -19
  346. package/dist/config.js +82 -16
  347. package/dist/config.js.map +1 -1
  348. package/dist/context-builder/anchors.d.ts +12 -0
  349. package/dist/context-builder/anchors.js +22 -0
  350. package/dist/context-builder/anchors.js.map +1 -0
  351. package/dist/context-builder/bundle.d.ts +109 -0
  352. package/dist/context-builder/bundle.js +1498 -0
  353. package/dist/context-builder/bundle.js.map +1 -0
  354. package/dist/context-builder/codemap.d.ts +22 -0
  355. package/dist/context-builder/codemap.js +675 -0
  356. package/dist/context-builder/codemap.js.map +1 -0
  357. package/dist/context-builder/config.d.ts +61 -0
  358. package/dist/context-builder/config.js +216 -0
  359. package/dist/context-builder/config.js.map +1 -0
  360. package/dist/context-builder/context-run-catalog.d.ts +17 -0
  361. package/dist/context-builder/context-run-catalog.js +99 -0
  362. package/dist/context-builder/context-run-catalog.js.map +1 -0
  363. package/dist/context-builder/delegate-packet.d.ts +219 -0
  364. package/dist/context-builder/delegate-packet.js +1190 -0
  365. package/dist/context-builder/delegate-packet.js.map +1 -0
  366. package/dist/context-builder/delegate-reconstruction.d.ts +13 -0
  367. package/dist/context-builder/delegate-reconstruction.js +220 -0
  368. package/dist/context-builder/delegate-reconstruction.js.map +1 -0
  369. package/dist/context-builder/delegate-validation.d.ts +27 -0
  370. package/dist/context-builder/delegate-validation.js +479 -0
  371. package/dist/context-builder/delegate-validation.js.map +1 -0
  372. package/dist/context-builder/delegate.d.ts +26 -0
  373. package/dist/context-builder/delegate.js +134 -0
  374. package/dist/context-builder/delegate.js.map +1 -0
  375. package/dist/context-builder/diagnostics.d.ts +36 -0
  376. package/dist/context-builder/diagnostics.js +107 -0
  377. package/dist/context-builder/diagnostics.js.map +1 -0
  378. package/dist/context-builder/discovery.d.ts +146 -0
  379. package/dist/context-builder/discovery.js +2129 -0
  380. package/dist/context-builder/discovery.js.map +1 -0
  381. package/dist/context-builder/globs.d.ts +14 -0
  382. package/dist/context-builder/globs.js +175 -0
  383. package/dist/context-builder/globs.js.map +1 -0
  384. package/dist/context-builder/handler.d.ts +41 -0
  385. package/dist/context-builder/handler.js +2590 -0
  386. package/dist/context-builder/handler.js.map +1 -0
  387. package/dist/context-builder/lifecycle-contract.d.ts +424 -0
  388. package/dist/context-builder/lifecycle-contract.js +523 -0
  389. package/dist/context-builder/lifecycle-contract.js.map +1 -0
  390. package/dist/context-builder/lifecycle-journal.d.ts +62 -0
  391. package/dist/context-builder/lifecycle-journal.js +88 -0
  392. package/dist/context-builder/lifecycle-journal.js.map +1 -0
  393. package/dist/context-builder/lifecycle-observer.d.ts +102 -0
  394. package/dist/context-builder/lifecycle-observer.js +258 -0
  395. package/dist/context-builder/lifecycle-observer.js.map +1 -0
  396. package/dist/context-builder/lifecycle-reconcile.d.ts +32 -0
  397. package/dist/context-builder/lifecycle-reconcile.js +191 -0
  398. package/dist/context-builder/lifecycle-reconcile.js.map +1 -0
  399. package/dist/context-builder/lifecycle-recovery.d.ts +2 -0
  400. package/dist/context-builder/lifecycle-recovery.js +14 -0
  401. package/dist/context-builder/lifecycle-recovery.js.map +1 -0
  402. package/dist/context-builder/path-signals.d.ts +7 -0
  403. package/dist/context-builder/path-signals.js +70 -0
  404. package/dist/context-builder/path-signals.js.map +1 -0
  405. package/dist/context-builder/paths.d.ts +30 -0
  406. package/dist/context-builder/paths.js +153 -0
  407. package/dist/context-builder/paths.js.map +1 -0
  408. package/dist/context-builder/progress.d.ts +17 -0
  409. package/dist/context-builder/progress.js +93 -0
  410. package/dist/context-builder/progress.js.map +1 -0
  411. package/dist/context-builder/receipt.d.ts +91 -0
  412. package/dist/context-builder/receipt.js +180 -0
  413. package/dist/context-builder/receipt.js.map +1 -0
  414. package/dist/context-builder/refine-handler.d.ts +14 -0
  415. package/dist/context-builder/refine-handler.js +790 -0
  416. package/dist/context-builder/refine-handler.js.map +1 -0
  417. package/dist/context-builder/review-coverage.d.ts +84 -0
  418. package/dist/context-builder/review-coverage.js +437 -0
  419. package/dist/context-builder/review-coverage.js.map +1 -0
  420. package/dist/context-builder/run-id.d.ts +7 -0
  421. package/dist/context-builder/run-id.js +31 -0
  422. package/dist/context-builder/run-id.js.map +1 -0
  423. package/dist/context-builder/schema.d.ts +669 -0
  424. package/dist/context-builder/schema.js +399 -0
  425. package/dist/context-builder/schema.js.map +1 -0
  426. package/dist/context-builder/secrets.d.ts +41 -0
  427. package/dist/context-builder/secrets.js +1651 -0
  428. package/dist/context-builder/secrets.js.map +1 -0
  429. package/dist/context-builder/selection.d.ts +56 -0
  430. package/dist/context-builder/selection.js +1111 -0
  431. package/dist/context-builder/selection.js.map +1 -0
  432. package/dist/context-builder/session-log-identity.d.ts +1 -0
  433. package/dist/context-builder/session-log-identity.js +66 -0
  434. package/dist/context-builder/session-log-identity.js.map +1 -0
  435. package/dist/context-builder/slice-range-math.d.ts +16 -0
  436. package/dist/context-builder/slice-range-math.js +137 -0
  437. package/dist/context-builder/slice-range-math.js.map +1 -0
  438. package/dist/context-builder/status.d.ts +66 -0
  439. package/dist/context-builder/status.js +160 -0
  440. package/dist/context-builder/status.js.map +1 -0
  441. package/dist/context-builder/storage.d.ts +118 -0
  442. package/dist/context-builder/storage.js +1003 -0
  443. package/dist/context-builder/storage.js.map +1 -0
  444. package/dist/context-builder/token-estimate.d.ts +19 -0
  445. package/dist/context-builder/token-estimate.js +90 -0
  446. package/dist/context-builder/token-estimate.js.map +1 -0
  447. package/dist/context-builder/versions.d.ts +29 -0
  448. package/dist/context-builder/versions.js +30 -0
  449. package/dist/context-builder/versions.js.map +1 -0
  450. package/dist/context-builder/workflow-runner.d.ts +69 -0
  451. package/dist/context-builder/workflow-runner.js +1051 -0
  452. package/dist/context-builder/workflow-runner.js.map +1 -0
  453. package/dist/context-builder/write-watchdog.d.ts +14 -0
  454. package/dist/context-builder/write-watchdog.js +38 -0
  455. package/dist/context-builder/write-watchdog.js.map +1 -0
  456. package/dist/dashboard-adapter-catalog.d.ts +9 -0
  457. package/dist/dashboard-adapter-catalog.js +46 -0
  458. package/dist/dashboard-adapter-catalog.js.map +1 -0
  459. package/dist/dashboard-cli.d.ts +79 -0
  460. package/dist/dashboard-cli.js +585 -0
  461. package/dist/dashboard-cli.js.map +1 -0
  462. package/dist/dashboard-companion/client.d.ts +36 -0
  463. package/dist/dashboard-companion/client.js +210 -0
  464. package/dist/dashboard-companion/client.js.map +1 -0
  465. package/dist/dashboard-companion/config.d.ts +8 -0
  466. package/dist/dashboard-companion/config.js +52 -0
  467. package/dist/dashboard-companion/config.js.map +1 -0
  468. package/dist/dashboard-companion/http-proxy.d.ts +10 -0
  469. package/dist/dashboard-companion/http-proxy.js +234 -0
  470. package/dist/dashboard-companion/http-proxy.js.map +1 -0
  471. package/dist/dashboard-companion/protocol.d.ts +80 -0
  472. package/dist/dashboard-companion/protocol.js +455 -0
  473. package/dist/dashboard-companion/protocol.js.map +1 -0
  474. package/dist/dashboard-companion/standalone-assets.d.ts +12 -0
  475. package/dist/dashboard-companion/standalone-assets.js +72 -0
  476. package/dist/dashboard-companion/standalone-assets.js.map +1 -0
  477. package/dist/dashboard-companion/standalone-entry.d.ts +1 -0
  478. package/dist/dashboard-companion/standalone-entry.js +17 -0
  479. package/dist/dashboard-companion/standalone-entry.js.map +1 -0
  480. package/dist/dashboard-gateway/config.d.ts +9 -0
  481. package/dist/dashboard-gateway/config.js +44 -0
  482. package/dist/dashboard-gateway/config.js.map +1 -0
  483. package/dist/dashboard-gateway/index.d.ts +11 -0
  484. package/dist/dashboard-gateway/index.js +755 -0
  485. package/dist/dashboard-gateway/index.js.map +1 -0
  486. package/dist/dashboard-gateway/main.d.ts +1 -0
  487. package/dist/dashboard-gateway/main.js +25 -0
  488. package/dist/dashboard-gateway/main.js.map +1 -0
  489. package/dist/dashboard-pairing-token.d.ts +1 -0
  490. package/dist/dashboard-pairing-token.js +5 -0
  491. package/dist/dashboard-pairing-token.js.map +1 -0
  492. package/dist/elaris/client.d.ts +73 -0
  493. package/dist/elaris/client.js +825 -0
  494. package/dist/elaris/client.js.map +1 -0
  495. package/dist/elaris/config.d.ts +18 -0
  496. package/dist/elaris/config.js +75 -0
  497. package/dist/elaris/config.js.map +1 -0
  498. package/dist/elaris/errors.d.ts +41 -0
  499. package/dist/elaris/errors.js +205 -0
  500. package/dist/elaris/errors.js.map +1 -0
  501. package/dist/elaris/github-repo.d.ts +18 -0
  502. package/dist/elaris/github-repo.js +89 -0
  503. package/dist/elaris/github-repo.js.map +1 -0
  504. package/dist/elaris/index.d.ts +10 -0
  505. package/dist/elaris/index.js +11 -0
  506. package/dist/elaris/index.js.map +1 -0
  507. package/dist/elaris/redact.d.ts +89 -0
  508. package/dist/elaris/redact.js +423 -0
  509. package/dist/elaris/redact.js.map +1 -0
  510. package/dist/elaris/run-id.d.ts +5 -0
  511. package/dist/elaris/run-id.js +20 -0
  512. package/dist/elaris/run-id.js.map +1 -0
  513. package/dist/elaris/status.d.ts +45 -0
  514. package/dist/elaris/status.js +122 -0
  515. package/dist/elaris/status.js.map +1 -0
  516. package/dist/elaris/tool-schemas.d.ts +314 -0
  517. package/dist/elaris/tool-schemas.js +208 -0
  518. package/dist/elaris/tool-schemas.js.map +1 -0
  519. package/dist/elaris/tools.d.ts +73 -0
  520. package/dist/elaris/tools.js +1065 -0
  521. package/dist/elaris/tools.js.map +1 -0
  522. package/dist/elaris/types.d.ts +1541 -0
  523. package/dist/elaris/types.js +252 -0
  524. package/dist/elaris/types.js.map +1 -0
  525. package/dist/errors.d.ts +16 -0
  526. package/dist/errors.js +19 -0
  527. package/dist/errors.js.map +1 -1
  528. package/dist/handler.d.ts +4 -0
  529. package/dist/handler.js +10 -1
  530. package/dist/handler.js.map +1 -1
  531. package/dist/index.d.ts +2 -1
  532. package/dist/index.js +13 -3
  533. package/dist/index.js.map +1 -1
  534. package/dist/local-clink-backend.d.ts +3 -0
  535. package/dist/local-clink-backend.js +4 -0
  536. package/dist/local-clink-backend.js.map +1 -1
  537. package/dist/package-info.d.ts +2 -0
  538. package/dist/package-info.js +18 -0
  539. package/dist/package-info.js.map +1 -0
  540. package/dist/parsers/codex-jsonl.js +48 -3
  541. package/dist/parsers/codex-jsonl.js.map +1 -1
  542. package/dist/parsers/extract.d.ts +10 -1
  543. package/dist/parsers/extract.js +11 -3
  544. package/dist/parsers/extract.js.map +1 -1
  545. package/dist/paths.d.ts +8 -0
  546. package/dist/paths.js +31 -43
  547. package/dist/paths.js.map +1 -1
  548. package/dist/pipeline.d.ts +51 -3
  549. package/dist/pipeline.js +264 -47
  550. package/dist/pipeline.js.map +1 -1
  551. package/dist/run-dir.d.ts +24 -0
  552. package/dist/run-dir.js +201 -2
  553. package/dist/run-dir.js.map +1 -1
  554. package/dist/runner.d.ts +13 -0
  555. package/dist/runner.js +184 -15
  556. package/dist/runner.js.map +1 -1
  557. package/dist/safety.d.ts +4 -4
  558. package/dist/safety.js +4 -4
  559. package/dist/schema.d.ts +7 -7
  560. package/dist/server.d.ts +2 -0
  561. package/dist/server.js +396 -24
  562. package/dist/server.js.map +1 -1
  563. package/dist/session-log-stream.d.ts +18 -0
  564. package/dist/session-log-stream.js +319 -0
  565. package/dist/session-log-stream.js.map +1 -0
  566. package/package.json +47 -5
  567. package/conf/adapters/gemini.json +0 -44
  568. package/conf/adapters/hapi/gemini.json +0 -47
  569. package/dist/artifacts.d.ts +0 -9
  570. package/dist/artifacts.js +0 -24
  571. package/dist/artifacts.js.map +0 -1
  572. package/dist/parsers/gemini-json.d.ts +0 -2
  573. package/dist/parsers/gemini-json.js +0 -72
  574. package/dist/parsers/gemini-json.js.map +0 -1
@@ -5,23 +5,27 @@
5
5
  * - Standalone STDIO server (ClinkSession child process backend)
6
6
  * - In-process integration (LocalClinkBackend via root ClinkX)
7
7
  *
8
- * The `registerWorkflowTools()` function registers three tools on a Server:
8
+ * The `registerWorkflowTools()` function registers five tools on a Server:
9
9
  * - run_workflow: start a new workflow execution
10
10
  * - resume_workflow: resume a failed/cancelled run
11
11
  * - list_workflows: list available workflow definitions
12
+ * - workflow_run_status: read workflow run progress/result
13
+ * - cancel_workflow: cancel a live or orphaned workflow run
12
14
  *
13
15
  * The caller provides a `createBackend` factory that determines how clink
14
16
  * calls are dispatched — either via child process or in-process.
15
17
  */
16
18
  import { CallToolRequestSchema, ListToolsRequestSchema, ErrorCode, McpError, } from "@modelcontextprotocol/sdk/types.js";
19
+ import { existsSync } from "node:fs";
20
+ import { basename, dirname, join } from "node:path";
17
21
  import { loadWorkflow, listWorkflowCatalog } from "./loader.js";
18
22
  import { WorkflowEngine } from "./engine.js";
19
- import { WorkflowStateStore } from "./state.js";
23
+ import { readRunStateForStatus, reapRunCancelled, WorkflowStateStore } from "./state.js";
24
+ import { parseContextBuilderLifecycleStateV1, } from "./context-builder-lifecycle-contract.js";
20
25
  import { validateRunId } from "./run-id.js";
21
- import { getTemplatesPath } from "./config.js";
26
+ import { getMaxPromptBytes, getRunStateDir, getTemplatesPath, getWorkflowsContextDefault, getWorkflowsContextDefaultCurator, getWorkflowsMaxConcurrentAsync, resolveWorkspaceRoot, } from "./config.js";
22
27
  import { WorkflowCancellationError, } from "./errors.js";
23
28
  import { shutdownCoordinator } from "./shutdown.js";
24
- import { basename } from "node:path";
25
29
  import { logger } from "./logger.js";
26
30
  import { createValidatorRegistry } from "./validators/index.js";
27
31
  import { deriveDecisiveFailure } from "./run-summary-derived.js";
@@ -29,20 +33,93 @@ import { buildRunSummaryOrUndefined, buildFailureRunSummary } from "./run-summar
29
33
  import { extractCard } from "./run-summary/cards/index.js";
30
34
  import { renderWorkflowReceipt } from "./workflow-receipt.js";
31
35
  import { supportsRuntimeMembers, COUNCIL_RUNTIME_MEMBER_WORKFLOWS, expandCouncilComposition } from "./council-expander.js";
36
+ import { resolveWorkflowContextMode, WorkflowContextModeResolverError, } from "./context-mode-resolver.js";
37
+ import { WorkflowDelegateValidationError } from "./delegate-validate.js";
38
+ import { contextSearchTermsValidationError, contextSeedSourceValidationError, resolveSeedSource, } from "./seed-terms.js";
39
+ import { LEGACY_PLAN_REVIEW_RECEIPTS_WORKFLOW_NAME, PLAN_REVIEW_RECEIPTS_WORKFLOW, admitCouncilPlanReview, assertPlanReviewPromptBudget, isPlanReviewRunState, } from "./plan-review/admission.js";
40
+ import { PLAN_REVIEW_V1_CLI_NAMES } from "./plan-review/contract.js";
41
+ import { buildCouncilPlanReviewDiscovery } from "./plan-review/discovery.js";
42
+ import { expandCouncilPlanReview } from "./plan-review/expander.js";
43
+ import { buildPlanReviewContextPolicy, findRejectedPlanContextAnchors, } from "./plan-review/context-policy.js";
44
+ import { PLAN_SNAPSHOT_RELPATH, persistCanonicalPlanSnapshot, readCanonicalPlanSnapshot, writeExecutionContractSnapshot, } from "./plan-review/snapshot.js";
45
+ import { deriveSeatMappings, reconstructPlanReviewCallBindings, } from "./plan-review/run-state.js";
46
+ import { restorePlanReviewRun, } from "./plan-review/resume.js";
47
+ import { PlanReviewStageError } from "./plan-review/stage-error.js";
48
+ import { CouncilPlanReviewErrorSchema, createPlanReviewPublicationError, inspectPlanReviewPublication, } from "./plan-review/publication.js";
49
+ import { validatePlanReviewCallBindings, validatePlanReviewPreparedInputPolicy, } from "./plan-review/runtime-policy.js";
50
+ import { ContainedFilePolicyError, readContainedRelativeFile, } from "./contained-file.js";
51
+ import { createDashboardRunReader } from "./dashboard/read-model.js";
52
+ async function inspectLegacyWorkflowSessionLogGeneration(runDir, runId) {
53
+ try {
54
+ const result = await createDashboardRunReader().inspectGeneration?.(dirname(runDir), runId);
55
+ if (result?.kind === "present")
56
+ return result.runGeneration;
57
+ logger.debug({
58
+ run_id: runId,
59
+ reason: result === undefined ? "unsupported" : result.kind,
60
+ }, "workflow session-log generation unavailable; capture disabled");
61
+ }
62
+ catch {
63
+ logger.debug({ run_id: runId, reason: "inspection_failed" }, "workflow session-log generation unavailable; capture disabled");
64
+ }
65
+ return undefined;
66
+ }
67
+ /**
68
+ * Allowed roots for reading the caller's `plan_file`.
69
+ *
70
+ * Deliberately the workspace root ONLY. The state/artifact dirs are
71
+ * artifact-only roots — they are where runtime-owned output goes, never a
72
+ * source of reviewable plan text.
73
+ */
74
+ function planFileAllowedRoots() {
75
+ return [resolveWorkspaceRoot()];
76
+ }
77
+ function createPreflightOnlyBackend(advertisedCliNames) {
78
+ return {
79
+ cliNames: [...(advertisedCliNames ?? [])],
80
+ roleNames: [],
81
+ isTerminated: false,
82
+ hasChildDied: false,
83
+ connect: async () => { },
84
+ callTool: async () => {
85
+ throw new Error("preflight-only backend cannot dispatch provider calls");
86
+ },
87
+ terminate: async () => { },
88
+ };
89
+ }
32
90
  // ---------------------------------------------------------------------------
33
91
  // Admission control
34
92
  // ---------------------------------------------------------------------------
35
- let activeMutex = null;
36
- function acquireMutex(runId) {
37
- if (activeMutex != null) {
38
- throw new McpError(ErrorCode.InvalidRequest, `A workflow is already running (run_id: "${activeMutex.runId}", started: ${activeMutex.startedAt}). ` +
39
- `Wait for it to complete or cancel it before starting another. ` +
40
- `Only one workflow can run at a time per server instance.`);
41
- }
42
- activeMutex = { runId, startedAt: new Date().toISOString() };
93
+ const activeRuns = new Map();
94
+ /**
95
+ * In-process registry of AbortControllers for live async runs, keyed by run_id.
96
+ * cancel_workflow aborts the controller to drive the run's own cancellation
97
+ * unwind. Registered at async admission (foreground) and deleted in the detached
98
+ * run's finally (a delete on an absent key is a harmless no-op, so the
99
+ * synchronous run path is unaffected).
100
+ */
101
+ const cancelRegistry = new Map();
102
+ export function resolveWorkflowAsyncAdmissionCap(options) {
103
+ const injected = options?.asyncAdmissionCap;
104
+ if (typeof injected === "number" &&
105
+ Number.isSafeInteger(injected) &&
106
+ injected >= 1) {
107
+ return injected;
108
+ }
109
+ return getWorkflowsMaxConcurrentAsync();
110
+ }
111
+ function acquireMutex(runId, options) {
112
+ if (activeRuns.has(runId)) {
113
+ throw new McpError(ErrorCode.InvalidParams, `run_id ${runId} already running`);
114
+ }
115
+ const maxConcurrent = resolveWorkflowAsyncAdmissionCap(options);
116
+ if (activeRuns.size >= maxConcurrent) {
117
+ throw new McpError(ErrorCode.InvalidRequest, `max ${maxConcurrent} concurrent workflow(s); retry later`);
118
+ }
119
+ activeRuns.set(runId, { startedAt: new Date().toISOString() });
43
120
  }
44
- function releaseMutex() {
45
- activeMutex = null;
121
+ function releaseMutex(runId) {
122
+ activeRuns.delete(runId);
46
123
  }
47
124
  export function parseMembersOverride(raw, knownCliNames) {
48
125
  if (raw == null)
@@ -125,6 +202,44 @@ export function parseChairmanOverride(raw, knownCliNames) {
125
202
  }
126
203
  return trimmed;
127
204
  }
205
+ /**
206
+ * Point a generic-council caller at the input shape the workflow actually takes.
207
+ *
208
+ * Appended by the boundary decorator wrapping the whole pre-mutex admission
209
+ * block, so it rides on ANY `InvalidParams` rejection of a generic-shaped
210
+ * `plan-review-receipts` request — not only the `members`/`chairman` roster
211
+ * gates. A generic `user_request`-shaped caller can trip several independent
212
+ * pre-state checks (the roster-override parsers themselves, the two roster
213
+ * gates, unknown `user_request`/`cb_delegate` variables, context-mode and root
214
+ * validation, the conditional `cb_task` seed, task-secret validation, and the
215
+ * missing typed `plan_review`); whichever fires first now names the
216
+ * destination, instead of the caller re-submitting and being told something
217
+ * different each round trip.
218
+ *
219
+ * The decorator is gated on a RAW-`args` shape predicate, not on parsed
220
+ * values, specifically because a parser throw means no parsed value exists
221
+ * yet: `parseMembersOverride` rejects `[]` and unknown `cli_name`s before
222
+ * `parsedMembers` is ever bound, and that is exactly a case the hint must
223
+ * cover.
224
+ *
225
+ * Deliberately a HINT appended to the existing message, not a reordered check.
226
+ * Hoisting the `plan_review` requirement above the roster gate would mask
227
+ * malformed roster overrides behind a missing-contract error, and split the
228
+ * requirement from its authoritative mirror in `admitCouncilPlanReview`. The
229
+ * decorator changes no ordering, so that reasoning still holds.
230
+ */
231
+ function planReviewShapeHint() {
232
+ return (`. ${PLAN_REVIEW_RECEIPTS_WORKFLOW} takes a typed "plan_review" contract instead:` +
233
+ ` the roster is plan_review.seats[] and the chairman is plan_review.chairman_seat` +
234
+ ` (a seat_id, not an adapter name). It declares neither a "user_request" nor a` +
235
+ ` "cb_delegate" variable, and its "cb_task" seed is conditionally required:` +
236
+ ` trimmed-nonblank whenever context preflight is available (the default` +
237
+ ` deployment), and genuinely optional only on a server where preflight is` +
238
+ ` unavailable. It is driven by the plan-review-loop skill, not a generic` +
239
+ ` council template. For a generic members/chairman/user_request plan review` +
240
+ ` without the typed contract, use the "council-plan-review" workflow; the` +
241
+ ` typed plan_review contract belongs only to "plan-review-receipts".`);
242
+ }
128
243
  // ---------------------------------------------------------------------------
129
244
  // Variable validation (6-6)
130
245
  // ---------------------------------------------------------------------------
@@ -136,7 +251,8 @@ function formatExpectedVariables(schema) {
136
251
  }
137
252
  return parts.length > 0 ? `Expected variables: ${parts.join(", ")}` : "";
138
253
  }
139
- function validateVariables(supplied, schema) {
254
+ /** @internal Shared with the compiled producer-fixture admission check. */
255
+ export function validateVariables(supplied, schema) {
140
256
  const resolved = {};
141
257
  const errors = [];
142
258
  for (const [name, def] of Object.entries(schema)) {
@@ -174,6 +290,65 @@ function validateVariables(supplied, schema) {
174
290
  }
175
291
  return resolved;
176
292
  }
293
+ function validateContextBuilderAnchorSufficiency(variables, runner, contextDefault, defaultCurator) {
294
+ if (runner == null) {
295
+ return undefined;
296
+ }
297
+ let policy;
298
+ try {
299
+ // Throws on an invalid cb_context_mode / cb_delegate combination (surfaced
300
+ // below as -32602). The resolved policy is also returned for admission-time
301
+ // checks that must stay inert under soft off.
302
+ policy = resolveWorkflowContextMode(variables["cb_context_mode"], contextDefault, true, variables["cb_delegate"], defaultCurator);
303
+ }
304
+ catch (error) {
305
+ if (error instanceof WorkflowContextModeResolverError ||
306
+ error instanceof WorkflowDelegateValidationError) {
307
+ throw new McpError(ErrorCode.InvalidParams, error.message);
308
+ }
309
+ throw error;
310
+ }
311
+ const cbRoots = readStringArrayForAnchorCheck(variables["cb_roots"]);
312
+ // Eager allowlist validation: explicit cb_roots are validated whenever
313
+ // provided, including soft off. Out-of-allowlist roots are rejected at the
314
+ // boundary as -32602. The disabled hard kill never reaches this validator.
315
+ if (cbRoots.length > 0) {
316
+ try {
317
+ runner.assertRootsAllowed(cbRoots);
318
+ }
319
+ catch (err) {
320
+ throw new McpError(ErrorCode.InvalidParams, err instanceof Error ? err.message : String(err));
321
+ }
322
+ }
323
+ // policy.preflightDisabled is unreachable here: this validator early-returns
324
+ // when runner == null and always resolves with preflightEnabled:true, so the
325
+ // resolver never returns a disabled policy on this path.
326
+ return policy;
327
+ }
328
+ function validateContextSeedSourceAtAdmission(variables, policy) {
329
+ if (policy == null) {
330
+ return;
331
+ }
332
+ const cbTask = typeof variables["cb_task"] === "string"
333
+ ? variables["cb_task"]
334
+ : undefined;
335
+ const userRequest = typeof variables["user_request"] === "string"
336
+ ? variables["user_request"]
337
+ : undefined;
338
+ const seedSource = resolveSeedSource(cbTask, userRequest);
339
+ const seedSourceError = contextSeedSourceValidationError(seedSource);
340
+ if (seedSourceError != null) {
341
+ throw new McpError(ErrorCode.InvalidParams, seedSourceError);
342
+ }
343
+ const searchTerms = readStringArrayForAnchorCheck(variables["cb_search_terms"]);
344
+ const searchTermsError = contextSearchTermsValidationError(searchTerms);
345
+ if (searchTermsError != null) {
346
+ throw new McpError(ErrorCode.InvalidParams, searchTermsError);
347
+ }
348
+ }
349
+ function readStringArrayForAnchorCheck(value) {
350
+ return Array.isArray(value) && value.every((entry) => typeof entry === "string") ? value : [];
351
+ }
177
352
  function validateVariableType(name, value, expectedType) {
178
353
  switch (expectedType) {
179
354
  case "string":
@@ -597,6 +772,37 @@ function finalizeWorkflowPresentation(options) {
597
772
  }
598
773
  return content;
599
774
  }
775
+ /**
776
+ * Wire projection of the warn-only duplicate-replay signal recorded at
777
+ * `WorkflowStateStore.create()` (failure investigation F6). Additive and
778
+ * advisory: identical snapshot hashes do NOT imply an identical outcome —
779
+ * workspace content is not covered by the hashes and providers are
780
+ * nondeterministic — so this field never gates admission or execution.
781
+ * Defensive optional access: partial store mocks may omit the property.
782
+ */
783
+ function buildDuplicateOfFailedRunField(store) {
784
+ const signal = store?.duplicateOfFailedRun;
785
+ if (signal === undefined || signal === null)
786
+ return undefined;
787
+ return {
788
+ run_id: signal.runId,
789
+ ...(signal.completedAt !== undefined ? { completed_at: signal.completedAt } : {}),
790
+ };
791
+ }
792
+ /**
793
+ * Appends the duplicate-replay signal as a trailing additive content block on
794
+ * sync run_workflow results (success and error). No block is added when no
795
+ * signal was detected, so existing content ordering is untouched.
796
+ */
797
+ function appendDuplicateReplayBlock(content, store) {
798
+ const duplicateOfFailedRun = buildDuplicateOfFailedRunField(store);
799
+ if (duplicateOfFailedRun === undefined)
800
+ return;
801
+ content.push({
802
+ type: "text",
803
+ text: JSON.stringify({ duplicate_of_failed_run: duplicateOfFailedRun }),
804
+ });
805
+ }
600
806
  // ---------------------------------------------------------------------------
601
807
  // Actionable error formatting (5-6)
602
808
  // ---------------------------------------------------------------------------
@@ -655,15 +861,46 @@ function formatWorkflowError(runId, error, store, workflowSource) {
655
861
  let nextAction = "";
656
862
  if (store != null) {
657
863
  const isResumable = state?.resumable === true;
658
- nextAction = isResumable
659
- ? `Use resume_workflow with run_id "${runId}" to retry from the last checkpoint.`
660
- : `Start a new run with run_workflow (this run is not resumable — set CLINKX_WORKFLOWS_CHILD_CONFIG_PATH for resumable runs).`;
864
+ const isTerminalPlanReview = state != null &&
865
+ isPlanReviewRunState(state) &&
866
+ (state.status === "failed" ||
867
+ state.status === "cancelled" ||
868
+ state.status === "succeeded");
869
+ nextAction = isTerminalPlanReview
870
+ ? `Start a new run with run_workflow and a new run_id; terminal plan-review-receipts runs are immutable.`
871
+ : isResumable
872
+ ? `Use resume_workflow with run_id "${runId}" to retry from the last checkpoint.`
873
+ : `Start a new run with run_workflow (this run is not resumable — set CLINKX_WORKFLOWS_CHILD_CONFIG_PATH for resumable runs).`;
661
874
  }
662
875
  return (`Workflow failed [run_id: ${runId}]${sourceLabel}${location}` +
663
876
  (retryCount > 0 ? ` (after ${String(retryCount)} retries)` : "") +
664
877
  `: ${message}${metaBlock}` +
665
878
  (nextAction !== "" ? `\n\n${nextAction}` : ""));
666
879
  }
880
+ function toCouncilPlanReviewError(runId, error) {
881
+ if (!(error instanceof PlanReviewStageError)) {
882
+ return undefined;
883
+ }
884
+ return CouncilPlanReviewErrorSchema.parse({
885
+ schema_version: "council-plan-review-error-v1",
886
+ run_id: runId,
887
+ execution_status: "failed",
888
+ stage: error.stage,
889
+ error: {
890
+ reason: error.reason,
891
+ detail: error.message.slice(0, 2_048),
892
+ },
893
+ });
894
+ }
895
+ async function markWorkflowFailed(store, runId, error) {
896
+ const message = error instanceof Error ? error.message : String(error);
897
+ const planReviewError = toCouncilPlanReviewError(runId, error);
898
+ if (planReviewError === undefined) {
899
+ await store.markFailed(message);
900
+ return;
901
+ }
902
+ await store.markFailed(message, undefined, planReviewError);
903
+ }
667
904
  function getCallEntryByLocation(stages, location) {
668
905
  const dotIndex = location.indexOf(".");
669
906
  if (dotIndex === -1) {
@@ -704,30 +941,350 @@ async function executeRunWorkflow(server, options, args, signal, progressToken)
704
941
  const knownCliNames = options.availableCliNames != null
705
942
  ? new Set(options.availableCliNames)
706
943
  : undefined;
707
- const parsedMembers = parseMembersOverride(args["members"], knownCliNames);
708
- const parsedChairman = parseChairmanOverride(args["chairman"], knownCliNames);
709
- if (parsedMembers != null && !COUNCIL_RUNTIME_MEMBER_WORKFLOWS.has(workflow)) {
710
- throw new McpError(ErrorCode.InvalidParams, `members parameter is not supported for workflow "${workflow}"`);
944
+ // Read RAW `args`, not parsed values: `parseMembersOverride` throws on `[]`
945
+ // and on an unknown `cli_name`, so in exactly the cases the hint exists to
946
+ // cover, `parsedMembers` is never bound.
947
+ const genericShaped = workflow === PLAN_REVIEW_RECEIPTS_WORKFLOW &&
948
+ (args["plan_review"] == null ||
949
+ args["members"] != null ||
950
+ args["chairman"] != null);
951
+ // Declarations hoisted out of the decorator's try so the bindings stay in
952
+ // scope for the post-admission code below; the assignments and their order
953
+ // are unchanged.
954
+ let parsedMembers;
955
+ let parsedChairman;
956
+ let resolvedVariables;
957
+ let initialContextPolicy;
958
+ let planReviewAdmission;
959
+ try {
960
+ parsedMembers = parseMembersOverride(args["members"], knownCliNames);
961
+ parsedChairman = parseChairmanOverride(args["chairman"], knownCliNames);
962
+ if (parsedMembers != null && !COUNCIL_RUNTIME_MEMBER_WORKFLOWS.has(workflow)) {
963
+ throw new McpError(ErrorCode.InvalidParams, `members parameter is not supported for workflow "${workflow}"`);
964
+ }
965
+ if (parsedChairman != null && !COUNCIL_RUNTIME_MEMBER_WORKFLOWS.has(workflow)) {
966
+ throw new McpError(ErrorCode.InvalidParams, `chairman parameter is not supported for workflow "${workflow}"`);
967
+ }
968
+ resolvedVariables = validateVariables(variables, loaded.definition.variables);
969
+ // When the runner is absent (preflight disabled / hard kill), the boundary
970
+ // validator is inert. Skip it WITHOUT evaluating the context-default env
971
+ // parsers — getWorkflowsContextDefaultCurator() throws on empty env, and a
972
+ // runner-less run_workflow must never trip context env parsing.
973
+ let contextPolicy;
974
+ if (options.contextBuilderRunner != null) {
975
+ contextPolicy = validateContextBuilderAnchorSufficiency(resolvedVariables, options.contextBuilderRunner, options.contextDefault ?? getWorkflowsContextDefault(), options.defaultCurator ?? getWorkflowsContextDefaultCurator());
976
+ }
977
+ validateContextSeedSourceAtAdmission(resolvedVariables, contextPolicy);
978
+ initialContextPolicy = contextPolicy ?? resolveWorkflowContextMode(undefined, options.contextDefault ?? "off", false, undefined, options.defaultCurator);
979
+ if (options.contextBuilderRunner != null) {
980
+ try {
981
+ options.contextBuilderRunner.assertTaskSecretsAllowed(resolvedVariables);
982
+ }
983
+ catch (err) {
984
+ if (err instanceof McpError) {
985
+ throw err;
986
+ }
987
+ throw new McpError(ErrorCode.InvalidParams, err instanceof Error ? err.message : String(err));
988
+ }
989
+ }
990
+ const planReviewCliNames = options.planReviewAvailableCliNames ?? options.availableCliNames;
991
+ planReviewAdmission = admitCouncilPlanReview({
992
+ workflow,
993
+ planReview: args["plan_review"],
994
+ variables: resolvedVariables,
995
+ ...(planReviewCliNames !== undefined
996
+ ? { availableCliNames: planReviewCliNames }
997
+ : {}),
998
+ });
711
999
  }
712
- if (parsedChairman != null && !COUNCIL_RUNTIME_MEMBER_WORKFLOWS.has(workflow)) {
713
- throw new McpError(ErrorCode.InvalidParams, `chairman parameter is not supported for workflow "${workflow}"`);
1000
+ catch (err) {
1001
+ // Append the hint by MUTATING the caught error and rethrowing that same
1002
+ // instance. Do NOT re-wrap it in `new McpError(err.code, err.message + …)`:
1003
+ // the SDK constructor prefixes its argument with `MCP error <code>: `, so
1004
+ // `err.message` already carries that prefix and re-constructing emits it
1005
+ // twice on the wire (`MCP error -32602: MCP error -32602: …`). Mutating in
1006
+ // place also preserves `code`, `data`, and the original stack.
1007
+ if (genericShaped && err instanceof McpError && err.code === ErrorCode.InvalidParams) {
1008
+ err.message = `${err.message}${planReviewShapeHint()}`;
1009
+ }
1010
+ throw err;
714
1011
  }
715
- const resolvedVariables = validateVariables(variables, loaded.definition.variables);
716
- acquireMutex(runId);
717
- let store = null;
1012
+ // A-7: reject caller context anchors that resolve to the bound plan or any
1013
+ // plan-workspace PLAN.md. Pre-mutex, so a rejected request creates no state:
1014
+ // the caller must not be able to hand actors a MUTABLE plan alongside the
1015
+ // immutable snapshot.
1016
+ let planReviewPlan;
1017
+ if (planReviewAdmission !== undefined) {
1018
+ const contextPolicy = buildPlanReviewContextPolicy(planReviewAdmission.input.plan_file);
1019
+ const rejected = findRejectedPlanContextAnchors(contextPolicy, resolvedVariables, resolveWorkspaceRoot());
1020
+ if (rejected.length > 0) {
1021
+ throw new McpError(ErrorCode.InvalidParams, `plan-review-receipts forbids plan context anchors; reviewers receive only the immutable snapshot. Offending entries: ${rejected
1022
+ .map((entry) => `${entry.variable}: ${entry.path}`)
1023
+ .join(", ")}`);
1024
+ }
1025
+ planReviewPlan = {
1026
+ admission: planReviewAdmission,
1027
+ contextPolicy,
1028
+ // Deterministic, runtime-owned: the definition must name the snapshot
1029
+ // before `create()` makes the run dir, so the path is derived rather than
1030
+ // discovered. A post-create guard asserts the store agrees.
1031
+ snapshotAbsolutePath: join(getRunStateDir(runId), PLAN_SNAPSHOT_RELPATH),
1032
+ };
1033
+ }
1034
+ acquireMutex(runId, options);
1035
+ const isAsync = args["async"] === true;
1036
+ if (isAsync) {
1037
+ // Everything between acquireMutex and the detached spawn must release the
1038
+ // mutex on ANY throw, or the run slot leaks permanently (bricks the server
1039
+ // at the default cap of 1).
1040
+ let store;
1041
+ let effectiveDefinition;
1042
+ let initializedPlanReview;
1043
+ let preparedSnapshot;
1044
+ try {
1045
+ const runDir = getRunStateDir(runId);
1046
+ if (existsSync(runDir)) {
1047
+ throw new McpError(ErrorCode.InvalidParams, `run_id ${runId} already has persisted state at ${runDir}`);
1048
+ }
1049
+ // Read + digest-verify the plan BEFORE composing: the expander inlines
1050
+ // the snapshot text, so it must exist by expansion time (D9).
1051
+ preparedSnapshot = planReviewPlan === undefined
1052
+ ? undefined
1053
+ : await preparePlanReviewSnapshot(planReviewPlan);
1054
+ // Expand composition ONCE in the foreground; the detached run reuses it.
1055
+ effectiveDefinition = expandRunWorkflowDefinition(loaded.normalized, workflow, parsedMembers, parsedChairman, planReviewPlan, preparedSnapshot);
1056
+ // Create the durable, pollable state record in the FOREGROUND so a failure
1057
+ // here surfaces synchronously (a real error, never a false "running").
1058
+ try {
1059
+ store = await WorkflowStateStore.create({
1060
+ runId,
1061
+ definition: effectiveDefinition,
1062
+ variables: resolvedVariables,
1063
+ debugKeepArtifacts: debug,
1064
+ async: true,
1065
+ initialContextPolicy,
1066
+ });
1067
+ }
1068
+ catch (err) {
1069
+ throw new McpError(ErrorCode.InternalError, `Failed to initialize async run ${runId}: ${err instanceof Error ? err.message : String(err)}`);
1070
+ }
1071
+ // A-10: the async acknowledgement is a durability boundary. A caller
1072
+ // cannot observe `running` until every plan-review snapshot and the
1073
+ // strict typed state subtree have committed successfully.
1074
+ if (planReviewPlan !== undefined) {
1075
+ initializedPlanReview = await initializePlanReviewRunState(store, planReviewPlan, effectiveDefinition, preparedSnapshot);
1076
+ }
1077
+ }
1078
+ catch (err) {
1079
+ if (store !== undefined && planReviewPlan !== undefined) {
1080
+ try {
1081
+ try {
1082
+ await markWorkflowFailed(store, runId, err);
1083
+ }
1084
+ catch (markFailedError) {
1085
+ logger.warn({ err: markFailedError }, "markFailed during async initialization error handling failed");
1086
+ }
1087
+ const errorText = formatWorkflowError(runId, err, store, {
1088
+ source: loaded.source,
1089
+ sourcePath: loaded.sourcePath,
1090
+ });
1091
+ let failureSummary;
1092
+ if (receipt) {
1093
+ try {
1094
+ failureSummary = buildFailureRunSummary(store);
1095
+ }
1096
+ catch (summaryError) {
1097
+ logger.warn({ err: summaryError }, "failure summary construction failed, omitting receipt");
1098
+ }
1099
+ }
1100
+ const content = finalizeWorkflowPresentation({
1101
+ outputText: errorText,
1102
+ receipt,
1103
+ debug,
1104
+ summary: failureSummary,
1105
+ debugBlock: debug ? buildErrorDebugBlock(runId, store) : undefined,
1106
+ });
1107
+ return { content, isError: true };
1108
+ }
1109
+ finally {
1110
+ try {
1111
+ await store.close();
1112
+ }
1113
+ catch (closeError) {
1114
+ logger.warn({ err: closeError }, "store.close() failed during async initialization cleanup");
1115
+ }
1116
+ releaseMutex(runId);
1117
+ }
1118
+ }
1119
+ releaseMutex(runId);
1120
+ throw err;
1121
+ }
1122
+ if (store === undefined || effectiveDefinition === undefined) {
1123
+ releaseMutex(runId);
1124
+ throw new McpError(ErrorCode.InternalError, `Failed to initialize async run ${runId}`);
1125
+ }
1126
+ const detached = new AbortController();
1127
+ cancelRegistry.set(runId, detached);
1128
+ void runWorkflowDetached(server, options, workflow, runId, resolvedVariables, debug, receipt, validatorRegistry, loaded, parsedMembers, parsedChairman, true, detached.signal, undefined, store, effectiveDefinition, undefined, initializedPlanReview, initialContextPolicy).catch((err) => {
1129
+ if (err instanceof WorkflowCancellationError) {
1130
+ logger.info({ runId }, "workflow run cancelled");
1131
+ return;
1132
+ }
1133
+ logger.error({ err, runId }, "detached async run failed");
1134
+ });
1135
+ // Additive-only ack shape: base {run_id, status:"running"} is unchanged;
1136
+ // the duplicate-replay field appears only when detected (warn-only, F6).
1137
+ const duplicateOfFailedRun = buildDuplicateOfFailedRunField(store);
1138
+ return {
1139
+ content: [{
1140
+ type: "text",
1141
+ text: JSON.stringify({
1142
+ run_id: runId,
1143
+ status: "running",
1144
+ ...(duplicateOfFailedRun !== undefined
1145
+ ? { duplicate_of_failed_run: duplicateOfFailedRun }
1146
+ : {}),
1147
+ }),
1148
+ }],
1149
+ };
1150
+ }
1151
+ return await runWorkflowDetached(server, options, workflow, runId, resolvedVariables, debug, receipt, validatorRegistry, loaded, parsedMembers, parsedChairman, false, signal, progressToken, undefined, undefined, planReviewPlan, undefined, initialContextPolicy);
1152
+ }
1153
+ async function preparePlanReviewSnapshot(plan) {
1154
+ try {
1155
+ const canonical = await readCanonicalPlanSnapshot({
1156
+ planFile: plan.admission.input.plan_file,
1157
+ allowedRoots: planFileAllowedRoots(),
1158
+ claimedDigest: plan.admission.input.digest,
1159
+ });
1160
+ // Fail fast on a roster that cannot fit its own prompts. This is caller
1161
+ // sizing, not a TOCTOU: it is deterministic and pre-state.
1162
+ //
1163
+ // It raises `McpError(-32602)`, which the re-throw below deliberately lets
1164
+ // escape rather than carrying as `{ok:false}` — but the CODE only reaches
1165
+ // the caller on the ASYNC path. `executeWorkflowRun` wraps the sync path in
1166
+ // its generic failure handler, so the same error surfaces there as an
1167
+ // `isError` TEXT result carrying the message but NOT the code. Do not
1168
+ // describe this as "-32602" unqualified; the invariant that holds on BOTH
1169
+ // paths is: no run directory, no persisted state, no provider work.
1170
+ assertPlanReviewPromptBudget({
1171
+ seatCount: plan.admission.execution_contract.seats.length,
1172
+ snapshotBytes: canonical.canonicalBytes.byteLength,
1173
+ maxPromptBytes: getMaxPromptBytes(),
1174
+ });
1175
+ return { ok: true, canonical };
1176
+ }
1177
+ catch (err) {
1178
+ if (err instanceof McpError)
1179
+ throw err;
1180
+ return { ok: false, error: err };
1181
+ }
1182
+ }
1183
+ /**
1184
+ * A-5/A-6: materialize the immutable snapshot + persisted execution contract
1185
+ * into runtime-owned storage and commit the typed plan-review subtree, all
1186
+ * BEFORE any seat is dispatched.
1187
+ *
1188
+ * A failure here is a post-state PRE-SEAT failure: the run is marked `failed`
1189
+ * with the offending `stage`, and no result sidecar is ever attempted.
1190
+ */
1191
+ async function initializePlanReviewRunState(store, plan, definition, prepared) {
1192
+ // Raise a carried read/verify failure now that state exists, so a swapped or
1193
+ // unreadable plan is a `failed` run rather than a pre-state rejection.
1194
+ if (!prepared.ok)
1195
+ throw prepared.error;
1196
+ const contract = plan.admission.execution_contract;
1197
+ const snapshot = await persistCanonicalPlanSnapshot(store.runDir, prepared.canonical);
1198
+ if (snapshot.absolutePath !== plan.snapshotAbsolutePath) {
1199
+ throw new PlanReviewStageError("snapshot", `Snapshot path ${snapshot.absolutePath} does not match the runtime-owned path derived for this run (${plan.snapshotAbsolutePath})`);
1200
+ }
1201
+ const persistedContract = await writeExecutionContractSnapshot(store.runDir, contract);
1202
+ const state = {
1203
+ schema_version: "council-plan-review-state-v1",
1204
+ execution_contract_path: persistedContract.relPath,
1205
+ execution_contract_sha256: persistedContract.sha256,
1206
+ execution_contract_digest: plan.admission.execution_contract_digest,
1207
+ snapshot_path: snapshot.relPath,
1208
+ snapshot_sha256: snapshot.sha256,
1209
+ plan_digest: snapshot.planDigest,
1210
+ seat_mappings: deriveSeatMappings(contract),
1211
+ call_bindings: reconstructPlanReviewCallBindings(contract, resolvePlanReviewChairmanCallId(definition)),
1212
+ context_policy: plan.contextPolicy,
1213
+ };
1214
+ validatePlanReviewCallBindings(definition, state.call_bindings);
1215
+ await store.commitPlanReviewState(state);
1216
+ return state;
1217
+ }
1218
+ /** The chairman stage's call id, read from the normalized definition. */
1219
+ function resolvePlanReviewChairmanCallId(definition) {
1220
+ const stage = definition.stages.find((entry) => entry.id === "chairman");
1221
+ const callId = stage?.calls[0]?.id;
1222
+ if (callId === undefined) {
1223
+ throw new PlanReviewStageError("execution_contract", "Plan-review definition has no chairman call");
1224
+ }
1225
+ return callId;
1226
+ }
1227
+ function expandRunWorkflowDefinition(definition, workflow, parsedMembers, parsedChairman, planReviewPlan,
1228
+ /**
1229
+ * The already-verified snapshot. This function must NEVER read `plan_file`:
1230
+ * the text it inlines is the digest-verified canonical copy or nothing.
1231
+ */
1232
+ preparedSnapshot) {
1233
+ if (planReviewPlan !== undefined) {
1234
+ return expandCouncilPlanReview(definition, planReviewPlan.admission.execution_contract,
1235
+ // On a carried read failure the run is already doomed; expand without a
1236
+ // snapshot so the store can be created and mark the run `failed`.
1237
+ //
1238
+ // The verified snapshot is passed WHOLE rather than flattened to `.text`:
1239
+ // the expander's guard re-checks the digest-bearing shape, which a bare
1240
+ // string cannot carry. This is the single read — never re-read `plan_file`.
1241
+ preparedSnapshot?.ok === true ? preparedSnapshot.canonical : undefined);
1242
+ }
1243
+ return parsedMembers != null || parsedChairman != null
1244
+ ? expandCouncilComposition(definition, parsedMembers, workflow, parsedChairman)
1245
+ : definition;
1246
+ }
1247
+ async function runWorkflowDetached(server, options, workflow, runId, resolvedVariables, debug, receipt, validatorRegistry, loaded, parsedMembers, parsedChairman, isAsync, signal, progressToken, preCreatedStore, preExpandedDefinition, planReviewPlan, preInitializedPlanReview, initialContextPolicy) {
1248
+ let store = preCreatedStore ?? null;
718
1249
  let backend = null;
719
1250
  let reporter;
1251
+ let shutdownHandle;
1252
+ let releaseLifecycleBinding;
1253
+ let initializedPlanReview = preInitializedPlanReview;
720
1254
  try {
721
- const effectiveDefinition = (parsedMembers != null || parsedChairman != null)
722
- ? expandCouncilComposition(loaded.normalized, parsedMembers ?? undefined, workflow, parsedChairman)
723
- : loaded.normalized;
724
- // Create state store (resumable if child config path is set)
725
- store = await WorkflowStateStore.create({
726
- runId,
727
- definition: effectiveDefinition,
728
- variables: resolvedVariables,
729
- debugKeepArtifacts: debug,
730
- });
1255
+ // The async admission path already expanded the composition (and read the
1256
+ // snapshot) in the foreground; reuse both to avoid redundant work and a
1257
+ // second `plan_file` read.
1258
+ const preparedSnapshot = preExpandedDefinition !== undefined ||
1259
+ planReviewPlan === undefined
1260
+ ? undefined
1261
+ : await preparePlanReviewSnapshot(planReviewPlan);
1262
+ const effectiveDefinition = preExpandedDefinition ??
1263
+ expandRunWorkflowDefinition(loaded.normalized, workflow, parsedMembers, parsedChairman, planReviewPlan, preparedSnapshot);
1264
+ // Create state store (resumable if child config path is set) unless the
1265
+ // async admission path already created it in the foreground.
1266
+ if (store == null) {
1267
+ store = await WorkflowStateStore.create({
1268
+ runId,
1269
+ definition: effectiveDefinition,
1270
+ variables: resolvedVariables,
1271
+ debugKeepArtifacts: debug,
1272
+ async: isAsync,
1273
+ initialContextPolicy,
1274
+ });
1275
+ }
1276
+ releaseLifecycleBinding = options.contextBuilderLifecycleRouter?.bind(runId, store);
1277
+ // A-5/A-6: snapshot + contract + typed subtree land here — after state
1278
+ // exists, before the backend is even created, so a snapshot/digest failure
1279
+ // is a `failed` RunState with zero seats dispatched (never a result
1280
+ // sidecar, never a pre-state -32602).
1281
+ if (planReviewPlan !== undefined && preparedSnapshot !== undefined) {
1282
+ initializedPlanReview = await initializePlanReviewRunState(store, planReviewPlan, effectiveDefinition, preparedSnapshot);
1283
+ }
1284
+ const sessionLogGeneration = store.sessionLogGeneration ??
1285
+ await inspectLegacyWorkflowSessionLogGeneration(store.runDir, runId);
1286
+ if (signal.aborted)
1287
+ throw new WorkflowCancellationError();
731
1288
  // Create backend via factory
732
1289
  backend = options.createBackend(runId);
733
1290
  await backend.connect();
@@ -741,7 +1298,7 @@ async function executeRunWorkflow(server, options, args, signal, progressToken)
741
1298
  controller.abort(new WorkflowCancellationError());
742
1299
  }, { once: true });
743
1300
  }
744
- shutdownCoordinator.register(backend, controller, async () => {
1301
+ shutdownHandle = shutdownCoordinator.register(backend, controller, async () => {
745
1302
  if (store != null) {
746
1303
  await store.markCancelled();
747
1304
  }
@@ -773,12 +1330,30 @@ async function executeRunWorkflow(server, options, args, signal, progressToken)
773
1330
  variables: resolvedVariables,
774
1331
  validatorRegistry,
775
1332
  runId,
1333
+ sessionLogGeneration,
776
1334
  runDir: storeRef.runDir,
777
1335
  signal: controller.signal,
778
1336
  hooks,
1337
+ contextBuilderRunner: options.contextBuilderRunner,
1338
+ contextDefault: options.contextDefault,
1339
+ defaultCurator: options.defaultCurator,
1340
+ commitPreflightContext: storeRef.commitPreflightContext?.bind(storeRef),
1341
+ initialContextPolicy,
1342
+ ...(initializedPlanReview !== undefined
1343
+ ? {
1344
+ planReviewContextPolicy: initializedPlanReview.context_policy,
1345
+ planReviewCallBindings: initializedPlanReview.call_bindings,
1346
+ commitPlanReviewContextProvenance: storeRef.commitPlanReviewContextProvenance?.bind(storeRef),
1347
+ terminalResultPolicy: {
1348
+ kind: "plan_review_artifacts",
1349
+ project: storeRef.projectPlanReviewTerminalResult.bind(storeRef),
1350
+ },
1351
+ }
1352
+ : {}),
779
1353
  });
780
1354
  const result = await engine.execute();
781
- shutdownCoordinator.unregister();
1355
+ if (shutdownHandle !== undefined)
1356
+ shutdownCoordinator.unregister(shutdownHandle);
782
1357
  // Build summary (fail-open)
783
1358
  let summary;
784
1359
  if (receipt) {
@@ -797,16 +1372,29 @@ async function executeRunWorkflow(server, options, args, signal, progressToken)
797
1372
  summary,
798
1373
  debugBlock: debug ? buildDebugBlock(runId, result, store) : undefined,
799
1374
  });
1375
+ appendDuplicateReplayBlock(content, store);
800
1376
  return { content };
801
1377
  }
802
1378
  catch (error) {
803
- shutdownCoordinator.unregister();
1379
+ if (shutdownHandle !== undefined)
1380
+ shutdownCoordinator.unregister(shutdownHandle);
804
1381
  if (error instanceof WorkflowCancellationError) {
805
1382
  if (store != null) {
806
1383
  await store.markCancelled();
807
1384
  }
808
1385
  throw error;
809
1386
  }
1387
+ // Persist a terminal "failed" record for post-admission setup failures
1388
+ // (e.g. backend.connect() / engine construction). Idempotent: the engine
1389
+ // already persists "failed" for in-engine failures, so this won't clobber.
1390
+ if (store != null) {
1391
+ try {
1392
+ await markWorkflowFailed(store, runId, error);
1393
+ }
1394
+ catch (err) {
1395
+ logger.warn({ err }, "markFailed during error handling failed");
1396
+ }
1397
+ }
810
1398
  const errorText = formatWorkflowError(runId, error, store, {
811
1399
  source: loaded.source,
812
1400
  sourcePath: loaded.sourcePath,
@@ -828,9 +1416,11 @@ async function executeRunWorkflow(server, options, args, signal, progressToken)
828
1416
  summary: failureSummary,
829
1417
  debugBlock: debug && store != null ? buildErrorDebugBlock(runId, store) : undefined,
830
1418
  });
1419
+ appendDuplicateReplayBlock(content, store);
831
1420
  return { content, isError: true };
832
1421
  }
833
1422
  finally {
1423
+ releaseLifecycleBinding?.();
834
1424
  reporter?.stop();
835
1425
  try {
836
1426
  if (backend != null && !backend.isTerminated) {
@@ -848,7 +1438,8 @@ async function executeRunWorkflow(server, options, args, signal, progressToken)
848
1438
  catch (cleanupErr) {
849
1439
  logger.warn({ err: cleanupErr }, "store.close() failed during cleanup");
850
1440
  }
851
- releaseMutex();
1441
+ cancelRegistry.delete(runId);
1442
+ releaseMutex(runId);
852
1443
  }
853
1444
  }
854
1445
  async function executeResumeWorkflow(server, options, args, signal, progressToken) {
@@ -864,18 +1455,133 @@ async function executeResumeWorkflow(server, options, args, signal, progressToke
864
1455
  catch {
865
1456
  throw new McpError(ErrorCode.InvalidParams, `Invalid run_id "${runId}". Must match [A-Za-z0-9_-]{1,64}.`);
866
1457
  }
867
- acquireMutex(runId);
1458
+ // Plan-review terminal state is authoritative even while the detached run's
1459
+ // finally still owns the in-process mutex. Read it before duplicate-run
1460
+ // arbitration so terminal-before-cleanup never leaks as "already running".
1461
+ // Any read/validation error is retried by WorkflowStateStore.resume under the
1462
+ // run lock, preserving the existing error surface for corrupt generic state.
1463
+ let persistedBeforeMutex;
1464
+ try {
1465
+ persistedBeforeMutex = await readRunStateForStatus(runId);
1466
+ }
1467
+ catch {
1468
+ persistedBeforeMutex = null;
1469
+ }
1470
+ const persistedState = persistedBeforeMutex?.state;
1471
+ if (persistedState != null &&
1472
+ isPlanReviewRunState(persistedState) &&
1473
+ (persistedState.status === "failed" ||
1474
+ persistedState.status === "cancelled" ||
1475
+ persistedState.status === "succeeded")) {
1476
+ const error = new Error(`Run ${runId} is terminal (${persistedState.status}) and cannot be resumed; start a new run instead.`);
1477
+ return {
1478
+ content: finalizeWorkflowPresentation({
1479
+ outputText: formatWorkflowError(runId, error, null),
1480
+ receipt,
1481
+ debug,
1482
+ summary: undefined,
1483
+ debugBlock: undefined,
1484
+ }),
1485
+ isError: true,
1486
+ };
1487
+ }
1488
+ acquireMutex(runId, options);
868
1489
  let store = null;
869
1490
  let backend = null;
870
1491
  let reporter;
1492
+ let shutdownHandle;
1493
+ let releaseLifecycleBinding;
871
1494
  try {
872
1495
  const resumeOptions = {
873
1496
  runId,
874
1497
  debugKeepArtifacts: debug,
875
1498
  };
876
1499
  store = await WorkflowStateStore.resume(resumeOptions);
1500
+ releaseLifecycleBinding = options.contextBuilderLifecycleRouter?.bind(runId, store);
877
1501
  const validatorRegistry = createValidatorRegistry();
878
- // Create backend via factory
1502
+ const initialContextPolicy = store.runState.initialContextPolicy ??
1503
+ reconstructLegacyInitialContextPolicy(store.runState, options.defaultCurator);
1504
+ // A-5 resume fidelity: the persisted contract + snapshot are the SOLE
1505
+ // inputs — `plan_file` is never re-read and caller input is never
1506
+ // re-parsed. Decode + hash-verify + RECOMPUTE before the engine exists, so
1507
+ // a corrupt contract fails post-state/pre-seat rather than dispatching
1508
+ // seats under unverified (or absent) per-call bindings.
1509
+ // Typed detection is by PERSISTED plan-review state presence, not the
1510
+ // definition name alone: legacy runs persisted under the pre-rename name
1511
+ // "council-plan-review" (which now names the GENERIC plan-review council)
1512
+ // must keep routing through the typed restore path, while a new generic
1513
+ // council-plan-review run (no plan_review_state) must never enter it. A
1514
+ // tampered generic run that smuggles in a plan_review_state subtree fails
1515
+ // closed downstream: restorePlanReviewRun / validatePlanReviewCallBindings
1516
+ // reject any definition whose stages/calls do not match the contract.
1517
+ const persistedPlanReview = store.runState.plan_review_state;
1518
+ let restoredPlanReview;
1519
+ if (store.definition.name === PLAN_REVIEW_RECEIPTS_WORKFLOW &&
1520
+ persistedPlanReview === undefined) {
1521
+ throw new PlanReviewStageError("execution_contract", "Persisted plan-review-receipts run is missing plan_review_state");
1522
+ }
1523
+ if (persistedPlanReview !== undefined &&
1524
+ store.definition.name !== PLAN_REVIEW_RECEIPTS_WORKFLOW &&
1525
+ store.definition.name !== LEGACY_PLAN_REVIEW_RECEIPTS_WORKFLOW_NAME) {
1526
+ throw new PlanReviewStageError("execution_contract", `Persisted workflow "${store.definition.name}" cannot carry plan_review_state`);
1527
+ }
1528
+ if (persistedPlanReview !== undefined) {
1529
+ restoredPlanReview = await restorePlanReviewRun(store.runDir, persistedPlanReview, resolvePlanReviewChairmanCallId(store.definition));
1530
+ const bindingsByCall = validatePlanReviewCallBindings(store.definition, restoredPlanReview.call_bindings);
1531
+ /**
1532
+ * DELIBERATELY the FIRST of two identical checks; `WorkflowEngine`'s
1533
+ * constructor runs the same one. This is not drift.
1534
+ *
1535
+ * The engine's copy is the load-bearing seam: it is the one no caller can
1536
+ * bypass, and it is what a future construction path would inherit. This
1537
+ * copy buys ORDERING — it rejects tampered state before `createBackend` /
1538
+ * `connect`, so a doomed resume never spawns a provider process. The
1539
+ * function is pure and cheap over a handful of prepared inputs, so paying
1540
+ * twice costs nothing measurable and removing EITHER one loses a real
1541
+ * property. If you are here to delete a redundant call: don't.
1542
+ */
1543
+ validatePlanReviewPreparedInputPolicy(store.resume.preparedInputs ?? new Map(), bindingsByCall, {
1544
+ // From the SAME read that re-verified the snapshot above (D9).
1545
+ snapshotText: restoredPlanReview.snapshot.text,
1546
+ contextPolicy: restoredPlanReview.state.context_policy,
1547
+ });
1548
+ }
1549
+ // Make this resume attempt durable before its preflight gate. Generic runs
1550
+ // otherwise remain in the prior terminal state, causing markFailed /
1551
+ // markCancelled to suppress a new pre-dispatch outcome as "idempotent".
1552
+ await store.beginResumeAttempt();
1553
+ const sessionLogGeneration = store.sessionLogGeneration ??
1554
+ await inspectLegacyWorkflowSessionLogGeneration(store.runDir, runId);
1555
+ if (signal.aborted)
1556
+ throw new WorkflowCancellationError();
1557
+ if (store.runState.workflow_context === undefined) {
1558
+ const preflightEngine = new WorkflowEngine({
1559
+ definition: store.definition,
1560
+ session: createPreflightOnlyBackend(options.availableCliNames),
1561
+ variables: store.variables,
1562
+ validatorRegistry,
1563
+ runId,
1564
+ runDir: store.runDir,
1565
+ signal,
1566
+ resume: store.resume,
1567
+ contextBuilderRunner: options.contextBuilderRunner,
1568
+ contextDefault: options.contextDefault,
1569
+ defaultCurator: options.defaultCurator,
1570
+ commitPreflightContext: store.commitPreflightContext.bind(store),
1571
+ requireContextPreflight: true,
1572
+ initialContextPolicy,
1573
+ ...(restoredPlanReview === undefined
1574
+ ? {}
1575
+ : {
1576
+ planReviewContextPolicy: restoredPlanReview.state.context_policy,
1577
+ planReviewCallBindings: restoredPlanReview.call_bindings,
1578
+ planReviewSnapshotText: restoredPlanReview.snapshot.text,
1579
+ commitPlanReviewContextProvenance: store.commitPlanReviewContextProvenance.bind(store),
1580
+ }),
1581
+ });
1582
+ await preflightEngine.prepareContextPreflight(signal);
1583
+ }
1584
+ // Create backend only after the resume context gate has committed or bypassed.
879
1585
  backend = options.createBackend(runId);
880
1586
  await backend.connect();
881
1587
  // Wire up shutdown coordinator
@@ -888,7 +1594,7 @@ async function executeResumeWorkflow(server, options, args, signal, progressToke
888
1594
  controller.abort(new WorkflowCancellationError());
889
1595
  }, { once: true });
890
1596
  }
891
- shutdownCoordinator.register(backend, controller, async () => {
1597
+ shutdownHandle = shutdownCoordinator.register(backend, controller, async () => {
892
1598
  await store.markCancelled();
893
1599
  });
894
1600
  // Build engine hooks with progress reporter
@@ -922,13 +1628,36 @@ async function executeResumeWorkflow(server, options, args, signal, progressToke
922
1628
  variables: store.variables,
923
1629
  validatorRegistry,
924
1630
  runId,
1631
+ sessionLogGeneration,
925
1632
  runDir: store.runDir,
926
1633
  signal: controller.signal,
927
1634
  hooks: rpt != null ? hooks : store.hooks,
928
1635
  resume: store.resume,
1636
+ contextBuilderRunner: options.contextBuilderRunner,
1637
+ contextDefault: options.contextDefault,
1638
+ defaultCurator: options.defaultCurator,
1639
+ commitPreflightContext: store.commitPreflightContext?.bind(store),
1640
+ restoredWorkflowContext: store.runState.workflow_context,
1641
+ requireContextPreflight: false,
1642
+ initialContextPolicy,
1643
+ // A-7: replay the SAME enforced exclusion on resume, reconstructed from
1644
+ // persisted state rather than re-derived from caller variables.
1645
+ ...(restoredPlanReview !== undefined
1646
+ ? {
1647
+ planReviewContextPolicy: restoredPlanReview.state.context_policy,
1648
+ planReviewCallBindings: restoredPlanReview.call_bindings,
1649
+ planReviewSnapshotText: restoredPlanReview.snapshot.text,
1650
+ commitPlanReviewContextProvenance: store.commitPlanReviewContextProvenance?.bind(store),
1651
+ terminalResultPolicy: {
1652
+ kind: "plan_review_artifacts",
1653
+ project: store.projectPlanReviewTerminalResult.bind(store),
1654
+ },
1655
+ }
1656
+ : {}),
929
1657
  });
930
1658
  const result = await engine.execute();
931
- shutdownCoordinator.unregister();
1659
+ if (shutdownHandle !== undefined)
1660
+ shutdownCoordinator.unregister(shutdownHandle);
932
1661
  // Build summary (fail-open)
933
1662
  let summary;
934
1663
  if (receipt) {
@@ -950,13 +1679,24 @@ async function executeResumeWorkflow(server, options, args, signal, progressToke
950
1679
  return { content };
951
1680
  }
952
1681
  catch (error) {
953
- shutdownCoordinator.unregister();
1682
+ if (shutdownHandle !== undefined)
1683
+ shutdownCoordinator.unregister(shutdownHandle);
954
1684
  if (error instanceof WorkflowCancellationError) {
955
1685
  if (store != null) {
956
1686
  await store.markCancelled();
957
1687
  }
958
1688
  throw error;
959
1689
  }
1690
+ // Persist a terminal "failed" record for post-admission setup failures.
1691
+ // Idempotent: won't clobber an engine-emitted terminal status.
1692
+ if (store != null) {
1693
+ try {
1694
+ await markWorkflowFailed(store, runId, error);
1695
+ }
1696
+ catch (err) {
1697
+ logger.warn({ err }, "markFailed during error handling failed");
1698
+ }
1699
+ }
960
1700
  const errorText = formatWorkflowError(runId, error, store);
961
1701
  // Build failure summary (fail-open)
962
1702
  let failureSummary;
@@ -978,6 +1718,7 @@ async function executeResumeWorkflow(server, options, args, signal, progressToke
978
1718
  return { content, isError: true };
979
1719
  }
980
1720
  finally {
1721
+ releaseLifecycleBinding?.();
981
1722
  reporter?.stop();
982
1723
  try {
983
1724
  if (backend != null && !backend.isTerminated) {
@@ -995,28 +1736,435 @@ async function executeResumeWorkflow(server, options, args, signal, progressToke
995
1736
  catch (cleanupErr) {
996
1737
  logger.warn({ err: cleanupErr }, "store.close() failed during cleanup");
997
1738
  }
998
- releaseMutex();
1739
+ releaseMutex(runId);
999
1740
  }
1000
1741
  }
1001
- function executeListWorkflows(workflowsPath, availableCliNames) {
1002
- const workflows = listWorkflowCatalog(workflowsPath);
1742
+ // ---------------------------------------------------------------------------
1743
+ // Sidecar access (atomic, fd-based, cheap-poll-safe)
1744
+ // ---------------------------------------------------------------------------
1745
+ /**
1746
+ * Thrown for genuine containment-policy violations (absolute outputPath or an
1747
+ * escape past the run directory). These MUST reject even on a cheap default
1748
+ * poll. Everything else (missing file, fs race, permission) is swallowed on the
1749
+ * cheap path so liveness polling never throws for a recoverable condition.
1750
+ *
1751
+ * Alias of the shared reader's policy error so the existing `instanceof`
1752
+ * cheap-poll discrimination below is unchanged.
1753
+ */
1754
+ const SidecarPolicyError = ContainedFilePolicyError;
1755
+ /**
1756
+ * Read a run sidecar through the shared containment-safe, no-follow reader.
1757
+ *
1758
+ * The reader is shared (not re-implemented) with the plan-review snapshot and
1759
+ * contract paths: one audited realpath + `O_NOFOLLOW` + single-handle
1760
+ * implementation, so a fix to the TOCTOU handling lands everywhere at once.
1761
+ */
1762
+ async function readContainedSidecar(runDir, outputPath, wantContent) {
1763
+ const read = await readContainedRelativeFile(runDir, outputPath, {
1764
+ wantContent,
1765
+ // Preserve this path's established error wording byte-for-byte.
1766
+ containerLabel: "run directory",
1767
+ pathLabel: "outputPath",
1768
+ });
1769
+ return read.content !== undefined
1770
+ ? { size: read.size, content: read.content }
1771
+ : { size: read.size };
1772
+ }
1773
+ /**
1774
+ * Legacy state-v1 runs did not persist their admission-time default. Rebuild
1775
+ * only caller-explicit choices; otherwise require deterministic preflight so a
1776
+ * changed server default cannot silently introduce or switch a delegate.
1777
+ */
1778
+ export function reconstructLegacyInitialContextPolicy(state, defaultCurator) {
1779
+ const persistedMode = state.workflow_context?.context_builder.context_mode;
1780
+ if (persistedMode === "disabled") {
1781
+ return {
1782
+ requestedMode: "off",
1783
+ effectiveMode: "disabled",
1784
+ preflightDisabled: true,
1785
+ };
1786
+ }
1787
+ const legacyVariables = state.variables ?? {};
1788
+ const callerMode = legacyVariables["cb_context_mode"];
1789
+ const normalizedMode = typeof callerMode === "string" ? callerMode.trim() : "";
1790
+ if (normalizedMode === "off" || normalizedMode === "deterministic") {
1791
+ return {
1792
+ requestedMode: normalizedMode,
1793
+ effectiveMode: normalizedMode,
1794
+ preflightDisabled: false,
1795
+ };
1796
+ }
1797
+ if (normalizedMode === "full" && legacyVariables["cb_delegate"] != null) {
1798
+ try {
1799
+ return resolveWorkflowContextMode("full", "deterministic", true, legacyVariables["cb_delegate"], defaultCurator);
1800
+ }
1801
+ catch {
1802
+ // Legacy hard-disabled runs may contain a delegate value that was never
1803
+ // validated. Do not turn it into a new resume-time fatal or provider.
1804
+ }
1805
+ }
1806
+ return {
1807
+ requestedMode: normalizedMode === "full" || persistedMode === "full"
1808
+ ? "full"
1809
+ : "deterministic",
1810
+ effectiveMode: "deterministic",
1811
+ preflightDisabled: false,
1812
+ };
1813
+ }
1814
+ /**
1815
+ * Best-effort read-time parse of the persisted context-builder lifecycle
1816
+ * snapshot. Status projection is observability-only and must never throw for
1817
+ * a malformed or absent snapshot — invalid input yields undefined and the
1818
+ * dependent health/context fields are simply omitted.
1819
+ */
1820
+ function parseLifecycleForStatus(value) {
1821
+ if (value == null)
1822
+ return undefined;
1823
+ try {
1824
+ return parseContextBuilderLifecycleStateV1(value);
1825
+ }
1826
+ catch {
1827
+ return undefined;
1828
+ }
1829
+ }
1830
+ async function executeWorkflowRunStatus(_options, args) {
1831
+ const runId = args["run_id"];
1832
+ const includeResult = args["include_result"] === true;
1833
+ if (typeof runId !== "string" || runId === "") {
1834
+ throw new McpError(ErrorCode.InvalidParams, 'Missing or invalid "run_id" parameter');
1835
+ }
1836
+ try {
1837
+ validateRunId(runId);
1838
+ }
1839
+ catch {
1840
+ throw new McpError(ErrorCode.InvalidParams, `Invalid run_id "${runId}". Must match [A-Za-z0-9_-]{1,64}.`);
1841
+ }
1842
+ const read = await readRunStateForStatus(runId);
1843
+ if (read == null) {
1844
+ return {
1845
+ content: [{ type: "text", text: JSON.stringify({ status: "not_found", run_id: runId }) }],
1846
+ };
1847
+ }
1848
+ const { state, lock } = read;
1849
+ const status = (state.status === "running" || state.status === "pending") && lock === "stale_or_absent"
1850
+ ? "orphaned"
1851
+ : state.status;
1852
+ const runDir = getRunStateDir(runId);
1853
+ const stages = state.stages.map((stage) => {
1854
+ const completedCalls = stage.calls.filter((call) => call.state === "succeeded").length;
1855
+ const runningCalls = stage.calls.filter((call) => call.state === "running").length;
1856
+ const failedCalls = stage.calls.filter((call) => call.state === "failed").length;
1857
+ return {
1858
+ stage_id: stage.stageId,
1859
+ state: stage.state,
1860
+ total_calls: stage.calls.length,
1861
+ completed_calls: completedCalls,
1862
+ running_calls: runningCalls,
1863
+ failed_calls: failedCalls,
1864
+ calls: stage.calls.map((call) => ({
1865
+ call_id: call.callId,
1866
+ state: call.state,
1867
+ retry_count: call.retryCount,
1868
+ ...(call.startedAt != null ? { started_at: call.startedAt } : {}),
1869
+ ...(call.completedAt != null ? { completed_at: call.completedAt } : {}),
1870
+ ...(call.durationMs != null ? { duration_ms: call.durationMs } : {}),
1871
+ ...(call.errorMessage != null ? { error: call.errorMessage } : {}),
1872
+ })),
1873
+ };
1874
+ });
1875
+ const projection = {
1876
+ run_id: state.runId,
1877
+ status,
1878
+ async: state.async ?? false,
1879
+ workflow: {
1880
+ name: state.workflowName,
1881
+ version: state.workflowVersion,
1882
+ },
1883
+ ...(state.startedAt != null ? { started_at: state.startedAt } : {}),
1884
+ ...(state.completedAt != null ? { completed_at: state.completedAt } : {}),
1885
+ ...(lock === "unknown" ? { lock_ownership: "unknown" } : {}),
1886
+ stages,
1887
+ };
1888
+ // artifacts_dir is cheap (pure join, no I/O) and present for every found run.
1889
+ projection["artifacts_dir"] = join(runDir, "artifacts");
1890
+ // Read-only context-selection observability from already-persisted state.
1891
+ // No file I/O — only projects state.workflow_context.context_builder and
1892
+ // the state.context_builder_lifecycle snapshot.
1893
+ const lifecycle = parseLifecycleForStatus(state.context_builder_lifecycle);
1894
+ const cb = state.workflow_context?.context_builder;
1895
+ if (cb != null) {
1896
+ const context = {
1897
+ ...(cb.context_mode != null ? { context_mode: cb.context_mode } : {}),
1898
+ status: cb.status,
1899
+ files_selected: cb.metrics.files_selected,
1900
+ };
1901
+ if (cb.delegate != null) {
1902
+ context["delegate"] = {
1903
+ status: cb.delegate.status,
1904
+ fallback_used: cb.delegate.fallback_used,
1905
+ };
1906
+ }
1907
+ if (lifecycle != null) {
1908
+ // Additive lifecycle observability. `coverage.missing` is a bounded
1909
+ // integer count in the persisted schema (never a list), so it is
1910
+ // projected as-is with no truncation concern.
1911
+ context["lifecycle_status"] = lifecycle.status;
1912
+ context["coverage"] = {
1913
+ policy: lifecycle.coverage.policy,
1914
+ status: lifecycle.coverage.status,
1915
+ required: lifecycle.coverage.required,
1916
+ selected: lifecycle.coverage.selected,
1917
+ missing: lifecycle.coverage.missing,
1918
+ };
1919
+ }
1920
+ projection["context"] = context;
1921
+ }
1922
+ // F7 read-time health projection: top-level run status alone overstates
1923
+ // health when on_failure:continue swallowed seat failures or context
1924
+ // preflight completed partial/degraded. Computed only from already-persisted
1925
+ // data; additive and advisory — never mutates status semantics.
1926
+ const healthReasons = [];
1927
+ if (status === "succeeded" && stages.some((stage) => stage.failed_calls > 0)) {
1928
+ healthReasons.push("failed_calls_in_succeeded_run");
1929
+ }
1930
+ if (lifecycle?.status === "partial") {
1931
+ healthReasons.push("context_partial");
1932
+ }
1933
+ if (lifecycle?.status === "failed") {
1934
+ healthReasons.push("context_lifecycle_failed");
1935
+ }
1936
+ if (lifecycle?.coverage.status === "degraded") {
1937
+ healthReasons.push("coverage_degraded");
1938
+ }
1939
+ if (cb?.delegate?.fallback_used === true) {
1940
+ healthReasons.push("delegate_fallback_used");
1941
+ }
1942
+ projection["health"] = {
1943
+ classification: healthReasons.length > 0 ? "degraded" : "ok",
1944
+ reasons: healthReasons,
1945
+ };
1946
+ if (isPlanReviewRunState(state)) {
1947
+ const terminalExecutionStatus = state.status === "succeeded" ||
1948
+ state.status === "failed" ||
1949
+ state.status === "cancelled"
1950
+ ? state.status
1951
+ : undefined;
1952
+ const publication = state.plan_review_state?.publication;
1953
+ const inspected = await inspectPlanReviewPublication(runDir, runId, terminalExecutionStatus ??
1954
+ (includeResult && publication?.committed === true ? "failed" : undefined), publication, includeResult);
1955
+ projection["result_available"] = inspected.result_available;
1956
+ projection["result_bytes"] = inspected.result_bytes;
1957
+ if (inspected.result !== undefined) {
1958
+ projection["result"] = inspected.result;
1959
+ }
1960
+ let planReviewError = (includeResult && publication?.committed === true ? inspected.error : undefined) ??
1961
+ state.plan_review_error ??
1962
+ inspected.error;
1963
+ if (planReviewError === undefined &&
1964
+ publication === undefined &&
1965
+ state.plan_review_state === undefined &&
1966
+ (state.status === "succeeded" || state.status === "failed")) {
1967
+ planReviewError = createPlanReviewPublicationError(runId, state.status, "no_artifact", "Historical plan-review run has no publication marker; rerun with a Track-C-capable ClinkX runtime.");
1968
+ }
1969
+ if (planReviewError !== undefined) {
1970
+ projection["plan_review_error"] = planReviewError;
1971
+ }
1972
+ if (state.status === "failed" || state.status === "cancelled") {
1973
+ if (state.error != null) {
1974
+ projection["error"] = state.error;
1975
+ }
1976
+ if (state.failureClass != null) {
1977
+ projection["failure_class"] = state.failureClass;
1978
+ }
1979
+ }
1980
+ }
1981
+ else {
1982
+ projection["result_available"] = state.status === "succeeded";
1983
+ if (state.status === "succeeded") {
1984
+ const final = state.stages.at(-1);
1985
+ const call = final?.calls[0];
1986
+ // Cheap size hint without reading contents. Containment (via
1987
+ // readContainedSidecar) runs even on a default poll so an absolute path or
1988
+ // symlink escape is rejected regardless of include_result. Everything else
1989
+ // — a missing final call, a missing/unreadable file, an fs race — omits the
1990
+ // hint rather than throwing, preserving the "a cheap poll never throws for a
1991
+ // recoverable/missing-file condition" invariant that liveness polling needs.
1992
+ if (call?.outputInline != null) {
1993
+ projection["result_bytes"] = Buffer.byteLength(call.outputInline, "utf-8");
1994
+ }
1995
+ else if (!includeResult && call?.outputPath != null) {
1996
+ // Skip this size-only open when include_result is set — the block below
1997
+ // opens the sidecar once for both size and content, so a second open here
1998
+ // would be redundant.
1999
+ try {
2000
+ const { size } = await readContainedSidecar(runDir, call.outputPath, false);
2001
+ projection["result_bytes"] = size;
2002
+ }
2003
+ catch (err) {
2004
+ if (err instanceof SidecarPolicyError) {
2005
+ // Absolute path / containment escape rejects even on a cheap poll.
2006
+ throw new Error(`Succeeded run ${runId} ${err.message}`);
2007
+ }
2008
+ // Missing file / fs race / permission on a cheap poll: omit the hint.
2009
+ }
2010
+ }
2011
+ if (includeResult) {
2012
+ if (call == null) {
2013
+ throw new Error(`Succeeded run ${runId} is missing final call state`);
2014
+ }
2015
+ if (call.outputInline != null) {
2016
+ projection["result"] = call.outputInline;
2017
+ }
2018
+ else if (call.outputPath != null) {
2019
+ try {
2020
+ const { size, content } = await readContainedSidecar(runDir, call.outputPath, true);
2021
+ projection["result_bytes"] = size;
2022
+ projection["result"] = content;
2023
+ }
2024
+ catch (err) {
2025
+ if (err instanceof SidecarPolicyError) {
2026
+ throw new Error(`Succeeded run ${runId} ${err.message}`);
2027
+ }
2028
+ throw new Error(`Succeeded run ${runId} output file is missing or unreadable`);
2029
+ }
2030
+ }
2031
+ else {
2032
+ throw new Error(`Succeeded run ${runId} final call has neither inline nor persisted output`);
2033
+ }
2034
+ }
2035
+ }
2036
+ else if (state.status === "failed" || state.status === "cancelled") {
2037
+ if (state.error != null) {
2038
+ projection["error"] = state.error;
2039
+ }
2040
+ if (state.failureClass != null) {
2041
+ projection["failure_class"] = state.failureClass;
2042
+ }
2043
+ }
2044
+ }
2045
+ return {
2046
+ content: [{ type: "text", text: JSON.stringify(projection) }],
2047
+ };
2048
+ }
2049
+ async function executeCancelWorkflow(_options, args) {
2050
+ const runId = args["run_id"];
2051
+ if (typeof runId !== "string" || runId === "") {
2052
+ throw new McpError(ErrorCode.InvalidParams, 'Missing or invalid "run_id" parameter');
2053
+ }
2054
+ try {
2055
+ validateRunId(runId);
2056
+ }
2057
+ catch {
2058
+ throw new McpError(ErrorCode.InvalidParams, `Invalid run_id "${runId}". Must match [A-Za-z0-9_-]{1,64}.`);
2059
+ }
2060
+ const text = (value) => ({
2061
+ content: [{ type: "text", text: JSON.stringify(value) }],
2062
+ });
2063
+ // 1) Live in-process async run: abort its controller and let the run's own
2064
+ // unwind drive it to terminal "cancelled". We do NOT touch the mutex here —
2065
+ // the detached run's finally owns the slot (touching it risks a double-free).
2066
+ const controller = cancelRegistry.get(runId);
2067
+ if (controller != null) {
2068
+ // The engine can have already persisted a terminal status while its
2069
+ // controller is still in the registry (deleted only in the run's finally).
2070
+ // Re-read first so we never claim "cancelling" for an already-done run.
2071
+ const live = await readRunStateForStatus(runId);
2072
+ if (live != null && live.state.status !== "running" && live.state.status !== "pending") {
2073
+ return text({ run_id: runId, status: live.state.status, already_terminal: true });
2074
+ }
2075
+ if (!controller.signal.aborted) {
2076
+ controller.abort(new WorkflowCancellationError());
2077
+ }
2078
+ return text({ run_id: runId, status: "cancelling" });
2079
+ }
2080
+ // 2) Not in our registry — inspect persisted state via the same path as status.
2081
+ const read = await readRunStateForStatus(runId);
2082
+ if (read == null) {
2083
+ return text({ run_id: runId, status: "not_found" });
2084
+ }
2085
+ const { state, lock } = read;
2086
+ if (state.status !== "running" && state.status !== "pending") {
2087
+ return text({ run_id: runId, status: state.status, already_terminal: true });
2088
+ }
2089
+ if (lock === "unknown") {
2090
+ return text({
2091
+ run_id: runId,
2092
+ status: state.status,
2093
+ cancellable: false,
2094
+ lock_ownership: "unknown",
2095
+ error: `Run lock ownership is unknown for run_id "${runId}"; refusing cancellation until the lock record is repaired.`,
2096
+ });
2097
+ }
2098
+ // running|pending and not in our registry. The run.lock is the single source
2099
+ // of truth for ownership: reapRunCancelled acquires it itself (reclaiming a
2100
+ // truly stale/dead lock, refusing a live one), closing the TOCTOU where a
2101
+ // concurrent resume_workflow could grab the lock between read and reap.
2102
+ const outcome = await reapRunCancelled(runId);
2103
+ if (outcome === "cancelled") {
2104
+ return text({ run_id: runId, status: "cancelled", reaped: true });
2105
+ }
2106
+ if (outcome === "locked") {
2107
+ // The run is owned by another live worker (or a blocking run): the lock
2108
+ // could not be acquired. Refuse without mutating — this process cannot
2109
+ // safely kill a worker it does not own.
2110
+ return text({ run_id: runId, status: "running", cancellable: false });
2111
+ }
2112
+ if (outcome === "ownership_unknown") {
2113
+ return text({
2114
+ run_id: runId,
2115
+ status: state.status,
2116
+ cancellable: false,
2117
+ lock_ownership: "unknown",
2118
+ error: `Run lock ownership became unknown for run_id "${runId}"; refusing cancellation until the lock record is repaired.`,
2119
+ });
2120
+ }
2121
+ // "skipped": became terminal between our read and the reap. Re-read and report.
2122
+ const reread = await readRunStateForStatus(runId);
2123
+ const status = reread?.state.status ?? "cancelled";
2124
+ return text({ run_id: runId, status, already_terminal: true });
2125
+ }
2126
+ function executeListWorkflows(options) {
2127
+ const workflows = listWorkflowCatalog(options.workflowsPath);
2128
+ const preflightEnabled = options.contextBuilderRunner != null;
2129
+ const contextDefault = preflightEnabled
2130
+ ? options.contextDefault ?? getWorkflowsContextDefault()
2131
+ : options.contextDefault ?? "off";
2132
+ const effectiveContextDefault = resolveWorkflowContextMode(undefined, contextDefault, preflightEnabled, undefined, options.defaultCurator).effectiveMode;
1003
2133
  const entries = [];
1004
2134
  for (const [name, entry] of workflows) {
1005
2135
  const def = entry.definition;
2136
+ const contextStrategy = deriveContextStrategy(def);
2137
+ const runtimeMembers = supportsRuntimeMembers(name);
1006
2138
  entries.push({
1007
2139
  name,
1008
2140
  description: def.description,
1009
2141
  version: def.version,
1010
2142
  source: entry.source,
1011
2143
  shadowed_yaml: entry.shadowedYaml,
1012
- supports_members: supportsRuntimeMembers(name),
1013
- supports_chairman: supportsRuntimeMembers(name),
1014
- ...(supportsRuntimeMembers(name) ? {
2144
+ supports_members: runtimeMembers,
2145
+ supports_chairman: runtimeMembers,
2146
+ supports_context_preflight: contextStrategy.supports_context_preflight,
2147
+ effective_context_default: effectiveContextDefault,
2148
+ fails_on_empty_selection: contextStrategy.fails_on_empty_selection,
2149
+ context_anchor_variables: contextStrategy.context_anchor_variables,
2150
+ context_override_variables: contextStrategy.context_override_variables,
2151
+ ...(contextStrategy.default_context_profile !== undefined
2152
+ ? { default_context_profile: contextStrategy.default_context_profile }
2153
+ : {}),
2154
+ context_recipient_stages: contextStrategy.context_recipient_stages,
2155
+ ...(runtimeMembers ? {
1015
2156
  members_min: 1,
1016
2157
  members_max: 10,
1017
2158
  members_stance_required_from_index: 3,
1018
2159
  } : {}),
1019
2160
  ...(entry.path != null ? { path: basename(entry.path) } : {}),
2161
+ ...(name === PLAN_REVIEW_RECEIPTS_WORKFLOW
2162
+ ? {
2163
+ plan_review: buildCouncilPlanReviewDiscovery(options.planReviewAvailableCliNames ??
2164
+ options.availableCliNames ??
2165
+ []),
2166
+ }
2167
+ : {}),
1020
2168
  variables: Object.fromEntries(Object.entries(def.variables).map(([varName, varDef]) => [
1021
2169
  varName,
1022
2170
  {
@@ -1029,8 +2177,8 @@ function executeListWorkflows(workflowsPath, availableCliNames) {
1029
2177
  });
1030
2178
  }
1031
2179
  const response = { workflows: entries };
1032
- if (availableCliNames != null && availableCliNames.length > 0) {
1033
- response["available_cli_names"] = [...availableCliNames].sort();
2180
+ if (options.availableCliNames != null && options.availableCliNames.length > 0) {
2181
+ response["available_cli_names"] = [...options.availableCliNames].sort();
1034
2182
  }
1035
2183
  return {
1036
2184
  content: [
@@ -1041,10 +2189,137 @@ function executeListWorkflows(workflowsPath, availableCliNames) {
1041
2189
  ],
1042
2190
  };
1043
2191
  }
2192
+ function includesVar(from, name) {
2193
+ if (typeof from === "string") {
2194
+ return from === name;
2195
+ }
2196
+ return Array.isArray(from) && from.includes(name);
2197
+ }
2198
+ // cb_* variables that tune preflight execution rather than source selection —
2199
+ // overrides, not anchors. Advertised separately as context_override_variables.
2200
+ const CONTEXT_OVERRIDE_VARIABLES = new Set([
2201
+ "cb_allow_secrets",
2202
+ "cb_budget_max_estimated_tokens",
2203
+ "cb_budget_max_files",
2204
+ "cb_budget_max_scan_files",
2205
+ "cb_budget_max_bytes_per_file",
2206
+ "cb_budget_max_total_bytes",
2207
+ "cb_budget_max_search_results",
2208
+ "cb_budget_adapter",
2209
+ "cb_delegate",
2210
+ "cb_context_mode",
2211
+ ]);
2212
+ function deriveContextStrategy(def) {
2213
+ const contextRecipientStages = def.stages
2214
+ .filter((stage) => stage.calls.some((call) => includesVar(call.absolute_file_paths_from, "context_bundle_files")))
2215
+ .map((stage) => stage.id);
2216
+ const cbVariables = Object.keys(def.variables)
2217
+ .filter((name) => name.startsWith("cb_"))
2218
+ .sort();
2219
+ const contextAnchorVariables = cbVariables.filter((name) => !CONTEXT_OVERRIDE_VARIABLES.has(name));
2220
+ const contextOverrideVariables = cbVariables.filter((name) => CONTEXT_OVERRIDE_VARIABLES.has(name));
2221
+ const defaultContextProfile = def.variables["cb_profile"]?.default;
2222
+ const hasSelectionScopeVariable = "cb_focus_paths" in def.variables || "cb_search_terms" in def.variables;
2223
+ return {
2224
+ supports_context_preflight: "context_bundle_files" in def.variables &&
2225
+ contextRecipientStages.length > 0 &&
2226
+ hasSelectionScopeVariable,
2227
+ context_anchor_variables: contextAnchorVariables,
2228
+ context_override_variables: contextOverrideVariables,
2229
+ ...(typeof defaultContextProfile === "string"
2230
+ ? { default_context_profile: defaultContextProfile }
2231
+ : {}),
2232
+ context_recipient_stages: contextRecipientStages,
2233
+ fails_on_empty_selection: def.settings.fail_on_empty_context_selection,
2234
+ };
2235
+ }
1044
2236
  // ---------------------------------------------------------------------------
1045
2237
  // Tool input schemas (JSON Schema)
1046
2238
  // ---------------------------------------------------------------------------
1047
2239
  const MAX_INLINE_WORKFLOW_DETAILS = 8;
2240
+ const PLAN_REVIEW_INPUT_SCHEMA = {
2241
+ type: "object",
2242
+ description: 'Immutable plan-review contract. Accepted only when workflow is "plan-review-receipts".',
2243
+ properties: {
2244
+ schema_version: { type: "string", const: "council-plan-review-input-v1" },
2245
+ workspace_schema: { type: "string", const: "plan-workspace-v1" },
2246
+ digest_profile: { type: "string", const: "checkpoint-text-v1" },
2247
+ review_id: {
2248
+ type: "string",
2249
+ pattern: "^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$",
2250
+ },
2251
+ repository_id: {
2252
+ type: "string",
2253
+ maxLength: 128,
2254
+ pattern: "^[A-Za-z0-9][A-Za-z0-9._-]*(?:/[A-Za-z0-9][A-Za-z0-9._-]*)*$",
2255
+ },
2256
+ iteration: { type: "integer", minimum: 1 },
2257
+ plan_id: {
2258
+ type: "string",
2259
+ pattern: "^[a-z0-9][a-z0-9-]{0,62}$",
2260
+ },
2261
+ revision: { type: "integer", minimum: 1 },
2262
+ digest: { type: "string", pattern: "^[0-9a-f]{64}$" },
2263
+ expected_execution_contract_digest: {
2264
+ type: "string",
2265
+ pattern: "^[0-9a-f]{64}$",
2266
+ },
2267
+ threshold: { type: "number", minimum: 0, maximum: 10 },
2268
+ designated_editor: { type: "string", minLength: 1 },
2269
+ chairman_seat: {
2270
+ type: "string",
2271
+ pattern: "^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$",
2272
+ },
2273
+ plan_file: {
2274
+ type: "string",
2275
+ minLength: 1,
2276
+ description: "Absolute path to the plan snapshot source",
2277
+ },
2278
+ seats: {
2279
+ type: "array",
2280
+ minItems: 1,
2281
+ maxItems: 10,
2282
+ items: {
2283
+ type: "object",
2284
+ properties: {
2285
+ seat_id: {
2286
+ type: "string",
2287
+ pattern: "^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$",
2288
+ },
2289
+ reviewer: { type: "string", minLength: 1 },
2290
+ cli_name: {
2291
+ type: "string",
2292
+ enum: [...PLAN_REVIEW_V1_CLI_NAMES].sort(),
2293
+ },
2294
+ stance: {
2295
+ type: "string",
2296
+ enum: ["strict", "pragmatic", "adversarial"],
2297
+ },
2298
+ },
2299
+ required: ["seat_id", "reviewer", "cli_name", "stance"],
2300
+ additionalProperties: false,
2301
+ },
2302
+ },
2303
+ },
2304
+ required: [
2305
+ "schema_version",
2306
+ "workspace_schema",
2307
+ "digest_profile",
2308
+ "review_id",
2309
+ "repository_id",
2310
+ "iteration",
2311
+ "plan_id",
2312
+ "revision",
2313
+ "digest",
2314
+ "expected_execution_contract_digest",
2315
+ "threshold",
2316
+ "designated_editor",
2317
+ "chairman_seat",
2318
+ "plan_file",
2319
+ "seats",
2320
+ ],
2321
+ additionalProperties: false,
2322
+ };
1048
2323
  const RUN_WORKFLOW_SCHEMA = {
1049
2324
  type: "object",
1050
2325
  properties: {
@@ -1072,9 +2347,14 @@ const RUN_WORKFLOW_SCHEMA = {
1072
2347
  description: "When true, append a structured receipt summarizing the workflow run as a content block",
1073
2348
  default: false,
1074
2349
  },
2350
+ async: {
2351
+ type: "boolean",
2352
+ default: false,
2353
+ description: "When true, start the run detached and return {run_id, status:'running'} immediately; poll workflow_run_status for progress and result.",
2354
+ },
1075
2355
  members: {
1076
2356
  type: "array",
1077
- description: "Optional runtime council member composition override. Accepted only for council workflows (council-default, council-answer, council-code-review, council-debug, council-discover). Each entry specifies a CLI adapter and an optional analytical stance. Seats at indices 0-2 may omit stance (inherits default); seats at indices 3-9 MUST provide stance. When provided, both the member stage (stage 0) and reviewer stage (stage 1) are expanded to match the specified count and CLI mapping. Reviewer stances are NOT inherited — reviewers keep their default review lens.",
2357
+ description: "Optional runtime council member composition override. Accepted only for council workflows (council-default, council-answer, council-code-review, council-debug, council-discover, council-plan-review). Each entry specifies a CLI adapter and an optional analytical stance. Seats at indices 0-2 may omit stance (inherits default); seats at indices 3-9 MUST provide stance. When provided, both the member stage (stage 0) and reviewer stage (stage 1) are expanded to match the specified count and CLI mapping. Reviewer stances are NOT inherited — reviewers keep their default review lens.",
1078
2358
  minItems: 1,
1079
2359
  maxItems: 10,
1080
2360
  items: {
@@ -1097,6 +2377,7 @@ const RUN_WORKFLOW_SCHEMA = {
1097
2377
  type: "string",
1098
2378
  description: "Optional CLI adapter name override for the council chairman seat. Accepted only for council workflows. Only changes the LLM adapter; the chairman's role and prompt template are preserved.",
1099
2379
  },
2380
+ plan_review: PLAN_REVIEW_INPUT_SCHEMA,
1100
2381
  },
1101
2382
  required: ["workflow", "run_id"],
1102
2383
  additionalProperties: false,
@@ -1128,8 +2409,35 @@ const LIST_WORKFLOWS_SCHEMA = {
1128
2409
  properties: {},
1129
2410
  additionalProperties: false,
1130
2411
  };
2412
+ const WORKFLOW_RUN_STATUS_SCHEMA = {
2413
+ type: "object",
2414
+ properties: {
2415
+ run_id: {
2416
+ type: "string",
2417
+ pattern: "^[A-Za-z0-9_-]{1,64}$",
2418
+ },
2419
+ include_result: {
2420
+ type: "boolean",
2421
+ default: false,
2422
+ description: "When true, include the generic final result for a succeeded run or the committed plan-review envelope regardless of terminal status. Default false keeps polling body-blind.",
2423
+ },
2424
+ },
2425
+ required: ["run_id"],
2426
+ additionalProperties: false,
2427
+ };
2428
+ const CANCEL_WORKFLOW_SCHEMA = {
2429
+ type: "object",
2430
+ properties: {
2431
+ run_id: {
2432
+ type: "string",
2433
+ pattern: "^[A-Za-z0-9_-]{1,64}$",
2434
+ },
2435
+ },
2436
+ required: ["run_id"],
2437
+ additionalProperties: false,
2438
+ };
1131
2439
  /**
1132
- * Return the three workflow tool definitions for tools/list.
2440
+ * Return the workflow tool definitions for tools/list.
1133
2441
  *
1134
2442
  * Dynamically discovers available workflows to include in the
1135
2443
  * run_workflow description. Safe to call repeatedly — no side effects.
@@ -1170,6 +2478,22 @@ export function getWorkflowToolDefinitions(workflowsPath, availableCliNames) {
1170
2478
  }
1171
2479
  }
1172
2480
  }
2481
+ // plan_review.seats[].cli_name enum (intersection with the v1 provider set)
2482
+ const planReview = props["plan_review"];
2483
+ const planReviewProps = planReview?.["properties"];
2484
+ const planReviewSeats = planReviewProps?.["seats"];
2485
+ const planReviewItems = planReviewSeats?.["items"];
2486
+ const planReviewSeatProps = planReviewItems?.["properties"];
2487
+ const planReviewCliNameSet = new Set(PLAN_REVIEW_V1_CLI_NAMES);
2488
+ const supportedPlanReviewCliNames = sorted.filter((name) => planReviewCliNameSet.has(name));
2489
+ if (planReviewSeatProps?.["cli_name"] != null &&
2490
+ supportedPlanReviewCliNames.length > 0) {
2491
+ planReviewSeatProps["cli_name"] = {
2492
+ ...planReviewSeatProps["cli_name"],
2493
+ enum: supportedPlanReviewCliNames,
2494
+ description: `Configured v1 plan-review adapters: ${supportedPlanReviewCliNames.join(", ")}`,
2495
+ };
2496
+ }
1173
2497
  // chairman enum
1174
2498
  if (props["chairman"] != null) {
1175
2499
  props["chairman"] = {
@@ -1212,7 +2536,7 @@ export function getWorkflowToolDefinitions(workflowsPath, availableCliNames) {
1212
2536
  return [
1213
2537
  {
1214
2538
  name: "run_workflow",
1215
- description: `Start a new multi-stage workflow execution.${workflowSuffix}\nAccepts optional \`members\` and \`chairman\` parameters for council workflows (council-default, council-answer, council-code-review, council-debug, council-discover) to override member/reviewer composition and chairman CLI adapter at runtime.`,
2539
+ description: `Start a new multi-stage workflow execution.${workflowSuffix}\nAccepts optional \`members\` and \`chairman\` parameters for council workflows (council-default, council-answer, council-code-review, council-debug, council-discover, council-plan-review) to override member/reviewer composition and chairman CLI adapter at runtime.`,
1216
2540
  inputSchema: runSchema,
1217
2541
  },
1218
2542
  {
@@ -1227,6 +2551,18 @@ export function getWorkflowToolDefinitions(workflowsPath, availableCliNames) {
1227
2551
  inputSchema: LIST_WORKFLOWS_SCHEMA,
1228
2552
  annotations: { readOnlyHint: true },
1229
2553
  },
2554
+ {
2555
+ name: "workflow_run_status",
2556
+ description: "Read-only status/progress of a workflow run. The full result is included only with include_result:true. Generic workflows preserve succeeded-final-call behavior; plan-review-receipts reports marker-driven availability and bytes independently of terminal status, with body-blind default polling. Every found run also carries a top-level `health` block {classification: \"ok\"|\"degraded\", reasons: string[]} computed read-time from persisted data — a run can be status:\"succeeded\" yet degraded when on_failure:continue swallowed failed calls or context preflight was partial/degraded (reasons: failed_calls_in_succeeded_run, context_partial, context_lifecycle_failed, coverage_degraded, delegate_fallback_used). When a context-builder lifecycle snapshot is persisted, the `context` block additionally includes `lifecycle_status` and `coverage` {policy, status, required, selected, missing}.",
2557
+ inputSchema: WORKFLOW_RUN_STATUS_SCHEMA,
2558
+ annotations: { readOnlyHint: true },
2559
+ },
2560
+ {
2561
+ name: "cancel_workflow",
2562
+ description: "Cancel a workflow run by run_id. Aborts a live in-process async run (returns status:'cancelling'; poll workflow_run_status until it reaches terminal 'cancelled'). Reaps an orphaned/no-live-worker run to terminal 'cancelled' (reaped:true). Refuses a lock-held run owned by another live worker or a blocking run (cancellable:false). Idempotent; not_found and already_terminal are soft results.",
2563
+ inputSchema: CANCEL_WORKFLOW_SCHEMA,
2564
+ annotations: { destructiveHint: true },
2565
+ },
1230
2566
  ];
1231
2567
  }
1232
2568
  /** Names of the workflow tools (for dispatch matching). */
@@ -1234,6 +2570,8 @@ export const WORKFLOW_TOOL_NAMES = new Set([
1234
2570
  "run_workflow",
1235
2571
  "resume_workflow",
1236
2572
  "list_workflows",
2573
+ "workflow_run_status",
2574
+ "cancel_workflow",
1237
2575
  ]);
1238
2576
  /**
1239
2577
  * Dispatch a workflow tool call.
@@ -1248,7 +2586,11 @@ export async function handleWorkflowToolCall(server, options, name, args, signal
1248
2586
  case "resume_workflow":
1249
2587
  return await executeResumeWorkflow(server, options, args, signal, progressToken);
1250
2588
  case "list_workflows":
1251
- return executeListWorkflows(options.workflowsPath, options.availableCliNames);
2589
+ return executeListWorkflows(options);
2590
+ case "workflow_run_status":
2591
+ return await executeWorkflowRunStatus(options, args);
2592
+ case "cancel_workflow":
2593
+ return await executeCancelWorkflow(options, args);
1252
2594
  default:
1253
2595
  return null;
1254
2596
  }
@@ -1260,7 +2602,8 @@ export async function handleWorkflowToolCall(server, options, name, args, signal
1260
2602
  * Register workflow tools on an MCP Server instance.
1261
2603
  *
1262
2604
  * This is the reusable core extracted from createWorkflowServer().
1263
- * It registers three tools: run_workflow, resume_workflow, list_workflows.
2605
+ * It registers five tools: run_workflow, resume_workflow, list_workflows,
2606
+ * workflow_run_status, cancel_workflow.
1264
2607
  *
1265
2608
  * The `options.createBackend` factory determines how clink calls are
1266
2609
  * dispatched — callers provide either a child-process or in-process backend.