clinkx 1.0.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 (331) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/README.md +72 -0
  3. package/clinkx-workflows/dist/chairman-knobs.d.ts +24 -0
  4. package/clinkx-workflows/dist/chairman-knobs.js +27 -0
  5. package/clinkx-workflows/dist/chairman-knobs.js.map +1 -0
  6. package/clinkx-workflows/dist/clink-client.d.ts +10 -0
  7. package/clinkx-workflows/dist/clink-client.js +39 -1
  8. package/clinkx-workflows/dist/clink-client.js.map +1 -1
  9. package/clinkx-workflows/dist/config.d.ts +29 -1
  10. package/clinkx-workflows/dist/config.js +35 -2
  11. package/clinkx-workflows/dist/config.js.map +1 -1
  12. package/clinkx-workflows/dist/context-preflight.d.ts +5 -0
  13. package/clinkx-workflows/dist/context-preflight.js.map +1 -1
  14. package/clinkx-workflows/dist/council-execution-policy.d.ts +68 -0
  15. package/clinkx-workflows/dist/council-execution-policy.js +110 -0
  16. package/clinkx-workflows/dist/council-execution-policy.js.map +1 -0
  17. package/clinkx-workflows/dist/council-expander.d.ts +9 -8
  18. package/clinkx-workflows/dist/council-expander.js +18 -9
  19. package/clinkx-workflows/dist/council-expander.js.map +1 -1
  20. package/clinkx-workflows/dist/dashboard/activity-contract.d.ts +5557 -4
  21. package/clinkx-workflows/dist/dashboard/activity-contract.js +55 -3
  22. package/clinkx-workflows/dist/dashboard/activity-contract.js.map +1 -1
  23. package/clinkx-workflows/dist/dashboard/activity-reader.d.ts +9 -5
  24. package/clinkx-workflows/dist/dashboard/activity-reader.js +15 -2
  25. package/clinkx-workflows/dist/dashboard/activity-reader.js.map +1 -1
  26. package/clinkx-workflows/dist/dashboard/api-routes.d.ts +4 -0
  27. package/clinkx-workflows/dist/dashboard/api-routes.js +133 -25
  28. package/clinkx-workflows/dist/dashboard/api-routes.js.map +1 -1
  29. package/clinkx-workflows/dist/dashboard/context-builder-invocation-contract.d.ts +3865 -0
  30. package/clinkx-workflows/dist/dashboard/context-builder-invocation-contract.js +69 -0
  31. package/clinkx-workflows/dist/dashboard/context-builder-invocation-contract.js.map +1 -0
  32. package/clinkx-workflows/dist/dashboard/context-run-reader.d.ts +36 -2
  33. package/clinkx-workflows/dist/dashboard/context-run-reader.js +142 -19
  34. package/clinkx-workflows/dist/dashboard/context-run-reader.js.map +1 -1
  35. package/clinkx-workflows/dist/dashboard/contracts.d.ts +20027 -480
  36. package/clinkx-workflows/dist/dashboard/contracts.js +133 -6
  37. package/clinkx-workflows/dist/dashboard/contracts.js.map +1 -1
  38. package/clinkx-workflows/dist/dashboard/index.d.ts +4 -0
  39. package/clinkx-workflows/dist/dashboard/index.js +4 -0
  40. package/clinkx-workflows/dist/dashboard/index.js.map +1 -1
  41. package/clinkx-workflows/dist/dashboard/invocation-contract.d.ts +786 -0
  42. package/clinkx-workflows/dist/dashboard/invocation-contract.js +201 -0
  43. package/clinkx-workflows/dist/dashboard/invocation-contract.js.map +1 -0
  44. package/clinkx-workflows/dist/dashboard/lazy-content.d.ts +14 -1
  45. package/clinkx-workflows/dist/dashboard/lazy-content.js +53 -2
  46. package/clinkx-workflows/dist/dashboard/lazy-content.js.map +1 -1
  47. package/clinkx-workflows/dist/dashboard/native-transcript/contract.d.ts +90 -0
  48. package/clinkx-workflows/dist/dashboard/native-transcript/contract.js +31 -0
  49. package/clinkx-workflows/dist/dashboard/native-transcript/contract.js.map +1 -0
  50. package/clinkx-workflows/dist/dashboard/native-transcript/projection.d.ts +12 -0
  51. package/clinkx-workflows/dist/dashboard/native-transcript/projection.js +286 -0
  52. package/clinkx-workflows/dist/dashboard/native-transcript/projection.js.map +1 -0
  53. package/clinkx-workflows/dist/dashboard/progress-report-contract.d.ts +147 -4
  54. package/clinkx-workflows/dist/dashboard/progress-report-contract.js +11 -11
  55. package/clinkx-workflows/dist/dashboard/progress-report-contract.js.map +1 -1
  56. package/clinkx-workflows/dist/dashboard/progress-report-reader.js +4 -56
  57. package/clinkx-workflows/dist/dashboard/progress-report-reader.js.map +1 -1
  58. package/clinkx-workflows/dist/dashboard/progress-report-status-store.d.ts +2 -1
  59. package/clinkx-workflows/dist/dashboard/progress-report-status-store.js +6 -5
  60. package/clinkx-workflows/dist/dashboard/progress-report-status-store.js.map +1 -1
  61. package/clinkx-workflows/dist/dashboard/prompt-builder-library-contract.d.ts +194 -0
  62. package/clinkx-workflows/dist/dashboard/prompt-builder-library-contract.js +89 -0
  63. package/clinkx-workflows/dist/dashboard/prompt-builder-library-contract.js.map +1 -0
  64. package/clinkx-workflows/dist/dashboard/prompt-builder-library-store.d.ts +26 -0
  65. package/clinkx-workflows/dist/dashboard/prompt-builder-library-store.js +102 -0
  66. package/clinkx-workflows/dist/dashboard/prompt-builder-library-store.js.map +1 -0
  67. package/clinkx-workflows/dist/dashboard/request-admission.js +4 -0
  68. package/clinkx-workflows/dist/dashboard/request-admission.js.map +1 -1
  69. package/clinkx-workflows/dist/dashboard/scanner.d.ts +3 -0
  70. package/clinkx-workflows/dist/dashboard/scanner.js +52 -8
  71. package/clinkx-workflows/dist/dashboard/scanner.js.map +1 -1
  72. package/clinkx-workflows/dist/dashboard/server-factory.d.ts +14 -1
  73. package/clinkx-workflows/dist/dashboard/server-factory.js +73 -21
  74. package/clinkx-workflows/dist/dashboard/server-factory.js.map +1 -1
  75. package/clinkx-workflows/dist/dashboard/session-log-contract.d.ts +223 -56
  76. package/clinkx-workflows/dist/dashboard/session-log-contract.js +26 -0
  77. package/clinkx-workflows/dist/dashboard/session-log-contract.js.map +1 -1
  78. package/clinkx-workflows/dist/dashboard/session-log-journal.js +10 -3
  79. package/clinkx-workflows/dist/dashboard/session-log-journal.js.map +1 -1
  80. package/clinkx-workflows/dist/dashboard/session-log-redaction.d.ts +9 -1
  81. package/clinkx-workflows/dist/dashboard/session-log-redaction.js +793 -59
  82. package/clinkx-workflows/dist/dashboard/session-log-redaction.js.map +1 -1
  83. package/clinkx-workflows/dist/dashboard-ui/assets/index-JPHQVtn9.js +106 -0
  84. package/clinkx-workflows/dist/dashboard-ui/assets/style-DOMHk-j6.css +1 -0
  85. package/clinkx-workflows/dist/dashboard-ui/index.html +2 -2
  86. package/clinkx-workflows/dist/delegate-finalizer-policy.d.ts +26 -0
  87. package/clinkx-workflows/dist/delegate-finalizer-policy.js +72 -0
  88. package/clinkx-workflows/dist/delegate-finalizer-policy.js.map +1 -0
  89. package/clinkx-workflows/dist/delegate-signature.d.ts +14 -0
  90. package/clinkx-workflows/dist/delegate-signature.js +14 -0
  91. package/clinkx-workflows/dist/delegate-signature.js.map +1 -0
  92. package/clinkx-workflows/dist/delegate-validate.d.ts +32 -2
  93. package/clinkx-workflows/dist/delegate-validate.js +51 -0
  94. package/clinkx-workflows/dist/delegate-validate.js.map +1 -1
  95. package/clinkx-workflows/dist/engine.d.ts +113 -3
  96. package/clinkx-workflows/dist/engine.js +476 -80
  97. package/clinkx-workflows/dist/engine.js.map +1 -1
  98. package/clinkx-workflows/dist/errors.d.ts +22 -2
  99. package/clinkx-workflows/dist/errors.js +30 -2
  100. package/clinkx-workflows/dist/errors.js.map +1 -1
  101. package/clinkx-workflows/dist/fidelity.d.ts +47 -1
  102. package/clinkx-workflows/dist/fidelity.js +54 -1
  103. package/clinkx-workflows/dist/fidelity.js.map +1 -1
  104. package/clinkx-workflows/dist/fingerprint.d.ts +24 -6
  105. package/clinkx-workflows/dist/fingerprint.js +32 -9
  106. package/clinkx-workflows/dist/fingerprint.js.map +1 -1
  107. package/clinkx-workflows/dist/index.js +53 -1
  108. package/clinkx-workflows/dist/index.js.map +1 -1
  109. package/clinkx-workflows/dist/logger.d.ts +1 -1
  110. package/clinkx-workflows/dist/logger.js +17 -3
  111. package/clinkx-workflows/dist/logger.js.map +1 -1
  112. package/clinkx-workflows/dist/monotonic-clock.d.ts +19 -0
  113. package/clinkx-workflows/dist/monotonic-clock.js +22 -0
  114. package/clinkx-workflows/dist/monotonic-clock.js.map +1 -0
  115. package/clinkx-workflows/dist/plan-review/contract.d.ts +52 -52
  116. package/clinkx-workflows/dist/plan-review/publication.d.ts +4 -4
  117. package/clinkx-workflows/dist/plan-review/run-state.d.ts +16 -16
  118. package/clinkx-workflows/dist/roster-contract.d.ts +889 -0
  119. package/clinkx-workflows/dist/roster-contract.js +180 -0
  120. package/clinkx-workflows/dist/roster-contract.js.map +1 -0
  121. package/clinkx-workflows/dist/rosters.d.ts +14 -0
  122. package/clinkx-workflows/dist/rosters.js +169 -0
  123. package/clinkx-workflows/dist/rosters.js.map +1 -0
  124. package/clinkx-workflows/dist/run-health.d.ts +7 -0
  125. package/clinkx-workflows/dist/run-health.js +25 -0
  126. package/clinkx-workflows/dist/run-health.js.map +1 -0
  127. package/clinkx-workflows/dist/schema.d.ts +64 -0
  128. package/clinkx-workflows/dist/schema.js +24 -0
  129. package/clinkx-workflows/dist/schema.js.map +1 -1
  130. package/clinkx-workflows/dist/server.js +2 -0
  131. package/clinkx-workflows/dist/server.js.map +1 -1
  132. package/clinkx-workflows/dist/shutdown.d.ts +15 -2
  133. package/clinkx-workflows/dist/shutdown.js +60 -16
  134. package/clinkx-workflows/dist/shutdown.js.map +1 -1
  135. package/clinkx-workflows/dist/state-schema.d.ts +23 -0
  136. package/clinkx-workflows/dist/state.d.ts +5 -0
  137. package/clinkx-workflows/dist/state.js +128 -21
  138. package/clinkx-workflows/dist/state.js.map +1 -1
  139. package/clinkx-workflows/dist/stdio-watchdog.d.ts +52 -0
  140. package/clinkx-workflows/dist/stdio-watchdog.js +212 -0
  141. package/clinkx-workflows/dist/stdio-watchdog.js.map +1 -0
  142. package/clinkx-workflows/dist/types.d.ts +6 -0
  143. package/clinkx-workflows/dist/validators/council.d.ts +50 -50
  144. package/clinkx-workflows/dist/validators/plan-review.d.ts +3 -3
  145. package/clinkx-workflows/dist/workflow-tools.d.ts +70 -0
  146. package/clinkx-workflows/dist/workflow-tools.js +760 -168
  147. package/clinkx-workflows/dist/workflow-tools.js.map +1 -1
  148. package/clinkx-workflows/templates/council/answer-chairman.txt +3 -0
  149. package/clinkx-workflows/templates/council/answer-cross-reviewer.txt +3 -0
  150. package/clinkx-workflows/templates/council/answer-specialist.txt +3 -0
  151. package/clinkx-workflows/templates/council/chairman.txt +3 -0
  152. package/clinkx-workflows/templates/council/code-review-chairman.txt +3 -0
  153. package/clinkx-workflows/templates/council/code-review-cross-reviewer.txt +3 -0
  154. package/clinkx-workflows/templates/council/code-review-reviewer.txt +3 -0
  155. package/clinkx-workflows/templates/council/debug-chairman.txt +3 -0
  156. package/clinkx-workflows/templates/council/debug-cross-reviewer.txt +3 -0
  157. package/clinkx-workflows/templates/council/debug-debugger.txt +3 -0
  158. package/clinkx-workflows/templates/council/discover-analyst.txt +3 -0
  159. package/clinkx-workflows/templates/council/discover-chairman.txt +3 -0
  160. package/clinkx-workflows/templates/council/discover-cross-reviewer.txt +3 -0
  161. package/clinkx-workflows/templates/council/member.txt +3 -0
  162. package/clinkx-workflows/templates/council/plan-review-chairman.txt +3 -0
  163. package/clinkx-workflows/templates/council/plan-review-cross-reviewer.txt +3 -0
  164. package/clinkx-workflows/templates/council/plan-review-reviewer.txt +3 -0
  165. package/clinkx-workflows/templates/council/reviewer.txt +3 -0
  166. package/clinkx-workflows/workflows/council-answer.yaml +3 -0
  167. package/clinkx-workflows/workflows/council-code-review.yaml +3 -0
  168. package/clinkx-workflows/workflows/council-debug.yaml +3 -0
  169. package/clinkx-workflows/workflows/council-default.yaml +7 -0
  170. package/clinkx-workflows/workflows/council-discover.yaml +3 -0
  171. package/clinkx-workflows/workflows/council-plan-review.yaml +3 -0
  172. package/conf/adapters/agy.json +9 -2
  173. package/conf/adapters/claude.json +7 -0
  174. package/conf/adapters/codex.json +7 -0
  175. package/conf/adapters/glm.json +19 -3
  176. package/conf/adapters/grok.json +9 -2
  177. package/conf/adapters/hapi/agy.json +9 -2
  178. package/conf/adapters/hapi/claude.json +7 -0
  179. package/conf/adapters/hapi/codex.json +7 -0
  180. package/conf/adapters/hapi/glm.json +20 -4
  181. package/conf/rosters/default-council.json +21 -0
  182. package/dist/activity-journal.d.ts +17 -3
  183. package/dist/activity-journal.js +404 -79
  184. package/dist/activity-journal.js.map +1 -1
  185. package/dist/codex/agent-tools.js +36 -0
  186. package/dist/codex/agent-tools.js.map +1 -1
  187. package/dist/codex/app-server-client.d.ts +3 -0
  188. package/dist/codex/app-server-client.js +11 -0
  189. package/dist/codex/app-server-client.js.map +1 -1
  190. package/dist/codex/app-server-registry.d.ts +18 -0
  191. package/dist/codex/app-server-registry.js +88 -2
  192. package/dist/codex/app-server-registry.js.map +1 -1
  193. package/dist/codex/client-types.d.ts +7 -1
  194. package/dist/codex/protocol.d.ts +4 -4
  195. package/dist/config.d.ts +220 -23
  196. package/dist/config.js +104 -1
  197. package/dist/config.js.map +1 -1
  198. package/dist/context-builder/bundle.d.ts +13 -0
  199. package/dist/context-builder/bundle.js +46 -0
  200. package/dist/context-builder/bundle.js.map +1 -1
  201. package/dist/context-builder/delegate-finalizer-effective.d.ts +33 -0
  202. package/dist/context-builder/delegate-finalizer-effective.js +82 -0
  203. package/dist/context-builder/delegate-finalizer-effective.js.map +1 -0
  204. package/dist/context-builder/delegate-packet.d.ts +18 -0
  205. package/dist/context-builder/delegate-packet.js +73 -9
  206. package/dist/context-builder/delegate-packet.js.map +1 -1
  207. package/dist/context-builder/delegate-reconstruction.d.ts +29 -0
  208. package/dist/context-builder/delegate-reconstruction.js +110 -41
  209. package/dist/context-builder/delegate-reconstruction.js.map +1 -1
  210. package/dist/context-builder/delegate-validation.d.ts +31 -0
  211. package/dist/context-builder/delegate-validation.js +87 -10
  212. package/dist/context-builder/delegate-validation.js.map +1 -1
  213. package/dist/context-builder/delegate.d.ts +9 -0
  214. package/dist/context-builder/delegate.js +210 -59
  215. package/dist/context-builder/delegate.js.map +1 -1
  216. package/dist/context-builder/diagnostics.d.ts +1 -1
  217. package/dist/context-builder/diagnostics.js.map +1 -1
  218. package/dist/context-builder/discovery-progress.d.ts +25 -0
  219. package/dist/context-builder/discovery-progress.js +64 -0
  220. package/dist/context-builder/discovery-progress.js.map +1 -0
  221. package/dist/context-builder/discovery-watchdog.d.ts +15 -0
  222. package/dist/context-builder/discovery-watchdog.js +39 -0
  223. package/dist/context-builder/discovery-watchdog.js.map +1 -0
  224. package/dist/context-builder/discovery.d.ts +7 -0
  225. package/dist/context-builder/discovery.js +119 -7
  226. package/dist/context-builder/discovery.js.map +1 -1
  227. package/dist/context-builder/external-gaps.d.ts +73 -0
  228. package/dist/context-builder/external-gaps.js +248 -0
  229. package/dist/context-builder/external-gaps.js.map +1 -0
  230. package/dist/context-builder/globs.d.ts +1 -0
  231. package/dist/context-builder/globs.js +15 -0
  232. package/dist/context-builder/globs.js.map +1 -1
  233. package/dist/context-builder/handler.d.ts +42 -1
  234. package/dist/context-builder/handler.js +690 -62
  235. package/dist/context-builder/handler.js.map +1 -1
  236. package/dist/context-builder/invocation-sidecar.d.ts +41 -0
  237. package/dist/context-builder/invocation-sidecar.js +189 -0
  238. package/dist/context-builder/invocation-sidecar.js.map +1 -0
  239. package/dist/context-builder/lifecycle-contract.d.ts +51 -51
  240. package/dist/context-builder/mutation-lock.d.ts +32 -0
  241. package/dist/context-builder/mutation-lock.js +321 -0
  242. package/dist/context-builder/mutation-lock.js.map +1 -0
  243. package/dist/context-builder/review-coverage.d.ts +32 -0
  244. package/dist/context-builder/review-coverage.js +130 -23
  245. package/dist/context-builder/review-coverage.js.map +1 -1
  246. package/dist/context-builder/schema.d.ts +76 -1
  247. package/dist/context-builder/schema.js +21 -1
  248. package/dist/context-builder/schema.js.map +1 -1
  249. package/dist/context-builder/secrets.d.ts +31 -0
  250. package/dist/context-builder/secrets.js +865 -83
  251. package/dist/context-builder/secrets.js.map +1 -1
  252. package/dist/context-builder/status.d.ts +1 -1
  253. package/dist/context-builder/status.js +1 -0
  254. package/dist/context-builder/status.js.map +1 -1
  255. package/dist/context-builder/storage.d.ts +29 -5
  256. package/dist/context-builder/storage.js +311 -236
  257. package/dist/context-builder/storage.js.map +1 -1
  258. package/dist/context-builder/versions.d.ts +19 -4
  259. package/dist/context-builder/versions.js +19 -4
  260. package/dist/context-builder/versions.js.map +1 -1
  261. package/dist/context-builder/workflow-runner.d.ts +6 -0
  262. package/dist/context-builder/workflow-runner.js +363 -173
  263. package/dist/context-builder/workflow-runner.js.map +1 -1
  264. package/dist/dashboard-adapter-catalog.js +7 -0
  265. package/dist/dashboard-adapter-catalog.js.map +1 -1
  266. package/dist/dashboard-cli.d.ts +2 -0
  267. package/dist/dashboard-cli.js +40 -2
  268. package/dist/dashboard-cli.js.map +1 -1
  269. package/dist/dashboard-companion/client.d.ts +1 -0
  270. package/dist/dashboard-companion/client.js +22 -3
  271. package/dist/dashboard-companion/client.js.map +1 -1
  272. package/dist/dashboard-companion/protocol.js +4 -1
  273. package/dist/dashboard-companion/protocol.js.map +1 -1
  274. package/dist/dashboard-gateway/index.d.ts +1 -0
  275. package/dist/dashboard-gateway/index.js +33 -2
  276. package/dist/dashboard-gateway/index.js.map +1 -1
  277. package/dist/handler.js +5 -1
  278. package/dist/handler.js.map +1 -1
  279. package/dist/index.d.ts +18 -1
  280. package/dist/index.js +145 -7
  281. package/dist/index.js.map +1 -1
  282. package/dist/logger.d.ts +1 -1
  283. package/dist/logger.js +18 -3
  284. package/dist/logger.js.map +1 -1
  285. package/dist/parsers/agy-stream-json.d.ts +14 -0
  286. package/dist/parsers/agy-stream-json.js +172 -0
  287. package/dist/parsers/agy-stream-json.js.map +1 -0
  288. package/dist/parsers/extract.js +5 -1
  289. package/dist/parsers/extract.js.map +1 -1
  290. package/dist/parsers/observed-invocation.d.ts +20 -0
  291. package/dist/parsers/observed-invocation.js +42 -0
  292. package/dist/parsers/observed-invocation.js.map +1 -0
  293. package/dist/pipeline.d.ts +13 -0
  294. package/dist/pipeline.js +60 -10
  295. package/dist/pipeline.js.map +1 -1
  296. package/dist/progress-report/lock.d.ts +74 -0
  297. package/dist/progress-report/lock.js +343 -0
  298. package/dist/progress-report/lock.js.map +1 -0
  299. package/dist/progress-report/schema.d.ts +808 -0
  300. package/dist/progress-report/schema.js +119 -0
  301. package/dist/progress-report/schema.js.map +1 -0
  302. package/dist/progress-report/store.d.ts +66 -0
  303. package/dist/progress-report/store.js +766 -0
  304. package/dist/progress-report/store.js.map +1 -0
  305. package/dist/progress-report/tools.d.ts +20 -0
  306. package/dist/progress-report/tools.js +141 -0
  307. package/dist/progress-report/tools.js.map +1 -0
  308. package/dist/prompt-mode.d.ts +1 -1
  309. package/dist/prompt-mode.js +7 -2
  310. package/dist/prompt-mode.js.map +1 -1
  311. package/dist/runner.d.ts +7 -0
  312. package/dist/runner.js +10 -3
  313. package/dist/runner.js.map +1 -1
  314. package/dist/safety.d.ts +50 -0
  315. package/dist/safety.js +182 -1
  316. package/dist/safety.js.map +1 -1
  317. package/dist/schema.d.ts +8 -0
  318. package/dist/schema.js +20 -0
  319. package/dist/schema.js.map +1 -1
  320. package/dist/server.d.ts +6 -1
  321. package/dist/server.js +94 -8
  322. package/dist/server.js.map +1 -1
  323. package/dist/session-log-stream.d.ts +1 -1
  324. package/dist/session-log-stream.js +280 -33
  325. package/dist/session-log-stream.js.map +1 -1
  326. package/dist/stdio-activity.d.ts +28 -0
  327. package/dist/stdio-activity.js +72 -0
  328. package/dist/stdio-activity.js.map +1 -0
  329. package/package.json +2 -1
  330. package/clinkx-workflows/dist/dashboard-ui/assets/index-DOkMXTfB.js +0 -25
  331. package/clinkx-workflows/dist/dashboard-ui/assets/style-ChJsn0KD.css +0 -1
@@ -17,14 +17,16 @@
17
17
  */
18
18
  import { CallToolRequestSchema, ListToolsRequestSchema, ErrorCode, McpError, } from "@modelcontextprotocol/sdk/types.js";
19
19
  import { existsSync } from "node:fs";
20
- import { basename, dirname, join } from "node:path";
20
+ import { basename, dirname, isAbsolute, join, resolve } from "node:path";
21
+ import { computeRunHealth } from "./run-health.js";
21
22
  import { loadWorkflow, listWorkflowCatalog } from "./loader.js";
22
- import { WorkflowEngine } from "./engine.js";
23
+ import { defaultWorkflowAllowedRoots, WorkflowEngine } from "./engine.js";
23
24
  import { readRunStateForStatus, reapRunCancelled, WorkflowStateStore } from "./state.js";
24
- import { parseContextBuilderLifecycleStateV1, } from "./context-builder-lifecycle-contract.js";
25
+ import { isTerminalContextBuilderLifecycleStatus, parseContextBuilderLifecycleStateV1, } from "./context-builder-lifecycle-contract.js";
25
26
  import { validateRunId } from "./run-id.js";
27
+ import { validateDynamicPaths } from "./path-validation.js";
26
28
  import { getMaxPromptBytes, getRunStateDir, getTemplatesPath, getWorkflowsContextDefault, getWorkflowsContextDefaultCurator, getWorkflowsMaxConcurrentAsync, resolveWorkspaceRoot, } from "./config.js";
27
- import { WorkflowCancellationError, } from "./errors.js";
29
+ import { WorkflowCancellationError, WorkflowValidationError, } from "./errors.js";
28
30
  import { shutdownCoordinator } from "./shutdown.js";
29
31
  import { logger } from "./logger.js";
30
32
  import { createValidatorRegistry } from "./validators/index.js";
@@ -32,9 +34,11 @@ import { deriveDecisiveFailure } from "./run-summary-derived.js";
32
34
  import { buildRunSummaryOrUndefined, buildFailureRunSummary } from "./run-summary.js";
33
35
  import { extractCard } from "./run-summary/cards/index.js";
34
36
  import { renderWorkflowReceipt } from "./workflow-receipt.js";
35
- 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";
36
38
  import { resolveWorkflowContextMode, WorkflowContextModeResolverError, } from "./context-mode-resolver.js";
37
- import { WorkflowDelegateValidationError } from "./delegate-validate.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";
38
42
  import { contextSearchTermsValidationError, contextSeedSourceValidationError, resolveSeedSource, } from "./seed-terms.js";
39
43
  import { LEGACY_PLAN_REVIEW_RECEIPTS_WORKFLOW_NAME, PLAN_REVIEW_RECEIPTS_WORKFLOW, admitCouncilPlanReview, assertPlanReviewPromptBudget, isPlanReviewRunState, } from "./plan-review/admission.js";
40
44
  import { PLAN_REVIEW_V1_CLI_NAMES } from "./plan-review/contract.js";
@@ -49,6 +53,7 @@ import { CouncilPlanReviewErrorSchema, createPlanReviewPublicationError, inspect
49
53
  import { validatePlanReviewCallBindings, validatePlanReviewPreparedInputPolicy, } from "./plan-review/runtime-policy.js";
50
54
  import { ContainedFilePolicyError, readContainedRelativeFile, } from "./contained-file.js";
51
55
  import { createDashboardRunReader } from "./dashboard/read-model.js";
56
+ import { ROSTER_NAME_PATTERN, ResolvedRosterSchema, } from "./roster-contract.js";
52
57
  async function inspectLegacyWorkflowSessionLogGeneration(runDir, runId) {
53
58
  try {
54
59
  const result = await createDashboardRunReader().inspectGeneration?.(dirname(runDir), runId);
@@ -91,6 +96,38 @@ function createPreflightOnlyBackend(advertisedCliNames) {
91
96
  // Admission control
92
97
  // ---------------------------------------------------------------------------
93
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
+ }
94
131
  /**
95
132
  * In-process registry of AbortControllers for live async runs, keyed by run_id.
96
133
  * cancel_workflow aborts the controller to drive the run's own cancellation
@@ -109,6 +146,9 @@ export function resolveWorkflowAsyncAdmissionCap(options) {
109
146
  return getWorkflowsMaxConcurrentAsync();
110
147
  }
111
148
  function acquireMutex(runId, options) {
149
+ if (shutdownCoordinator.isShuttingDown()) {
150
+ throw new McpError(ErrorCode.InvalidRequest, "workflow server is shutting down");
151
+ }
112
152
  if (activeRuns.has(runId)) {
113
153
  throw new McpError(ErrorCode.InvalidParams, `run_id ${runId} already running`);
114
154
  }
@@ -120,6 +160,97 @@ function acquireMutex(runId, options) {
120
160
  }
121
161
  function releaseMutex(runId) {
122
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");
233
+ }
234
+ }
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;
123
254
  }
124
255
  export function parseMembersOverride(raw, knownCliNames) {
125
256
  if (raw == null)
@@ -140,7 +271,7 @@ export function parseMembersOverride(raw, knownCliNames) {
140
271
  throw new McpError(ErrorCode.InvalidParams, `members[${i}] must be a non-null object`);
141
272
  }
142
273
  const keys = Object.keys(item);
143
- const allowedKeys = new Set(["cli_name", "stance"]);
274
+ const allowedKeys = new Set(["cli_name", "stance", "model", "effort"]);
144
275
  for (const key of keys) {
145
276
  if (!allowedKeys.has(key)) {
146
277
  throw new McpError(ErrorCode.InvalidParams, `members[${i}] has unexpected key "${key}"`);
@@ -179,6 +310,12 @@ export function parseMembersOverride(raw, knownCliNames) {
179
310
  else if (i >= 3) {
180
311
  throw new McpError(ErrorCode.InvalidParams, "stance is required for members at index 3 and above");
181
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
+ }
182
319
  result.push(entry);
183
320
  }
184
321
  return result;
@@ -202,6 +339,93 @@ export function parseChairmanOverride(raw, knownCliNames) {
202
339
  }
203
340
  return trimmed;
204
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
+ }
205
429
  /**
206
430
  * Point a generic-council caller at the input shape the workflow actually takes.
207
431
  *
@@ -290,6 +514,94 @@ export function validateVariables(supplied, schema) {
290
514
  }
291
515
  return resolved;
292
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
+ }
293
605
  function validateContextBuilderAnchorSufficiency(variables, runner, contextDefault, defaultCurator) {
294
606
  if (runner == null) {
295
607
  return undefined;
@@ -803,6 +1115,21 @@ function appendDuplicateReplayBlock(content, store) {
803
1115
  text: JSON.stringify({ duplicate_of_failed_run: duplicateOfFailedRun }),
804
1116
  });
805
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
+ }
806
1133
  // ---------------------------------------------------------------------------
807
1134
  // Actionable error formatting (5-6)
808
1135
  // ---------------------------------------------------------------------------
@@ -892,14 +1219,45 @@ function toCouncilPlanReviewError(runId, error) {
892
1219
  },
893
1220
  });
894
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
+ }
895
1236
  async function markWorkflowFailed(store, runId, error) {
896
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.
897
1245
  const planReviewError = toCouncilPlanReviewError(runId, error);
898
- if (planReviewError === undefined) {
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.
899
1257
  await store.markFailed(message);
900
1258
  return;
901
1259
  }
902
- await store.markFailed(message, undefined, planReviewError);
1260
+ await store.markFailed(message, failureClass);
903
1261
  }
904
1262
  function getCallEntryByLocation(stages, location) {
905
1263
  const dotIndex = location.indexOf(".");
@@ -912,6 +1270,54 @@ function getCallEntryByLocation(stages, location) {
912
1270
  return stage?.calls.find((call) => call.callId === callId);
913
1271
  }
914
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
+ // ---------------------------------------------------------------------------
915
1321
  // Tool execution helpers
916
1322
  // ---------------------------------------------------------------------------
917
1323
  async function executeRunWorkflow(server, options, args, signal, progressToken) {
@@ -947,25 +1353,53 @@ async function executeRunWorkflow(server, options, args, signal, progressToken)
947
1353
  const genericShaped = workflow === PLAN_REVIEW_RECEIPTS_WORKFLOW &&
948
1354
  (args["plan_review"] == null ||
949
1355
  args["members"] != null ||
950
- args["chairman"] != null);
1356
+ args["chairman"] != null ||
1357
+ args["chairman_model"] != null ||
1358
+ args["chairman_effort"] != null ||
1359
+ args["roster"] != null);
951
1360
  // Declarations hoisted out of the decorator's try so the bindings stay in
952
1361
  // scope for the post-admission code below; the assignments and their order
953
1362
  // are unchanged.
954
1363
  let parsedMembers;
955
1364
  let parsedChairman;
1365
+ let parsedChairmanModel;
1366
+ let parsedChairmanEffort;
956
1367
  let resolvedVariables;
957
1368
  let initialContextPolicy;
958
1369
  let planReviewAdmission;
1370
+ let admittedRosterName;
959
1371
  try {
960
- parsedMembers = parseMembersOverride(args["members"], knownCliNames);
961
- parsedChairman = parseChairmanOverride(args["chairman"], knownCliNames);
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);
962
1390
  if (parsedMembers != null && !COUNCIL_RUNTIME_MEMBER_WORKFLOWS.has(workflow)) {
963
1391
  throw new McpError(ErrorCode.InvalidParams, `members parameter is not supported for workflow "${workflow}"`);
964
1392
  }
965
1393
  if (parsedChairman != null && !COUNCIL_RUNTIME_MEMBER_WORKFLOWS.has(workflow)) {
966
1394
  throw new McpError(ErrorCode.InvalidParams, `chairman parameter is not supported for workflow "${workflow}"`);
967
1395
  }
968
- resolvedVariables = validateVariables(variables, loaded.definition.variables);
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}"`);
1398
+ }
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);
969
1403
  // When the runner is absent (preflight disabled / hard kill), the boundary
970
1404
  // validator is inert. Skip it WITHOUT evaluating the context-default env
971
1405
  // parsers — getWorkflowsContextDefaultCurator() throws on empty env, and a
@@ -975,6 +1409,9 @@ async function executeRunWorkflow(server, options, args, signal, progressToken)
975
1409
  contextPolicy = validateContextBuilderAnchorSufficiency(resolvedVariables, options.contextBuilderRunner, options.contextDefault ?? getWorkflowsContextDefault(), options.defaultCurator ?? getWorkflowsContextDefaultCurator());
976
1410
  }
977
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);
978
1415
  initialContextPolicy = contextPolicy ?? resolveWorkflowContextMode(undefined, options.contextDefault ?? "off", false, undefined, options.defaultCurator);
979
1416
  if (options.contextBuilderRunner != null) {
980
1417
  try {
@@ -1009,6 +1446,11 @@ async function executeRunWorkflow(server, options, args, signal, progressToken)
1009
1446
  }
1010
1447
  throw err;
1011
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);
1012
1454
  // A-7: reject caller context anchors that resolve to the bound plan or any
1013
1455
  // plan-workspace PLAN.md. Pre-mutex, so a rejected request creates no state:
1014
1456
  // the caller must not be able to hand actors a MUTABLE plan alongside the
@@ -1031,8 +1473,8 @@ async function executeRunWorkflow(server, options, args, signal, progressToken)
1031
1473
  snapshotAbsolutePath: join(getRunStateDir(runId), PLAN_SNAPSHOT_RELPATH),
1032
1474
  };
1033
1475
  }
1034
- acquireMutex(runId, options);
1035
1476
  const isAsync = args["async"] === true;
1477
+ const ownership = acquireWorkflowRun(runId, options, isAsync ? undefined : signal);
1036
1478
  if (isAsync) {
1037
1479
  // Everything between acquireMutex and the detached spawn must release the
1038
1480
  // mutex on ANY throw, or the run slot leaks permanently (bricks the server
@@ -1042,6 +1484,7 @@ async function executeRunWorkflow(server, options, args, signal, progressToken)
1042
1484
  let initializedPlanReview;
1043
1485
  let preparedSnapshot;
1044
1486
  try {
1487
+ ownership.throwIfCancelled();
1045
1488
  const runDir = getRunStateDir(runId);
1046
1489
  if (existsSync(runDir)) {
1047
1490
  throw new McpError(ErrorCode.InvalidParams, `run_id ${runId} already has persisted state at ${runDir}`);
@@ -1051,8 +1494,10 @@ async function executeRunWorkflow(server, options, args, signal, progressToken)
1051
1494
  preparedSnapshot = planReviewPlan === undefined
1052
1495
  ? undefined
1053
1496
  : await preparePlanReviewSnapshot(planReviewPlan);
1497
+ ownership.throwIfCancelled();
1054
1498
  // Expand composition ONCE in the foreground; the detached run reuses it.
1055
- effectiveDefinition = expandRunWorkflowDefinition(loaded.normalized, workflow, parsedMembers, parsedChairman, planReviewPlan, preparedSnapshot);
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);
1056
1501
  // Create the durable, pollable state record in the FOREGROUND so a failure
1057
1502
  // here surfaces synchronously (a real error, never a false "running").
1058
1503
  try {
@@ -1063,21 +1508,29 @@ async function executeRunWorkflow(server, options, args, signal, progressToken)
1063
1508
  debugKeepArtifacts: debug,
1064
1509
  async: true,
1065
1510
  initialContextPolicy,
1511
+ ...(resolvedRoster !== undefined ? { resolvedRoster } : {}),
1512
+ ...(admittedRosterName !== undefined ? { rosterName: admittedRosterName } : {}),
1066
1513
  });
1067
1514
  }
1068
1515
  catch (err) {
1069
1516
  throw new McpError(ErrorCode.InternalError, `Failed to initialize async run ${runId}: ${err instanceof Error ? err.message : String(err)}`);
1070
1517
  }
1518
+ ownership.throwIfCancelled();
1071
1519
  // A-10: the async acknowledgement is a durability boundary. A caller
1072
1520
  // cannot observe `running` until every plan-review snapshot and the
1073
1521
  // strict typed state subtree have committed successfully.
1074
1522
  if (planReviewPlan !== undefined) {
1075
1523
  initializedPlanReview = await initializePlanReviewRunState(store, planReviewPlan, effectiveDefinition, preparedSnapshot);
1076
1524
  }
1525
+ ownership.throwIfCancelled();
1077
1526
  }
1078
1527
  catch (err) {
1079
- if (store !== undefined && planReviewPlan !== undefined) {
1080
- try {
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) {
1081
1534
  try {
1082
1535
  await markWorkflowFailed(store, runId, err);
1083
1536
  }
@@ -1106,26 +1559,17 @@ async function executeRunWorkflow(server, options, args, signal, progressToken)
1106
1559
  });
1107
1560
  return { content, isError: true };
1108
1561
  }
1109
- finally {
1110
- try {
1111
- await store.close();
1112
- }
1113
- catch (closeError) {
1114
- logger.warn({ err: closeError }, "store.close() failed during async initialization cleanup");
1115
- }
1116
- releaseMutex(runId);
1117
- }
1562
+ throw err;
1563
+ }
1564
+ finally {
1565
+ await ownership.finish(store);
1118
1566
  }
1119
- releaseMutex(runId);
1120
- throw err;
1121
1567
  }
1122
1568
  if (store === undefined || effectiveDefinition === undefined) {
1123
- releaseMutex(runId);
1569
+ await ownership.finish(store);
1124
1570
  throw new McpError(ErrorCode.InternalError, `Failed to initialize async run ${runId}`);
1125
1571
  }
1126
- const detached = new AbortController();
1127
- cancelRegistry.set(runId, detached);
1128
- void runWorkflowDetached(server, options, workflow, runId, resolvedVariables, debug, receipt, validatorRegistry, loaded, parsedMembers, parsedChairman, true, detached.signal, undefined, store, effectiveDefinition, undefined, initializedPlanReview, initialContextPolicy).catch((err) => {
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) => {
1129
1573
  if (err instanceof WorkflowCancellationError) {
1130
1574
  logger.info({ runId }, "workflow run cancelled");
1131
1575
  return;
@@ -1134,7 +1578,9 @@ async function executeRunWorkflow(server, options, args, signal, progressToken)
1134
1578
  });
1135
1579
  // Additive-only ack shape: base {run_id, status:"running"} is unchanged;
1136
1580
  // the duplicate-replay field appears only when detected (warn-only, F6).
1581
+ // resolved_roster is the admission-time requested-value projection (E2).
1137
1582
  const duplicateOfFailedRun = buildDuplicateOfFailedRunField(store);
1583
+ const resolvedRoster = store.runState.resolvedRoster;
1138
1584
  return {
1139
1585
  content: [{
1140
1586
  type: "text",
@@ -1144,11 +1590,14 @@ async function executeRunWorkflow(server, options, args, signal, progressToken)
1144
1590
  ...(duplicateOfFailedRun !== undefined
1145
1591
  ? { duplicate_of_failed_run: duplicateOfFailedRun }
1146
1592
  : {}),
1593
+ ...(resolvedRoster !== undefined
1594
+ ? { resolved_roster: resolvedRoster }
1595
+ : {}),
1147
1596
  }),
1148
1597
  }],
1149
1598
  };
1150
1599
  }
1151
- return await runWorkflowDetached(server, options, workflow, runId, resolvedVariables, debug, receipt, validatorRegistry, loaded, parsedMembers, parsedChairman, false, signal, progressToken, undefined, undefined, planReviewPlan, undefined, initialContextPolicy);
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);
1152
1601
  }
1153
1602
  async function preparePlanReviewSnapshot(plan) {
1154
1603
  try {
@@ -1224,7 +1673,100 @@ function resolvePlanReviewChairmanCallId(definition) {
1224
1673
  }
1225
1674
  return callId;
1226
1675
  }
1227
- function expandRunWorkflowDefinition(definition, workflow, parsedMembers, parsedChairman, planReviewPlan,
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,
1228
1770
  /**
1229
1771
  * The already-verified snapshot. This function must NEVER read `plan_file`:
1230
1772
  * the text it inlines is the digest-verified canonical copy or nothing.
@@ -1240,18 +1782,22 @@ preparedSnapshot) {
1240
1782
  // string cannot carry. This is the single read — never re-read `plan_file`.
1241
1783
  preparedSnapshot?.ok === true ? preparedSnapshot.canonical : undefined);
1242
1784
  }
1243
- return parsedMembers != null || parsedChairman != null
1244
- ? expandCouncilComposition(definition, parsedMembers, workflow, parsedChairman)
1785
+ return parsedMembers != null ||
1786
+ parsedChairman != null ||
1787
+ parsedChairmanModel !== undefined ||
1788
+ parsedChairmanEffort !== undefined
1789
+ ? expandCouncilComposition(definition, parsedMembers, workflow, parsedChairman, parsedChairmanModel, parsedChairmanEffort)
1245
1790
  : definition;
1246
1791
  }
1247
- async function runWorkflowDetached(server, options, workflow, runId, resolvedVariables, debug, receipt, validatorRegistry, loaded, parsedMembers, parsedChairman, isAsync, signal, progressToken, preCreatedStore, preExpandedDefinition, planReviewPlan, preInitializedPlanReview, initialContextPolicy) {
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) {
1248
1793
  let store = preCreatedStore ?? null;
1249
1794
  let backend = null;
1250
1795
  let reporter;
1251
- let shutdownHandle;
1252
1796
  let releaseLifecycleBinding;
1253
1797
  let initializedPlanReview = preInitializedPlanReview;
1798
+ const signal = ownership.signal;
1254
1799
  try {
1800
+ ownership.throwIfCancelled();
1255
1801
  // The async admission path already expanded the composition (and read the
1256
1802
  // snapshot) in the foreground; reuse both to avoid redundant work and a
1257
1803
  // second `plan_file` read.
@@ -1259,11 +1805,13 @@ async function runWorkflowDetached(server, options, workflow, runId, resolvedVar
1259
1805
  planReviewPlan === undefined
1260
1806
  ? undefined
1261
1807
  : await preparePlanReviewSnapshot(planReviewPlan);
1808
+ ownership.throwIfCancelled();
1262
1809
  const effectiveDefinition = preExpandedDefinition ??
1263
- expandRunWorkflowDefinition(loaded.normalized, workflow, parsedMembers, parsedChairman, planReviewPlan, preparedSnapshot);
1810
+ expandRunWorkflowDefinition(loaded.normalized, workflow, parsedMembers, parsedChairman, parsedChairmanModel, parsedChairmanEffort, planReviewPlan, preparedSnapshot);
1264
1811
  // Create state store (resumable if child config path is set) unless the
1265
1812
  // async admission path already created it in the foreground.
1266
1813
  if (store == null) {
1814
+ const resolvedRoster = resolvedRosterForAdmission(workflow, effectiveDefinition, parsedMembers, parsedChairman, parsedChairmanModel, parsedChairmanEffort, initialContextPolicy, resolvedVariables, rosterName);
1267
1815
  store = await WorkflowStateStore.create({
1268
1816
  runId,
1269
1817
  definition: effectiveDefinition,
@@ -1271,8 +1819,11 @@ async function runWorkflowDetached(server, options, workflow, runId, resolvedVar
1271
1819
  debugKeepArtifacts: debug,
1272
1820
  async: isAsync,
1273
1821
  initialContextPolicy,
1822
+ ...(resolvedRoster !== undefined ? { resolvedRoster } : {}),
1823
+ ...(rosterName !== undefined ? { rosterName } : {}),
1274
1824
  });
1275
1825
  }
1826
+ ownership.throwIfCancelled();
1276
1827
  releaseLifecycleBinding = options.contextBuilderLifecycleRouter?.bind(runId, store);
1277
1828
  // A-5/A-6: snapshot + contract + typed subtree land here — after state
1278
1829
  // exists, before the backend is even created, so a snapshot/digest failure
@@ -1281,28 +1832,15 @@ async function runWorkflowDetached(server, options, workflow, runId, resolvedVar
1281
1832
  if (planReviewPlan !== undefined && preparedSnapshot !== undefined) {
1282
1833
  initializedPlanReview = await initializePlanReviewRunState(store, planReviewPlan, effectiveDefinition, preparedSnapshot);
1283
1834
  }
1835
+ ownership.throwIfCancelled();
1284
1836
  const sessionLogGeneration = store.sessionLogGeneration ??
1285
1837
  await inspectLegacyWorkflowSessionLogGeneration(store.runDir, runId);
1286
- if (signal.aborted)
1287
- throw new WorkflowCancellationError();
1288
- // Create backend via factory
1838
+ ownership.throwIfCancelled();
1289
1839
  backend = options.createBackend(runId);
1840
+ ownership.setBackend(backend);
1841
+ ownership.throwIfCancelled();
1290
1842
  await backend.connect();
1291
- // Wire up shutdown coordinator
1292
- const controller = new AbortController();
1293
- if (signal.aborted) {
1294
- controller.abort(new WorkflowCancellationError());
1295
- }
1296
- else {
1297
- signal.addEventListener("abort", () => {
1298
- controller.abort(new WorkflowCancellationError());
1299
- }, { once: true });
1300
- }
1301
- shutdownHandle = shutdownCoordinator.register(backend, controller, async () => {
1302
- if (store != null) {
1303
- await store.markCancelled();
1304
- }
1305
- });
1843
+ ownership.throwIfCancelled();
1306
1844
  // Build engine hooks — capture store in a const for closure narrowing
1307
1845
  const storeRef = store;
1308
1846
  reporter = progressToken != null
@@ -1332,13 +1870,16 @@ async function runWorkflowDetached(server, options, workflow, runId, resolvedVar
1332
1870
  runId,
1333
1871
  sessionLogGeneration,
1334
1872
  runDir: storeRef.runDir,
1335
- signal: controller.signal,
1873
+ signal,
1336
1874
  hooks,
1337
1875
  contextBuilderRunner: options.contextBuilderRunner,
1338
1876
  contextDefault: options.contextDefault,
1339
1877
  defaultCurator: options.defaultCurator,
1340
1878
  commitPreflightContext: storeRef.commitPreflightContext?.bind(storeRef),
1341
1879
  initialContextPolicy,
1880
+ ...(options.deriveInvocationAudit === undefined
1881
+ ? {}
1882
+ : { deriveInvocationAudit: options.deriveInvocationAudit }),
1342
1883
  ...(initializedPlanReview !== undefined
1343
1884
  ? {
1344
1885
  planReviewContextPolicy: initializedPlanReview.context_policy,
@@ -1352,8 +1893,6 @@ async function runWorkflowDetached(server, options, workflow, runId, resolvedVar
1352
1893
  : {}),
1353
1894
  });
1354
1895
  const result = await engine.execute();
1355
- if (shutdownHandle !== undefined)
1356
- shutdownCoordinator.unregister(shutdownHandle);
1357
1896
  // Build summary (fail-open)
1358
1897
  let summary;
1359
1898
  if (receipt) {
@@ -1373,16 +1912,13 @@ async function runWorkflowDetached(server, options, workflow, runId, resolvedVar
1373
1912
  debugBlock: debug ? buildDebugBlock(runId, result, store) : undefined,
1374
1913
  });
1375
1914
  appendDuplicateReplayBlock(content, store);
1915
+ appendResolvedRosterBlock(content, store);
1376
1916
  return { content };
1377
1917
  }
1378
1918
  catch (error) {
1379
- if (shutdownHandle !== undefined)
1380
- shutdownCoordinator.unregister(shutdownHandle);
1381
- if (error instanceof WorkflowCancellationError) {
1382
- if (store != null) {
1383
- await store.markCancelled();
1384
- }
1385
- throw error;
1919
+ if (signal.aborted || error instanceof WorkflowCancellationError) {
1920
+ await markWorkflowCancelled(store);
1921
+ throw new WorkflowCancellationError();
1386
1922
  }
1387
1923
  // Persist a terminal "failed" record for post-admission setup failures
1388
1924
  // (e.g. backend.connect() / engine construction). Idempotent: the engine
@@ -1417,29 +1953,17 @@ async function runWorkflowDetached(server, options, workflow, runId, resolvedVar
1417
1953
  debugBlock: debug && store != null ? buildErrorDebugBlock(runId, store) : undefined,
1418
1954
  });
1419
1955
  appendDuplicateReplayBlock(content, store);
1956
+ appendResolvedRosterBlock(content, store);
1420
1957
  return { content, isError: true };
1421
1958
  }
1422
1959
  finally {
1423
- releaseLifecycleBinding?.();
1424
- reporter?.stop();
1425
- try {
1426
- if (backend != null && !backend.isTerminated) {
1427
- await backend.terminate();
1428
- }
1429
- }
1430
- catch (cleanupErr) {
1431
- logger.warn({ err: cleanupErr }, "backend.terminate() failed during cleanup");
1432
- }
1433
1960
  try {
1434
- if (store != null) {
1435
- await store.close();
1436
- }
1961
+ releaseLifecycleBinding?.();
1962
+ reporter?.stop();
1437
1963
  }
1438
- catch (cleanupErr) {
1439
- logger.warn({ err: cleanupErr }, "store.close() failed during cleanup");
1964
+ finally {
1965
+ await ownership.finish(store);
1440
1966
  }
1441
- cancelRegistry.delete(runId);
1442
- releaseMutex(runId);
1443
1967
  }
1444
1968
  }
1445
1969
  async function executeResumeWorkflow(server, options, args, signal, progressToken) {
@@ -1485,18 +2009,23 @@ async function executeResumeWorkflow(server, options, args, signal, progressToke
1485
2009
  isError: true,
1486
2010
  };
1487
2011
  }
1488
- acquireMutex(runId, options);
2012
+ const ownership = acquireWorkflowRun(runId, options, signal);
2013
+ const ownedSignal = ownership.signal;
1489
2014
  let store = null;
1490
2015
  let backend = null;
1491
2016
  let reporter;
1492
- let shutdownHandle;
1493
2017
  let releaseLifecycleBinding;
1494
2018
  try {
2019
+ ownership.throwIfCancelled();
1495
2020
  const resumeOptions = {
1496
2021
  runId,
1497
2022
  debugKeepArtifacts: debug,
1498
2023
  };
1499
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();
1500
2029
  releaseLifecycleBinding = options.contextBuilderLifecycleRouter?.bind(runId, store);
1501
2030
  const validatorRegistry = createValidatorRegistry();
1502
2031
  const initialContextPolicy = store.runState.initialContextPolicy ??
@@ -1546,14 +2075,10 @@ async function executeResumeWorkflow(server, options, args, signal, progressToke
1546
2075
  contextPolicy: restoredPlanReview.state.context_policy,
1547
2076
  });
1548
2077
  }
1549
- // Make this resume attempt durable before its preflight gate. Generic runs
1550
- // otherwise remain in the prior terminal state, causing markFailed /
1551
- // markCancelled to suppress a new pre-dispatch outcome as "idempotent".
1552
- await store.beginResumeAttempt();
2078
+ ownership.throwIfCancelled();
1553
2079
  const sessionLogGeneration = store.sessionLogGeneration ??
1554
2080
  await inspectLegacyWorkflowSessionLogGeneration(store.runDir, runId);
1555
- if (signal.aborted)
1556
- throw new WorkflowCancellationError();
2081
+ ownership.throwIfCancelled();
1557
2082
  if (store.runState.workflow_context === undefined) {
1558
2083
  const preflightEngine = new WorkflowEngine({
1559
2084
  definition: store.definition,
@@ -1562,7 +2087,7 @@ async function executeResumeWorkflow(server, options, args, signal, progressToke
1562
2087
  validatorRegistry,
1563
2088
  runId,
1564
2089
  runDir: store.runDir,
1565
- signal,
2090
+ signal: ownedSignal,
1566
2091
  resume: store.resume,
1567
2092
  contextBuilderRunner: options.contextBuilderRunner,
1568
2093
  contextDefault: options.contextDefault,
@@ -1570,6 +2095,9 @@ async function executeResumeWorkflow(server, options, args, signal, progressToke
1570
2095
  commitPreflightContext: store.commitPreflightContext.bind(store),
1571
2096
  requireContextPreflight: true,
1572
2097
  initialContextPolicy,
2098
+ ...(options.deriveInvocationAudit === undefined
2099
+ ? {}
2100
+ : { deriveInvocationAudit: options.deriveInvocationAudit }),
1573
2101
  ...(restoredPlanReview === undefined
1574
2102
  ? {}
1575
2103
  : {
@@ -1579,24 +2107,20 @@ async function executeResumeWorkflow(server, options, args, signal, progressToke
1579
2107
  commitPlanReviewContextProvenance: store.commitPlanReviewContextProvenance.bind(store),
1580
2108
  }),
1581
2109
  });
1582
- await preflightEngine.prepareContextPreflight(signal);
1583
- }
1584
- // Create backend only after the resume context gate has committed or bypassed.
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();
1585
2119
  backend = options.createBackend(runId);
2120
+ ownership.setBackend(backend);
2121
+ ownership.throwIfCancelled();
1586
2122
  await backend.connect();
1587
- // Wire up shutdown coordinator
1588
- const controller = new AbortController();
1589
- if (signal.aborted) {
1590
- controller.abort(new WorkflowCancellationError());
1591
- }
1592
- else {
1593
- signal.addEventListener("abort", () => {
1594
- controller.abort(new WorkflowCancellationError());
1595
- }, { once: true });
1596
- }
1597
- shutdownHandle = shutdownCoordinator.register(backend, controller, async () => {
1598
- await store.markCancelled();
1599
- });
2123
+ ownership.throwIfCancelled();
1600
2124
  // Build engine hooks with progress reporter
1601
2125
  reporter = progressToken != null
1602
2126
  ? new WorkflowProgressReporter(server, progressToken, runId, store.definition)
@@ -1630,7 +2154,7 @@ async function executeResumeWorkflow(server, options, args, signal, progressToke
1630
2154
  runId,
1631
2155
  sessionLogGeneration,
1632
2156
  runDir: store.runDir,
1633
- signal: controller.signal,
2157
+ signal: ownedSignal,
1634
2158
  hooks: rpt != null ? hooks : store.hooks,
1635
2159
  resume: store.resume,
1636
2160
  contextBuilderRunner: options.contextBuilderRunner,
@@ -1640,6 +2164,9 @@ async function executeResumeWorkflow(server, options, args, signal, progressToke
1640
2164
  restoredWorkflowContext: store.runState.workflow_context,
1641
2165
  requireContextPreflight: false,
1642
2166
  initialContextPolicy,
2167
+ ...(options.deriveInvocationAudit === undefined
2168
+ ? {}
2169
+ : { deriveInvocationAudit: options.deriveInvocationAudit }),
1643
2170
  // A-7: replay the SAME enforced exclusion on resume, reconstructed from
1644
2171
  // persisted state rather than re-derived from caller variables.
1645
2172
  ...(restoredPlanReview !== undefined
@@ -1656,8 +2183,6 @@ async function executeResumeWorkflow(server, options, args, signal, progressToke
1656
2183
  : {}),
1657
2184
  });
1658
2185
  const result = await engine.execute();
1659
- if (shutdownHandle !== undefined)
1660
- shutdownCoordinator.unregister(shutdownHandle);
1661
2186
  // Build summary (fail-open)
1662
2187
  let summary;
1663
2188
  if (receipt) {
@@ -1679,13 +2204,9 @@ async function executeResumeWorkflow(server, options, args, signal, progressToke
1679
2204
  return { content };
1680
2205
  }
1681
2206
  catch (error) {
1682
- if (shutdownHandle !== undefined)
1683
- shutdownCoordinator.unregister(shutdownHandle);
1684
- if (error instanceof WorkflowCancellationError) {
1685
- if (store != null) {
1686
- await store.markCancelled();
1687
- }
1688
- throw error;
2207
+ if (ownedSignal.aborted || error instanceof WorkflowCancellationError) {
2208
+ await markWorkflowCancelled(store);
2209
+ throw new WorkflowCancellationError();
1689
2210
  }
1690
2211
  // Persist a terminal "failed" record for post-admission setup failures.
1691
2212
  // Idempotent: won't clobber an engine-emitted terminal status.
@@ -1718,25 +2239,13 @@ async function executeResumeWorkflow(server, options, args, signal, progressToke
1718
2239
  return { content, isError: true };
1719
2240
  }
1720
2241
  finally {
1721
- releaseLifecycleBinding?.();
1722
- reporter?.stop();
1723
- try {
1724
- if (backend != null && !backend.isTerminated) {
1725
- await backend.terminate();
1726
- }
1727
- }
1728
- catch (cleanupErr) {
1729
- logger.warn({ err: cleanupErr }, "backend.terminate() failed during cleanup");
1730
- }
1731
2242
  try {
1732
- if (store != null) {
1733
- await store.close();
1734
- }
2243
+ releaseLifecycleBinding?.();
2244
+ reporter?.stop();
1735
2245
  }
1736
- catch (cleanupErr) {
1737
- logger.warn({ err: cleanupErr }, "store.close() failed during cleanup");
2246
+ finally {
2247
+ await ownership.finish(store);
1738
2248
  }
1739
- releaseMutex(runId);
1740
2249
  }
1741
2250
  }
1742
2251
  // ---------------------------------------------------------------------------
@@ -1916,33 +2425,60 @@ async function executeWorkflowRunStatus(_options, args) {
1916
2425
  selected: lifecycle.coverage.selected,
1917
2426
  missing: lifecycle.coverage.missing,
1918
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
+ };
2451
+ }
1919
2452
  }
1920
2453
  projection["context"] = context;
1921
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
+ }
1922
2474
  // F7 read-time health projection: top-level run status alone overstates
1923
2475
  // health when on_failure:continue swallowed seat failures or context
1924
2476
  // preflight completed partial/degraded. Computed only from already-persisted
1925
2477
  // data; additive and advisory — never mutates status semantics.
1926
- const healthReasons = [];
1927
- if (status === "succeeded" && stages.some((stage) => stage.failed_calls > 0)) {
1928
- healthReasons.push("failed_calls_in_succeeded_run");
1929
- }
1930
- if (lifecycle?.status === "partial") {
1931
- healthReasons.push("context_partial");
2478
+ projection["health"] = computeRunHealth(state, lifecycle);
2479
+ if (state.resolvedRoster !== undefined) {
2480
+ projection["resolved_roster"] = state.resolvedRoster;
1932
2481
  }
1933
- if (lifecycle?.status === "failed") {
1934
- healthReasons.push("context_lifecycle_failed");
1935
- }
1936
- if (lifecycle?.coverage.status === "degraded") {
1937
- healthReasons.push("coverage_degraded");
1938
- }
1939
- if (cb?.delegate?.fallback_used === true) {
1940
- healthReasons.push("delegate_fallback_used");
1941
- }
1942
- projection["health"] = {
1943
- classification: healthReasons.length > 0 ? "degraded" : "ok",
1944
- reasons: healthReasons,
1945
- };
1946
2482
  if (isPlanReviewRunState(state)) {
1947
2483
  const terminalExecutionStatus = state.status === "succeeded" ||
1948
2484
  state.status === "failed" ||
@@ -2180,6 +2716,14 @@ function executeListWorkflows(options) {
2180
2716
  if (options.availableCliNames != null && options.availableCliNames.length > 0) {
2181
2717
  response["available_cli_names"] = [...options.availableCliNames].sort();
2182
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();
2726
+ }
2183
2727
  return {
2184
2728
  content: [
2185
2729
  {
@@ -2352,9 +2896,15 @@ const RUN_WORKFLOW_SCHEMA = {
2352
2896
  default: false,
2353
2897
  description: "When true, start the run detached and return {run_id, status:'running'} immediately; poll workflow_run_status for progress and result.",
2354
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
+ },
2355
2905
  members: {
2356
2906
  type: "array",
2357
- description: "Optional runtime council member composition override. Accepted only for council workflows (council-default, council-answer, council-code-review, council-debug, council-discover, council-plan-review). Each entry specifies a CLI adapter and an optional analytical stance. Seats at indices 0-2 may omit stance (inherits default); seats at indices 3-9 MUST provide stance. When provided, both the member stage (stage 0) and reviewer stage (stage 1) are expanded to match the specified count and CLI mapping. Reviewer stances are NOT inherited — reviewers keep their default review lens.",
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.",
2358
2908
  minItems: 1,
2359
2909
  maxItems: 10,
2360
2910
  items: {
@@ -2368,6 +2918,20 @@ const RUN_WORKFLOW_SCHEMA = {
2368
2918
  type: "string",
2369
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 }}.",
2370
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
+ },
2371
2935
  },
2372
2936
  required: ["cli_name"],
2373
2937
  additionalProperties: false,
@@ -2377,6 +2941,20 @@ const RUN_WORKFLOW_SCHEMA = {
2377
2941
  type: "string",
2378
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.",
2379
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
+ },
2380
2958
  plan_review: PLAN_REVIEW_INPUT_SCHEMA,
2381
2959
  },
2382
2960
  required: ["workflow", "run_id"],
@@ -2536,7 +3114,7 @@ export function getWorkflowToolDefinitions(workflowsPath, availableCliNames) {
2536
3114
  return [
2537
3115
  {
2538
3116
  name: "run_workflow",
2539
- description: `Start a new multi-stage workflow execution.${workflowSuffix}\nAccepts optional \`members\` and \`chairman\` parameters for council workflows (council-default, council-answer, council-code-review, council-debug, council-discover, council-plan-review) to override member/reviewer composition and chairman CLI adapter at runtime.`,
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.`,
2540
3118
  inputSchema: runSchema,
2541
3119
  },
2542
3120
  {
@@ -2553,7 +3131,7 @@ export function getWorkflowToolDefinitions(workflowsPath, availableCliNames) {
2553
3131
  },
2554
3132
  {
2555
3133
  name: "workflow_run_status",
2556
- description: "Read-only status/progress of a workflow run. The full result is included only with include_result:true. Generic workflows preserve succeeded-final-call behavior; plan-review-receipts reports marker-driven availability and bytes independently of terminal status, with body-blind default polling. Every found run also carries a top-level `health` block {classification: \"ok\"|\"degraded\", reasons: string[]} computed read-time from persisted data — a run can be status:\"succeeded\" yet degraded when on_failure:continue swallowed failed calls or context preflight was partial/degraded (reasons: failed_calls_in_succeeded_run, context_partial, context_lifecycle_failed, coverage_degraded, delegate_fallback_used). When a context-builder lifecycle snapshot is persisted, the `context` block additionally includes `lifecycle_status` and `coverage` {policy, status, required, selected, missing}.",
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.",
2557
3135
  inputSchema: WORKFLOW_RUN_STATUS_SCHEMA,
2558
3136
  annotations: { readOnlyHint: true },
2559
3137
  },
@@ -2615,21 +3193,35 @@ export async function handleWorkflowToolCall(server, options, name, args, signal
2615
3193
  export function registerWorkflowTools(server, options) {
2616
3194
  // tools/list
2617
3195
  server.setRequestHandler(ListToolsRequestSchema, async () => {
2618
- 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
+ }
2619
3204
  });
2620
3205
  // tools/call
2621
3206
  server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
2622
- const { name, arguments: args } = request.params;
2623
- const progressToken = request.params._meta?.progressToken;
2624
- const result = await handleWorkflowToolCall(server, options, name, args ?? {}, extra.signal, progressToken);
2625
- if (result != null) {
2626
- 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();
2627
3224
  }
2628
- logger.warn({ tool: name }, "unknown tool requested");
2629
- return {
2630
- content: [{ type: "text", text: `Unknown tool: ${String(name)}` }],
2631
- isError: true,
2632
- };
2633
3225
  });
2634
3226
  }
2635
3227
  //# sourceMappingURL=workflow-tools.js.map