clinkx 0.3.0 → 1.1.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 (669) hide show
  1. package/CHANGELOG.md +119 -0
  2. package/LICENSE +10 -0
  3. package/README.md +154 -26
  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/chairman-knobs.d.ts +24 -0
  12. package/clinkx-workflows/dist/chairman-knobs.js +27 -0
  13. package/clinkx-workflows/dist/chairman-knobs.js.map +1 -0
  14. package/clinkx-workflows/dist/citation-check.d.ts +40 -0
  15. package/clinkx-workflows/dist/citation-check.js +348 -0
  16. package/clinkx-workflows/dist/citation-check.js.map +1 -0
  17. package/clinkx-workflows/dist/clink-client.d.ts +12 -1
  18. package/clinkx-workflows/dist/clink-client.js +56 -5
  19. package/clinkx-workflows/dist/clink-client.js.map +1 -1
  20. package/clinkx-workflows/dist/config.d.ts +173 -16
  21. package/clinkx-workflows/dist/config.js +417 -26
  22. package/clinkx-workflows/dist/config.js.map +1 -1
  23. package/clinkx-workflows/dist/contained-file.d.ts +172 -0
  24. package/clinkx-workflows/dist/contained-file.js +483 -0
  25. package/clinkx-workflows/dist/contained-file.js.map +1 -0
  26. package/clinkx-workflows/dist/context-builder-lifecycle-contract.d.ts +416 -0
  27. package/clinkx-workflows/dist/context-builder-lifecycle-contract.js +328 -0
  28. package/clinkx-workflows/dist/context-builder-lifecycle-contract.js.map +1 -0
  29. package/clinkx-workflows/dist/context-builder-lifecycle-router.d.ts +24 -0
  30. package/clinkx-workflows/dist/context-builder-lifecycle-router.js +39 -0
  31. package/clinkx-workflows/dist/context-builder-lifecycle-router.js.map +1 -0
  32. package/clinkx-workflows/dist/context-cache-key.d.ts +53 -0
  33. package/clinkx-workflows/dist/context-cache-key.js +91 -0
  34. package/clinkx-workflows/dist/context-cache-key.js.map +1 -0
  35. package/clinkx-workflows/dist/context-mode-resolver.d.ts +27 -0
  36. package/clinkx-workflows/dist/context-mode-resolver.js +126 -0
  37. package/clinkx-workflows/dist/context-mode-resolver.js.map +1 -0
  38. package/clinkx-workflows/dist/context-preflight.d.ts +189 -0
  39. package/clinkx-workflows/dist/context-preflight.js +91 -0
  40. package/clinkx-workflows/dist/context-preflight.js.map +1 -0
  41. package/clinkx-workflows/dist/council-execution-policy.d.ts +68 -0
  42. package/clinkx-workflows/dist/council-execution-policy.js +110 -0
  43. package/clinkx-workflows/dist/council-execution-policy.js.map +1 -0
  44. package/clinkx-workflows/dist/council-expander.d.ts +10 -9
  45. package/clinkx-workflows/dist/council-expander.js +20 -10
  46. package/clinkx-workflows/dist/council-expander.js.map +1 -1
  47. package/clinkx-workflows/dist/dashboard/activity-contract.d.ts +5718 -0
  48. package/clinkx-workflows/dist/dashboard/activity-contract.js +157 -0
  49. package/clinkx-workflows/dist/dashboard/activity-contract.js.map +1 -0
  50. package/clinkx-workflows/dist/dashboard/activity-lease.d.ts +25 -0
  51. package/clinkx-workflows/dist/dashboard/activity-lease.js +34 -0
  52. package/clinkx-workflows/dist/dashboard/activity-lease.js.map +1 -0
  53. package/clinkx-workflows/dist/dashboard/activity-reader.d.ts +90 -0
  54. package/clinkx-workflows/dist/dashboard/activity-reader.js +493 -0
  55. package/clinkx-workflows/dist/dashboard/activity-reader.js.map +1 -0
  56. package/clinkx-workflows/dist/dashboard/api-routes.d.ts +40 -0
  57. package/clinkx-workflows/dist/dashboard/api-routes.js +1155 -0
  58. package/clinkx-workflows/dist/dashboard/api-routes.js.map +1 -0
  59. package/clinkx-workflows/dist/dashboard/artifact-index.d.ts +37 -0
  60. package/clinkx-workflows/dist/dashboard/artifact-index.js +145 -0
  61. package/clinkx-workflows/dist/dashboard/artifact-index.js.map +1 -0
  62. package/clinkx-workflows/dist/dashboard/auth/challenge-store.d.ts +31 -0
  63. package/clinkx-workflows/dist/dashboard/auth/challenge-store.js +65 -0
  64. package/clinkx-workflows/dist/dashboard/auth/challenge-store.js.map +1 -0
  65. package/clinkx-workflows/dist/dashboard/auth/controller.d.ts +26 -0
  66. package/clinkx-workflows/dist/dashboard/auth/controller.js +596 -0
  67. package/clinkx-workflows/dist/dashboard/auth/controller.js.map +1 -0
  68. package/clinkx-workflows/dist/dashboard/auth/credential-store.d.ts +60 -0
  69. package/clinkx-workflows/dist/dashboard/auth/credential-store.js +776 -0
  70. package/clinkx-workflows/dist/dashboard/auth/credential-store.js.map +1 -0
  71. package/clinkx-workflows/dist/dashboard/auth/enrollment-gate.d.ts +9 -0
  72. package/clinkx-workflows/dist/dashboard/auth/enrollment-gate.js +44 -0
  73. package/clinkx-workflows/dist/dashboard/auth/enrollment-gate.js.map +1 -0
  74. package/clinkx-workflows/dist/dashboard/auth/session-store.d.ts +17 -0
  75. package/clinkx-workflows/dist/dashboard/auth/session-store.js +77 -0
  76. package/clinkx-workflows/dist/dashboard/auth/session-store.js.map +1 -0
  77. package/clinkx-workflows/dist/dashboard/auth/webauthn-service.d.ts +25 -0
  78. package/clinkx-workflows/dist/dashboard/auth/webauthn-service.js +71 -0
  79. package/clinkx-workflows/dist/dashboard/auth/webauthn-service.js.map +1 -0
  80. package/clinkx-workflows/dist/dashboard/context-builder-invocation-contract.d.ts +3865 -0
  81. package/clinkx-workflows/dist/dashboard/context-builder-invocation-contract.js +69 -0
  82. package/clinkx-workflows/dist/dashboard/context-builder-invocation-contract.js.map +1 -0
  83. package/clinkx-workflows/dist/dashboard/context-run-liveness.d.ts +37 -0
  84. package/clinkx-workflows/dist/dashboard/context-run-liveness.js +49 -0
  85. package/clinkx-workflows/dist/dashboard/context-run-liveness.js.map +1 -0
  86. package/clinkx-workflows/dist/dashboard/context-run-reader.d.ts +256 -0
  87. package/clinkx-workflows/dist/dashboard/context-run-reader.js +1301 -0
  88. package/clinkx-workflows/dist/dashboard/context-run-reader.js.map +1 -0
  89. package/clinkx-workflows/dist/dashboard/contracts.d.ts +28483 -0
  90. package/clinkx-workflows/dist/dashboard/contracts.js +1284 -0
  91. package/clinkx-workflows/dist/dashboard/contracts.js.map +1 -0
  92. package/clinkx-workflows/dist/dashboard/generation.d.ts +14 -0
  93. package/clinkx-workflows/dist/dashboard/generation.js +41 -0
  94. package/clinkx-workflows/dist/dashboard/generation.js.map +1 -0
  95. package/clinkx-workflows/dist/dashboard/index.d.ts +32 -0
  96. package/clinkx-workflows/dist/dashboard/index.js +20 -0
  97. package/clinkx-workflows/dist/dashboard/index.js.map +1 -0
  98. package/clinkx-workflows/dist/dashboard/invocation-contract.d.ts +786 -0
  99. package/clinkx-workflows/dist/dashboard/invocation-contract.js +201 -0
  100. package/clinkx-workflows/dist/dashboard/invocation-contract.js.map +1 -0
  101. package/clinkx-workflows/dist/dashboard/lazy-content.d.ts +114 -0
  102. package/clinkx-workflows/dist/dashboard/lazy-content.js +799 -0
  103. package/clinkx-workflows/dist/dashboard/lazy-content.js.map +1 -0
  104. package/clinkx-workflows/dist/dashboard/native-transcript/contract.d.ts +90 -0
  105. package/clinkx-workflows/dist/dashboard/native-transcript/contract.js +31 -0
  106. package/clinkx-workflows/dist/dashboard/native-transcript/contract.js.map +1 -0
  107. package/clinkx-workflows/dist/dashboard/native-transcript/projection.d.ts +12 -0
  108. package/clinkx-workflows/dist/dashboard/native-transcript/projection.js +286 -0
  109. package/clinkx-workflows/dist/dashboard/native-transcript/projection.js.map +1 -0
  110. package/clinkx-workflows/dist/dashboard/progress-report-contract.d.ts +446 -0
  111. package/clinkx-workflows/dist/dashboard/progress-report-contract.js +221 -0
  112. package/clinkx-workflows/dist/dashboard/progress-report-contract.js.map +1 -0
  113. package/clinkx-workflows/dist/dashboard/progress-report-reader.d.ts +50 -0
  114. package/clinkx-workflows/dist/dashboard/progress-report-reader.js +529 -0
  115. package/clinkx-workflows/dist/dashboard/progress-report-reader.js.map +1 -0
  116. package/clinkx-workflows/dist/dashboard/progress-report-status-overlay.d.ts +89 -0
  117. package/clinkx-workflows/dist/dashboard/progress-report-status-overlay.js +71 -0
  118. package/clinkx-workflows/dist/dashboard/progress-report-status-overlay.js.map +1 -0
  119. package/clinkx-workflows/dist/dashboard/progress-report-status-store.d.ts +29 -0
  120. package/clinkx-workflows/dist/dashboard/progress-report-status-store.js +111 -0
  121. package/clinkx-workflows/dist/dashboard/progress-report-status-store.js.map +1 -0
  122. package/clinkx-workflows/dist/dashboard/prompt-builder-library-contract.d.ts +194 -0
  123. package/clinkx-workflows/dist/dashboard/prompt-builder-library-contract.js +89 -0
  124. package/clinkx-workflows/dist/dashboard/prompt-builder-library-contract.js.map +1 -0
  125. package/clinkx-workflows/dist/dashboard/prompt-builder-library-store.d.ts +26 -0
  126. package/clinkx-workflows/dist/dashboard/prompt-builder-library-store.js +102 -0
  127. package/clinkx-workflows/dist/dashboard/prompt-builder-library-store.js.map +1 -0
  128. package/clinkx-workflows/dist/dashboard/read-model.d.ts +86 -0
  129. package/clinkx-workflows/dist/dashboard/read-model.js +440 -0
  130. package/clinkx-workflows/dist/dashboard/read-model.js.map +1 -0
  131. package/clinkx-workflows/dist/dashboard/request-admission.d.ts +31 -0
  132. package/clinkx-workflows/dist/dashboard/request-admission.js +130 -0
  133. package/clinkx-workflows/dist/dashboard/request-admission.js.map +1 -0
  134. package/clinkx-workflows/dist/dashboard/scanner.d.ts +78 -0
  135. package/clinkx-workflows/dist/dashboard/scanner.js +1154 -0
  136. package/clinkx-workflows/dist/dashboard/scanner.js.map +1 -0
  137. package/clinkx-workflows/dist/dashboard/server-factory.d.ts +63 -0
  138. package/clinkx-workflows/dist/dashboard/server-factory.js +483 -0
  139. package/clinkx-workflows/dist/dashboard/server-factory.js.map +1 -0
  140. package/clinkx-workflows/dist/dashboard/session-log-contract.d.ts +2778 -0
  141. package/clinkx-workflows/dist/dashboard/session-log-contract.js +506 -0
  142. package/clinkx-workflows/dist/dashboard/session-log-contract.js.map +1 -0
  143. package/clinkx-workflows/dist/dashboard/session-log-identity.d.ts +21 -0
  144. package/clinkx-workflows/dist/dashboard/session-log-identity.js +101 -0
  145. package/clinkx-workflows/dist/dashboard/session-log-identity.js.map +1 -0
  146. package/clinkx-workflows/dist/dashboard/session-log-journal.d.ts +43 -0
  147. package/clinkx-workflows/dist/dashboard/session-log-journal.js +529 -0
  148. package/clinkx-workflows/dist/dashboard/session-log-journal.js.map +1 -0
  149. package/clinkx-workflows/dist/dashboard/session-log-reader.d.ts +61 -0
  150. package/clinkx-workflows/dist/dashboard/session-log-reader.js +378 -0
  151. package/clinkx-workflows/dist/dashboard/session-log-reader.js.map +1 -0
  152. package/clinkx-workflows/dist/dashboard/session-log-redaction.d.ts +14 -0
  153. package/clinkx-workflows/dist/dashboard/session-log-redaction.js +1101 -0
  154. package/clinkx-workflows/dist/dashboard/session-log-redaction.js.map +1 -0
  155. package/clinkx-workflows/dist/dashboard/session-log-retention.d.ts +23 -0
  156. package/clinkx-workflows/dist/dashboard/session-log-retention.js +212 -0
  157. package/clinkx-workflows/dist/dashboard/session-log-retention.js.map +1 -0
  158. package/clinkx-workflows/dist/dashboard/static-responses.d.ts +10 -0
  159. package/clinkx-workflows/dist/dashboard/static-responses.js +94 -0
  160. package/clinkx-workflows/dist/dashboard/static-responses.js.map +1 -0
  161. package/clinkx-workflows/dist/dashboard/worker-lock-observer.d.ts +28 -0
  162. package/clinkx-workflows/dist/dashboard/worker-lock-observer.js +241 -0
  163. package/clinkx-workflows/dist/dashboard/worker-lock-observer.js.map +1 -0
  164. package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
  165. package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
  166. package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
  167. package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
  168. package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
  169. package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
  170. package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2 +0 -0
  171. package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff +0 -0
  172. package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
  173. package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
  174. package/clinkx-workflows/dist/dashboard-ui/assets/index-JPHQVtn9.js +106 -0
  175. package/clinkx-workflows/dist/dashboard-ui/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  176. package/clinkx-workflows/dist/dashboard-ui/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  177. package/clinkx-workflows/dist/dashboard-ui/assets/jetbrains-mono-latin-500-normal-BWZEU5yA.woff2 +0 -0
  178. package/clinkx-workflows/dist/dashboard-ui/assets/jetbrains-mono-latin-500-normal-CJOVTJB7.woff +0 -0
  179. package/clinkx-workflows/dist/dashboard-ui/assets/style-DOMHk-j6.css +1 -0
  180. package/clinkx-workflows/dist/dashboard-ui/contracts/council-plan-review-input-v1.json +158 -0
  181. package/clinkx-workflows/dist/dashboard-ui/index.html +15 -0
  182. package/clinkx-workflows/dist/dashboard-ui/licenses/IBM-Plex-Mono-OFL.txt +93 -0
  183. package/clinkx-workflows/dist/dashboard-ui/licenses/IBM-Plex-Sans-OFL.txt +93 -0
  184. package/clinkx-workflows/dist/dashboard-ui/licenses/JetBrains-Mono-OFL.txt +93 -0
  185. package/clinkx-workflows/dist/dashboard-ui/licenses/NOTICE.md +32 -0
  186. package/clinkx-workflows/dist/dashboard-ui/theme-init.js +8 -0
  187. package/clinkx-workflows/dist/definition-normalizer.d.ts +19 -0
  188. package/clinkx-workflows/dist/definition-normalizer.js.map +1 -1
  189. package/clinkx-workflows/dist/delegate-finalizer-policy.d.ts +26 -0
  190. package/clinkx-workflows/dist/delegate-finalizer-policy.js +72 -0
  191. package/clinkx-workflows/dist/delegate-finalizer-policy.js.map +1 -0
  192. package/clinkx-workflows/dist/delegate-signature.d.ts +14 -0
  193. package/clinkx-workflows/dist/delegate-signature.js +14 -0
  194. package/clinkx-workflows/dist/delegate-signature.js.map +1 -0
  195. package/clinkx-workflows/dist/delegate-validate.d.ts +108 -0
  196. package/clinkx-workflows/dist/delegate-validate.js +148 -0
  197. package/clinkx-workflows/dist/delegate-validate.js.map +1 -0
  198. package/clinkx-workflows/dist/engine.d.ts +251 -0
  199. package/clinkx-workflows/dist/engine.js +1342 -92
  200. package/clinkx-workflows/dist/engine.js.map +1 -1
  201. package/clinkx-workflows/dist/errors.d.ts +24 -2
  202. package/clinkx-workflows/dist/errors.js +30 -2
  203. package/clinkx-workflows/dist/errors.js.map +1 -1
  204. package/clinkx-workflows/dist/fidelity.d.ts +47 -1
  205. package/clinkx-workflows/dist/fidelity.js +54 -1
  206. package/clinkx-workflows/dist/fidelity.js.map +1 -1
  207. package/clinkx-workflows/dist/fingerprint.d.ts +25 -7
  208. package/clinkx-workflows/dist/fingerprint.js +33 -10
  209. package/clinkx-workflows/dist/fingerprint.js.map +1 -1
  210. package/clinkx-workflows/dist/index.d.ts +19 -0
  211. package/clinkx-workflows/dist/index.js +68 -1
  212. package/clinkx-workflows/dist/index.js.map +1 -1
  213. package/clinkx-workflows/dist/loader.d.ts +9 -3
  214. package/clinkx-workflows/dist/loader.js +92 -21
  215. package/clinkx-workflows/dist/loader.js.map +1 -1
  216. package/clinkx-workflows/dist/logger.d.ts +1 -1
  217. package/clinkx-workflows/dist/logger.js +17 -3
  218. package/clinkx-workflows/dist/logger.js.map +1 -1
  219. package/clinkx-workflows/dist/monotonic-clock.d.ts +19 -0
  220. package/clinkx-workflows/dist/monotonic-clock.js +22 -0
  221. package/clinkx-workflows/dist/monotonic-clock.js.map +1 -0
  222. package/clinkx-workflows/dist/plan-review/admission.d.ts +160 -0
  223. package/clinkx-workflows/dist/plan-review/admission.js +290 -0
  224. package/clinkx-workflows/dist/plan-review/admission.js.map +1 -0
  225. package/clinkx-workflows/dist/plan-review/context-policy.d.ts +55 -0
  226. package/clinkx-workflows/dist/plan-review/context-policy.js +146 -0
  227. package/clinkx-workflows/dist/plan-review/context-policy.js.map +1 -0
  228. package/clinkx-workflows/dist/plan-review/contract.d.ts +763 -0
  229. package/clinkx-workflows/dist/plan-review/contract.js +289 -0
  230. package/clinkx-workflows/dist/plan-review/contract.js.map +1 -0
  231. package/clinkx-workflows/dist/plan-review/digest.d.ts +68 -0
  232. package/clinkx-workflows/dist/plan-review/digest.js +241 -0
  233. package/clinkx-workflows/dist/plan-review/digest.js.map +1 -0
  234. package/clinkx-workflows/dist/plan-review/discovery.d.ts +45 -0
  235. package/clinkx-workflows/dist/plan-review/discovery.js +36 -0
  236. package/clinkx-workflows/dist/plan-review/discovery.js.map +1 -0
  237. package/clinkx-workflows/dist/plan-review/execution-contract.d.ts +298 -0
  238. package/clinkx-workflows/dist/plan-review/execution-contract.js +194 -0
  239. package/clinkx-workflows/dist/plan-review/execution-contract.js.map +1 -0
  240. package/clinkx-workflows/dist/plan-review/expander.d.ts +48 -0
  241. package/clinkx-workflows/dist/plan-review/expander.js +243 -0
  242. package/clinkx-workflows/dist/plan-review/expander.js.map +1 -0
  243. package/clinkx-workflows/dist/plan-review/projector.d.ts +126 -0
  244. package/clinkx-workflows/dist/plan-review/projector.js +437 -0
  245. package/clinkx-workflows/dist/plan-review/projector.js.map +1 -0
  246. package/clinkx-workflows/dist/plan-review/publication.d.ts +95 -0
  247. package/clinkx-workflows/dist/plan-review/publication.js +182 -0
  248. package/clinkx-workflows/dist/plan-review/publication.js.map +1 -0
  249. package/clinkx-workflows/dist/plan-review/ranking-reducer.d.ts +34 -0
  250. package/clinkx-workflows/dist/plan-review/ranking-reducer.js +139 -0
  251. package/clinkx-workflows/dist/plan-review/ranking-reducer.js.map +1 -0
  252. package/clinkx-workflows/dist/plan-review/resume.d.ts +38 -0
  253. package/clinkx-workflows/dist/plan-review/resume.js +122 -0
  254. package/clinkx-workflows/dist/plan-review/resume.js.map +1 -0
  255. package/clinkx-workflows/dist/plan-review/rubric.d.ts +7 -0
  256. package/clinkx-workflows/dist/plan-review/rubric.js +24 -0
  257. package/clinkx-workflows/dist/plan-review/rubric.js.map +1 -0
  258. package/clinkx-workflows/dist/plan-review/run-state.d.ts +398 -0
  259. package/clinkx-workflows/dist/plan-review/run-state.js +183 -0
  260. package/clinkx-workflows/dist/plan-review/run-state.js.map +1 -0
  261. package/clinkx-workflows/dist/plan-review/runtime-policy.d.ts +74 -0
  262. package/clinkx-workflows/dist/plan-review/runtime-policy.js +186 -0
  263. package/clinkx-workflows/dist/plan-review/runtime-policy.js.map +1 -0
  264. package/clinkx-workflows/dist/plan-review/snapshot.d.ts +81 -0
  265. package/clinkx-workflows/dist/plan-review/snapshot.js +170 -0
  266. package/clinkx-workflows/dist/plan-review/snapshot.js.map +1 -0
  267. package/clinkx-workflows/dist/plan-review/stage-error.d.ts +19 -0
  268. package/clinkx-workflows/dist/plan-review/stage-error.js +20 -0
  269. package/clinkx-workflows/dist/plan-review/stage-error.js.map +1 -0
  270. package/clinkx-workflows/dist/post-processors/index.d.ts +20 -0
  271. package/clinkx-workflows/dist/post-processors/index.js +21 -0
  272. package/clinkx-workflows/dist/post-processors/index.js.map +1 -0
  273. package/clinkx-workflows/dist/prompt-budget.d.ts +7 -0
  274. package/clinkx-workflows/dist/prompt-budget.js +7 -1
  275. package/clinkx-workflows/dist/prompt-budget.js.map +1 -1
  276. package/clinkx-workflows/dist/reducers/index.js +2 -0
  277. package/clinkx-workflows/dist/reducers/index.js.map +1 -1
  278. package/clinkx-workflows/dist/roster-contract.d.ts +889 -0
  279. package/clinkx-workflows/dist/roster-contract.js +180 -0
  280. package/clinkx-workflows/dist/roster-contract.js.map +1 -0
  281. package/clinkx-workflows/dist/rosters.d.ts +14 -0
  282. package/clinkx-workflows/dist/rosters.js +169 -0
  283. package/clinkx-workflows/dist/rosters.js.map +1 -0
  284. package/clinkx-workflows/dist/run-health.d.ts +7 -0
  285. package/clinkx-workflows/dist/run-health.js +25 -0
  286. package/clinkx-workflows/dist/run-health.js.map +1 -0
  287. package/clinkx-workflows/dist/run-lock-heartbeat.d.ts +2 -0
  288. package/clinkx-workflows/dist/run-lock-heartbeat.js +3 -0
  289. package/clinkx-workflows/dist/run-lock-heartbeat.js.map +1 -0
  290. package/clinkx-workflows/dist/run-summary/cards/council-plan-review.d.ts +34 -0
  291. package/clinkx-workflows/dist/run-summary/cards/council-plan-review.js +74 -0
  292. package/clinkx-workflows/dist/run-summary/cards/council-plan-review.js.map +1 -0
  293. package/clinkx-workflows/dist/run-summary/cards/index.js +5 -0
  294. package/clinkx-workflows/dist/run-summary/cards/index.js.map +1 -1
  295. package/clinkx-workflows/dist/run-summary.d.ts +13 -0
  296. package/clinkx-workflows/dist/run-summary.js +29 -1
  297. package/clinkx-workflows/dist/run-summary.js.map +1 -1
  298. package/clinkx-workflows/dist/schema.d.ts +140 -21
  299. package/clinkx-workflows/dist/schema.js +34 -1
  300. package/clinkx-workflows/dist/schema.js.map +1 -1
  301. package/clinkx-workflows/dist/seed-terms.d.ts +30 -0
  302. package/clinkx-workflows/dist/seed-terms.js +176 -0
  303. package/clinkx-workflows/dist/seed-terms.js.map +1 -0
  304. package/clinkx-workflows/dist/server.d.ts +5 -2
  305. package/clinkx-workflows/dist/server.js +84 -52
  306. package/clinkx-workflows/dist/server.js.map +1 -1
  307. package/clinkx-workflows/dist/shutdown.d.ts +29 -18
  308. package/clinkx-workflows/dist/shutdown.js +90 -64
  309. package/clinkx-workflows/dist/shutdown.js.map +1 -1
  310. package/clinkx-workflows/dist/state-schema.d.ts +61 -0
  311. package/clinkx-workflows/dist/state.d.ts +175 -3
  312. package/clinkx-workflows/dist/state.js +1398 -44
  313. package/clinkx-workflows/dist/state.js.map +1 -1
  314. package/clinkx-workflows/dist/stdio-watchdog.d.ts +52 -0
  315. package/clinkx-workflows/dist/stdio-watchdog.js +212 -0
  316. package/clinkx-workflows/dist/stdio-watchdog.js.map +1 -0
  317. package/clinkx-workflows/dist/transport.d.ts +15 -0
  318. package/clinkx-workflows/dist/transport.js +19 -10
  319. package/clinkx-workflows/dist/transport.js.map +1 -1
  320. package/clinkx-workflows/dist/types.d.ts +20 -2
  321. package/clinkx-workflows/dist/validators/council.d.ts +334 -101
  322. package/clinkx-workflows/dist/validators/council.js +52 -13
  323. package/clinkx-workflows/dist/validators/council.js.map +1 -1
  324. package/clinkx-workflows/dist/validators/index.d.ts +1 -0
  325. package/clinkx-workflows/dist/validators/index.js +3 -0
  326. package/clinkx-workflows/dist/validators/index.js.map +1 -1
  327. package/clinkx-workflows/dist/validators/plan-review.d.ts +398 -0
  328. package/clinkx-workflows/dist/validators/plan-review.js +316 -0
  329. package/clinkx-workflows/dist/validators/plan-review.js.map +1 -0
  330. package/clinkx-workflows/dist/workflow-receipt.js +188 -4
  331. package/clinkx-workflows/dist/workflow-receipt.js.map +1 -1
  332. package/clinkx-workflows/dist/workflow-tools.d.ts +118 -3
  333. package/clinkx-workflows/dist/workflow-tools.js +2110 -175
  334. package/clinkx-workflows/dist/workflow-tools.js.map +1 -1
  335. package/clinkx-workflows/dist/workspace-signature.d.ts +16 -0
  336. package/clinkx-workflows/dist/workspace-signature.js +388 -0
  337. package/clinkx-workflows/dist/workspace-signature.js.map +1 -0
  338. package/clinkx-workflows/templates/council/answer-chairman.txt +3 -0
  339. package/clinkx-workflows/templates/council/answer-cross-reviewer.txt +3 -0
  340. package/clinkx-workflows/templates/council/answer-specialist.txt +5 -1
  341. package/clinkx-workflows/templates/council/chairman.txt +3 -0
  342. package/clinkx-workflows/templates/council/code-review-chairman.txt +16 -0
  343. package/clinkx-workflows/templates/council/code-review-cross-reviewer.txt +7 -1
  344. package/clinkx-workflows/templates/council/code-review-reviewer.txt +22 -3
  345. package/clinkx-workflows/templates/council/debug-chairman.txt +3 -0
  346. package/clinkx-workflows/templates/council/debug-cross-reviewer.txt +3 -0
  347. package/clinkx-workflows/templates/council/debug-debugger.txt +5 -1
  348. package/clinkx-workflows/templates/council/discover-analyst.txt +5 -1
  349. package/clinkx-workflows/templates/council/discover-chairman.txt +3 -0
  350. package/clinkx-workflows/templates/council/discover-cross-reviewer.txt +3 -0
  351. package/clinkx-workflows/templates/council/member.txt +5 -1
  352. package/clinkx-workflows/templates/council/plan-review-chairman.txt +71 -0
  353. package/clinkx-workflows/templates/council/plan-review-cross-reviewer.txt +54 -0
  354. package/clinkx-workflows/templates/council/plan-review-reviewer.txt +63 -0
  355. package/clinkx-workflows/templates/council/reviewer.txt +3 -0
  356. package/clinkx-workflows/templates/plan-review/chairman.txt +48 -0
  357. package/clinkx-workflows/templates/plan-review/cross-review.txt +52 -0
  358. package/clinkx-workflows/templates/plan-review/ranking.txt +16 -0
  359. package/clinkx-workflows/templates/plan-review/reviewer.txt +57 -0
  360. package/clinkx-workflows/workflows/council-answer.yaml +100 -8
  361. package/clinkx-workflows/workflows/council-code-review.yaml +106 -8
  362. package/clinkx-workflows/workflows/council-debug.yaml +100 -8
  363. package/clinkx-workflows/workflows/council-default.yaml +104 -8
  364. package/clinkx-workflows/workflows/council-discover.yaml +100 -8
  365. package/clinkx-workflows/workflows/council-plan-review.yaml +253 -0
  366. package/clinkx-workflows/workflows/plan-review-receipts.yaml +230 -0
  367. package/conf/adapters/agy.json +56 -0
  368. package/conf/adapters/claude.json +12 -0
  369. package/conf/adapters/codex-app-server.json +44 -0
  370. package/conf/adapters/codex.json +14 -2
  371. package/conf/adapters/glm.json +24 -3
  372. package/conf/adapters/grok.json +67 -0
  373. package/conf/adapters/hapi/agy.json +59 -0
  374. package/conf/adapters/hapi/claude.json +12 -0
  375. package/conf/adapters/hapi/codex.json +14 -2
  376. package/conf/adapters/hapi/glm.json +25 -4
  377. package/conf/rosters/default-council.json +21 -0
  378. package/dist/activity-journal.d.ts +56 -0
  379. package/dist/activity-journal.js +706 -0
  380. package/dist/activity-journal.js.map +1 -0
  381. package/dist/allowed-roots.d.ts +41 -0
  382. package/dist/allowed-roots.js +125 -3
  383. package/dist/allowed-roots.js.map +1 -1
  384. package/dist/codex/activity-session-log.d.ts +14 -0
  385. package/dist/codex/activity-session-log.js +206 -0
  386. package/dist/codex/activity-session-log.js.map +1 -0
  387. package/dist/codex/agent-tools.d.ts +23 -0
  388. package/dist/codex/agent-tools.js +1306 -0
  389. package/dist/codex/agent-tools.js.map +1 -0
  390. package/dist/codex/app-server-client.d.ts +79 -0
  391. package/dist/codex/app-server-client.js +593 -0
  392. package/dist/codex/app-server-client.js.map +1 -0
  393. package/dist/codex/app-server-registry.d.ts +407 -0
  394. package/dist/codex/app-server-registry.js +1493 -0
  395. package/dist/codex/app-server-registry.js.map +1 -0
  396. package/dist/codex/client-types.d.ts +115 -0
  397. package/dist/codex/client-types.js +2 -0
  398. package/dist/codex/client-types.js.map +1 -0
  399. package/dist/codex/protocol.d.ts +5375 -0
  400. package/dist/codex/protocol.js +614 -0
  401. package/dist/codex/protocol.js.map +1 -0
  402. package/dist/codex/session.d.ts +263 -0
  403. package/dist/codex/session.js +466 -0
  404. package/dist/codex/session.js.map +1 -0
  405. package/dist/config.d.ts +386 -23
  406. package/dist/config.js +184 -15
  407. package/dist/config.js.map +1 -1
  408. package/dist/context-builder/anchors.d.ts +1 -0
  409. package/dist/context-builder/anchors.js +3 -0
  410. package/dist/context-builder/anchors.js.map +1 -1
  411. package/dist/context-builder/bundle.d.ts +48 -2
  412. package/dist/context-builder/bundle.js +388 -2417
  413. package/dist/context-builder/bundle.js.map +1 -1
  414. package/dist/context-builder/codemap.d.ts +5 -0
  415. package/dist/context-builder/codemap.js +387 -13
  416. package/dist/context-builder/codemap.js.map +1 -1
  417. package/dist/context-builder/config.d.ts +1 -0
  418. package/dist/context-builder/config.js +12 -0
  419. package/dist/context-builder/config.js.map +1 -1
  420. package/dist/context-builder/context-run-catalog.d.ts +17 -0
  421. package/dist/context-builder/context-run-catalog.js +99 -0
  422. package/dist/context-builder/context-run-catalog.js.map +1 -0
  423. package/dist/context-builder/delegate-finalizer-effective.d.ts +33 -0
  424. package/dist/context-builder/delegate-finalizer-effective.js +82 -0
  425. package/dist/context-builder/delegate-finalizer-effective.js.map +1 -0
  426. package/dist/context-builder/delegate-packet.d.ts +55 -0
  427. package/dist/context-builder/delegate-packet.js +430 -74
  428. package/dist/context-builder/delegate-packet.js.map +1 -1
  429. package/dist/context-builder/delegate-reconstruction.d.ts +29 -0
  430. package/dist/context-builder/delegate-reconstruction.js +110 -41
  431. package/dist/context-builder/delegate-reconstruction.js.map +1 -1
  432. package/dist/context-builder/delegate-validation.d.ts +41 -1
  433. package/dist/context-builder/delegate-validation.js +237 -48
  434. package/dist/context-builder/delegate-validation.js.map +1 -1
  435. package/dist/context-builder/delegate.d.ts +11 -0
  436. package/dist/context-builder/delegate.js +210 -58
  437. package/dist/context-builder/delegate.js.map +1 -1
  438. package/dist/context-builder/diagnostics.d.ts +11 -2
  439. package/dist/context-builder/diagnostics.js +39 -6
  440. package/dist/context-builder/diagnostics.js.map +1 -1
  441. package/dist/context-builder/discovery-progress.d.ts +25 -0
  442. package/dist/context-builder/discovery-progress.js +64 -0
  443. package/dist/context-builder/discovery-progress.js.map +1 -0
  444. package/dist/context-builder/discovery-watchdog.d.ts +15 -0
  445. package/dist/context-builder/discovery-watchdog.js +39 -0
  446. package/dist/context-builder/discovery-watchdog.js.map +1 -0
  447. package/dist/context-builder/discovery.d.ts +36 -1
  448. package/dist/context-builder/discovery.js +584 -172
  449. package/dist/context-builder/discovery.js.map +1 -1
  450. package/dist/context-builder/external-gaps.d.ts +73 -0
  451. package/dist/context-builder/external-gaps.js +248 -0
  452. package/dist/context-builder/external-gaps.js.map +1 -0
  453. package/dist/context-builder/globs.d.ts +1 -0
  454. package/dist/context-builder/globs.js +25 -10
  455. package/dist/context-builder/globs.js.map +1 -1
  456. package/dist/context-builder/handler.d.ts +67 -2
  457. package/dist/context-builder/handler.js +1810 -2440
  458. package/dist/context-builder/handler.js.map +1 -1
  459. package/dist/context-builder/invocation-sidecar.d.ts +41 -0
  460. package/dist/context-builder/invocation-sidecar.js +189 -0
  461. package/dist/context-builder/invocation-sidecar.js.map +1 -0
  462. package/dist/context-builder/lifecycle-contract.d.ts +424 -0
  463. package/dist/context-builder/lifecycle-contract.js +523 -0
  464. package/dist/context-builder/lifecycle-contract.js.map +1 -0
  465. package/dist/context-builder/lifecycle-journal.d.ts +62 -0
  466. package/dist/context-builder/lifecycle-journal.js +88 -0
  467. package/dist/context-builder/lifecycle-journal.js.map +1 -0
  468. package/dist/context-builder/lifecycle-observer.d.ts +102 -0
  469. package/dist/context-builder/lifecycle-observer.js +258 -0
  470. package/dist/context-builder/lifecycle-observer.js.map +1 -0
  471. package/dist/context-builder/lifecycle-reconcile.d.ts +32 -0
  472. package/dist/context-builder/lifecycle-reconcile.js +191 -0
  473. package/dist/context-builder/lifecycle-reconcile.js.map +1 -0
  474. package/dist/context-builder/lifecycle-recovery.d.ts +2 -0
  475. package/dist/context-builder/lifecycle-recovery.js +14 -0
  476. package/dist/context-builder/lifecycle-recovery.js.map +1 -0
  477. package/dist/context-builder/mutation-lock.d.ts +32 -0
  478. package/dist/context-builder/mutation-lock.js +321 -0
  479. package/dist/context-builder/mutation-lock.js.map +1 -0
  480. package/dist/context-builder/path-signals.js +9 -5
  481. package/dist/context-builder/path-signals.js.map +1 -1
  482. package/dist/context-builder/progress.d.ts +3 -1
  483. package/dist/context-builder/progress.js +23 -0
  484. package/dist/context-builder/progress.js.map +1 -1
  485. package/dist/context-builder/receipt.d.ts +6 -4
  486. package/dist/context-builder/receipt.js +12 -1
  487. package/dist/context-builder/receipt.js.map +1 -1
  488. package/dist/context-builder/refine-handler.d.ts +14 -0
  489. package/dist/context-builder/refine-handler.js +790 -0
  490. package/dist/context-builder/refine-handler.js.map +1 -0
  491. package/dist/context-builder/review-coverage.d.ts +116 -0
  492. package/dist/context-builder/review-coverage.js +544 -0
  493. package/dist/context-builder/review-coverage.js.map +1 -0
  494. package/dist/context-builder/schema.d.ts +315 -2
  495. package/dist/context-builder/schema.js +133 -7
  496. package/dist/context-builder/schema.js.map +1 -1
  497. package/dist/context-builder/secrets.d.ts +59 -0
  498. package/dist/context-builder/secrets.js +2255 -44
  499. package/dist/context-builder/secrets.js.map +1 -1
  500. package/dist/context-builder/selection.d.ts +7 -3
  501. package/dist/context-builder/selection.js +200 -738
  502. package/dist/context-builder/selection.js.map +1 -1
  503. package/dist/context-builder/session-log-identity.d.ts +1 -0
  504. package/dist/context-builder/session-log-identity.js +66 -0
  505. package/dist/context-builder/session-log-identity.js.map +1 -0
  506. package/dist/context-builder/slice-range-math.d.ts +16 -0
  507. package/dist/context-builder/slice-range-math.js +137 -0
  508. package/dist/context-builder/slice-range-math.js.map +1 -0
  509. package/dist/context-builder/status.d.ts +23 -2
  510. package/dist/context-builder/status.js +60 -4
  511. package/dist/context-builder/status.js.map +1 -1
  512. package/dist/context-builder/storage.d.ts +59 -2
  513. package/dist/context-builder/storage.js +500 -168
  514. package/dist/context-builder/storage.js.map +1 -1
  515. package/dist/context-builder/token-estimate.d.ts +13 -2
  516. package/dist/context-builder/token-estimate.js +77 -4
  517. package/dist/context-builder/token-estimate.js.map +1 -1
  518. package/dist/context-builder/versions.d.ts +44 -0
  519. package/dist/context-builder/versions.js +45 -0
  520. package/dist/context-builder/versions.js.map +1 -0
  521. package/dist/context-builder/workflow-runner.d.ts +75 -0
  522. package/dist/context-builder/workflow-runner.js +1241 -0
  523. package/dist/context-builder/workflow-runner.js.map +1 -0
  524. package/dist/context-builder/write-watchdog.d.ts +14 -0
  525. package/dist/context-builder/write-watchdog.js +38 -0
  526. package/dist/context-builder/write-watchdog.js.map +1 -0
  527. package/dist/dashboard-adapter-catalog.d.ts +9 -0
  528. package/dist/dashboard-adapter-catalog.js +53 -0
  529. package/dist/dashboard-adapter-catalog.js.map +1 -0
  530. package/dist/dashboard-cli.d.ts +81 -0
  531. package/dist/dashboard-cli.js +623 -0
  532. package/dist/dashboard-cli.js.map +1 -0
  533. package/dist/dashboard-companion/client.d.ts +37 -0
  534. package/dist/dashboard-companion/client.js +229 -0
  535. package/dist/dashboard-companion/client.js.map +1 -0
  536. package/dist/dashboard-companion/config.d.ts +8 -0
  537. package/dist/dashboard-companion/config.js +52 -0
  538. package/dist/dashboard-companion/config.js.map +1 -0
  539. package/dist/dashboard-companion/http-proxy.d.ts +10 -0
  540. package/dist/dashboard-companion/http-proxy.js +234 -0
  541. package/dist/dashboard-companion/http-proxy.js.map +1 -0
  542. package/dist/dashboard-companion/protocol.d.ts +80 -0
  543. package/dist/dashboard-companion/protocol.js +458 -0
  544. package/dist/dashboard-companion/protocol.js.map +1 -0
  545. package/dist/dashboard-companion/standalone-assets.d.ts +12 -0
  546. package/dist/dashboard-companion/standalone-assets.js +72 -0
  547. package/dist/dashboard-companion/standalone-assets.js.map +1 -0
  548. package/dist/dashboard-companion/standalone-entry.d.ts +1 -0
  549. package/dist/dashboard-companion/standalone-entry.js +17 -0
  550. package/dist/dashboard-companion/standalone-entry.js.map +1 -0
  551. package/dist/dashboard-gateway/config.d.ts +9 -0
  552. package/dist/dashboard-gateway/config.js +44 -0
  553. package/dist/dashboard-gateway/config.js.map +1 -0
  554. package/dist/dashboard-gateway/index.d.ts +12 -0
  555. package/dist/dashboard-gateway/index.js +786 -0
  556. package/dist/dashboard-gateway/index.js.map +1 -0
  557. package/dist/dashboard-gateway/main.d.ts +1 -0
  558. package/dist/dashboard-gateway/main.js +25 -0
  559. package/dist/dashboard-gateway/main.js.map +1 -0
  560. package/dist/dashboard-pairing-token.d.ts +1 -0
  561. package/dist/dashboard-pairing-token.js +5 -0
  562. package/dist/dashboard-pairing-token.js.map +1 -0
  563. package/dist/elaris/client.d.ts +73 -0
  564. package/dist/elaris/client.js +825 -0
  565. package/dist/elaris/client.js.map +1 -0
  566. package/dist/elaris/config.d.ts +18 -0
  567. package/dist/elaris/config.js +75 -0
  568. package/dist/elaris/config.js.map +1 -0
  569. package/dist/elaris/errors.d.ts +41 -0
  570. package/dist/elaris/errors.js +205 -0
  571. package/dist/elaris/errors.js.map +1 -0
  572. package/dist/elaris/github-repo.d.ts +18 -0
  573. package/dist/elaris/github-repo.js +89 -0
  574. package/dist/elaris/github-repo.js.map +1 -0
  575. package/dist/elaris/index.d.ts +10 -0
  576. package/dist/elaris/index.js +11 -0
  577. package/dist/elaris/index.js.map +1 -0
  578. package/dist/elaris/redact.d.ts +89 -0
  579. package/dist/elaris/redact.js +423 -0
  580. package/dist/elaris/redact.js.map +1 -0
  581. package/dist/elaris/run-id.d.ts +5 -0
  582. package/dist/elaris/run-id.js +20 -0
  583. package/dist/elaris/run-id.js.map +1 -0
  584. package/dist/elaris/status.d.ts +45 -0
  585. package/dist/elaris/status.js +122 -0
  586. package/dist/elaris/status.js.map +1 -0
  587. package/dist/elaris/tool-schemas.d.ts +314 -0
  588. package/dist/elaris/tool-schemas.js +208 -0
  589. package/dist/elaris/tool-schemas.js.map +1 -0
  590. package/dist/elaris/tools.d.ts +73 -0
  591. package/dist/elaris/tools.js +1065 -0
  592. package/dist/elaris/tools.js.map +1 -0
  593. package/dist/elaris/types.d.ts +1541 -0
  594. package/dist/elaris/types.js +252 -0
  595. package/dist/elaris/types.js.map +1 -0
  596. package/dist/errors.d.ts +16 -0
  597. package/dist/errors.js +19 -0
  598. package/dist/errors.js.map +1 -1
  599. package/dist/handler.d.ts +4 -0
  600. package/dist/handler.js +15 -2
  601. package/dist/handler.js.map +1 -1
  602. package/dist/index.d.ts +20 -2
  603. package/dist/index.js +154 -6
  604. package/dist/index.js.map +1 -1
  605. package/dist/local-clink-backend.d.ts +3 -0
  606. package/dist/local-clink-backend.js +4 -0
  607. package/dist/local-clink-backend.js.map +1 -1
  608. package/dist/logger.d.ts +1 -1
  609. package/dist/logger.js +18 -3
  610. package/dist/logger.js.map +1 -1
  611. package/dist/package-info.d.ts +2 -0
  612. package/dist/package-info.js +18 -0
  613. package/dist/package-info.js.map +1 -0
  614. package/dist/parsers/agy-stream-json.d.ts +14 -0
  615. package/dist/parsers/agy-stream-json.js +172 -0
  616. package/dist/parsers/agy-stream-json.js.map +1 -0
  617. package/dist/parsers/codex-jsonl.js +48 -3
  618. package/dist/parsers/codex-jsonl.js.map +1 -1
  619. package/dist/parsers/extract.d.ts +10 -1
  620. package/dist/parsers/extract.js +16 -4
  621. package/dist/parsers/extract.js.map +1 -1
  622. package/dist/parsers/observed-invocation.d.ts +20 -0
  623. package/dist/parsers/observed-invocation.js +42 -0
  624. package/dist/parsers/observed-invocation.js.map +1 -0
  625. package/dist/pipeline.d.ts +17 -0
  626. package/dist/pipeline.js +181 -10
  627. package/dist/pipeline.js.map +1 -1
  628. package/dist/progress-report/lock.d.ts +74 -0
  629. package/dist/progress-report/lock.js +343 -0
  630. package/dist/progress-report/lock.js.map +1 -0
  631. package/dist/progress-report/schema.d.ts +808 -0
  632. package/dist/progress-report/schema.js +119 -0
  633. package/dist/progress-report/schema.js.map +1 -0
  634. package/dist/progress-report/store.d.ts +66 -0
  635. package/dist/progress-report/store.js +766 -0
  636. package/dist/progress-report/store.js.map +1 -0
  637. package/dist/progress-report/tools.d.ts +20 -0
  638. package/dist/progress-report/tools.js +141 -0
  639. package/dist/progress-report/tools.js.map +1 -0
  640. package/dist/prompt-mode.d.ts +1 -1
  641. package/dist/prompt-mode.js +7 -2
  642. package/dist/prompt-mode.js.map +1 -1
  643. package/dist/run-dir.d.ts +24 -0
  644. package/dist/run-dir.js +201 -2
  645. package/dist/run-dir.js.map +1 -1
  646. package/dist/runner.d.ts +20 -0
  647. package/dist/runner.js +192 -16
  648. package/dist/runner.js.map +1 -1
  649. package/dist/safety.d.ts +54 -4
  650. package/dist/safety.js +186 -5
  651. package/dist/safety.js.map +1 -1
  652. package/dist/schema.d.ts +15 -7
  653. package/dist/schema.js +20 -0
  654. package/dist/schema.js.map +1 -1
  655. package/dist/server.d.ts +7 -1
  656. package/dist/server.js +451 -23
  657. package/dist/server.js.map +1 -1
  658. package/dist/session-log-stream.d.ts +18 -0
  659. package/dist/session-log-stream.js +566 -0
  660. package/dist/session-log-stream.js.map +1 -0
  661. package/dist/stdio-activity.d.ts +28 -0
  662. package/dist/stdio-activity.js +72 -0
  663. package/dist/stdio-activity.js.map +1 -0
  664. package/package.json +47 -5
  665. package/conf/adapters/gemini.json +0 -44
  666. package/conf/adapters/hapi/gemini.json +0 -47
  667. package/dist/parsers/gemini-json.d.ts +0 -2
  668. package/dist/parsers/gemini-json.js +0 -72
  669. package/dist/parsers/gemini-json.js.map +0 -1
@@ -5,44 +5,252 @@
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, isAbsolute, join, resolve } from "node:path";
21
+ import { computeRunHealth } from "./run-health.js";
17
22
  import { loadWorkflow, listWorkflowCatalog } from "./loader.js";
18
- import { WorkflowEngine } from "./engine.js";
19
- import { WorkflowStateStore } from "./state.js";
23
+ import { defaultWorkflowAllowedRoots, WorkflowEngine } from "./engine.js";
24
+ import { readRunStateForStatus, reapRunCancelled, WorkflowStateStore } from "./state.js";
25
+ import { isTerminalContextBuilderLifecycleStatus, parseContextBuilderLifecycleStateV1, } from "./context-builder-lifecycle-contract.js";
20
26
  import { validateRunId } from "./run-id.js";
21
- import { getTemplatesPath } from "./config.js";
22
- import { WorkflowCancellationError, } from "./errors.js";
27
+ import { validateDynamicPaths } from "./path-validation.js";
28
+ import { getMaxPromptBytes, getRunStateDir, getTemplatesPath, getWorkflowsContextDefault, getWorkflowsContextDefaultCurator, getWorkflowsMaxConcurrentAsync, resolveWorkspaceRoot, } from "./config.js";
29
+ import { WorkflowCancellationError, WorkflowValidationError, } from "./errors.js";
23
30
  import { shutdownCoordinator } from "./shutdown.js";
24
- import { basename } from "node:path";
25
31
  import { logger } from "./logger.js";
26
32
  import { createValidatorRegistry } from "./validators/index.js";
27
33
  import { deriveDecisiveFailure } from "./run-summary-derived.js";
28
34
  import { buildRunSummaryOrUndefined, buildFailureRunSummary } from "./run-summary.js";
29
35
  import { extractCard } from "./run-summary/cards/index.js";
30
36
  import { renderWorkflowReceipt } from "./workflow-receipt.js";
31
- import { supportsRuntimeMembers, COUNCIL_RUNTIME_MEMBER_WORKFLOWS, expandCouncilComposition } from "./council-expander.js";
37
+ import { supportsRuntimeMembers, COUNCIL_RUNTIME_MEMBER_WORKFLOWS, COUNCIL_STAGE_COUNT, expandCouncilComposition } from "./council-expander.js";
38
+ import { resolveWorkflowContextMode, WorkflowContextModeResolverError, } from "./context-mode-resolver.js";
39
+ import { resolveEffectiveChairmanCall } from "./chairman-knobs.js";
40
+ import { disabledFinalizerOverrideRejection, effectiveFinalizerEnabled, } from "./delegate-finalizer-policy.js";
41
+ import { delegateFinalizerMinScoreExplicit, WorkflowDelegateValidationError, } from "./delegate-validate.js";
42
+ import { contextSearchTermsValidationError, contextSeedSourceValidationError, resolveSeedSource, } from "./seed-terms.js";
43
+ import { LEGACY_PLAN_REVIEW_RECEIPTS_WORKFLOW_NAME, PLAN_REVIEW_RECEIPTS_WORKFLOW, admitCouncilPlanReview, assertPlanReviewPromptBudget, isPlanReviewRunState, } from "./plan-review/admission.js";
44
+ import { PLAN_REVIEW_V1_CLI_NAMES } from "./plan-review/contract.js";
45
+ import { buildCouncilPlanReviewDiscovery } from "./plan-review/discovery.js";
46
+ import { expandCouncilPlanReview } from "./plan-review/expander.js";
47
+ import { buildPlanReviewContextPolicy, findRejectedPlanContextAnchors, } from "./plan-review/context-policy.js";
48
+ import { PLAN_SNAPSHOT_RELPATH, persistCanonicalPlanSnapshot, readCanonicalPlanSnapshot, writeExecutionContractSnapshot, } from "./plan-review/snapshot.js";
49
+ import { deriveSeatMappings, reconstructPlanReviewCallBindings, } from "./plan-review/run-state.js";
50
+ import { restorePlanReviewRun, } from "./plan-review/resume.js";
51
+ import { PlanReviewStageError } from "./plan-review/stage-error.js";
52
+ import { CouncilPlanReviewErrorSchema, createPlanReviewPublicationError, inspectPlanReviewPublication, } from "./plan-review/publication.js";
53
+ import { validatePlanReviewCallBindings, validatePlanReviewPreparedInputPolicy, } from "./plan-review/runtime-policy.js";
54
+ import { ContainedFilePolicyError, readContainedRelativeFile, } from "./contained-file.js";
55
+ import { createDashboardRunReader } from "./dashboard/read-model.js";
56
+ import { ROSTER_NAME_PATTERN, ResolvedRosterSchema, } from "./roster-contract.js";
57
+ async function inspectLegacyWorkflowSessionLogGeneration(runDir, runId) {
58
+ try {
59
+ const result = await createDashboardRunReader().inspectGeneration?.(dirname(runDir), runId);
60
+ if (result?.kind === "present")
61
+ return result.runGeneration;
62
+ logger.debug({
63
+ run_id: runId,
64
+ reason: result === undefined ? "unsupported" : result.kind,
65
+ }, "workflow session-log generation unavailable; capture disabled");
66
+ }
67
+ catch {
68
+ logger.debug({ run_id: runId, reason: "inspection_failed" }, "workflow session-log generation unavailable; capture disabled");
69
+ }
70
+ return undefined;
71
+ }
72
+ /**
73
+ * Allowed roots for reading the caller's `plan_file`.
74
+ *
75
+ * Deliberately the workspace root ONLY. The state/artifact dirs are
76
+ * artifact-only roots — they are where runtime-owned output goes, never a
77
+ * source of reviewable plan text.
78
+ */
79
+ function planFileAllowedRoots() {
80
+ return [resolveWorkspaceRoot()];
81
+ }
82
+ function createPreflightOnlyBackend(advertisedCliNames) {
83
+ return {
84
+ cliNames: [...(advertisedCliNames ?? [])],
85
+ roleNames: [],
86
+ isTerminated: false,
87
+ hasChildDied: false,
88
+ connect: async () => { },
89
+ callTool: async () => {
90
+ throw new Error("preflight-only backend cannot dispatch provider calls");
91
+ },
92
+ terminate: async () => { },
93
+ };
94
+ }
32
95
  // ---------------------------------------------------------------------------
33
96
  // Admission control
34
97
  // ---------------------------------------------------------------------------
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.`);
98
+ const activeRuns = new Map();
99
+ /**
100
+ * Number of workflow tool handlers currently executing in the standalone
101
+ * registration path. This includes non-run tools such as status/list/cancel;
102
+ * it is kept separate from activeRuns because an admitted run can outlive its
103
+ * request handler (async mode), while a handler can be busy before admission.
104
+ */
105
+ let activeWorkflowHandlers = 0;
106
+ let workflowActivityObserver;
107
+ /** Internal standalone-lifecycle seam; not part of the public package API. */
108
+ export function getActiveWorkflowHandlerCount() {
109
+ return activeWorkflowHandlers;
110
+ }
111
+ /** Internal shared lifecycle seam used by the root server's compiled import. */
112
+ export function getActiveWorkflowRunCount() {
113
+ return activeRuns.size;
114
+ }
115
+ /**
116
+ * Install the standalone server's activity sink. The observer is optional and
117
+ * process-local; library callers leave it unset, so importing this module or
118
+ * using the in-process registration path has no timer/lifecycle side effects.
119
+ */
120
+ export function setWorkflowActivityObserver(observer) {
121
+ workflowActivityObserver = observer;
122
+ }
123
+ function notifyWorkflowActivity() {
124
+ try {
125
+ workflowActivityObserver?.();
126
+ }
127
+ catch (error) {
128
+ logger.warn({ err: error instanceof Error ? error.message : String(error) }, "workflow activity observer failed");
129
+ }
130
+ }
131
+ /**
132
+ * In-process registry of AbortControllers for live async runs, keyed by run_id.
133
+ * cancel_workflow aborts the controller to drive the run's own cancellation
134
+ * unwind. Registered at async admission (foreground) and deleted in the detached
135
+ * run's finally (a delete on an absent key is a harmless no-op, so the
136
+ * synchronous run path is unaffected).
137
+ */
138
+ const cancelRegistry = new Map();
139
+ export function resolveWorkflowAsyncAdmissionCap(options) {
140
+ const injected = options?.asyncAdmissionCap;
141
+ if (typeof injected === "number" &&
142
+ Number.isSafeInteger(injected) &&
143
+ injected >= 1) {
144
+ return injected;
145
+ }
146
+ return getWorkflowsMaxConcurrentAsync();
147
+ }
148
+ function acquireMutex(runId, options) {
149
+ if (shutdownCoordinator.isShuttingDown()) {
150
+ throw new McpError(ErrorCode.InvalidRequest, "workflow server is shutting down");
151
+ }
152
+ if (activeRuns.has(runId)) {
153
+ throw new McpError(ErrorCode.InvalidParams, `run_id ${runId} already running`);
154
+ }
155
+ const maxConcurrent = resolveWorkflowAsyncAdmissionCap(options);
156
+ if (activeRuns.size >= maxConcurrent) {
157
+ throw new McpError(ErrorCode.InvalidRequest, `max ${maxConcurrent} concurrent workflow(s); retry later`);
158
+ }
159
+ activeRuns.set(runId, { startedAt: new Date().toISOString() });
160
+ }
161
+ function releaseMutex(runId) {
162
+ activeRuns.delete(runId);
163
+ notifyWorkflowActivity();
164
+ }
165
+ /** Own setup before its first await, and keep that ownership through cleanup. */
166
+ function acquireWorkflowRun(runId, options, requestSignal) {
167
+ acquireMutex(runId, options);
168
+ const controller = new AbortController();
169
+ const signal = requestSignal === undefined
170
+ ? controller.signal
171
+ : AbortSignal.any([requestSignal, controller.signal]);
172
+ let backend;
173
+ let termination;
174
+ const terminate = () => {
175
+ if (backend === undefined || backend.isTerminated)
176
+ return termination ?? Promise.resolve();
177
+ termination ??= Promise.resolve().then(() => backend.terminate());
178
+ return termination;
179
+ };
180
+ let complete;
181
+ const completion = new Promise((resolve) => { complete = resolve; });
182
+ let handle;
183
+ try {
184
+ // The run owns persistence and close; shutdown only interrupts the lazy
185
+ // backend and waits until the run's own finally releases all resources.
186
+ handle = shutdownCoordinator.register({ terminate }, controller, async () => { }, completion);
187
+ }
188
+ catch (error) {
189
+ releaseMutex(runId);
190
+ complete();
191
+ throw error;
192
+ }
193
+ if (requestSignal === undefined)
194
+ cancelRegistry.set(runId, controller);
195
+ return {
196
+ signal,
197
+ throwIfCancelled: () => {
198
+ if (signal.aborted)
199
+ throw new WorkflowCancellationError();
200
+ },
201
+ setBackend: (value) => { backend = value; },
202
+ finish: async (store) => {
203
+ try {
204
+ try {
205
+ await terminate();
206
+ }
207
+ catch (err) {
208
+ logger.warn({ err }, "backend.terminate() failed during cleanup");
209
+ }
210
+ try {
211
+ await store?.close();
212
+ }
213
+ catch (err) {
214
+ logger.warn({ err }, "store.close() failed during cleanup");
215
+ }
216
+ }
217
+ finally {
218
+ cancelRegistry.delete(runId);
219
+ releaseMutex(runId);
220
+ shutdownCoordinator.unregister(handle);
221
+ complete();
222
+ }
223
+ },
224
+ };
225
+ }
226
+ async function markWorkflowCancelled(store) {
227
+ try {
228
+ await store?.markCancelled();
229
+ }
230
+ catch (err) {
231
+ // Preserve cancellation/no-response even if best-effort persistence fails.
232
+ logger.warn({ err }, "markCancelled during cancellation handling failed");
41
233
  }
42
- activeMutex = { runId, startedAt: new Date().toISOString() };
43
234
  }
44
- function releaseMutex() {
45
- activeMutex = null;
235
+ // Keep in sync with schema.ts model regex and ModelSchema (delegate-validate.ts).
236
+ const MODEL_OVERRIDE_PATTERN = /^[^\s\u0000-][^\s\u0000]*$/;
237
+ // Keep in sync with schema.ts effort regex and EffortSchema (delegate-validate.ts).
238
+ const EFFORT_OVERRIDE_PATTERN = /^[a-z0-9][a-z0-9_-]*$/;
239
+ function parseInvocationOverride(raw, label, maxLength, pattern) {
240
+ if (typeof raw !== "string") {
241
+ throw new McpError(ErrorCode.InvalidParams, `${label} must be a string`);
242
+ }
243
+ const value = raw.trim();
244
+ if (value.length === 0) {
245
+ throw new McpError(ErrorCode.InvalidParams, `${label} must not be empty`);
246
+ }
247
+ if (value.length > maxLength) {
248
+ throw new McpError(ErrorCode.InvalidParams, `${label} must be at most ${maxLength} characters`);
249
+ }
250
+ if (!pattern.test(value)) {
251
+ throw new McpError(ErrorCode.InvalidParams, `${label} has an invalid format`);
252
+ }
253
+ return value;
46
254
  }
47
255
  export function parseMembersOverride(raw, knownCliNames) {
48
256
  if (raw == null)
@@ -63,7 +271,7 @@ export function parseMembersOverride(raw, knownCliNames) {
63
271
  throw new McpError(ErrorCode.InvalidParams, `members[${i}] must be a non-null object`);
64
272
  }
65
273
  const keys = Object.keys(item);
66
- const allowedKeys = new Set(["cli_name", "stance"]);
274
+ const allowedKeys = new Set(["cli_name", "stance", "model", "effort"]);
67
275
  for (const key of keys) {
68
276
  if (!allowedKeys.has(key)) {
69
277
  throw new McpError(ErrorCode.InvalidParams, `members[${i}] has unexpected key "${key}"`);
@@ -102,6 +310,12 @@ export function parseMembersOverride(raw, knownCliNames) {
102
310
  else if (i >= 3) {
103
311
  throw new McpError(ErrorCode.InvalidParams, "stance is required for members at index 3 and above");
104
312
  }
313
+ if (obj["model"] !== undefined) {
314
+ entry.model = parseInvocationOverride(obj["model"], `members[${i}].model`, 128, MODEL_OVERRIDE_PATTERN);
315
+ }
316
+ if (obj["effort"] !== undefined) {
317
+ entry.effort = parseInvocationOverride(obj["effort"], `members[${i}].effort`, 32, EFFORT_OVERRIDE_PATTERN);
318
+ }
105
319
  result.push(entry);
106
320
  }
107
321
  return result;
@@ -125,6 +339,131 @@ export function parseChairmanOverride(raw, knownCliNames) {
125
339
  }
126
340
  return trimmed;
127
341
  }
342
+ function parseChairmanModelOverride(raw) {
343
+ if (raw === undefined)
344
+ return undefined;
345
+ return parseInvocationOverride(raw, "chairman_model", 128, MODEL_OVERRIDE_PATTERN);
346
+ }
347
+ function parseChairmanEffortOverride(raw) {
348
+ if (raw === undefined)
349
+ return undefined;
350
+ return parseInvocationOverride(raw, "chairman_effort", 32, EFFORT_OVERRIDE_PATTERN);
351
+ }
352
+ function invocationOverrideKnobs(value) {
353
+ if (value.model === undefined && value.effort === undefined) {
354
+ return undefined;
355
+ }
356
+ return {
357
+ ...(value.model === undefined ? {} : { model: value.model }),
358
+ ...(value.effort === undefined ? {} : { effort: value.effort }),
359
+ };
360
+ }
361
+ function validateInvocationOverridesAtAdmission(workflow, definition, parsedMembers, parsedChairman, parsedChairmanModel, parsedChairmanEffort, initialContextPolicy, resolvedVariables, validate, callerDelegateRaw) {
362
+ // Structural E5: independent of the optional adapter-capability hook so the
363
+ // standalone server rejects a disabled-finalizer override too. No-op when
364
+ // the policy has no enabled delegate (including the no-runner `off` default).
365
+ const admissionDelegate = initialContextPolicy.effectiveDelegate;
366
+ if (admissionDelegate?.enabled === true) {
367
+ const rejection = disabledFinalizerOverrideRejection(admissionDelegate, resolvedVariables, delegateFinalizerMinScoreExplicit(callerDelegateRaw));
368
+ if (rejection !== undefined) {
369
+ throw new McpError(ErrorCode.InvalidParams, rejection);
370
+ }
371
+ }
372
+ if (validate == null)
373
+ return;
374
+ const councilWorkflow = COUNCIL_RUNTIME_MEMBER_WORKFLOWS.has(workflow);
375
+ if (councilWorkflow && definition.stages.length !== COUNCIL_STAGE_COUNT) {
376
+ throw new McpError(ErrorCode.InvalidParams, `Council workflow "${workflow}" expects ${COUNCIL_STAGE_COUNT} stages, found ${definition.stages.length}`);
377
+ }
378
+ const staticChairman = councilWorkflow
379
+ ? definition.stages[3]?.calls[0]
380
+ : undefined;
381
+ for (const [stageIndex, stage] of definition.stages.entries()) {
382
+ for (const call of stage.calls) {
383
+ if (call === staticChairman) {
384
+ continue;
385
+ }
386
+ if (parsedMembers !== undefined && stageIndex < 2) {
387
+ continue;
388
+ }
389
+ const knobs = invocationOverrideKnobs(call);
390
+ if (knobs !== undefined) {
391
+ validate(call.cli_name, knobs);
392
+ }
393
+ }
394
+ }
395
+ if (parsedMembers !== undefined) {
396
+ for (const member of parsedMembers) {
397
+ const knobs = invocationOverrideKnobs(member);
398
+ if (knobs !== undefined) {
399
+ validate(member.cli_name, knobs);
400
+ }
401
+ }
402
+ }
403
+ if (councilWorkflow && staticChairman !== undefined) {
404
+ const effectiveChairman = resolveEffectiveChairmanCall({
405
+ staticCall: staticChairman,
406
+ chairmanCliName: parsedChairman,
407
+ chairmanModel: parsedChairmanModel,
408
+ chairmanEffort: parsedChairmanEffort,
409
+ });
410
+ const knobs = invocationOverrideKnobs(effectiveChairman);
411
+ if (knobs !== undefined) {
412
+ validate(effectiveChairman.cli_name, knobs);
413
+ }
414
+ }
415
+ const delegate = initialContextPolicy.effectiveDelegate;
416
+ if (delegate?.enabled === true) {
417
+ const curatorKnobs = invocationOverrideKnobs(delegate.curator);
418
+ if (curatorKnobs !== undefined) {
419
+ validate(delegate.curator.cli_name, curatorKnobs);
420
+ }
421
+ if (effectiveFinalizerEnabled(delegate, resolvedVariables)) {
422
+ const finalizerKnobs = invocationOverrideKnobs(delegate.finalizer);
423
+ if (finalizerKnobs !== undefined) {
424
+ validate(delegate.finalizer.cli_name, finalizerKnobs);
425
+ }
426
+ }
427
+ }
428
+ }
429
+ /**
430
+ * Point a generic-council caller at the input shape the workflow actually takes.
431
+ *
432
+ * Appended by the boundary decorator wrapping the whole pre-mutex admission
433
+ * block, so it rides on ANY `InvalidParams` rejection of a generic-shaped
434
+ * `plan-review-receipts` request — not only the `members`/`chairman` roster
435
+ * gates. A generic `user_request`-shaped caller can trip several independent
436
+ * pre-state checks (the roster-override parsers themselves, the two roster
437
+ * gates, unknown `user_request`/`cb_delegate` variables, context-mode and root
438
+ * validation, the conditional `cb_task` seed, task-secret validation, and the
439
+ * missing typed `plan_review`); whichever fires first now names the
440
+ * destination, instead of the caller re-submitting and being told something
441
+ * different each round trip.
442
+ *
443
+ * The decorator is gated on a RAW-`args` shape predicate, not on parsed
444
+ * values, specifically because a parser throw means no parsed value exists
445
+ * yet: `parseMembersOverride` rejects `[]` and unknown `cli_name`s before
446
+ * `parsedMembers` is ever bound, and that is exactly a case the hint must
447
+ * cover.
448
+ *
449
+ * Deliberately a HINT appended to the existing message, not a reordered check.
450
+ * Hoisting the `plan_review` requirement above the roster gate would mask
451
+ * malformed roster overrides behind a missing-contract error, and split the
452
+ * requirement from its authoritative mirror in `admitCouncilPlanReview`. The
453
+ * decorator changes no ordering, so that reasoning still holds.
454
+ */
455
+ function planReviewShapeHint() {
456
+ return (`. ${PLAN_REVIEW_RECEIPTS_WORKFLOW} takes a typed "plan_review" contract instead:` +
457
+ ` the roster is plan_review.seats[] and the chairman is plan_review.chairman_seat` +
458
+ ` (a seat_id, not an adapter name). It declares neither a "user_request" nor a` +
459
+ ` "cb_delegate" variable, and its "cb_task" seed is conditionally required:` +
460
+ ` trimmed-nonblank whenever context preflight is available (the default` +
461
+ ` deployment), and genuinely optional only on a server where preflight is` +
462
+ ` unavailable. It is driven by the plan-review-loop skill, not a generic` +
463
+ ` council template. For a generic members/chairman/user_request plan review` +
464
+ ` without the typed contract, use the "council-plan-review" workflow; the` +
465
+ ` typed plan_review contract belongs only to "plan-review-receipts".`);
466
+ }
128
467
  // ---------------------------------------------------------------------------
129
468
  // Variable validation (6-6)
130
469
  // ---------------------------------------------------------------------------
@@ -136,7 +475,8 @@ function formatExpectedVariables(schema) {
136
475
  }
137
476
  return parts.length > 0 ? `Expected variables: ${parts.join(", ")}` : "";
138
477
  }
139
- function validateVariables(supplied, schema) {
478
+ /** @internal Shared with the compiled producer-fixture admission check. */
479
+ export function validateVariables(supplied, schema) {
140
480
  const resolved = {};
141
481
  const errors = [];
142
482
  for (const [name, def] of Object.entries(schema)) {
@@ -174,6 +514,153 @@ function validateVariables(supplied, schema) {
174
514
  }
175
515
  return resolved;
176
516
  }
517
+ /**
518
+ * Reject caller-supplied dynamic path variables that fall outside the workflow
519
+ * engine's allowed roots, at ADMISSION — before the concurrency mutex, before
520
+ * any run state exists, and before context preflight spends a delegate
521
+ * round-trip. The engine already rejects these paths, but only at
522
+ * `validateInputPathsExist()`, which runs AFTER preflight: by then the caller
523
+ * has paid for a curator/finalizer subprocess and is left with a `failed` run
524
+ * directory to inspect and prune.
525
+ *
526
+ * This is a pre-check, NOT a replacement. The engine's post-preflight check
527
+ * must stay: it is the only thing that covers preflight-produced bundle copies
528
+ * appended to `context_bundle_files`, which do not exist yet at admission.
529
+ *
530
+ * Three deliberate narrowings, each of which exists to avoid rejecting a run
531
+ * that succeeds today:
532
+ *
533
+ * (i) Only variable names the DEFINITION actually wires through
534
+ * `absolute_file_paths_from` / `images_from` are validated. A caller may
535
+ * legitimately pass any other declared variable that happens to hold
536
+ * path-like strings; the engine never resolves those as paths, so
537
+ * validating them here would reject working runs.
538
+ * (ii) `plan-review-receipts` is skipped entirely.
539
+ * `enforcePlanReviewContextExclusions` strips force-excluded plan paths
540
+ * BEFORE the engine's check, so a path destined for stripping is never
541
+ * validated today and must not start being rejected at admission.
542
+ * (iii) The resume path does not call this. Resume prefers persisted inputs and
543
+ * skips completed calls, so re-validating current variables could reject a
544
+ * resume whose original admission passed.
545
+ *
546
+ * Non-array values and non-string entries are deliberately NOT rejected here —
547
+ * they are left to the engine so its existing error text is unchanged.
548
+ */
549
+ function assertCallerDynamicPathsAllowed(normalized, variables, workflowName, runId) {
550
+ // Caveat (ii).
551
+ if (workflowName === PLAN_REVIEW_RECEIPTS_WORKFLOW) {
552
+ return;
553
+ }
554
+ // Caveat (i): the set of variable names this definition wires as paths.
555
+ const wiredNames = new Set();
556
+ const collectWiredNames = (declared) => {
557
+ if (typeof declared === "string") {
558
+ wiredNames.add(declared);
559
+ return;
560
+ }
561
+ if (declared === undefined) {
562
+ return;
563
+ }
564
+ for (const name of declared) {
565
+ wiredNames.add(name);
566
+ }
567
+ };
568
+ for (const stage of normalized.stages) {
569
+ for (const call of stage.calls) {
570
+ collectWiredNames(call.absolute_file_paths_from);
571
+ collectWiredNames(call.images_from);
572
+ }
573
+ }
574
+ if (wiredNames.size === 0) {
575
+ return;
576
+ }
577
+ // The engine derives its own roots and never consults CLINKX_ALLOWED_ROOTS;
578
+ // mirror that exact set so admission cannot be stricter or looser than
579
+ // dispatch.
580
+ const allowedRoots = defaultWorkflowAllowedRoots(getRunStateDir(runId));
581
+ const workspaceRoot = resolveWorkspaceRoot();
582
+ for (const name of wiredNames) {
583
+ const rawValue = variables[name];
584
+ if (!Array.isArray(rawValue)) {
585
+ continue;
586
+ }
587
+ // Mirrors resolveDynamicPathVariable (engine.ts) exactly.
588
+ const values = rawValue
589
+ .filter((entry) => typeof entry === "string")
590
+ .map((entry) => (isAbsolute(entry) ? entry : resolve(workspaceRoot, entry)));
591
+ if (values.length === 0) {
592
+ continue;
593
+ }
594
+ try {
595
+ validateDynamicPaths(values, allowedRoots, `${workflowName}.${name}`);
596
+ }
597
+ catch (err) {
598
+ if (err instanceof WorkflowValidationError) {
599
+ throw new McpError(ErrorCode.InvalidParams, err.message);
600
+ }
601
+ throw err;
602
+ }
603
+ }
604
+ }
605
+ function validateContextBuilderAnchorSufficiency(variables, runner, contextDefault, defaultCurator) {
606
+ if (runner == null) {
607
+ return undefined;
608
+ }
609
+ let policy;
610
+ try {
611
+ // Throws on an invalid cb_context_mode / cb_delegate combination (surfaced
612
+ // below as -32602). The resolved policy is also returned for admission-time
613
+ // checks that must stay inert under soft off.
614
+ policy = resolveWorkflowContextMode(variables["cb_context_mode"], contextDefault, true, variables["cb_delegate"], defaultCurator);
615
+ }
616
+ catch (error) {
617
+ if (error instanceof WorkflowContextModeResolverError ||
618
+ error instanceof WorkflowDelegateValidationError) {
619
+ throw new McpError(ErrorCode.InvalidParams, error.message);
620
+ }
621
+ throw error;
622
+ }
623
+ const cbRoots = readStringArrayForAnchorCheck(variables["cb_roots"]);
624
+ // Eager allowlist validation: explicit cb_roots are validated whenever
625
+ // provided, including soft off. Out-of-allowlist roots are rejected at the
626
+ // boundary as -32602. The disabled hard kill never reaches this validator.
627
+ if (cbRoots.length > 0) {
628
+ try {
629
+ runner.assertRootsAllowed(cbRoots);
630
+ }
631
+ catch (err) {
632
+ throw new McpError(ErrorCode.InvalidParams, err instanceof Error ? err.message : String(err));
633
+ }
634
+ }
635
+ // policy.preflightDisabled is unreachable here: this validator early-returns
636
+ // when runner == null and always resolves with preflightEnabled:true, so the
637
+ // resolver never returns a disabled policy on this path.
638
+ return policy;
639
+ }
640
+ function validateContextSeedSourceAtAdmission(variables, policy) {
641
+ if (policy == null) {
642
+ return;
643
+ }
644
+ const cbTask = typeof variables["cb_task"] === "string"
645
+ ? variables["cb_task"]
646
+ : undefined;
647
+ const userRequest = typeof variables["user_request"] === "string"
648
+ ? variables["user_request"]
649
+ : undefined;
650
+ const seedSource = resolveSeedSource(cbTask, userRequest);
651
+ const seedSourceError = contextSeedSourceValidationError(seedSource);
652
+ if (seedSourceError != null) {
653
+ throw new McpError(ErrorCode.InvalidParams, seedSourceError);
654
+ }
655
+ const searchTerms = readStringArrayForAnchorCheck(variables["cb_search_terms"]);
656
+ const searchTermsError = contextSearchTermsValidationError(searchTerms);
657
+ if (searchTermsError != null) {
658
+ throw new McpError(ErrorCode.InvalidParams, searchTermsError);
659
+ }
660
+ }
661
+ function readStringArrayForAnchorCheck(value) {
662
+ return Array.isArray(value) && value.every((entry) => typeof entry === "string") ? value : [];
663
+ }
177
664
  function validateVariableType(name, value, expectedType) {
178
665
  switch (expectedType) {
179
666
  case "string":
@@ -597,6 +1084,52 @@ function finalizeWorkflowPresentation(options) {
597
1084
  }
598
1085
  return content;
599
1086
  }
1087
+ /**
1088
+ * Wire projection of the warn-only duplicate-replay signal recorded at
1089
+ * `WorkflowStateStore.create()` (failure investigation F6). Additive and
1090
+ * advisory: identical snapshot hashes do NOT imply an identical outcome —
1091
+ * workspace content is not covered by the hashes and providers are
1092
+ * nondeterministic — so this field never gates admission or execution.
1093
+ * Defensive optional access: partial store mocks may omit the property.
1094
+ */
1095
+ function buildDuplicateOfFailedRunField(store) {
1096
+ const signal = store?.duplicateOfFailedRun;
1097
+ if (signal === undefined || signal === null)
1098
+ return undefined;
1099
+ return {
1100
+ run_id: signal.runId,
1101
+ ...(signal.completedAt !== undefined ? { completed_at: signal.completedAt } : {}),
1102
+ };
1103
+ }
1104
+ /**
1105
+ * Appends the duplicate-replay signal as a trailing additive content block on
1106
+ * sync run_workflow results (success and error). No block is added when no
1107
+ * signal was detected, so existing content ordering is untouched.
1108
+ */
1109
+ function appendDuplicateReplayBlock(content, store) {
1110
+ const duplicateOfFailedRun = buildDuplicateOfFailedRunField(store);
1111
+ if (duplicateOfFailedRun === undefined)
1112
+ return;
1113
+ content.push({
1114
+ type: "text",
1115
+ text: JSON.stringify({ duplicate_of_failed_run: duplicateOfFailedRun }),
1116
+ });
1117
+ }
1118
+ /**
1119
+ * Trailing additive content block for the admission-time requested-value
1120
+ * roster. Omitted when the run did not persist one (non-council, plan-review,
1121
+ * or pre-roster state). Defensive optional access: partial store mocks may
1122
+ * omit runState.
1123
+ */
1124
+ function appendResolvedRosterBlock(content, store) {
1125
+ const resolvedRoster = store?.runState?.resolvedRoster;
1126
+ if (resolvedRoster === undefined)
1127
+ return;
1128
+ content.push({
1129
+ type: "text",
1130
+ text: JSON.stringify({ resolved_roster: resolvedRoster }),
1131
+ });
1132
+ }
600
1133
  // ---------------------------------------------------------------------------
601
1134
  // Actionable error formatting (5-6)
602
1135
  // ---------------------------------------------------------------------------
@@ -655,15 +1188,77 @@ function formatWorkflowError(runId, error, store, workflowSource) {
655
1188
  let nextAction = "";
656
1189
  if (store != null) {
657
1190
  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).`;
1191
+ const isTerminalPlanReview = state != null &&
1192
+ isPlanReviewRunState(state) &&
1193
+ (state.status === "failed" ||
1194
+ state.status === "cancelled" ||
1195
+ state.status === "succeeded");
1196
+ nextAction = isTerminalPlanReview
1197
+ ? `Start a new run with run_workflow and a new run_id; terminal plan-review-receipts runs are immutable.`
1198
+ : isResumable
1199
+ ? `Use resume_workflow with run_id "${runId}" to retry from the last checkpoint.`
1200
+ : `Start a new run with run_workflow (this run is not resumable — set CLINKX_WORKFLOWS_CHILD_CONFIG_PATH for resumable runs).`;
661
1201
  }
662
1202
  return (`Workflow failed [run_id: ${runId}]${sourceLabel}${location}` +
663
1203
  (retryCount > 0 ? ` (after ${String(retryCount)} retries)` : "") +
664
1204
  `: ${message}${metaBlock}` +
665
1205
  (nextAction !== "" ? `\n\n${nextAction}` : ""));
666
1206
  }
1207
+ function toCouncilPlanReviewError(runId, error) {
1208
+ if (!(error instanceof PlanReviewStageError)) {
1209
+ return undefined;
1210
+ }
1211
+ return CouncilPlanReviewErrorSchema.parse({
1212
+ schema_version: "council-plan-review-error-v1",
1213
+ run_id: runId,
1214
+ execution_status: "failed",
1215
+ stage: error.stage,
1216
+ error: {
1217
+ reason: error.reason,
1218
+ detail: error.message.slice(0, 2_048),
1219
+ },
1220
+ });
1221
+ }
1222
+ /**
1223
+ * Read a well-formed `failureClass` the error already carries. Structural on
1224
+ * purpose: errors crossing the engine boundary cannot be narrowed by
1225
+ * `instanceof` here, and this must never GUESS a class — an error without one
1226
+ * yields undefined, which `markFailed` treats as "leave the existing class
1227
+ * alone".
1228
+ */
1229
+ function carriedFailureClass(error) {
1230
+ if (error == null || typeof error !== "object") {
1231
+ return undefined;
1232
+ }
1233
+ const value = error.failureClass;
1234
+ return value === "transient" || value === "permanent" ? value : undefined;
1235
+ }
1236
+ async function markWorkflowFailed(store, runId, error) {
1237
+ const message = error instanceof Error ? error.message : String(error);
1238
+ // Forward a class the error already carries. Load-bearing on the RESUME path:
1239
+ // `beginResumeAttempt` clears `failureClass`, and the resume preflight engine
1240
+ // never runs `execute()`, so its catch never stamps one — without this a
1241
+ // bounded-preflight timeout persists `failed` with no class at all, while the
1242
+ // identical first-run failure keeps `transient`. Safe for the other call
1243
+ // sites: `markFailed` assigns only when defined, so an error carrying no
1244
+ // class still cannot clobber a class the engine already emitted.
1245
+ const planReviewError = toCouncilPlanReviewError(runId, error);
1246
+ if (planReviewError !== undefined) {
1247
+ // Plan-review terminal intent is first-wins. Forwarding a class here would
1248
+ // flip `replaceFailureClass` and overwrite a recorded intent, so this call
1249
+ // stays exactly as it was.
1250
+ await store.markFailed(message, undefined, planReviewError);
1251
+ return;
1252
+ }
1253
+ const failureClass = carriedFailureClass(error);
1254
+ if (failureClass === undefined) {
1255
+ // Nothing to add: keep the original single-argument call so callers that
1256
+ // rely on `markFailed`'s existing behavior are byte-identical.
1257
+ await store.markFailed(message);
1258
+ return;
1259
+ }
1260
+ await store.markFailed(message, failureClass);
1261
+ }
667
1262
  function getCallEntryByLocation(stages, location) {
668
1263
  const dotIndex = location.indexOf(".");
669
1264
  if (dotIndex === -1) {
@@ -675,6 +1270,54 @@ function getCallEntryByLocation(stages, location) {
675
1270
  return stage?.calls.find((call) => call.callId === callId);
676
1271
  }
677
1272
  // ---------------------------------------------------------------------------
1273
+ // Roster preset admission (pre-mutex, pre-state)
1274
+ // ---------------------------------------------------------------------------
1275
+ function applyRosterPresetAtAdmission(workflow, args, variables, rosterLoader) {
1276
+ const raw = args["roster"];
1277
+ if (typeof raw !== "string") {
1278
+ throw new McpError(ErrorCode.InvalidParams, "roster must be a string");
1279
+ }
1280
+ if (!COUNCIL_RUNTIME_MEMBER_WORKFLOWS.has(workflow)) {
1281
+ throw new McpError(ErrorCode.InvalidParams, `roster parameter is not supported for workflow "${workflow}"`);
1282
+ }
1283
+ const conflicts = [];
1284
+ if (args["members"] != null)
1285
+ conflicts.push("members");
1286
+ if (args["chairman"] != null)
1287
+ conflicts.push("chairman");
1288
+ if (args["chairman_model"] != null)
1289
+ conflicts.push("chairman_model");
1290
+ if (args["chairman_effort"] != null)
1291
+ conflicts.push("chairman_effort");
1292
+ const cbDelegate = variables?.["cb_delegate"];
1293
+ if (typeof cbDelegate === "string" && cbDelegate !== "") {
1294
+ conflicts.push("cb_delegate");
1295
+ }
1296
+ if (conflicts.length > 0) {
1297
+ throw new McpError(ErrorCode.InvalidParams, `roster is exclusive and cannot be combined with ${conflicts.join(", ")}`);
1298
+ }
1299
+ if (rosterLoader == null) {
1300
+ throw new McpError(ErrorCode.InvalidParams, "rosters are not available on this server");
1301
+ }
1302
+ const preset = rosterLoader.load(raw);
1303
+ if (preset === undefined) {
1304
+ const available = rosterLoader.list().map((entry) => entry.name).join(", ");
1305
+ throw new McpError(ErrorCode.InvalidParams, `roster "${raw}" is not a known roster. Available: ${available}`);
1306
+ }
1307
+ const chairman = preset.chairman;
1308
+ const nextVariables = preset.delegate !== undefined
1309
+ ? { ...(variables ?? {}), cb_delegate: JSON.stringify(preset.delegate) }
1310
+ : variables;
1311
+ return {
1312
+ rosterName: raw,
1313
+ membersRaw: preset.members,
1314
+ chairmanRaw: chairman?.cli_name,
1315
+ chairmanModelRaw: chairman?.model,
1316
+ chairmanEffortRaw: chairman?.effort,
1317
+ variables: nextVariables,
1318
+ };
1319
+ }
1320
+ // ---------------------------------------------------------------------------
678
1321
  // Tool execution helpers
679
1322
  // ---------------------------------------------------------------------------
680
1323
  async function executeRunWorkflow(server, options, args, signal, progressToken) {
@@ -704,108 +1347,589 @@ async function executeRunWorkflow(server, options, args, signal, progressToken)
704
1347
  const knownCliNames = options.availableCliNames != null
705
1348
  ? new Set(options.availableCliNames)
706
1349
  : 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}"`);
711
- }
712
- if (parsedChairman != null && !COUNCIL_RUNTIME_MEMBER_WORKFLOWS.has(workflow)) {
713
- throw new McpError(ErrorCode.InvalidParams, `chairman parameter is not supported for workflow "${workflow}"`);
714
- }
715
- const resolvedVariables = validateVariables(variables, loaded.definition.variables);
716
- acquireMutex(runId);
717
- let store = null;
718
- let backend = null;
719
- let reporter;
1350
+ // Read RAW `args`, not parsed values: `parseMembersOverride` throws on `[]`
1351
+ // and on an unknown `cli_name`, so in exactly the cases the hint exists to
1352
+ // cover, `parsedMembers` is never bound.
1353
+ const genericShaped = workflow === PLAN_REVIEW_RECEIPTS_WORKFLOW &&
1354
+ (args["plan_review"] == null ||
1355
+ args["members"] != null ||
1356
+ args["chairman"] != null ||
1357
+ args["chairman_model"] != null ||
1358
+ args["chairman_effort"] != null ||
1359
+ args["roster"] != null);
1360
+ // Declarations hoisted out of the decorator's try so the bindings stay in
1361
+ // scope for the post-admission code below; the assignments and their order
1362
+ // are unchanged.
1363
+ let parsedMembers;
1364
+ let parsedChairman;
1365
+ let parsedChairmanModel;
1366
+ let parsedChairmanEffort;
1367
+ let resolvedVariables;
1368
+ let initialContextPolicy;
1369
+ let planReviewAdmission;
1370
+ let admittedRosterName;
720
1371
  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
- });
731
- // Create backend via factory
732
- backend = options.createBackend(runId);
733
- await backend.connect();
734
- // Wire up shutdown coordinator
735
- const controller = new AbortController();
736
- if (signal.aborted) {
737
- controller.abort(new WorkflowCancellationError());
1372
+ let membersRaw = args["members"];
1373
+ let chairmanRaw = args["chairman"];
1374
+ let chairmanModelRaw = args["chairman_model"];
1375
+ let chairmanEffortRaw = args["chairman_effort"];
1376
+ let suppliedVariables = variables;
1377
+ if (args["roster"] !== undefined) {
1378
+ const applied = applyRosterPresetAtAdmission(workflow, args, variables, options.rosterLoader);
1379
+ admittedRosterName = applied.rosterName;
1380
+ membersRaw = applied.membersRaw;
1381
+ chairmanRaw = applied.chairmanRaw;
1382
+ chairmanModelRaw = applied.chairmanModelRaw;
1383
+ chairmanEffortRaw = applied.chairmanEffortRaw;
1384
+ suppliedVariables = applied.variables;
1385
+ }
1386
+ parsedMembers = parseMembersOverride(membersRaw, knownCliNames);
1387
+ parsedChairman = parseChairmanOverride(chairmanRaw, knownCliNames);
1388
+ parsedChairmanModel = parseChairmanModelOverride(chairmanModelRaw);
1389
+ parsedChairmanEffort = parseChairmanEffortOverride(chairmanEffortRaw);
1390
+ if (parsedMembers != null && !COUNCIL_RUNTIME_MEMBER_WORKFLOWS.has(workflow)) {
1391
+ throw new McpError(ErrorCode.InvalidParams, `members parameter is not supported for workflow "${workflow}"`);
1392
+ }
1393
+ if (parsedChairman != null && !COUNCIL_RUNTIME_MEMBER_WORKFLOWS.has(workflow)) {
1394
+ throw new McpError(ErrorCode.InvalidParams, `chairman parameter is not supported for workflow "${workflow}"`);
738
1395
  }
739
- else {
740
- signal.addEventListener("abort", () => {
741
- controller.abort(new WorkflowCancellationError());
742
- }, { once: true });
1396
+ if (parsedChairmanModel != null && !COUNCIL_RUNTIME_MEMBER_WORKFLOWS.has(workflow)) {
1397
+ throw new McpError(ErrorCode.InvalidParams, `chairman_model parameter is not supported for workflow "${workflow}"`);
743
1398
  }
744
- shutdownCoordinator.register(backend, controller, async () => {
745
- if (store != null) {
746
- await store.markCancelled();
1399
+ if (parsedChairmanEffort != null && !COUNCIL_RUNTIME_MEMBER_WORKFLOWS.has(workflow)) {
1400
+ throw new McpError(ErrorCode.InvalidParams, `chairman_effort parameter is not supported for workflow "${workflow}"`);
1401
+ }
1402
+ resolvedVariables = validateVariables(suppliedVariables, loaded.definition.variables);
1403
+ // When the runner is absent (preflight disabled / hard kill), the boundary
1404
+ // validator is inert. Skip it WITHOUT evaluating the context-default env
1405
+ // parsers — getWorkflowsContextDefaultCurator() throws on empty env, and a
1406
+ // runner-less run_workflow must never trip context env parsing.
1407
+ let contextPolicy;
1408
+ if (options.contextBuilderRunner != null) {
1409
+ contextPolicy = validateContextBuilderAnchorSufficiency(resolvedVariables, options.contextBuilderRunner, options.contextDefault ?? getWorkflowsContextDefault(), options.defaultCurator ?? getWorkflowsContextDefaultCurator());
1410
+ }
1411
+ validateContextSeedSourceAtAdmission(resolvedVariables, contextPolicy);
1412
+ // Pre-mutex, pre-state, pre-preflight: an out-of-roots caller path costs
1413
+ // zero delegate round-trips and leaves no run directory behind.
1414
+ assertCallerDynamicPathsAllowed(loaded.normalized, resolvedVariables, workflow, runId);
1415
+ initialContextPolicy = contextPolicy ?? resolveWorkflowContextMode(undefined, options.contextDefault ?? "off", false, undefined, options.defaultCurator);
1416
+ if (options.contextBuilderRunner != null) {
1417
+ try {
1418
+ options.contextBuilderRunner.assertTaskSecretsAllowed(resolvedVariables);
747
1419
  }
748
- });
749
- // Build engine hooks — capture store in a const for closure narrowing
750
- const storeRef = store;
751
- reporter = progressToken != null
752
- ? new WorkflowProgressReporter(server, progressToken, runId, effectiveDefinition)
753
- : undefined;
754
- const rpt = reporter; // local const for closure narrowing
755
- const hooks = {
756
- ...storeRef.hooks,
757
- ...(rpt != null
758
- ? {
759
- onProgress: async (event) => {
760
- await storeRef.hooks.onProgress?.(event);
761
- await rpt.onProgress(event);
762
- },
763
- onRetryScheduled: async (event) => {
764
- await storeRef.hooks.onRetryScheduled?.(event);
765
- await rpt.onRetryScheduled(event);
766
- },
1420
+ catch (err) {
1421
+ if (err instanceof McpError) {
1422
+ throw err;
767
1423
  }
768
- : {}),
769
- };
770
- const engine = new WorkflowEngine({
771
- definition: effectiveDefinition,
772
- session: backend,
1424
+ throw new McpError(ErrorCode.InvalidParams, err instanceof Error ? err.message : String(err));
1425
+ }
1426
+ }
1427
+ const planReviewCliNames = options.planReviewAvailableCliNames ?? options.availableCliNames;
1428
+ planReviewAdmission = admitCouncilPlanReview({
1429
+ workflow,
1430
+ planReview: args["plan_review"],
773
1431
  variables: resolvedVariables,
774
- validatorRegistry,
775
- runId,
776
- runDir: storeRef.runDir,
777
- signal: controller.signal,
778
- hooks,
1432
+ ...(planReviewCliNames !== undefined
1433
+ ? { availableCliNames: planReviewCliNames }
1434
+ : {}),
779
1435
  });
780
- const result = await engine.execute();
781
- shutdownCoordinator.unregister();
782
- // Build summary (fail-open)
783
- let summary;
784
- if (receipt) {
1436
+ }
1437
+ catch (err) {
1438
+ // Append the hint by MUTATING the caught error and rethrowing that same
1439
+ // instance. Do NOT re-wrap it in `new McpError(err.code, err.message + …)`:
1440
+ // the SDK constructor prefixes its argument with `MCP error <code>: `, so
1441
+ // `err.message` already carries that prefix and re-constructing emits it
1442
+ // twice on the wire (`MCP error -32602: MCP error -32602: …`). Mutating in
1443
+ // place also preserves `code`, `data`, and the original stack.
1444
+ if (genericShaped && err instanceof McpError && err.code === ErrorCode.InvalidParams) {
1445
+ err.message = `${err.message}${planReviewShapeHint()}`;
1446
+ }
1447
+ throw err;
1448
+ }
1449
+ validateInvocationOverridesAtAdmission(workflow, loaded.normalized, parsedMembers, parsedChairman, parsedChairmanModel, parsedChairmanEffort, initialContextPolicy, resolvedVariables, options.validateInvocationOverrides,
1450
+ // Caller-supplied cb_delegate only, never the roster-materialized one: a
1451
+ // roster preset is Zod-parsed at load, which defaults min_score and erases
1452
+ // whether the preset file actually requested it.
1453
+ admittedRosterName === undefined ? variables?.["cb_delegate"] : undefined);
1454
+ // A-7: reject caller context anchors that resolve to the bound plan or any
1455
+ // plan-workspace PLAN.md. Pre-mutex, so a rejected request creates no state:
1456
+ // the caller must not be able to hand actors a MUTABLE plan alongside the
1457
+ // immutable snapshot.
1458
+ let planReviewPlan;
1459
+ if (planReviewAdmission !== undefined) {
1460
+ const contextPolicy = buildPlanReviewContextPolicy(planReviewAdmission.input.plan_file);
1461
+ const rejected = findRejectedPlanContextAnchors(contextPolicy, resolvedVariables, resolveWorkspaceRoot());
1462
+ if (rejected.length > 0) {
1463
+ throw new McpError(ErrorCode.InvalidParams, `plan-review-receipts forbids plan context anchors; reviewers receive only the immutable snapshot. Offending entries: ${rejected
1464
+ .map((entry) => `${entry.variable}: ${entry.path}`)
1465
+ .join(", ")}`);
1466
+ }
1467
+ planReviewPlan = {
1468
+ admission: planReviewAdmission,
1469
+ contextPolicy,
1470
+ // Deterministic, runtime-owned: the definition must name the snapshot
1471
+ // before `create()` makes the run dir, so the path is derived rather than
1472
+ // discovered. A post-create guard asserts the store agrees.
1473
+ snapshotAbsolutePath: join(getRunStateDir(runId), PLAN_SNAPSHOT_RELPATH),
1474
+ };
1475
+ }
1476
+ const isAsync = args["async"] === true;
1477
+ const ownership = acquireWorkflowRun(runId, options, isAsync ? undefined : signal);
1478
+ if (isAsync) {
1479
+ // Everything between acquireMutex and the detached spawn must release the
1480
+ // mutex on ANY throw, or the run slot leaks permanently (bricks the server
1481
+ // at the default cap of 1).
1482
+ let store;
1483
+ let effectiveDefinition;
1484
+ let initializedPlanReview;
1485
+ let preparedSnapshot;
1486
+ try {
1487
+ ownership.throwIfCancelled();
1488
+ const runDir = getRunStateDir(runId);
1489
+ if (existsSync(runDir)) {
1490
+ throw new McpError(ErrorCode.InvalidParams, `run_id ${runId} already has persisted state at ${runDir}`);
1491
+ }
1492
+ // Read + digest-verify the plan BEFORE composing: the expander inlines
1493
+ // the snapshot text, so it must exist by expansion time (D9).
1494
+ preparedSnapshot = planReviewPlan === undefined
1495
+ ? undefined
1496
+ : await preparePlanReviewSnapshot(planReviewPlan);
1497
+ ownership.throwIfCancelled();
1498
+ // Expand composition ONCE in the foreground; the detached run reuses it.
1499
+ effectiveDefinition = expandRunWorkflowDefinition(loaded.normalized, workflow, parsedMembers, parsedChairman, parsedChairmanModel, parsedChairmanEffort, planReviewPlan, preparedSnapshot);
1500
+ const resolvedRoster = resolvedRosterForAdmission(workflow, effectiveDefinition, parsedMembers, parsedChairman, parsedChairmanModel, parsedChairmanEffort, initialContextPolicy, resolvedVariables, admittedRosterName);
1501
+ // Create the durable, pollable state record in the FOREGROUND so a failure
1502
+ // here surfaces synchronously (a real error, never a false "running").
785
1503
  try {
786
- const cardExtractor = createCardExtractor(effectiveDefinition);
787
- summary = buildRunSummaryOrUndefined(result, store, cardExtractor);
1504
+ store = await WorkflowStateStore.create({
1505
+ runId,
1506
+ definition: effectiveDefinition,
1507
+ variables: resolvedVariables,
1508
+ debugKeepArtifacts: debug,
1509
+ async: true,
1510
+ initialContextPolicy,
1511
+ ...(resolvedRoster !== undefined ? { resolvedRoster } : {}),
1512
+ ...(admittedRosterName !== undefined ? { rosterName: admittedRosterName } : {}),
1513
+ });
788
1514
  }
789
1515
  catch (err) {
790
- logger.warn({ err }, "run summary construction failed, omitting receipt");
1516
+ throw new McpError(ErrorCode.InternalError, `Failed to initialize async run ${runId}: ${err instanceof Error ? err.message : String(err)}`);
1517
+ }
1518
+ ownership.throwIfCancelled();
1519
+ // A-10: the async acknowledgement is a durability boundary. A caller
1520
+ // cannot observe `running` until every plan-review snapshot and the
1521
+ // strict typed state subtree have committed successfully.
1522
+ if (planReviewPlan !== undefined) {
1523
+ initializedPlanReview = await initializePlanReviewRunState(store, planReviewPlan, effectiveDefinition, preparedSnapshot);
791
1524
  }
1525
+ ownership.throwIfCancelled();
792
1526
  }
793
- const content = finalizeWorkflowPresentation({
794
- outputText: result.outputText,
795
- receipt,
796
- debug,
1527
+ catch (err) {
1528
+ try {
1529
+ if (ownership.signal.aborted || err instanceof WorkflowCancellationError) {
1530
+ await markWorkflowCancelled(store);
1531
+ throw new WorkflowCancellationError();
1532
+ }
1533
+ if (store !== undefined && planReviewPlan !== undefined) {
1534
+ try {
1535
+ await markWorkflowFailed(store, runId, err);
1536
+ }
1537
+ catch (markFailedError) {
1538
+ logger.warn({ err: markFailedError }, "markFailed during async initialization error handling failed");
1539
+ }
1540
+ const errorText = formatWorkflowError(runId, err, store, {
1541
+ source: loaded.source,
1542
+ sourcePath: loaded.sourcePath,
1543
+ });
1544
+ let failureSummary;
1545
+ if (receipt) {
1546
+ try {
1547
+ failureSummary = buildFailureRunSummary(store);
1548
+ }
1549
+ catch (summaryError) {
1550
+ logger.warn({ err: summaryError }, "failure summary construction failed, omitting receipt");
1551
+ }
1552
+ }
1553
+ const content = finalizeWorkflowPresentation({
1554
+ outputText: errorText,
1555
+ receipt,
1556
+ debug,
1557
+ summary: failureSummary,
1558
+ debugBlock: debug ? buildErrorDebugBlock(runId, store) : undefined,
1559
+ });
1560
+ return { content, isError: true };
1561
+ }
1562
+ throw err;
1563
+ }
1564
+ finally {
1565
+ await ownership.finish(store);
1566
+ }
1567
+ }
1568
+ if (store === undefined || effectiveDefinition === undefined) {
1569
+ await ownership.finish(store);
1570
+ throw new McpError(ErrorCode.InternalError, `Failed to initialize async run ${runId}`);
1571
+ }
1572
+ void runWorkflowDetached(server, options, workflow, runId, resolvedVariables, debug, receipt, validatorRegistry, loaded, parsedMembers, parsedChairman, parsedChairmanModel, parsedChairmanEffort, true, ownership, undefined, store, effectiveDefinition, undefined, initializedPlanReview, initialContextPolicy, admittedRosterName).catch((err) => {
1573
+ if (err instanceof WorkflowCancellationError) {
1574
+ logger.info({ runId }, "workflow run cancelled");
1575
+ return;
1576
+ }
1577
+ logger.error({ err, runId }, "detached async run failed");
1578
+ });
1579
+ // Additive-only ack shape: base {run_id, status:"running"} is unchanged;
1580
+ // the duplicate-replay field appears only when detected (warn-only, F6).
1581
+ // resolved_roster is the admission-time requested-value projection (E2).
1582
+ const duplicateOfFailedRun = buildDuplicateOfFailedRunField(store);
1583
+ const resolvedRoster = store.runState.resolvedRoster;
1584
+ return {
1585
+ content: [{
1586
+ type: "text",
1587
+ text: JSON.stringify({
1588
+ run_id: runId,
1589
+ status: "running",
1590
+ ...(duplicateOfFailedRun !== undefined
1591
+ ? { duplicate_of_failed_run: duplicateOfFailedRun }
1592
+ : {}),
1593
+ ...(resolvedRoster !== undefined
1594
+ ? { resolved_roster: resolvedRoster }
1595
+ : {}),
1596
+ }),
1597
+ }],
1598
+ };
1599
+ }
1600
+ return await runWorkflowDetached(server, options, workflow, runId, resolvedVariables, debug, receipt, validatorRegistry, loaded, parsedMembers, parsedChairman, parsedChairmanModel, parsedChairmanEffort, false, ownership, progressToken, undefined, undefined, planReviewPlan, undefined, initialContextPolicy, admittedRosterName);
1601
+ }
1602
+ async function preparePlanReviewSnapshot(plan) {
1603
+ try {
1604
+ const canonical = await readCanonicalPlanSnapshot({
1605
+ planFile: plan.admission.input.plan_file,
1606
+ allowedRoots: planFileAllowedRoots(),
1607
+ claimedDigest: plan.admission.input.digest,
1608
+ });
1609
+ // Fail fast on a roster that cannot fit its own prompts. This is caller
1610
+ // sizing, not a TOCTOU: it is deterministic and pre-state.
1611
+ //
1612
+ // It raises `McpError(-32602)`, which the re-throw below deliberately lets
1613
+ // escape rather than carrying as `{ok:false}` — but the CODE only reaches
1614
+ // the caller on the ASYNC path. `executeWorkflowRun` wraps the sync path in
1615
+ // its generic failure handler, so the same error surfaces there as an
1616
+ // `isError` TEXT result carrying the message but NOT the code. Do not
1617
+ // describe this as "-32602" unqualified; the invariant that holds on BOTH
1618
+ // paths is: no run directory, no persisted state, no provider work.
1619
+ assertPlanReviewPromptBudget({
1620
+ seatCount: plan.admission.execution_contract.seats.length,
1621
+ snapshotBytes: canonical.canonicalBytes.byteLength,
1622
+ maxPromptBytes: getMaxPromptBytes(),
1623
+ });
1624
+ return { ok: true, canonical };
1625
+ }
1626
+ catch (err) {
1627
+ if (err instanceof McpError)
1628
+ throw err;
1629
+ return { ok: false, error: err };
1630
+ }
1631
+ }
1632
+ /**
1633
+ * A-5/A-6: materialize the immutable snapshot + persisted execution contract
1634
+ * into runtime-owned storage and commit the typed plan-review subtree, all
1635
+ * BEFORE any seat is dispatched.
1636
+ *
1637
+ * A failure here is a post-state PRE-SEAT failure: the run is marked `failed`
1638
+ * with the offending `stage`, and no result sidecar is ever attempted.
1639
+ */
1640
+ async function initializePlanReviewRunState(store, plan, definition, prepared) {
1641
+ // Raise a carried read/verify failure now that state exists, so a swapped or
1642
+ // unreadable plan is a `failed` run rather than a pre-state rejection.
1643
+ if (!prepared.ok)
1644
+ throw prepared.error;
1645
+ const contract = plan.admission.execution_contract;
1646
+ const snapshot = await persistCanonicalPlanSnapshot(store.runDir, prepared.canonical);
1647
+ if (snapshot.absolutePath !== plan.snapshotAbsolutePath) {
1648
+ throw new PlanReviewStageError("snapshot", `Snapshot path ${snapshot.absolutePath} does not match the runtime-owned path derived for this run (${plan.snapshotAbsolutePath})`);
1649
+ }
1650
+ const persistedContract = await writeExecutionContractSnapshot(store.runDir, contract);
1651
+ const state = {
1652
+ schema_version: "council-plan-review-state-v1",
1653
+ execution_contract_path: persistedContract.relPath,
1654
+ execution_contract_sha256: persistedContract.sha256,
1655
+ execution_contract_digest: plan.admission.execution_contract_digest,
1656
+ snapshot_path: snapshot.relPath,
1657
+ snapshot_sha256: snapshot.sha256,
1658
+ plan_digest: snapshot.planDigest,
1659
+ seat_mappings: deriveSeatMappings(contract),
1660
+ call_bindings: reconstructPlanReviewCallBindings(contract, resolvePlanReviewChairmanCallId(definition)),
1661
+ context_policy: plan.contextPolicy,
1662
+ };
1663
+ validatePlanReviewCallBindings(definition, state.call_bindings);
1664
+ await store.commitPlanReviewState(state);
1665
+ return state;
1666
+ }
1667
+ /** The chairman stage's call id, read from the normalized definition. */
1668
+ function resolvePlanReviewChairmanCallId(definition) {
1669
+ const stage = definition.stages.find((entry) => entry.id === "chairman");
1670
+ const callId = stage?.calls[0]?.id;
1671
+ if (callId === undefined) {
1672
+ throw new PlanReviewStageError("execution_contract", "Plan-review definition has no chairman call");
1673
+ }
1674
+ return callId;
1675
+ }
1676
+ function projectResolvedRosterSeat(call, index, stancePresent) {
1677
+ return {
1678
+ index,
1679
+ cli_name: call.cli_name,
1680
+ ...(call.model !== undefined ? { model: call.model } : {}),
1681
+ ...(call.effort !== undefined ? { effort: call.effort } : {}),
1682
+ stance_present: stancePresent,
1683
+ };
1684
+ }
1685
+ function projectResolvedRosterDelegate(policy, resolvedVariables) {
1686
+ const delegate = policy.effectiveDelegate;
1687
+ if (delegate?.enabled !== true) {
1688
+ return { enabled: false };
1689
+ }
1690
+ const finalizerEnabled = effectiveFinalizerEnabled(delegate, resolvedVariables);
1691
+ return {
1692
+ enabled: true,
1693
+ curator: {
1694
+ cli_name: delegate.curator.cli_name,
1695
+ role: delegate.curator.role,
1696
+ ...(delegate.curator.model !== undefined ? { model: delegate.curator.model } : {}),
1697
+ ...(delegate.curator.effort !== undefined ? { effort: delegate.curator.effort } : {}),
1698
+ },
1699
+ finalizer: {
1700
+ cli_name: delegate.finalizer.cli_name,
1701
+ role: delegate.finalizer.role,
1702
+ ...(delegate.finalizer.model !== undefined ? { model: delegate.finalizer.model } : {}),
1703
+ ...(delegate.finalizer.effort !== undefined ? { effort: delegate.finalizer.effort } : {}),
1704
+ effective_enabled: finalizerEnabled,
1705
+ },
1706
+ };
1707
+ }
1708
+ /**
1709
+ * Requested-value roster projection for council workflows. Call AFTER
1710
+ * `expandCouncilComposition` / `expandRunWorkflowDefinition`, AFTER
1711
+ * `resolveEffectiveChairmanCall` values are on the definition (or passed as
1712
+ * parsed chairman knobs), and AFTER `effectiveFinalizerEnabled` can be
1713
+ * evaluated from `initialContextPolicy` — and BEFORE `WorkflowStateStore.create()`.
1714
+ * `plan-review-receipts` and any non-council workflow return undefined.
1715
+ */
1716
+ export function buildResolvedRoster(input) {
1717
+ if (!supportsRuntimeMembers(input.workflow)) {
1718
+ return undefined;
1719
+ }
1720
+ const stages = input.definition.stages;
1721
+ if (stages.length !== COUNCIL_STAGE_COUNT) {
1722
+ return undefined;
1723
+ }
1724
+ const memberCalls = stages[0]?.calls;
1725
+ const reviewerCalls = stages[1]?.calls;
1726
+ const chairmanCall = stages[3]?.calls[0];
1727
+ if (memberCalls === undefined || reviewerCalls === undefined || chairmanCall === undefined) {
1728
+ return undefined;
1729
+ }
1730
+ const members = memberCalls.map((call, index) => projectResolvedRosterSeat(call, index, input.parsedMembers?.[index]?.stance !== undefined));
1731
+ const reviewers = reviewerCalls.map((call, index) => projectResolvedRosterSeat(call, index, false));
1732
+ const effectiveChairman = resolveEffectiveChairmanCall({
1733
+ staticCall: chairmanCall,
1734
+ chairmanCliName: input.parsedChairman,
1735
+ chairmanModel: input.parsedChairmanModel,
1736
+ chairmanEffort: input.parsedChairmanEffort,
1737
+ });
1738
+ const delegate = projectResolvedRosterDelegate(input.initialContextPolicy, input.resolvedVariables);
1739
+ const preflightCalls = delegate.enabled === true
1740
+ ? 1 + (delegate.finalizer.effective_enabled ? 1 : 0)
1741
+ : 0;
1742
+ return ResolvedRosterSchema.parse({
1743
+ ...(input.rosterName !== undefined ? { roster: input.rosterName } : {}),
1744
+ members,
1745
+ reviewers,
1746
+ chairman: {
1747
+ cli_name: effectiveChairman.cli_name,
1748
+ ...(effectiveChairman.model !== undefined ? { model: effectiveChairman.model } : {}),
1749
+ ...(effectiveChairman.effort !== undefined ? { effort: effectiveChairman.effort } : {}),
1750
+ },
1751
+ delegate,
1752
+ provider_spawns: members.length + reviewers.length + 1,
1753
+ preflight_calls: preflightCalls,
1754
+ });
1755
+ }
1756
+ function resolvedRosterForAdmission(workflow, definition, parsedMembers, parsedChairman, parsedChairmanModel, parsedChairmanEffort, initialContextPolicy, resolvedVariables, rosterName) {
1757
+ return buildResolvedRoster({
1758
+ workflow,
1759
+ definition,
1760
+ ...(parsedMembers !== undefined ? { parsedMembers } : {}),
1761
+ ...(parsedChairman !== undefined ? { parsedChairman } : {}),
1762
+ ...(parsedChairmanModel !== undefined ? { parsedChairmanModel } : {}),
1763
+ ...(parsedChairmanEffort !== undefined ? { parsedChairmanEffort } : {}),
1764
+ initialContextPolicy,
1765
+ resolvedVariables,
1766
+ ...(rosterName !== undefined ? { rosterName } : {}),
1767
+ });
1768
+ }
1769
+ function expandRunWorkflowDefinition(definition, workflow, parsedMembers, parsedChairman, parsedChairmanModel, parsedChairmanEffort, planReviewPlan,
1770
+ /**
1771
+ * The already-verified snapshot. This function must NEVER read `plan_file`:
1772
+ * the text it inlines is the digest-verified canonical copy or nothing.
1773
+ */
1774
+ preparedSnapshot) {
1775
+ if (planReviewPlan !== undefined) {
1776
+ return expandCouncilPlanReview(definition, planReviewPlan.admission.execution_contract,
1777
+ // On a carried read failure the run is already doomed; expand without a
1778
+ // snapshot so the store can be created and mark the run `failed`.
1779
+ //
1780
+ // The verified snapshot is passed WHOLE rather than flattened to `.text`:
1781
+ // the expander's guard re-checks the digest-bearing shape, which a bare
1782
+ // string cannot carry. This is the single read — never re-read `plan_file`.
1783
+ preparedSnapshot?.ok === true ? preparedSnapshot.canonical : undefined);
1784
+ }
1785
+ return parsedMembers != null ||
1786
+ parsedChairman != null ||
1787
+ parsedChairmanModel !== undefined ||
1788
+ parsedChairmanEffort !== undefined
1789
+ ? expandCouncilComposition(definition, parsedMembers, workflow, parsedChairman, parsedChairmanModel, parsedChairmanEffort)
1790
+ : definition;
1791
+ }
1792
+ async function runWorkflowDetached(server, options, workflow, runId, resolvedVariables, debug, receipt, validatorRegistry, loaded, parsedMembers, parsedChairman, parsedChairmanModel, parsedChairmanEffort, isAsync, ownership, progressToken, preCreatedStore, preExpandedDefinition, planReviewPlan, preInitializedPlanReview, initialContextPolicy, rosterName) {
1793
+ let store = preCreatedStore ?? null;
1794
+ let backend = null;
1795
+ let reporter;
1796
+ let releaseLifecycleBinding;
1797
+ let initializedPlanReview = preInitializedPlanReview;
1798
+ const signal = ownership.signal;
1799
+ try {
1800
+ ownership.throwIfCancelled();
1801
+ // The async admission path already expanded the composition (and read the
1802
+ // snapshot) in the foreground; reuse both to avoid redundant work and a
1803
+ // second `plan_file` read.
1804
+ const preparedSnapshot = preExpandedDefinition !== undefined ||
1805
+ planReviewPlan === undefined
1806
+ ? undefined
1807
+ : await preparePlanReviewSnapshot(planReviewPlan);
1808
+ ownership.throwIfCancelled();
1809
+ const effectiveDefinition = preExpandedDefinition ??
1810
+ expandRunWorkflowDefinition(loaded.normalized, workflow, parsedMembers, parsedChairman, parsedChairmanModel, parsedChairmanEffort, planReviewPlan, preparedSnapshot);
1811
+ // Create state store (resumable if child config path is set) unless the
1812
+ // async admission path already created it in the foreground.
1813
+ if (store == null) {
1814
+ const resolvedRoster = resolvedRosterForAdmission(workflow, effectiveDefinition, parsedMembers, parsedChairman, parsedChairmanModel, parsedChairmanEffort, initialContextPolicy, resolvedVariables, rosterName);
1815
+ store = await WorkflowStateStore.create({
1816
+ runId,
1817
+ definition: effectiveDefinition,
1818
+ variables: resolvedVariables,
1819
+ debugKeepArtifacts: debug,
1820
+ async: isAsync,
1821
+ initialContextPolicy,
1822
+ ...(resolvedRoster !== undefined ? { resolvedRoster } : {}),
1823
+ ...(rosterName !== undefined ? { rosterName } : {}),
1824
+ });
1825
+ }
1826
+ ownership.throwIfCancelled();
1827
+ releaseLifecycleBinding = options.contextBuilderLifecycleRouter?.bind(runId, store);
1828
+ // A-5/A-6: snapshot + contract + typed subtree land here — after state
1829
+ // exists, before the backend is even created, so a snapshot/digest failure
1830
+ // is a `failed` RunState with zero seats dispatched (never a result
1831
+ // sidecar, never a pre-state -32602).
1832
+ if (planReviewPlan !== undefined && preparedSnapshot !== undefined) {
1833
+ initializedPlanReview = await initializePlanReviewRunState(store, planReviewPlan, effectiveDefinition, preparedSnapshot);
1834
+ }
1835
+ ownership.throwIfCancelled();
1836
+ const sessionLogGeneration = store.sessionLogGeneration ??
1837
+ await inspectLegacyWorkflowSessionLogGeneration(store.runDir, runId);
1838
+ ownership.throwIfCancelled();
1839
+ backend = options.createBackend(runId);
1840
+ ownership.setBackend(backend);
1841
+ ownership.throwIfCancelled();
1842
+ await backend.connect();
1843
+ ownership.throwIfCancelled();
1844
+ // Build engine hooks — capture store in a const for closure narrowing
1845
+ const storeRef = store;
1846
+ reporter = progressToken != null
1847
+ ? new WorkflowProgressReporter(server, progressToken, runId, effectiveDefinition)
1848
+ : undefined;
1849
+ const rpt = reporter; // local const for closure narrowing
1850
+ const hooks = {
1851
+ ...storeRef.hooks,
1852
+ ...(rpt != null
1853
+ ? {
1854
+ onProgress: async (event) => {
1855
+ await storeRef.hooks.onProgress?.(event);
1856
+ await rpt.onProgress(event);
1857
+ },
1858
+ onRetryScheduled: async (event) => {
1859
+ await storeRef.hooks.onRetryScheduled?.(event);
1860
+ await rpt.onRetryScheduled(event);
1861
+ },
1862
+ }
1863
+ : {}),
1864
+ };
1865
+ const engine = new WorkflowEngine({
1866
+ definition: effectiveDefinition,
1867
+ session: backend,
1868
+ variables: resolvedVariables,
1869
+ validatorRegistry,
1870
+ runId,
1871
+ sessionLogGeneration,
1872
+ runDir: storeRef.runDir,
1873
+ signal,
1874
+ hooks,
1875
+ contextBuilderRunner: options.contextBuilderRunner,
1876
+ contextDefault: options.contextDefault,
1877
+ defaultCurator: options.defaultCurator,
1878
+ commitPreflightContext: storeRef.commitPreflightContext?.bind(storeRef),
1879
+ initialContextPolicy,
1880
+ ...(options.deriveInvocationAudit === undefined
1881
+ ? {}
1882
+ : { deriveInvocationAudit: options.deriveInvocationAudit }),
1883
+ ...(initializedPlanReview !== undefined
1884
+ ? {
1885
+ planReviewContextPolicy: initializedPlanReview.context_policy,
1886
+ planReviewCallBindings: initializedPlanReview.call_bindings,
1887
+ commitPlanReviewContextProvenance: storeRef.commitPlanReviewContextProvenance?.bind(storeRef),
1888
+ terminalResultPolicy: {
1889
+ kind: "plan_review_artifacts",
1890
+ project: storeRef.projectPlanReviewTerminalResult.bind(storeRef),
1891
+ },
1892
+ }
1893
+ : {}),
1894
+ });
1895
+ const result = await engine.execute();
1896
+ // Build summary (fail-open)
1897
+ let summary;
1898
+ if (receipt) {
1899
+ try {
1900
+ const cardExtractor = createCardExtractor(effectiveDefinition);
1901
+ summary = buildRunSummaryOrUndefined(result, store, cardExtractor);
1902
+ }
1903
+ catch (err) {
1904
+ logger.warn({ err }, "run summary construction failed, omitting receipt");
1905
+ }
1906
+ }
1907
+ const content = finalizeWorkflowPresentation({
1908
+ outputText: result.outputText,
1909
+ receipt,
1910
+ debug,
797
1911
  summary,
798
1912
  debugBlock: debug ? buildDebugBlock(runId, result, store) : undefined,
799
1913
  });
1914
+ appendDuplicateReplayBlock(content, store);
1915
+ appendResolvedRosterBlock(content, store);
800
1916
  return { content };
801
1917
  }
802
1918
  catch (error) {
803
- shutdownCoordinator.unregister();
804
- if (error instanceof WorkflowCancellationError) {
805
- if (store != null) {
806
- await store.markCancelled();
1919
+ if (signal.aborted || error instanceof WorkflowCancellationError) {
1920
+ await markWorkflowCancelled(store);
1921
+ throw new WorkflowCancellationError();
1922
+ }
1923
+ // Persist a terminal "failed" record for post-admission setup failures
1924
+ // (e.g. backend.connect() / engine construction). Idempotent: the engine
1925
+ // already persists "failed" for in-engine failures, so this won't clobber.
1926
+ if (store != null) {
1927
+ try {
1928
+ await markWorkflowFailed(store, runId, error);
1929
+ }
1930
+ catch (err) {
1931
+ logger.warn({ err }, "markFailed during error handling failed");
807
1932
  }
808
- throw error;
809
1933
  }
810
1934
  const errorText = formatWorkflowError(runId, error, store, {
811
1935
  source: loaded.source,
@@ -828,27 +1952,18 @@ async function executeRunWorkflow(server, options, args, signal, progressToken)
828
1952
  summary: failureSummary,
829
1953
  debugBlock: debug && store != null ? buildErrorDebugBlock(runId, store) : undefined,
830
1954
  });
1955
+ appendDuplicateReplayBlock(content, store);
1956
+ appendResolvedRosterBlock(content, store);
831
1957
  return { content, isError: true };
832
1958
  }
833
1959
  finally {
834
- reporter?.stop();
835
- try {
836
- if (backend != null && !backend.isTerminated) {
837
- await backend.terminate();
838
- }
839
- }
840
- catch (cleanupErr) {
841
- logger.warn({ err: cleanupErr }, "backend.terminate() failed during cleanup");
842
- }
843
1960
  try {
844
- if (store != null) {
845
- await store.close();
846
- }
1961
+ releaseLifecycleBinding?.();
1962
+ reporter?.stop();
847
1963
  }
848
- catch (cleanupErr) {
849
- logger.warn({ err: cleanupErr }, "store.close() failed during cleanup");
1964
+ finally {
1965
+ await ownership.finish(store);
850
1966
  }
851
- releaseMutex();
852
1967
  }
853
1968
  }
854
1969
  async function executeResumeWorkflow(server, options, args, signal, progressToken) {
@@ -864,33 +1979,148 @@ async function executeResumeWorkflow(server, options, args, signal, progressToke
864
1979
  catch {
865
1980
  throw new McpError(ErrorCode.InvalidParams, `Invalid run_id "${runId}". Must match [A-Za-z0-9_-]{1,64}.`);
866
1981
  }
867
- acquireMutex(runId);
1982
+ // Plan-review terminal state is authoritative even while the detached run's
1983
+ // finally still owns the in-process mutex. Read it before duplicate-run
1984
+ // arbitration so terminal-before-cleanup never leaks as "already running".
1985
+ // Any read/validation error is retried by WorkflowStateStore.resume under the
1986
+ // run lock, preserving the existing error surface for corrupt generic state.
1987
+ let persistedBeforeMutex;
1988
+ try {
1989
+ persistedBeforeMutex = await readRunStateForStatus(runId);
1990
+ }
1991
+ catch {
1992
+ persistedBeforeMutex = null;
1993
+ }
1994
+ const persistedState = persistedBeforeMutex?.state;
1995
+ if (persistedState != null &&
1996
+ isPlanReviewRunState(persistedState) &&
1997
+ (persistedState.status === "failed" ||
1998
+ persistedState.status === "cancelled" ||
1999
+ persistedState.status === "succeeded")) {
2000
+ const error = new Error(`Run ${runId} is terminal (${persistedState.status}) and cannot be resumed; start a new run instead.`);
2001
+ return {
2002
+ content: finalizeWorkflowPresentation({
2003
+ outputText: formatWorkflowError(runId, error, null),
2004
+ receipt,
2005
+ debug,
2006
+ summary: undefined,
2007
+ debugBlock: undefined,
2008
+ }),
2009
+ isError: true,
2010
+ };
2011
+ }
2012
+ const ownership = acquireWorkflowRun(runId, options, signal);
2013
+ const ownedSignal = ownership.signal;
868
2014
  let store = null;
869
2015
  let backend = null;
870
2016
  let reporter;
2017
+ let releaseLifecycleBinding;
871
2018
  try {
2019
+ ownership.throwIfCancelled();
872
2020
  const resumeOptions = {
873
2021
  runId,
874
2022
  debugKeepArtifacts: debug,
875
2023
  };
876
2024
  store = await WorkflowStateStore.resume(resumeOptions);
2025
+ // Open this attempt before classifying an abort during reconstruction;
2026
+ // otherwise a prior terminal record would suppress markCancelled.
2027
+ await store.beginResumeAttempt();
2028
+ ownership.throwIfCancelled();
2029
+ releaseLifecycleBinding = options.contextBuilderLifecycleRouter?.bind(runId, store);
877
2030
  const validatorRegistry = createValidatorRegistry();
878
- // Create backend via factory
879
- backend = options.createBackend(runId);
880
- await backend.connect();
881
- // Wire up shutdown coordinator
882
- const controller = new AbortController();
883
- if (signal.aborted) {
884
- controller.abort(new WorkflowCancellationError());
2031
+ const initialContextPolicy = store.runState.initialContextPolicy ??
2032
+ reconstructLegacyInitialContextPolicy(store.runState, options.defaultCurator);
2033
+ // A-5 resume fidelity: the persisted contract + snapshot are the SOLE
2034
+ // inputs — `plan_file` is never re-read and caller input is never
2035
+ // re-parsed. Decode + hash-verify + RECOMPUTE before the engine exists, so
2036
+ // a corrupt contract fails post-state/pre-seat rather than dispatching
2037
+ // seats under unverified (or absent) per-call bindings.
2038
+ // Typed detection is by PERSISTED plan-review state presence, not the
2039
+ // definition name alone: legacy runs persisted under the pre-rename name
2040
+ // "council-plan-review" (which now names the GENERIC plan-review council)
2041
+ // must keep routing through the typed restore path, while a new generic
2042
+ // council-plan-review run (no plan_review_state) must never enter it. A
2043
+ // tampered generic run that smuggles in a plan_review_state subtree fails
2044
+ // closed downstream: restorePlanReviewRun / validatePlanReviewCallBindings
2045
+ // reject any definition whose stages/calls do not match the contract.
2046
+ const persistedPlanReview = store.runState.plan_review_state;
2047
+ let restoredPlanReview;
2048
+ if (store.definition.name === PLAN_REVIEW_RECEIPTS_WORKFLOW &&
2049
+ persistedPlanReview === undefined) {
2050
+ throw new PlanReviewStageError("execution_contract", "Persisted plan-review-receipts run is missing plan_review_state");
885
2051
  }
886
- else {
887
- signal.addEventListener("abort", () => {
888
- controller.abort(new WorkflowCancellationError());
889
- }, { once: true });
2052
+ if (persistedPlanReview !== undefined &&
2053
+ store.definition.name !== PLAN_REVIEW_RECEIPTS_WORKFLOW &&
2054
+ store.definition.name !== LEGACY_PLAN_REVIEW_RECEIPTS_WORKFLOW_NAME) {
2055
+ throw new PlanReviewStageError("execution_contract", `Persisted workflow "${store.definition.name}" cannot carry plan_review_state`);
890
2056
  }
891
- shutdownCoordinator.register(backend, controller, async () => {
892
- await store.markCancelled();
893
- });
2057
+ if (persistedPlanReview !== undefined) {
2058
+ restoredPlanReview = await restorePlanReviewRun(store.runDir, persistedPlanReview, resolvePlanReviewChairmanCallId(store.definition));
2059
+ const bindingsByCall = validatePlanReviewCallBindings(store.definition, restoredPlanReview.call_bindings);
2060
+ /**
2061
+ * DELIBERATELY the FIRST of two identical checks; `WorkflowEngine`'s
2062
+ * constructor runs the same one. This is not drift.
2063
+ *
2064
+ * The engine's copy is the load-bearing seam: it is the one no caller can
2065
+ * bypass, and it is what a future construction path would inherit. This
2066
+ * copy buys ORDERING — it rejects tampered state before `createBackend` /
2067
+ * `connect`, so a doomed resume never spawns a provider process. The
2068
+ * function is pure and cheap over a handful of prepared inputs, so paying
2069
+ * twice costs nothing measurable and removing EITHER one loses a real
2070
+ * property. If you are here to delete a redundant call: don't.
2071
+ */
2072
+ validatePlanReviewPreparedInputPolicy(store.resume.preparedInputs ?? new Map(), bindingsByCall, {
2073
+ // From the SAME read that re-verified the snapshot above (D9).
2074
+ snapshotText: restoredPlanReview.snapshot.text,
2075
+ contextPolicy: restoredPlanReview.state.context_policy,
2076
+ });
2077
+ }
2078
+ ownership.throwIfCancelled();
2079
+ const sessionLogGeneration = store.sessionLogGeneration ??
2080
+ await inspectLegacyWorkflowSessionLogGeneration(store.runDir, runId);
2081
+ ownership.throwIfCancelled();
2082
+ if (store.runState.workflow_context === undefined) {
2083
+ const preflightEngine = new WorkflowEngine({
2084
+ definition: store.definition,
2085
+ session: createPreflightOnlyBackend(options.availableCliNames),
2086
+ variables: store.variables,
2087
+ validatorRegistry,
2088
+ runId,
2089
+ runDir: store.runDir,
2090
+ signal: ownedSignal,
2091
+ resume: store.resume,
2092
+ contextBuilderRunner: options.contextBuilderRunner,
2093
+ contextDefault: options.contextDefault,
2094
+ defaultCurator: options.defaultCurator,
2095
+ commitPreflightContext: store.commitPreflightContext.bind(store),
2096
+ requireContextPreflight: true,
2097
+ initialContextPolicy,
2098
+ ...(options.deriveInvocationAudit === undefined
2099
+ ? {}
2100
+ : { deriveInvocationAudit: options.deriveInvocationAudit }),
2101
+ ...(restoredPlanReview === undefined
2102
+ ? {}
2103
+ : {
2104
+ planReviewContextPolicy: restoredPlanReview.state.context_policy,
2105
+ planReviewCallBindings: restoredPlanReview.call_bindings,
2106
+ planReviewSnapshotText: restoredPlanReview.snapshot.text,
2107
+ commitPlanReviewContextProvenance: store.commitPlanReviewContextProvenance.bind(store),
2108
+ }),
2109
+ });
2110
+ // D1: bound the resume-path preflight by the same fresh `timeout_seconds`
2111
+ // budget a resumed run receives. `execute()` is never called on this engine,
2112
+ // so without this the curator/finalizer delegate runs unbounded. The engine is
2113
+ // constructed with `signal: ownedSignal`, which createDeadlineController chains,
2114
+ // so cancellation ownership is unchanged.
2115
+ await preflightEngine.prepareContextPreflightWithDeadline();
2116
+ }
2117
+ // Create backend only after the owned resume preflight has completed.
2118
+ ownership.throwIfCancelled();
2119
+ backend = options.createBackend(runId);
2120
+ ownership.setBackend(backend);
2121
+ ownership.throwIfCancelled();
2122
+ await backend.connect();
2123
+ ownership.throwIfCancelled();
894
2124
  // Build engine hooks with progress reporter
895
2125
  reporter = progressToken != null
896
2126
  ? new WorkflowProgressReporter(server, progressToken, runId, store.definition)
@@ -922,13 +2152,37 @@ async function executeResumeWorkflow(server, options, args, signal, progressToke
922
2152
  variables: store.variables,
923
2153
  validatorRegistry,
924
2154
  runId,
2155
+ sessionLogGeneration,
925
2156
  runDir: store.runDir,
926
- signal: controller.signal,
2157
+ signal: ownedSignal,
927
2158
  hooks: rpt != null ? hooks : store.hooks,
928
2159
  resume: store.resume,
2160
+ contextBuilderRunner: options.contextBuilderRunner,
2161
+ contextDefault: options.contextDefault,
2162
+ defaultCurator: options.defaultCurator,
2163
+ commitPreflightContext: store.commitPreflightContext?.bind(store),
2164
+ restoredWorkflowContext: store.runState.workflow_context,
2165
+ requireContextPreflight: false,
2166
+ initialContextPolicy,
2167
+ ...(options.deriveInvocationAudit === undefined
2168
+ ? {}
2169
+ : { deriveInvocationAudit: options.deriveInvocationAudit }),
2170
+ // A-7: replay the SAME enforced exclusion on resume, reconstructed from
2171
+ // persisted state rather than re-derived from caller variables.
2172
+ ...(restoredPlanReview !== undefined
2173
+ ? {
2174
+ planReviewContextPolicy: restoredPlanReview.state.context_policy,
2175
+ planReviewCallBindings: restoredPlanReview.call_bindings,
2176
+ planReviewSnapshotText: restoredPlanReview.snapshot.text,
2177
+ commitPlanReviewContextProvenance: store.commitPlanReviewContextProvenance?.bind(store),
2178
+ terminalResultPolicy: {
2179
+ kind: "plan_review_artifacts",
2180
+ project: store.projectPlanReviewTerminalResult.bind(store),
2181
+ },
2182
+ }
2183
+ : {}),
929
2184
  });
930
2185
  const result = await engine.execute();
931
- shutdownCoordinator.unregister();
932
2186
  // Build summary (fail-open)
933
2187
  let summary;
934
2188
  if (receipt) {
@@ -950,12 +2204,19 @@ async function executeResumeWorkflow(server, options, args, signal, progressToke
950
2204
  return { content };
951
2205
  }
952
2206
  catch (error) {
953
- shutdownCoordinator.unregister();
954
- if (error instanceof WorkflowCancellationError) {
955
- if (store != null) {
956
- await store.markCancelled();
2207
+ if (ownedSignal.aborted || error instanceof WorkflowCancellationError) {
2208
+ await markWorkflowCancelled(store);
2209
+ throw new WorkflowCancellationError();
2210
+ }
2211
+ // Persist a terminal "failed" record for post-admission setup failures.
2212
+ // Idempotent: won't clobber an engine-emitted terminal status.
2213
+ if (store != null) {
2214
+ try {
2215
+ await markWorkflowFailed(store, runId, error);
2216
+ }
2217
+ catch (err) {
2218
+ logger.warn({ err }, "markFailed during error handling failed");
957
2219
  }
958
- throw error;
959
2220
  }
960
2221
  const errorText = formatWorkflowError(runId, error, store);
961
2222
  // Build failure summary (fail-open)
@@ -978,45 +2239,468 @@ async function executeResumeWorkflow(server, options, args, signal, progressToke
978
2239
  return { content, isError: true };
979
2240
  }
980
2241
  finally {
981
- reporter?.stop();
982
2242
  try {
983
- if (backend != null && !backend.isTerminated) {
984
- await backend.terminate();
985
- }
2243
+ releaseLifecycleBinding?.();
2244
+ reporter?.stop();
986
2245
  }
987
- catch (cleanupErr) {
988
- logger.warn({ err: cleanupErr }, "backend.terminate() failed during cleanup");
2246
+ finally {
2247
+ await ownership.finish(store);
989
2248
  }
2249
+ }
2250
+ }
2251
+ // ---------------------------------------------------------------------------
2252
+ // Sidecar access (atomic, fd-based, cheap-poll-safe)
2253
+ // ---------------------------------------------------------------------------
2254
+ /**
2255
+ * Thrown for genuine containment-policy violations (absolute outputPath or an
2256
+ * escape past the run directory). These MUST reject even on a cheap default
2257
+ * poll. Everything else (missing file, fs race, permission) is swallowed on the
2258
+ * cheap path so liveness polling never throws for a recoverable condition.
2259
+ *
2260
+ * Alias of the shared reader's policy error so the existing `instanceof`
2261
+ * cheap-poll discrimination below is unchanged.
2262
+ */
2263
+ const SidecarPolicyError = ContainedFilePolicyError;
2264
+ /**
2265
+ * Read a run sidecar through the shared containment-safe, no-follow reader.
2266
+ *
2267
+ * The reader is shared (not re-implemented) with the plan-review snapshot and
2268
+ * contract paths: one audited realpath + `O_NOFOLLOW` + single-handle
2269
+ * implementation, so a fix to the TOCTOU handling lands everywhere at once.
2270
+ */
2271
+ async function readContainedSidecar(runDir, outputPath, wantContent) {
2272
+ const read = await readContainedRelativeFile(runDir, outputPath, {
2273
+ wantContent,
2274
+ // Preserve this path's established error wording byte-for-byte.
2275
+ containerLabel: "run directory",
2276
+ pathLabel: "outputPath",
2277
+ });
2278
+ return read.content !== undefined
2279
+ ? { size: read.size, content: read.content }
2280
+ : { size: read.size };
2281
+ }
2282
+ /**
2283
+ * Legacy state-v1 runs did not persist their admission-time default. Rebuild
2284
+ * only caller-explicit choices; otherwise require deterministic preflight so a
2285
+ * changed server default cannot silently introduce or switch a delegate.
2286
+ */
2287
+ export function reconstructLegacyInitialContextPolicy(state, defaultCurator) {
2288
+ const persistedMode = state.workflow_context?.context_builder.context_mode;
2289
+ if (persistedMode === "disabled") {
2290
+ return {
2291
+ requestedMode: "off",
2292
+ effectiveMode: "disabled",
2293
+ preflightDisabled: true,
2294
+ };
2295
+ }
2296
+ const legacyVariables = state.variables ?? {};
2297
+ const callerMode = legacyVariables["cb_context_mode"];
2298
+ const normalizedMode = typeof callerMode === "string" ? callerMode.trim() : "";
2299
+ if (normalizedMode === "off" || normalizedMode === "deterministic") {
2300
+ return {
2301
+ requestedMode: normalizedMode,
2302
+ effectiveMode: normalizedMode,
2303
+ preflightDisabled: false,
2304
+ };
2305
+ }
2306
+ if (normalizedMode === "full" && legacyVariables["cb_delegate"] != null) {
990
2307
  try {
991
- if (store != null) {
992
- await store.close();
2308
+ return resolveWorkflowContextMode("full", "deterministic", true, legacyVariables["cb_delegate"], defaultCurator);
2309
+ }
2310
+ catch {
2311
+ // Legacy hard-disabled runs may contain a delegate value that was never
2312
+ // validated. Do not turn it into a new resume-time fatal or provider.
2313
+ }
2314
+ }
2315
+ return {
2316
+ requestedMode: normalizedMode === "full" || persistedMode === "full"
2317
+ ? "full"
2318
+ : "deterministic",
2319
+ effectiveMode: "deterministic",
2320
+ preflightDisabled: false,
2321
+ };
2322
+ }
2323
+ /**
2324
+ * Best-effort read-time parse of the persisted context-builder lifecycle
2325
+ * snapshot. Status projection is observability-only and must never throw for
2326
+ * a malformed or absent snapshot — invalid input yields undefined and the
2327
+ * dependent health/context fields are simply omitted.
2328
+ */
2329
+ function parseLifecycleForStatus(value) {
2330
+ if (value == null)
2331
+ return undefined;
2332
+ try {
2333
+ return parseContextBuilderLifecycleStateV1(value);
2334
+ }
2335
+ catch {
2336
+ return undefined;
2337
+ }
2338
+ }
2339
+ async function executeWorkflowRunStatus(_options, args) {
2340
+ const runId = args["run_id"];
2341
+ const includeResult = args["include_result"] === true;
2342
+ if (typeof runId !== "string" || runId === "") {
2343
+ throw new McpError(ErrorCode.InvalidParams, 'Missing or invalid "run_id" parameter');
2344
+ }
2345
+ try {
2346
+ validateRunId(runId);
2347
+ }
2348
+ catch {
2349
+ throw new McpError(ErrorCode.InvalidParams, `Invalid run_id "${runId}". Must match [A-Za-z0-9_-]{1,64}.`);
2350
+ }
2351
+ const read = await readRunStateForStatus(runId);
2352
+ if (read == null) {
2353
+ return {
2354
+ content: [{ type: "text", text: JSON.stringify({ status: "not_found", run_id: runId }) }],
2355
+ };
2356
+ }
2357
+ const { state, lock } = read;
2358
+ const status = (state.status === "running" || state.status === "pending") && lock === "stale_or_absent"
2359
+ ? "orphaned"
2360
+ : state.status;
2361
+ const runDir = getRunStateDir(runId);
2362
+ const stages = state.stages.map((stage) => {
2363
+ const completedCalls = stage.calls.filter((call) => call.state === "succeeded").length;
2364
+ const runningCalls = stage.calls.filter((call) => call.state === "running").length;
2365
+ const failedCalls = stage.calls.filter((call) => call.state === "failed").length;
2366
+ return {
2367
+ stage_id: stage.stageId,
2368
+ state: stage.state,
2369
+ total_calls: stage.calls.length,
2370
+ completed_calls: completedCalls,
2371
+ running_calls: runningCalls,
2372
+ failed_calls: failedCalls,
2373
+ calls: stage.calls.map((call) => ({
2374
+ call_id: call.callId,
2375
+ state: call.state,
2376
+ retry_count: call.retryCount,
2377
+ ...(call.startedAt != null ? { started_at: call.startedAt } : {}),
2378
+ ...(call.completedAt != null ? { completed_at: call.completedAt } : {}),
2379
+ ...(call.durationMs != null ? { duration_ms: call.durationMs } : {}),
2380
+ ...(call.errorMessage != null ? { error: call.errorMessage } : {}),
2381
+ })),
2382
+ };
2383
+ });
2384
+ const projection = {
2385
+ run_id: state.runId,
2386
+ status,
2387
+ async: state.async ?? false,
2388
+ workflow: {
2389
+ name: state.workflowName,
2390
+ version: state.workflowVersion,
2391
+ },
2392
+ ...(state.startedAt != null ? { started_at: state.startedAt } : {}),
2393
+ ...(state.completedAt != null ? { completed_at: state.completedAt } : {}),
2394
+ ...(lock === "unknown" ? { lock_ownership: "unknown" } : {}),
2395
+ stages,
2396
+ };
2397
+ // artifacts_dir is cheap (pure join, no I/O) and present for every found run.
2398
+ projection["artifacts_dir"] = join(runDir, "artifacts");
2399
+ // Read-only context-selection observability from already-persisted state.
2400
+ // No file I/O — only projects state.workflow_context.context_builder and
2401
+ // the state.context_builder_lifecycle snapshot.
2402
+ const lifecycle = parseLifecycleForStatus(state.context_builder_lifecycle);
2403
+ const cb = state.workflow_context?.context_builder;
2404
+ if (cb != null) {
2405
+ const context = {
2406
+ ...(cb.context_mode != null ? { context_mode: cb.context_mode } : {}),
2407
+ status: cb.status,
2408
+ files_selected: cb.metrics.files_selected,
2409
+ };
2410
+ if (cb.delegate != null) {
2411
+ context["delegate"] = {
2412
+ status: cb.delegate.status,
2413
+ fallback_used: cb.delegate.fallback_used,
2414
+ };
2415
+ }
2416
+ if (lifecycle != null) {
2417
+ // Additive lifecycle observability. `coverage.missing` is a bounded
2418
+ // integer count in the persisted schema (never a list), so it is
2419
+ // projected as-is with no truncation concern.
2420
+ context["lifecycle_status"] = lifecycle.status;
2421
+ context["coverage"] = {
2422
+ policy: lifecycle.coverage.policy,
2423
+ status: lifecycle.coverage.status,
2424
+ required: lifecycle.coverage.required,
2425
+ selected: lifecycle.coverage.selected,
2426
+ missing: lifecycle.coverage.missing,
2427
+ };
2428
+ const finalizer = lifecycle.finalizer;
2429
+ if (typeof finalizer.effective_enabled === "boolean" &&
2430
+ typeof finalizer.execution_status === "string") {
2431
+ const existingDelegate = context["delegate"] !== undefined &&
2432
+ typeof context["delegate"] === "object" &&
2433
+ context["delegate"] !== null
2434
+ ? context["delegate"]
2435
+ : {};
2436
+ context["delegate"] = {
2437
+ ...existingDelegate,
2438
+ finalizer: {
2439
+ effective_enabled: finalizer.effective_enabled,
2440
+ execution_status: finalizer.execution_status,
2441
+ // The lifecycle contract only admits a score on succeeded
2442
+ // execution; re-check here so the projection cannot surface one
2443
+ // for any other outcome.
2444
+ ...(finalizer.execution_status === "succeeded" &&
2445
+ typeof finalizer.score === "number" &&
2446
+ Number.isFinite(finalizer.score)
2447
+ ? { score: finalizer.score }
2448
+ : {}),
2449
+ },
2450
+ };
993
2451
  }
994
2452
  }
995
- catch (cleanupErr) {
996
- logger.warn({ err: cleanupErr }, "store.close() failed during cleanup");
2453
+ projection["context"] = context;
2454
+ }
2455
+ else if (lifecycle != null &&
2456
+ !isTerminalContextBuilderLifecycleStatus(lifecycle.status)) {
2457
+ // Preflight in flight: `context` is reserved for committed-preflight
2458
+ // semantics, so mid-scan progress rides on its own top-level key.
2459
+ projection["context_progress"] = {
2460
+ phase: lifecycle.phase,
2461
+ status: lifecycle.status,
2462
+ updated_at: lifecycle.updated_at,
2463
+ counts: {
2464
+ files_seen: lifecycle.counts.files_seen,
2465
+ files_considered: lifecycle.counts.files_considered,
2466
+ files_selected: lifecycle.counts.files_selected,
2467
+ files_omitted: lifecycle.counts.files_omitted,
2468
+ search_results: lifecycle.counts.search_results,
2469
+ bytes_read: lifecycle.counts.bytes_read,
2470
+ bytes_written: lifecycle.counts.bytes_written,
2471
+ },
2472
+ };
2473
+ }
2474
+ // F7 read-time health projection: top-level run status alone overstates
2475
+ // health when on_failure:continue swallowed seat failures or context
2476
+ // preflight completed partial/degraded. Computed only from already-persisted
2477
+ // data; additive and advisory — never mutates status semantics.
2478
+ projection["health"] = computeRunHealth(state, lifecycle);
2479
+ if (state.resolvedRoster !== undefined) {
2480
+ projection["resolved_roster"] = state.resolvedRoster;
2481
+ }
2482
+ if (isPlanReviewRunState(state)) {
2483
+ const terminalExecutionStatus = state.status === "succeeded" ||
2484
+ state.status === "failed" ||
2485
+ state.status === "cancelled"
2486
+ ? state.status
2487
+ : undefined;
2488
+ const publication = state.plan_review_state?.publication;
2489
+ const inspected = await inspectPlanReviewPublication(runDir, runId, terminalExecutionStatus ??
2490
+ (includeResult && publication?.committed === true ? "failed" : undefined), publication, includeResult);
2491
+ projection["result_available"] = inspected.result_available;
2492
+ projection["result_bytes"] = inspected.result_bytes;
2493
+ if (inspected.result !== undefined) {
2494
+ projection["result"] = inspected.result;
2495
+ }
2496
+ let planReviewError = (includeResult && publication?.committed === true ? inspected.error : undefined) ??
2497
+ state.plan_review_error ??
2498
+ inspected.error;
2499
+ if (planReviewError === undefined &&
2500
+ publication === undefined &&
2501
+ state.plan_review_state === undefined &&
2502
+ (state.status === "succeeded" || state.status === "failed")) {
2503
+ planReviewError = createPlanReviewPublicationError(runId, state.status, "no_artifact", "Historical plan-review run has no publication marker; rerun with a Track-C-capable ClinkX runtime.");
2504
+ }
2505
+ if (planReviewError !== undefined) {
2506
+ projection["plan_review_error"] = planReviewError;
2507
+ }
2508
+ if (state.status === "failed" || state.status === "cancelled") {
2509
+ if (state.error != null) {
2510
+ projection["error"] = state.error;
2511
+ }
2512
+ if (state.failureClass != null) {
2513
+ projection["failure_class"] = state.failureClass;
2514
+ }
997
2515
  }
998
- releaseMutex();
999
2516
  }
2517
+ else {
2518
+ projection["result_available"] = state.status === "succeeded";
2519
+ if (state.status === "succeeded") {
2520
+ const final = state.stages.at(-1);
2521
+ const call = final?.calls[0];
2522
+ // Cheap size hint without reading contents. Containment (via
2523
+ // readContainedSidecar) runs even on a default poll so an absolute path or
2524
+ // symlink escape is rejected regardless of include_result. Everything else
2525
+ // — a missing final call, a missing/unreadable file, an fs race — omits the
2526
+ // hint rather than throwing, preserving the "a cheap poll never throws for a
2527
+ // recoverable/missing-file condition" invariant that liveness polling needs.
2528
+ if (call?.outputInline != null) {
2529
+ projection["result_bytes"] = Buffer.byteLength(call.outputInline, "utf-8");
2530
+ }
2531
+ else if (!includeResult && call?.outputPath != null) {
2532
+ // Skip this size-only open when include_result is set — the block below
2533
+ // opens the sidecar once for both size and content, so a second open here
2534
+ // would be redundant.
2535
+ try {
2536
+ const { size } = await readContainedSidecar(runDir, call.outputPath, false);
2537
+ projection["result_bytes"] = size;
2538
+ }
2539
+ catch (err) {
2540
+ if (err instanceof SidecarPolicyError) {
2541
+ // Absolute path / containment escape rejects even on a cheap poll.
2542
+ throw new Error(`Succeeded run ${runId} ${err.message}`);
2543
+ }
2544
+ // Missing file / fs race / permission on a cheap poll: omit the hint.
2545
+ }
2546
+ }
2547
+ if (includeResult) {
2548
+ if (call == null) {
2549
+ throw new Error(`Succeeded run ${runId} is missing final call state`);
2550
+ }
2551
+ if (call.outputInline != null) {
2552
+ projection["result"] = call.outputInline;
2553
+ }
2554
+ else if (call.outputPath != null) {
2555
+ try {
2556
+ const { size, content } = await readContainedSidecar(runDir, call.outputPath, true);
2557
+ projection["result_bytes"] = size;
2558
+ projection["result"] = content;
2559
+ }
2560
+ catch (err) {
2561
+ if (err instanceof SidecarPolicyError) {
2562
+ throw new Error(`Succeeded run ${runId} ${err.message}`);
2563
+ }
2564
+ throw new Error(`Succeeded run ${runId} output file is missing or unreadable`);
2565
+ }
2566
+ }
2567
+ else {
2568
+ throw new Error(`Succeeded run ${runId} final call has neither inline nor persisted output`);
2569
+ }
2570
+ }
2571
+ }
2572
+ else if (state.status === "failed" || state.status === "cancelled") {
2573
+ if (state.error != null) {
2574
+ projection["error"] = state.error;
2575
+ }
2576
+ if (state.failureClass != null) {
2577
+ projection["failure_class"] = state.failureClass;
2578
+ }
2579
+ }
2580
+ }
2581
+ return {
2582
+ content: [{ type: "text", text: JSON.stringify(projection) }],
2583
+ };
1000
2584
  }
1001
- function executeListWorkflows(workflowsPath, availableCliNames) {
1002
- const workflows = listWorkflowCatalog(workflowsPath);
2585
+ async function executeCancelWorkflow(_options, args) {
2586
+ const runId = args["run_id"];
2587
+ if (typeof runId !== "string" || runId === "") {
2588
+ throw new McpError(ErrorCode.InvalidParams, 'Missing or invalid "run_id" parameter');
2589
+ }
2590
+ try {
2591
+ validateRunId(runId);
2592
+ }
2593
+ catch {
2594
+ throw new McpError(ErrorCode.InvalidParams, `Invalid run_id "${runId}". Must match [A-Za-z0-9_-]{1,64}.`);
2595
+ }
2596
+ const text = (value) => ({
2597
+ content: [{ type: "text", text: JSON.stringify(value) }],
2598
+ });
2599
+ // 1) Live in-process async run: abort its controller and let the run's own
2600
+ // unwind drive it to terminal "cancelled". We do NOT touch the mutex here —
2601
+ // the detached run's finally owns the slot (touching it risks a double-free).
2602
+ const controller = cancelRegistry.get(runId);
2603
+ if (controller != null) {
2604
+ // The engine can have already persisted a terminal status while its
2605
+ // controller is still in the registry (deleted only in the run's finally).
2606
+ // Re-read first so we never claim "cancelling" for an already-done run.
2607
+ const live = await readRunStateForStatus(runId);
2608
+ if (live != null && live.state.status !== "running" && live.state.status !== "pending") {
2609
+ return text({ run_id: runId, status: live.state.status, already_terminal: true });
2610
+ }
2611
+ if (!controller.signal.aborted) {
2612
+ controller.abort(new WorkflowCancellationError());
2613
+ }
2614
+ return text({ run_id: runId, status: "cancelling" });
2615
+ }
2616
+ // 2) Not in our registry — inspect persisted state via the same path as status.
2617
+ const read = await readRunStateForStatus(runId);
2618
+ if (read == null) {
2619
+ return text({ run_id: runId, status: "not_found" });
2620
+ }
2621
+ const { state, lock } = read;
2622
+ if (state.status !== "running" && state.status !== "pending") {
2623
+ return text({ run_id: runId, status: state.status, already_terminal: true });
2624
+ }
2625
+ if (lock === "unknown") {
2626
+ return text({
2627
+ run_id: runId,
2628
+ status: state.status,
2629
+ cancellable: false,
2630
+ lock_ownership: "unknown",
2631
+ error: `Run lock ownership is unknown for run_id "${runId}"; refusing cancellation until the lock record is repaired.`,
2632
+ });
2633
+ }
2634
+ // running|pending and not in our registry. The run.lock is the single source
2635
+ // of truth for ownership: reapRunCancelled acquires it itself (reclaiming a
2636
+ // truly stale/dead lock, refusing a live one), closing the TOCTOU where a
2637
+ // concurrent resume_workflow could grab the lock between read and reap.
2638
+ const outcome = await reapRunCancelled(runId);
2639
+ if (outcome === "cancelled") {
2640
+ return text({ run_id: runId, status: "cancelled", reaped: true });
2641
+ }
2642
+ if (outcome === "locked") {
2643
+ // The run is owned by another live worker (or a blocking run): the lock
2644
+ // could not be acquired. Refuse without mutating — this process cannot
2645
+ // safely kill a worker it does not own.
2646
+ return text({ run_id: runId, status: "running", cancellable: false });
2647
+ }
2648
+ if (outcome === "ownership_unknown") {
2649
+ return text({
2650
+ run_id: runId,
2651
+ status: state.status,
2652
+ cancellable: false,
2653
+ lock_ownership: "unknown",
2654
+ error: `Run lock ownership became unknown for run_id "${runId}"; refusing cancellation until the lock record is repaired.`,
2655
+ });
2656
+ }
2657
+ // "skipped": became terminal between our read and the reap. Re-read and report.
2658
+ const reread = await readRunStateForStatus(runId);
2659
+ const status = reread?.state.status ?? "cancelled";
2660
+ return text({ run_id: runId, status, already_terminal: true });
2661
+ }
2662
+ function executeListWorkflows(options) {
2663
+ const workflows = listWorkflowCatalog(options.workflowsPath);
2664
+ const preflightEnabled = options.contextBuilderRunner != null;
2665
+ const contextDefault = preflightEnabled
2666
+ ? options.contextDefault ?? getWorkflowsContextDefault()
2667
+ : options.contextDefault ?? "off";
2668
+ const effectiveContextDefault = resolveWorkflowContextMode(undefined, contextDefault, preflightEnabled, undefined, options.defaultCurator).effectiveMode;
1003
2669
  const entries = [];
1004
2670
  for (const [name, entry] of workflows) {
1005
2671
  const def = entry.definition;
2672
+ const contextStrategy = deriveContextStrategy(def);
2673
+ const runtimeMembers = supportsRuntimeMembers(name);
1006
2674
  entries.push({
1007
2675
  name,
1008
2676
  description: def.description,
1009
2677
  version: def.version,
1010
2678
  source: entry.source,
1011
2679
  shadowed_yaml: entry.shadowedYaml,
1012
- supports_members: supportsRuntimeMembers(name),
1013
- supports_chairman: supportsRuntimeMembers(name),
1014
- ...(supportsRuntimeMembers(name) ? {
2680
+ supports_members: runtimeMembers,
2681
+ supports_chairman: runtimeMembers,
2682
+ supports_context_preflight: contextStrategy.supports_context_preflight,
2683
+ effective_context_default: effectiveContextDefault,
2684
+ fails_on_empty_selection: contextStrategy.fails_on_empty_selection,
2685
+ context_anchor_variables: contextStrategy.context_anchor_variables,
2686
+ context_override_variables: contextStrategy.context_override_variables,
2687
+ ...(contextStrategy.default_context_profile !== undefined
2688
+ ? { default_context_profile: contextStrategy.default_context_profile }
2689
+ : {}),
2690
+ context_recipient_stages: contextStrategy.context_recipient_stages,
2691
+ ...(runtimeMembers ? {
1015
2692
  members_min: 1,
1016
2693
  members_max: 10,
1017
2694
  members_stance_required_from_index: 3,
1018
2695
  } : {}),
1019
2696
  ...(entry.path != null ? { path: basename(entry.path) } : {}),
2697
+ ...(name === PLAN_REVIEW_RECEIPTS_WORKFLOW
2698
+ ? {
2699
+ plan_review: buildCouncilPlanReviewDiscovery(options.planReviewAvailableCliNames ??
2700
+ options.availableCliNames ??
2701
+ []),
2702
+ }
2703
+ : {}),
1020
2704
  variables: Object.fromEntries(Object.entries(def.variables).map(([varName, varDef]) => [
1021
2705
  varName,
1022
2706
  {
@@ -1029,8 +2713,16 @@ function executeListWorkflows(workflowsPath, availableCliNames) {
1029
2713
  });
1030
2714
  }
1031
2715
  const response = { workflows: entries };
1032
- if (availableCliNames != null && availableCliNames.length > 0) {
1033
- response["available_cli_names"] = [...availableCliNames].sort();
2716
+ if (options.availableCliNames != null && options.availableCliNames.length > 0) {
2717
+ response["available_cli_names"] = [...options.availableCliNames].sort();
2718
+ }
2719
+ if (options.adapterCapabilities != null) {
2720
+ const availableClis = [...options.adapterCapabilities()];
2721
+ availableClis.sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
2722
+ response["available_clis"] = availableClis;
2723
+ }
2724
+ if (options.rosterLoader != null) {
2725
+ response["available_rosters"] = options.rosterLoader.list();
1034
2726
  }
1035
2727
  return {
1036
2728
  content: [
@@ -1041,10 +2733,137 @@ function executeListWorkflows(workflowsPath, availableCliNames) {
1041
2733
  ],
1042
2734
  };
1043
2735
  }
2736
+ function includesVar(from, name) {
2737
+ if (typeof from === "string") {
2738
+ return from === name;
2739
+ }
2740
+ return Array.isArray(from) && from.includes(name);
2741
+ }
2742
+ // cb_* variables that tune preflight execution rather than source selection —
2743
+ // overrides, not anchors. Advertised separately as context_override_variables.
2744
+ const CONTEXT_OVERRIDE_VARIABLES = new Set([
2745
+ "cb_allow_secrets",
2746
+ "cb_budget_max_estimated_tokens",
2747
+ "cb_budget_max_files",
2748
+ "cb_budget_max_scan_files",
2749
+ "cb_budget_max_bytes_per_file",
2750
+ "cb_budget_max_total_bytes",
2751
+ "cb_budget_max_search_results",
2752
+ "cb_budget_adapter",
2753
+ "cb_delegate",
2754
+ "cb_context_mode",
2755
+ ]);
2756
+ function deriveContextStrategy(def) {
2757
+ const contextRecipientStages = def.stages
2758
+ .filter((stage) => stage.calls.some((call) => includesVar(call.absolute_file_paths_from, "context_bundle_files")))
2759
+ .map((stage) => stage.id);
2760
+ const cbVariables = Object.keys(def.variables)
2761
+ .filter((name) => name.startsWith("cb_"))
2762
+ .sort();
2763
+ const contextAnchorVariables = cbVariables.filter((name) => !CONTEXT_OVERRIDE_VARIABLES.has(name));
2764
+ const contextOverrideVariables = cbVariables.filter((name) => CONTEXT_OVERRIDE_VARIABLES.has(name));
2765
+ const defaultContextProfile = def.variables["cb_profile"]?.default;
2766
+ const hasSelectionScopeVariable = "cb_focus_paths" in def.variables || "cb_search_terms" in def.variables;
2767
+ return {
2768
+ supports_context_preflight: "context_bundle_files" in def.variables &&
2769
+ contextRecipientStages.length > 0 &&
2770
+ hasSelectionScopeVariable,
2771
+ context_anchor_variables: contextAnchorVariables,
2772
+ context_override_variables: contextOverrideVariables,
2773
+ ...(typeof defaultContextProfile === "string"
2774
+ ? { default_context_profile: defaultContextProfile }
2775
+ : {}),
2776
+ context_recipient_stages: contextRecipientStages,
2777
+ fails_on_empty_selection: def.settings.fail_on_empty_context_selection,
2778
+ };
2779
+ }
1044
2780
  // ---------------------------------------------------------------------------
1045
2781
  // Tool input schemas (JSON Schema)
1046
2782
  // ---------------------------------------------------------------------------
1047
2783
  const MAX_INLINE_WORKFLOW_DETAILS = 8;
2784
+ const PLAN_REVIEW_INPUT_SCHEMA = {
2785
+ type: "object",
2786
+ description: 'Immutable plan-review contract. Accepted only when workflow is "plan-review-receipts".',
2787
+ properties: {
2788
+ schema_version: { type: "string", const: "council-plan-review-input-v1" },
2789
+ workspace_schema: { type: "string", const: "plan-workspace-v1" },
2790
+ digest_profile: { type: "string", const: "checkpoint-text-v1" },
2791
+ review_id: {
2792
+ type: "string",
2793
+ pattern: "^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$",
2794
+ },
2795
+ repository_id: {
2796
+ type: "string",
2797
+ maxLength: 128,
2798
+ pattern: "^[A-Za-z0-9][A-Za-z0-9._-]*(?:/[A-Za-z0-9][A-Za-z0-9._-]*)*$",
2799
+ },
2800
+ iteration: { type: "integer", minimum: 1 },
2801
+ plan_id: {
2802
+ type: "string",
2803
+ pattern: "^[a-z0-9][a-z0-9-]{0,62}$",
2804
+ },
2805
+ revision: { type: "integer", minimum: 1 },
2806
+ digest: { type: "string", pattern: "^[0-9a-f]{64}$" },
2807
+ expected_execution_contract_digest: {
2808
+ type: "string",
2809
+ pattern: "^[0-9a-f]{64}$",
2810
+ },
2811
+ threshold: { type: "number", minimum: 0, maximum: 10 },
2812
+ designated_editor: { type: "string", minLength: 1 },
2813
+ chairman_seat: {
2814
+ type: "string",
2815
+ pattern: "^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$",
2816
+ },
2817
+ plan_file: {
2818
+ type: "string",
2819
+ minLength: 1,
2820
+ description: "Absolute path to the plan snapshot source",
2821
+ },
2822
+ seats: {
2823
+ type: "array",
2824
+ minItems: 1,
2825
+ maxItems: 10,
2826
+ items: {
2827
+ type: "object",
2828
+ properties: {
2829
+ seat_id: {
2830
+ type: "string",
2831
+ pattern: "^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$",
2832
+ },
2833
+ reviewer: { type: "string", minLength: 1 },
2834
+ cli_name: {
2835
+ type: "string",
2836
+ enum: [...PLAN_REVIEW_V1_CLI_NAMES].sort(),
2837
+ },
2838
+ stance: {
2839
+ type: "string",
2840
+ enum: ["strict", "pragmatic", "adversarial"],
2841
+ },
2842
+ },
2843
+ required: ["seat_id", "reviewer", "cli_name", "stance"],
2844
+ additionalProperties: false,
2845
+ },
2846
+ },
2847
+ },
2848
+ required: [
2849
+ "schema_version",
2850
+ "workspace_schema",
2851
+ "digest_profile",
2852
+ "review_id",
2853
+ "repository_id",
2854
+ "iteration",
2855
+ "plan_id",
2856
+ "revision",
2857
+ "digest",
2858
+ "expected_execution_contract_digest",
2859
+ "threshold",
2860
+ "designated_editor",
2861
+ "chairman_seat",
2862
+ "plan_file",
2863
+ "seats",
2864
+ ],
2865
+ additionalProperties: false,
2866
+ };
1048
2867
  const RUN_WORKFLOW_SCHEMA = {
1049
2868
  type: "object",
1050
2869
  properties: {
@@ -1072,9 +2891,20 @@ const RUN_WORKFLOW_SCHEMA = {
1072
2891
  description: "When true, append a structured receipt summarizing the workflow run as a content block",
1073
2892
  default: false,
1074
2893
  },
2894
+ async: {
2895
+ type: "boolean",
2896
+ default: false,
2897
+ description: "When true, start the run detached and return {run_id, status:'running'} immediately; poll workflow_run_status for progress and result.",
2898
+ },
2899
+ roster: {
2900
+ type: "string",
2901
+ pattern: ROSTER_NAME_PATTERN.source,
2902
+ maxLength: 64,
2903
+ description: "Optional named roster preset; exclusive with members, chairman, chairman_model, chairman_effort, and a non-empty variables.cb_delegate.",
2904
+ },
1075
2905
  members: {
1076
2906
  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.",
2907
+ 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, an optional analytical stance, and optional requested model/effort values whose support is adapter-declared. 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
2908
  minItems: 1,
1079
2909
  maxItems: 10,
1080
2910
  items: {
@@ -1088,6 +2918,20 @@ const RUN_WORKFLOW_SCHEMA = {
1088
2918
  type: "string",
1089
2919
  description: "Analytical stance or perspective for this council seat. Required for seats at indices 3-9. Server enforces a 200-character limit after whitespace normalization (trim + collapse). Must not contain {{ or }}.",
1090
2920
  },
2921
+ model: {
2922
+ type: "string",
2923
+ minLength: 1,
2924
+ maxLength: 128,
2925
+ pattern: MODEL_OVERRIDE_PATTERN.source,
2926
+ description: "Optional requested model for this seat. Support is declared by the selected adapter; omission preserves its configured default.",
2927
+ },
2928
+ effort: {
2929
+ type: "string",
2930
+ minLength: 1,
2931
+ maxLength: 32,
2932
+ pattern: "^[a-z0-9][a-z0-9_-]*$",
2933
+ description: "Optional requested reasoning effort for this seat. Support is declared by the selected adapter; omission preserves its configured default.",
2934
+ },
1091
2935
  },
1092
2936
  required: ["cli_name"],
1093
2937
  additionalProperties: false,
@@ -1097,6 +2941,21 @@ const RUN_WORKFLOW_SCHEMA = {
1097
2941
  type: "string",
1098
2942
  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
2943
  },
2944
+ chairman_model: {
2945
+ type: "string",
2946
+ minLength: 1,
2947
+ maxLength: 128,
2948
+ pattern: MODEL_OVERRIDE_PATTERN.source,
2949
+ description: "Optional requested model for the council chairman. Support is declared by the chairman adapter; omission preserves its configured default. May be supplied without chairman.",
2950
+ },
2951
+ chairman_effort: {
2952
+ type: "string",
2953
+ minLength: 1,
2954
+ maxLength: 32,
2955
+ pattern: "^[a-z0-9][a-z0-9_-]*$",
2956
+ description: "Optional requested reasoning effort for the council chairman. Support is declared by the chairman adapter; omission preserves its configured default. May be supplied without chairman.",
2957
+ },
2958
+ plan_review: PLAN_REVIEW_INPUT_SCHEMA,
1100
2959
  },
1101
2960
  required: ["workflow", "run_id"],
1102
2961
  additionalProperties: false,
@@ -1128,8 +2987,35 @@ const LIST_WORKFLOWS_SCHEMA = {
1128
2987
  properties: {},
1129
2988
  additionalProperties: false,
1130
2989
  };
2990
+ const WORKFLOW_RUN_STATUS_SCHEMA = {
2991
+ type: "object",
2992
+ properties: {
2993
+ run_id: {
2994
+ type: "string",
2995
+ pattern: "^[A-Za-z0-9_-]{1,64}$",
2996
+ },
2997
+ include_result: {
2998
+ type: "boolean",
2999
+ default: false,
3000
+ 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.",
3001
+ },
3002
+ },
3003
+ required: ["run_id"],
3004
+ additionalProperties: false,
3005
+ };
3006
+ const CANCEL_WORKFLOW_SCHEMA = {
3007
+ type: "object",
3008
+ properties: {
3009
+ run_id: {
3010
+ type: "string",
3011
+ pattern: "^[A-Za-z0-9_-]{1,64}$",
3012
+ },
3013
+ },
3014
+ required: ["run_id"],
3015
+ additionalProperties: false,
3016
+ };
1131
3017
  /**
1132
- * Return the three workflow tool definitions for tools/list.
3018
+ * Return the workflow tool definitions for tools/list.
1133
3019
  *
1134
3020
  * Dynamically discovers available workflows to include in the
1135
3021
  * run_workflow description. Safe to call repeatedly — no side effects.
@@ -1170,6 +3056,22 @@ export function getWorkflowToolDefinitions(workflowsPath, availableCliNames) {
1170
3056
  }
1171
3057
  }
1172
3058
  }
3059
+ // plan_review.seats[].cli_name enum (intersection with the v1 provider set)
3060
+ const planReview = props["plan_review"];
3061
+ const planReviewProps = planReview?.["properties"];
3062
+ const planReviewSeats = planReviewProps?.["seats"];
3063
+ const planReviewItems = planReviewSeats?.["items"];
3064
+ const planReviewSeatProps = planReviewItems?.["properties"];
3065
+ const planReviewCliNameSet = new Set(PLAN_REVIEW_V1_CLI_NAMES);
3066
+ const supportedPlanReviewCliNames = sorted.filter((name) => planReviewCliNameSet.has(name));
3067
+ if (planReviewSeatProps?.["cli_name"] != null &&
3068
+ supportedPlanReviewCliNames.length > 0) {
3069
+ planReviewSeatProps["cli_name"] = {
3070
+ ...planReviewSeatProps["cli_name"],
3071
+ enum: supportedPlanReviewCliNames,
3072
+ description: `Configured v1 plan-review adapters: ${supportedPlanReviewCliNames.join(", ")}`,
3073
+ };
3074
+ }
1173
3075
  // chairman enum
1174
3076
  if (props["chairman"] != null) {
1175
3077
  props["chairman"] = {
@@ -1212,7 +3114,7 @@ export function getWorkflowToolDefinitions(workflowsPath, availableCliNames) {
1212
3114
  return [
1213
3115
  {
1214
3116
  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.`,
3117
+ 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. Council runs include a \`resolved_roster\` of requested seat and delegate values (no provenance) on the async ack, a trailing sync content block, and workflow_run_status.`,
1216
3118
  inputSchema: runSchema,
1217
3119
  },
1218
3120
  {
@@ -1227,6 +3129,18 @@ export function getWorkflowToolDefinitions(workflowsPath, availableCliNames) {
1227
3129
  inputSchema: LIST_WORKFLOWS_SCHEMA,
1228
3130
  annotations: { readOnlyHint: true },
1229
3131
  },
3132
+ {
3133
+ name: "workflow_run_status",
3134
+ 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, context_external_gaps — the finalizer reported evidence gaps outside the delegate packet under external_gap_policy:\"degrade\", so conclusions depending on them are conditional). When a context-builder lifecycle snapshot is persisted, the `context` block additionally includes `lifecycle_status` and `coverage` {policy, status, required, selected, missing}. A parsed snapshot also projects `context.delegate.finalizer` {effective_enabled, execution_status, score?}; `score` is present only for a succeeded finalizer that recorded one, and the finalizer key is omitted when the lifecycle is missing, unparseable, or those fields are absent. While context preflight is still running (non-terminal lifecycle, no committed context yet) the `context` block is absent and a top-level `context_progress` {phase, status, updated_at, counts} reports scan progress instead. Found council runs also include a top-level `resolved_roster` of requested seat and delegate values (no provenance) when the run persisted one; older runs omit the key.",
3135
+ inputSchema: WORKFLOW_RUN_STATUS_SCHEMA,
3136
+ annotations: { readOnlyHint: true },
3137
+ },
3138
+ {
3139
+ name: "cancel_workflow",
3140
+ 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.",
3141
+ inputSchema: CANCEL_WORKFLOW_SCHEMA,
3142
+ annotations: { destructiveHint: true },
3143
+ },
1230
3144
  ];
1231
3145
  }
1232
3146
  /** Names of the workflow tools (for dispatch matching). */
@@ -1234,6 +3148,8 @@ export const WORKFLOW_TOOL_NAMES = new Set([
1234
3148
  "run_workflow",
1235
3149
  "resume_workflow",
1236
3150
  "list_workflows",
3151
+ "workflow_run_status",
3152
+ "cancel_workflow",
1237
3153
  ]);
1238
3154
  /**
1239
3155
  * Dispatch a workflow tool call.
@@ -1248,7 +3164,11 @@ export async function handleWorkflowToolCall(server, options, name, args, signal
1248
3164
  case "resume_workflow":
1249
3165
  return await executeResumeWorkflow(server, options, args, signal, progressToken);
1250
3166
  case "list_workflows":
1251
- return executeListWorkflows(options.workflowsPath, options.availableCliNames);
3167
+ return executeListWorkflows(options);
3168
+ case "workflow_run_status":
3169
+ return await executeWorkflowRunStatus(options, args);
3170
+ case "cancel_workflow":
3171
+ return await executeCancelWorkflow(options, args);
1252
3172
  default:
1253
3173
  return null;
1254
3174
  }
@@ -1260,7 +3180,8 @@ export async function handleWorkflowToolCall(server, options, name, args, signal
1260
3180
  * Register workflow tools on an MCP Server instance.
1261
3181
  *
1262
3182
  * This is the reusable core extracted from createWorkflowServer().
1263
- * It registers three tools: run_workflow, resume_workflow, list_workflows.
3183
+ * It registers five tools: run_workflow, resume_workflow, list_workflows,
3184
+ * workflow_run_status, cancel_workflow.
1264
3185
  *
1265
3186
  * The `options.createBackend` factory determines how clink calls are
1266
3187
  * dispatched — callers provide either a child-process or in-process backend.
@@ -1272,21 +3193,35 @@ export async function handleWorkflowToolCall(server, options, name, args, signal
1272
3193
  export function registerWorkflowTools(server, options) {
1273
3194
  // tools/list
1274
3195
  server.setRequestHandler(ListToolsRequestSchema, async () => {
1275
- return { tools: getWorkflowToolDefinitions(options.workflowsPath, options.availableCliNames) };
3196
+ activeWorkflowHandlers += 1;
3197
+ try {
3198
+ return { tools: getWorkflowToolDefinitions(options.workflowsPath, options.availableCliNames) };
3199
+ }
3200
+ finally {
3201
+ activeWorkflowHandlers -= 1;
3202
+ notifyWorkflowActivity();
3203
+ }
1276
3204
  });
1277
3205
  // tools/call
1278
3206
  server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
1279
- const { name, arguments: args } = request.params;
1280
- const progressToken = request.params._meta?.progressToken;
1281
- const result = await handleWorkflowToolCall(server, options, name, args ?? {}, extra.signal, progressToken);
1282
- if (result != null) {
1283
- return result;
3207
+ activeWorkflowHandlers += 1;
3208
+ try {
3209
+ const { name, arguments: args } = request.params;
3210
+ const progressToken = request.params._meta?.progressToken;
3211
+ const result = await handleWorkflowToolCall(server, options, name, args ?? {}, extra.signal, progressToken);
3212
+ if (result != null) {
3213
+ return result;
3214
+ }
3215
+ logger.warn({ tool: name }, "unknown tool requested");
3216
+ return {
3217
+ content: [{ type: "text", text: `Unknown tool: ${String(name)}` }],
3218
+ isError: true,
3219
+ };
3220
+ }
3221
+ finally {
3222
+ activeWorkflowHandlers -= 1;
3223
+ notifyWorkflowActivity();
1284
3224
  }
1285
- logger.warn({ tool: name }, "unknown tool requested");
1286
- return {
1287
- content: [{ type: "text", text: `Unknown tool: ${String(name)}` }],
1288
- isError: true,
1289
- };
1290
3225
  });
1291
3226
  }
1292
3227
  //# sourceMappingURL=workflow-tools.js.map