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
@@ -62,18 +62,21 @@
62
62
  * isError: true with termination_reason: "wall" or "idle", which the
63
63
  * workflow engine treats as a transient failure eligible for retry.
64
64
  */
65
+ import { closeSync, fstatSync, openSync, readSync } from "node:fs";
65
66
  import { stat as fsStat } from "node:fs/promises";
66
67
  import { ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js";
67
68
  import { basename, isAbsolute, join, resolve, sep } from "node:path";
68
- import { callClink } from "./clink-client.js";
69
+ import { callClink, extractObservedInvocation } from "./clink-client.js";
69
70
  import { getArtifactsDir, getChildMaxConcurrent, getWorkflowsContextDefault, getWorkflowsContextDefaultCurator, getMaxParallel, getMaxPromptBytes, getStateDir, resolveWorkspaceRoot, } from "./config.js";
70
71
  import { validatePlanReviewCallBindings, validatePlanReviewPreparedInputPolicy, } from "./plan-review/runtime-policy.js";
71
72
  import { CouncilPlanReviewErrorSchema, } from "./plan-review/publication.js";
72
73
  import { PlanReviewStageError } from "./plan-review/stage-error.js";
73
- import { CallExecutionError, InvalidCallError, WorkflowCancellationError, WorkflowError, } from "./errors.js";
74
- import { analyzeOutputFidelity, enforceOutputFidelity } from "./fidelity.js";
74
+ import { CallExecutionError, InvalidCallError, WorkflowDispatchDeadlineError, WorkflowCancellationError, WorkflowError, } from "./errors.js";
75
+ import { analyzeOutputFidelity, enforceOutputFidelity, enforceTypedResultFidelity, TypedResultFidelityError, } from "./fidelity.js";
76
+ import { monotonicNowMs } from "./monotonic-clock.js";
75
77
  import { validateDynamicPaths } from "./path-validation.js";
76
- import { estimatePromptBudget, enforcePromptBudget, createPathContributor } from "./prompt-budget.js";
78
+ import { DEFAULT_RESERVE_BYTES, estimatePromptBudget, enforcePromptBudget, createPathContributor, } from "./prompt-budget.js";
79
+ import { buildCouncilDeadlineGuidance, COUNCIL_DEADLINE_GUIDANCE_RESERVE_BYTES, isCouncilExecutionWorkflow, shouldVetoWallTimeoutRetry, } from "./council-execution-policy.js";
77
80
  import { DispatchQueue } from "./queue.js";
78
81
  import { renderCallPrompt } from "./template.js";
79
82
  import { ArtifactStore, OutputSizeError, SchemaValidationError } from "./artifacts.js";
@@ -85,6 +88,7 @@ import { addPathWithBasename, buildAllowlistFromDiff } from "./citation-check.js
85
88
  import { logger } from "./logger.js";
86
89
  import { resolveWorkflowContextMode, WorkflowContextModeResolverError, } from "./context-mode-resolver.js";
87
90
  import { WorkflowDelegateValidationError } from "./delegate-validate.js";
91
+ import { isDelegateExternalGapDegrade } from "./delegate-signature.js";
88
92
  import { contextSeedSourceValidationError, resolveSeedSource, synthesizeSeedTerms, } from "./seed-terms.js";
89
93
  /**
90
94
  * Declarations for variables the ENGINE derives at runtime (not declared in the
@@ -214,6 +218,26 @@ function receiptDelegateFailureReason(result) {
214
218
  ? failureReason
215
219
  : null;
216
220
  }
221
+ /**
222
+ * The allowed roots a workflow engine derives for itself when the caller supplies
223
+ * none. Exported so admission-time path checks can validate against the SAME set
224
+ * the engine will enforce at dispatch, rather than re-deriving a near-copy.
225
+ *
226
+ * Note this set is engine-owned: `CLINKX_ALLOWED_ROOTS` is not consulted and
227
+ * cannot widen it.
228
+ */
229
+ export function defaultWorkflowAllowedRoots(runDir) {
230
+ const workspaceRoot = resolveWorkspaceRoot();
231
+ const stateDir = getStateDir();
232
+ const configuredArtifactsDir = getArtifactsDir();
233
+ return [
234
+ workspaceRoot,
235
+ stateDir,
236
+ configuredArtifactsDir,
237
+ runDir,
238
+ join(runDir, "artifacts"),
239
+ ];
240
+ }
217
241
  export class WorkflowEngine {
218
242
  definition;
219
243
  session;
@@ -247,6 +271,21 @@ export class WorkflowEngine {
247
271
  planReviewCallBindings;
248
272
  commitPlanReviewContextProvenance;
249
273
  terminalResultPolicy;
274
+ deriveInvocationAudit;
275
+ /**
276
+ * Workflow budget expiry on the MONOTONIC clock (`monotonicNowMs()`), not on
277
+ * the wall clock. Never compare it to `Date.now()`; read it only through
278
+ * `workflowRemainingMs()`.
279
+ */
280
+ workflowDeadlineAtMonoMs;
281
+ /**
282
+ * `stage.id`.`call.id` keys already written to a terminal state by
283
+ * `emitTerminalCallAbort`. A parallel stage still surfaces that throw through
284
+ * `materializeUnexpectedFailure`, whose emit carries no startedAt/durationMs
285
+ * and attempt 1 / retryCount 0 — persisted unconditionally, it would erase the
286
+ * terminal entry's timing. Bounded by the run's call count.
287
+ */
288
+ terminalizedCallKeys = new Set();
250
289
  constructor(options) {
251
290
  this.definition = options.definition;
252
291
  this.session = options.session;
@@ -284,13 +323,16 @@ export class WorkflowEngine {
284
323
  });
285
324
  this.commitPlanReviewContextProvenance = options.commitPlanReviewContextProvenance;
286
325
  this.terminalResultPolicy = options.terminalResultPolicy ?? { kind: "final_call" };
326
+ this.deriveInvocationAudit = options.deriveInvocationAudit;
287
327
  this.validateFinalStage();
288
328
  this.validateAdvertisedCliNames();
289
329
  this.contextProducingCalls = collectContextProducingCalls(this.definition);
290
330
  this.seedResumedArtifacts();
291
331
  }
292
332
  async execute() {
293
- const deadlineController = this.createDeadlineController();
333
+ this.workflowDeadlineAtMonoMs =
334
+ monotonicNowMs() + this.definition.settings.timeout_seconds * 1000;
335
+ const deadlineController = this.createDeadlineController(this.definition.settings.timeout_seconds * 1000);
294
336
  const stages = [];
295
337
  const startedAt = new Date().toISOString();
296
338
  try {
@@ -339,7 +381,14 @@ export class WorkflowEngine {
339
381
  message: `Stage ${stage.id} ${executedStage.ok ? "succeeded" : "failed"}`,
340
382
  });
341
383
  if (!executedStage.ok && stage.on_failure === "abort") {
342
- throw new WorkflowError(`Stage ${stage.id} failed and on_failure=abort`);
384
+ // Generic boundary for ALL stage aborts, so it must stay permanent by
385
+ // default — a stage that aborts on an ordinary permanent call failure
386
+ // is not a replayable run. The one exception is a genuinely exhausted
387
+ // wall clock: a parallel stage funnels the deadline throw through
388
+ // materializeUnexpectedFailure, which drops the class, so without this
389
+ // the environmental deadline would persist permanent and re-arm the
390
+ // spurious duplicate_of_failed_run warning on an honest replay.
391
+ throw new WorkflowError(`Stage ${stage.id} failed and on_failure=abort`, this.isWorkflowDeadlineExhausted() ? "transient" : undefined);
343
392
  }
344
393
  }
345
394
  const outputText = await this.resolveTerminalResult(stages);
@@ -419,6 +468,26 @@ export class WorkflowEngine {
419
468
  }
420
469
  finally {
421
470
  deadlineController.dispose();
471
+ this.workflowDeadlineAtMonoMs = undefined;
472
+ }
473
+ }
474
+ /**
475
+ * Resume-path entry point. `execute()` is never called on the resume preflight
476
+ * engine, so without this the deadline is never installed and the
477
+ * curator/finalizer delegate runs unbounded. Installs the SAME fresh full
478
+ * `timeout_seconds` budget a resumed run gets (D1), runs preflight under it,
479
+ * and always tears it down.
480
+ */
481
+ async prepareContextPreflightWithDeadline() {
482
+ this.workflowDeadlineAtMonoMs =
483
+ monotonicNowMs() + this.definition.settings.timeout_seconds * 1000;
484
+ const deadlineController = this.createDeadlineController(this.definition.settings.timeout_seconds * 1000);
485
+ try {
486
+ await this.prepareContextPreflight(deadlineController.signal);
487
+ }
488
+ finally {
489
+ deadlineController.dispose();
490
+ this.workflowDeadlineAtMonoMs = undefined;
422
491
  }
423
492
  }
424
493
  /**
@@ -633,7 +702,9 @@ export class WorkflowEngine {
633
702
  const maxBackoffSeconds = stage.retry.max_backoff_seconds;
634
703
  // Resume never replenishes an already-consumed provider attempt.
635
704
  const initialRetryCount = this.resumeNextRetryCounts.get(callKey) ?? 0;
636
- // Emit call prepared ONCE (input is stable across retries)
705
+ // Emit call prepared ONCE (input is stable across retries).
706
+ // Invocation audit rides the event, never ClinkInput / input.json.
707
+ const invocation = this.maybeInvocationAudit(input);
637
708
  await this.emitCallPrepared({
638
709
  runId: this.runId,
639
710
  workflowName: this.definition.name,
@@ -643,6 +714,7 @@ export class WorkflowEngine {
643
714
  retryCount: initialRetryCount,
644
715
  contextProducing,
645
716
  input: structuredClone(input),
717
+ ...(invocation === undefined ? {} : { invocation }),
646
718
  });
647
719
  let lastError = initialRetryCount > maxRetries
648
720
  ? new Error("Retry budget exhausted before resume")
@@ -653,10 +725,23 @@ export class WorkflowEngine {
653
725
  // the original `input.prompt`, so it never stacks. In-memory only — a
654
726
  // resumed retry has no lastError and simply gets no suffix.
655
727
  let correctiveSuffix;
728
+ // D7: at most ONE extra JSON-corrective attempt per call under
729
+ // transport_only, whatever max_retries allows. Loop-local, so a resumed
730
+ // call can earn it again — bounded by the persisted retry count anyway.
731
+ let correctiveRetryUsed = false;
656
732
  for (let retryCount = initialRetryCount; retryCount <= maxRetries; retryCount += 1) {
657
733
  const attempt = retryCount + 1;
658
734
  lastRetryCount = retryCount;
659
- this.throwIfAborted(signal);
735
+ // Deliberately guarded HERE rather than moved inside the attempt `try`:
736
+ // inside it, a bare deadline WorkflowError would become retryable under
737
+ // retry.mode:"all". Record the terminal outcome, then rethrow unchanged.
738
+ try {
739
+ this.throwIfAborted(signal);
740
+ }
741
+ catch (error) {
742
+ await this.emitTerminalCallAbort(stage, call, attempt, retryCount, startedAt, startedAtMs, error, error instanceof WorkflowCancellationError ? "cancelled" : "failed");
743
+ throw error;
744
+ }
660
745
  await this.emitCallStateChange({
661
746
  runId: this.runId,
662
747
  workflowName: this.definition.name,
@@ -689,6 +774,11 @@ export class WorkflowEngine {
689
774
  prompt: `${typeof input["prompt"] === "string" ? input["prompt"] : ""}${correctiveSuffix}`,
690
775
  };
691
776
  const result = await this.invokeCall(stage, call, attemptInput, signal, attempt);
777
+ enforceTypedResultFidelity(result.debugMetadata, {
778
+ stageId: stage.id,
779
+ callId: call.id,
780
+ typedOutput: call.response_contract === "json" || call.validator_schema_id != null,
781
+ });
692
782
  const fidelity = analyzeOutputFidelity(result.outputText, result.debugMetadata);
693
783
  enforceOutputFidelity(fidelity, {
694
784
  stageId: stage.id,
@@ -698,6 +788,7 @@ export class WorkflowEngine {
698
788
  const artifact = this.maybeWriteArtifact(stage, call, stageIndex, callIndex, result.outputText);
699
789
  const completedAt = new Date().toISOString();
700
790
  const durationMs = Date.now() - startedAtMs;
791
+ const observedInvocation = extractObservedInvocation(result.debugMetadata);
701
792
  await this.emitCallStateChange({
702
793
  runId: this.runId,
703
794
  workflowName: this.definition.name,
@@ -712,6 +803,7 @@ export class WorkflowEngine {
712
803
  outputText: result.outputText,
713
804
  fidelity,
714
805
  artifact,
806
+ ...(observedInvocation === undefined ? {} : { observedInvocation }),
715
807
  });
716
808
  await this.emitProgress({
717
809
  runId: this.runId,
@@ -735,42 +827,32 @@ export class WorkflowEngine {
735
827
  };
736
828
  }
737
829
  catch (error) {
830
+ // The deadline is never retryable, but the call still has to land in a
831
+ // terminal state before the throw leaves the loop.
832
+ if (error instanceof WorkflowDispatchDeadlineError) {
833
+ await this.emitTerminalCallAbort(stage, call, attempt, retryCount, startedAt, startedAtMs, error, "failed");
834
+ throw error;
835
+ }
738
836
  // Cancellation is never retryable
739
837
  if (error instanceof WorkflowCancellationError) {
740
- const completedAt = new Date().toISOString();
741
- const durationMs = Date.now() - startedAtMs;
742
- await this.emitCallStateChange({
743
- runId: this.runId,
744
- workflowName: this.definition.name,
745
- stageId: stage.id,
746
- callId: call.id,
747
- state: "cancelled",
748
- attempt,
749
- retryCount,
750
- startedAt,
751
- completedAt,
752
- durationMs,
753
- error: error.message,
754
- });
755
- await this.emitProgress({
756
- runId: this.runId,
757
- workflowName: this.definition.name,
758
- scope: "call",
759
- stageId: stage.id,
760
- stageDescription: stage.description,
761
- callId: call.id,
762
- state: "cancelled",
763
- attempt,
764
- retryCount,
765
- message: `Call ${callKey} cancelled`,
766
- });
838
+ await this.emitTerminalCallAbort(stage, call, attempt, retryCount, startedAt, startedAtMs, error, "cancelled");
767
839
  throw error;
768
840
  }
769
841
  const failureClass = classifyFailureClass(error);
770
842
  lastError = error;
771
843
  lastFailureClass = failureClass;
772
844
  // Determine if this error is retryable
773
- const canRetry = retryCount < maxRetries && isRetryable(error, failureClass, retryMode);
845
+ const ordinaryRetry = retryCount < maxRetries &&
846
+ isRetryable(error, failureClass, retryMode) &&
847
+ !shouldVetoWallTimeoutRetry(this.definition.name, error instanceof CallExecutionError ? error.terminationReason : undefined);
848
+ const correctiveRetry = !ordinaryRetry &&
849
+ !correctiveRetryUsed &&
850
+ retryCount < maxRetries &&
851
+ this.isCorrectiveRetryEligible(stage, call, error);
852
+ if (correctiveRetry) {
853
+ correctiveRetryUsed = true;
854
+ }
855
+ const canRetry = ordinaryRetry || correctiveRetry;
774
856
  if (canRetry) {
775
857
  // F4: corrective suffix rides only on the retry that was already
776
858
  // decided above — it never changes retry eligibility. Unconditional
@@ -850,6 +932,51 @@ export class WorkflowEngine {
850
932
  error: message,
851
933
  };
852
934
  }
935
+ /**
936
+ * Terminalize a call that is about to be abandoned by a throw rather than
937
+ * by the retry loop's own failure path.
938
+ *
939
+ * Both abort/deadline exits from executeSingleCall leave the loop OUTSIDE
940
+ * the attempt `try`, so without this the persisted entry is stranded as
941
+ * "running" with no completedAt — a durable lie that resume, the dashboard
942
+ * and run health all read. The caller still rethrows: this only records the
943
+ * outcome, it never swallows it.
944
+ */
945
+ async emitTerminalCallAbort(stage, call, attempt, retryCount, startedAt, startedAtMs, error, state) {
946
+ const callKey = `${stage.id}.${call.id}`;
947
+ this.terminalizedCallKeys.add(callKey);
948
+ const message = error instanceof Error ? error.message : String(error);
949
+ const completedAt = new Date().toISOString();
950
+ const durationMs = Date.now() - startedAtMs;
951
+ await this.emitCallStateChange({
952
+ runId: this.runId,
953
+ workflowName: this.definition.name,
954
+ stageId: stage.id,
955
+ callId: call.id,
956
+ state,
957
+ attempt,
958
+ retryCount,
959
+ startedAt,
960
+ completedAt,
961
+ durationMs,
962
+ error: message,
963
+ failureClass: classifyFailureClass(error),
964
+ });
965
+ await this.emitProgress({
966
+ runId: this.runId,
967
+ workflowName: this.definition.name,
968
+ scope: "call",
969
+ stageId: stage.id,
970
+ stageDescription: stage.description,
971
+ callId: call.id,
972
+ state,
973
+ attempt,
974
+ retryCount,
975
+ message: state === "cancelled"
976
+ ? `Call ${callKey} cancelled`
977
+ : `Call ${callKey} failed: ${message}`,
978
+ });
979
+ }
853
980
  /**
854
981
  * F4: bounded corrective suffix for the next already-scheduled retry after
855
982
  * a JSON/schema-shaped failure. Returns undefined (no suffix) unless ALL of:
@@ -868,13 +995,40 @@ export class WorkflowEngine {
868
995
  * The suffix is always derived from the ORIGINAL prompt, so it can never
869
996
  * stack across attempts, and it is never persisted to state.
870
997
  */
998
+ /**
999
+ * Typed plan review (identity: the plan-review-receipts workflow name, plus
1000
+ * the plan-review runtime signals as defense-in-depth). Its dispatch bytes
1001
+ * and retry behavior are pinned, so no corrective machinery may touch it.
1002
+ */
1003
+ isTypedPlanReviewCall(call) {
1004
+ return (this.definition.name === "plan-review-receipts" ||
1005
+ this.planReviewContextPolicy !== undefined ||
1006
+ call.plan_review_snapshot_text !== undefined);
1007
+ }
1008
+ /**
1009
+ * D7: under retry.mode "transport_only" a malformed-JSON answer is a
1010
+ * permanent failure, so it would never be retried. A JSON-contract call gets
1011
+ * ONE corrective attempt when the failure is primarily JSON-shaped; a
1012
+ * secondary-only invalid_json_output reason never creates eligibility.
1013
+ * failureClass is not reclassified.
1014
+ */
1015
+ isCorrectiveRetryEligible(stage, call, error) {
1016
+ if (stage.retry.mode !== "transport_only") {
1017
+ return false;
1018
+ }
1019
+ if (!declaresJsonContract(call) || this.isTypedPlanReviewCall(call)) {
1020
+ return false;
1021
+ }
1022
+ return (error instanceof SchemaValidationError ||
1023
+ error instanceof OutputSizeError ||
1024
+ (error instanceof CallExecutionError &&
1025
+ error.childDebugMetadata?.semantic_failure_reason === "invalid_json_output"));
1026
+ }
871
1027
  buildCorrectiveRetrySuffix(stage, call, input, error) {
872
- if (call.response_contract !== "json" && call.validator_schema_id == null) {
1028
+ if (!declaresJsonContract(call)) {
873
1029
  return undefined;
874
1030
  }
875
- if (this.definition.name === "plan-review-receipts" ||
876
- this.planReviewContextPolicy !== undefined ||
877
- call.plan_review_snapshot_text !== undefined) {
1031
+ if (this.isTypedPlanReviewCall(call)) {
878
1032
  return undefined;
879
1033
  }
880
1034
  const detail = correctiveFailureDetail(error);
@@ -896,7 +1050,9 @@ export class WorkflowEngine {
896
1050
  fileContributors: files.map((path) => createPathContributor(`absolute_file_paths:${path}`, path, "file")),
897
1051
  imageContributors: images.map((path) => createPathContributor(`image:${path}`, path, "image")),
898
1052
  maxBytes: this.maxPromptBytes,
899
- reserveBytes: this.promptReserveBytes,
1053
+ reserveBytes: isCouncilExecutionWorkflow(this.definition.name)
1054
+ ? (this.promptReserveBytes ?? DEFAULT_RESERVE_BYTES) + COUNCIL_DEADLINE_GUIDANCE_RESERVE_BYTES
1055
+ : this.promptReserveBytes,
900
1056
  });
901
1057
  if (estimate.exceeded) {
902
1058
  logger.warn({ run_id: this.runId, stage_id: stage.id, call_id: call.id }, "corrective retry suffix skipped: appending would exceed the prompt budget");
@@ -915,9 +1071,8 @@ export class WorkflowEngine {
915
1071
  return suffix;
916
1072
  }
917
1073
  async invokeCall(stage, call, input, signal, attempt) {
918
- const timeoutMs = call.timeout_seconds != null
919
- ? Math.max((call.timeout_seconds * 1000) + CALL_TIMEOUT_BUFFER_MS, DEFAULT_PARENT_CALL_TIMEOUT_MS)
920
- : undefined;
1074
+ const dispatchDeadline = this.computeDispatchDeadline(call);
1075
+ const dispatchInput = this.prepareDispatchInput(input, dispatchDeadline.callRemainingMs);
921
1076
  const sessionLogOwner = this.sessionLogGeneration !== undefined && this.session.supportsSessionLogOwner === true
922
1077
  ? {
923
1078
  family: "workflow",
@@ -928,7 +1083,69 @@ export class WorkflowEngine {
928
1083
  attempt,
929
1084
  }
930
1085
  : undefined;
931
- return await callClink(this.session, input, signal, { stageId: stage.id, callId: call.id }, timeoutMs, sessionLogOwner);
1086
+ // Prompt sizing above performs synchronous filesystem work, during which
1087
+ // the deadline timer cannot run. Recheck at dispatch; the already-rendered
1088
+ // guidance remains a conservative completion target if preparation took time.
1089
+ const finalDispatchDeadline = this.computeDispatchDeadline(call);
1090
+ this.throwIfAborted(signal);
1091
+ return await callClink(this.session, dispatchInput, signal, { stageId: stage.id, callId: call.id }, finalDispatchDeadline.timeoutMs, sessionLogOwner);
1092
+ }
1093
+ computeDispatchDeadline(call) {
1094
+ const configuredCallTimeoutMs = call.timeout_seconds != null
1095
+ ? Math.max((call.timeout_seconds * 1000) + CALL_TIMEOUT_BUFFER_MS, DEFAULT_PARENT_CALL_TIMEOUT_MS)
1096
+ : undefined;
1097
+ const workflowRemainingMs = this.workflowRemainingMs();
1098
+ if (workflowRemainingMs != null && workflowRemainingMs <= 0) {
1099
+ throw new WorkflowDispatchDeadlineError();
1100
+ }
1101
+ // Keep the existing backend timeout contract for generic and excluded
1102
+ // workflows. The workflow AbortSignal still enforces the outer deadline;
1103
+ // only admitted councils with an explicit call cap receive the tighter
1104
+ // per-dispatch timeout when the workflow is closer to expiry.
1105
+ const timeoutMs = !isCouncilExecutionWorkflow(this.definition.name) ||
1106
+ configuredCallTimeoutMs == null || workflowRemainingMs == null
1107
+ ? configuredCallTimeoutMs
1108
+ : Math.min(configuredCallTimeoutMs, workflowRemainingMs);
1109
+ const callRemainingMs = configuredCallTimeoutMs == null
1110
+ ? workflowRemainingMs
1111
+ : (isCouncilExecutionWorkflow(this.definition.name) && workflowRemainingMs != null
1112
+ ? Math.min(configuredCallTimeoutMs, workflowRemainingMs)
1113
+ : configuredCallTimeoutMs);
1114
+ return {
1115
+ timeoutMs,
1116
+ callRemainingMs,
1117
+ };
1118
+ }
1119
+ prepareDispatchInput(input, callRemainingMs) {
1120
+ if (!isCouncilExecutionWorkflow(this.definition.name) ||
1121
+ this.workflowDeadlineAtMonoMs == null ||
1122
+ callRemainingMs == null ||
1123
+ typeof input["prompt"] !== "string") {
1124
+ return input;
1125
+ }
1126
+ const guidedInput = {
1127
+ ...input,
1128
+ prompt: `${input["prompt"]}${buildCouncilDeadlineGuidance({
1129
+ workflowRemainingMs: this.workflowRemainingMs() ?? 0,
1130
+ callRemainingMs,
1131
+ })}`,
1132
+ };
1133
+ const filePaths = Array.isArray(input["absolute_file_paths"])
1134
+ ? input["absolute_file_paths"].filter((path) => typeof path === "string")
1135
+ : [];
1136
+ const imagePaths = Array.isArray(input["images"])
1137
+ ? input["images"].filter((path) => typeof path === "string")
1138
+ : [];
1139
+ const estimate = estimatePromptBudget({
1140
+ promptText: guidedInput["prompt"],
1141
+ contextText: typeof input["context"] === "string" ? input["context"] : undefined,
1142
+ fileContributors: filePaths.map((path) => createPathContributor(`absolute_file_paths:${path}`, path, "file")),
1143
+ imageContributors: imagePaths.map((path) => createPathContributor(`image:${path}`, path, "image")),
1144
+ maxBytes: this.maxPromptBytes,
1145
+ reserveBytes: this.promptReserveBytes,
1146
+ });
1147
+ enforcePromptBudget(estimate);
1148
+ return guidedInput;
932
1149
  }
933
1150
  maybeWriteArtifact(stage, call, stageIndex, callIndex, outputText) {
934
1151
  const requiresValidation = call.validator_schema_id != null;
@@ -1043,9 +1260,15 @@ export class WorkflowEngine {
1043
1260
  }
1044
1261
  /**
1045
1262
  * Build the citation allowlist for a post-processor run: the union of files
1046
- * parsed from the `diff_context` variable, the resolved `context_files`, and
1047
- * the resolved `context_bundle_files`. Both full paths and basenames are
1048
- * stored so citation matching is lenient.
1263
+ * parsed from the `diff_context` variable, the resolved `context_files`, the
1264
+ * resolved `context_bundle_files`, and the source files selected by any
1265
+ * curated context-builder bundle among them. Both full paths and basenames
1266
+ * are stored so citation matching is lenient.
1267
+ *
1268
+ * A curated preflight copies only `bundle.md` and `manifest.json` into
1269
+ * `context_bundle_files`, so without the manifest's `selected_context`
1270
+ * every citation to a real source file would be stripped. Reading from the
1271
+ * (persisted) variable keeps this working on resume.
1049
1272
  */
1050
1273
  buildPostProcessorAllowlist() {
1051
1274
  const diffRaw = this.effectiveVariables["diff_context"];
@@ -1055,8 +1278,92 @@ export class WorkflowEngine {
1055
1278
  addPathWithBasename(allowlist, path);
1056
1279
  }
1057
1280
  }
1281
+ const manifestPaths = new Set(this.resolvePathListLenient("context_bundle_files").filter((path) => isAbsolute(path) && basename(path) === "manifest.json"));
1282
+ for (const manifestPath of manifestPaths) {
1283
+ this.addCuratedBundleSelections(allowlist, manifestPath);
1284
+ }
1058
1285
  return allowlist;
1059
1286
  }
1287
+ /**
1288
+ * Add each `selected_context` entry of a context-builder manifest to the
1289
+ * allowlist: its root-relative `path` and, when its root resolves, the
1290
+ * absolute form. The allowlist is advisory, so an unreadable, oversized
1291
+ * (> 8 MiB), non-JSON, or non-context-builder manifest is skipped silently
1292
+ * and malformed entries are ignored.
1293
+ */
1294
+ addCuratedBundleSelections(allowlist, manifestPath) {
1295
+ const maxBytes = 8 * 1024 * 1024;
1296
+ let text;
1297
+ let fd;
1298
+ try {
1299
+ fd = openSync(manifestPath, "r");
1300
+ const stats = fstatSync(fd);
1301
+ if (!stats.isFile() || stats.size > maxBytes) {
1302
+ return;
1303
+ }
1304
+ // One spare byte detects a file that grew after fstat; skip it rather
1305
+ // than parse a torn read.
1306
+ const buffer = Buffer.alloc(stats.size + 1);
1307
+ let length = 0;
1308
+ while (length < buffer.length) {
1309
+ const read = readSync(fd, buffer, length, buffer.length - length, null);
1310
+ if (read === 0) {
1311
+ break;
1312
+ }
1313
+ length += read;
1314
+ }
1315
+ if (length > stats.size) {
1316
+ return;
1317
+ }
1318
+ text = buffer.toString("utf-8", 0, length);
1319
+ }
1320
+ catch {
1321
+ return;
1322
+ }
1323
+ finally {
1324
+ if (fd !== undefined) {
1325
+ try {
1326
+ closeSync(fd);
1327
+ }
1328
+ catch {
1329
+ // Advisory read; a close failure must not fail the call.
1330
+ }
1331
+ }
1332
+ }
1333
+ let manifest;
1334
+ try {
1335
+ manifest = JSON.parse(text);
1336
+ }
1337
+ catch {
1338
+ return;
1339
+ }
1340
+ if (typeof manifest !== "object" || manifest === null || Array.isArray(manifest)) {
1341
+ return;
1342
+ }
1343
+ const { selected_context: selected, roots } = manifest;
1344
+ if (!Array.isArray(selected) || !Array.isArray(roots)) {
1345
+ return;
1346
+ }
1347
+ for (const entry of selected) {
1348
+ if (typeof entry !== "object" || entry === null) {
1349
+ continue;
1350
+ }
1351
+ const { path, root_index: rootIndex } = entry;
1352
+ if (typeof path !== "string" || path.length === 0) {
1353
+ continue;
1354
+ }
1355
+ addPathWithBasename(allowlist, path);
1356
+ const root = typeof rootIndex === "number" && Number.isInteger(rootIndex) && rootIndex >= 0
1357
+ ? roots[rootIndex]
1358
+ : undefined;
1359
+ if (typeof root === "object" && root !== null) {
1360
+ const rootPath = root["path"];
1361
+ if (typeof rootPath === "string" && rootPath.length > 0) {
1362
+ addPathWithBasename(allowlist, join(rootPath, path));
1363
+ }
1364
+ }
1365
+ }
1366
+ }
1060
1367
  /**
1061
1368
  * Resolve a path-list variable to absolute paths without enforcing allowed
1062
1369
  * roots (the post-processor allowlist is advisory, not a security boundary).
@@ -1162,7 +1469,11 @@ export class WorkflowEngine {
1162
1469
  ? {}
1163
1470
  : { context_budget_adapter: contextBudgetAdapter }),
1164
1471
  ...(budget == null ? {} : { budget }),
1165
- ...(policy.effectiveDelegate == null ? {} : { delegate: policy.effectiveDelegate }),
1472
+ ...(policy.effectiveDelegate == null
1473
+ ? {}
1474
+ : {
1475
+ delegate: foldExternalGapPolicy(policy.effectiveDelegate, this.definition.settings.context_external_gap_policy),
1476
+ }),
1166
1477
  };
1167
1478
  // Light-caller provenance: capture how anchors were derived. `root_wide_used`
1168
1479
  // records the REALIZED `input.allowRootWideDiscovery` flag actually passed to
@@ -1190,7 +1501,12 @@ export class WorkflowEngine {
1190
1501
  const effectiveCoverageStatus = result.metrics.files_selected > 0
1191
1502
  ? result.coverage?.status ?? "not_evaluated"
1192
1503
  : "not_evaluated";
1193
- this.setEffectiveVariable("context_quality", effectiveCoverageStatus);
1504
+ // External gaps under external_gap_policy:"degrade" are the only producer of
1505
+ // a partial delegate without fallback; the coverage report itself is left
1506
+ // untouched so persisted coverage stays the policy's own verdict.
1507
+ const externalGapsDegraded = isDelegateExternalGapDegrade(result.delegate);
1508
+ const contextQuality = externalGapsDegraded ? "degraded" : effectiveCoverageStatus;
1509
+ this.setEffectiveVariable("context_quality", contextQuality);
1194
1510
  const delegateInput = policy.effectiveDelegate?.enabled === true ? policy.effectiveDelegate : undefined;
1195
1511
  const fallbackUsed = result.delegate?.fallback_used === true;
1196
1512
  const fallbackAggregate = recordPreflightFallbackAggregate({
@@ -1248,6 +1564,12 @@ export class WorkflowEngine {
1248
1564
  files_selected: 0,
1249
1565
  }, "context-builder preflight selected no files; proceeding without bundle");
1250
1566
  }
1567
+ if (externalGapsDegraded && effectiveCoverageStatus !== "degraded") {
1568
+ if (this.definition.settings.fail_on_degraded_context_coverage === true) {
1569
+ throw new WorkflowError("Context-builder preflight is degraded: the finalizer reported evidence gaps outside the delegate packet");
1570
+ }
1571
+ logger.warn({ run_id: this.runId, source_run_id: result.source_run_id }, "context-builder preflight reported external evidence gaps; proceeding degraded");
1572
+ }
1251
1573
  if (effectiveCoverageStatus === "degraded") {
1252
1574
  if (this.definition.settings.fail_on_degraded_context_coverage === true) {
1253
1575
  throw new WorkflowError("Context-builder preflight coverage is degraded");
@@ -1394,11 +1716,11 @@ export class WorkflowEngine {
1394
1716
  }
1395
1717
  return value;
1396
1718
  }
1397
- createDeadlineController() {
1719
+ createDeadlineController(delayMs) {
1398
1720
  const controller = new AbortController();
1399
1721
  const timeout = setTimeout(() => {
1400
1722
  controller.abort(new Error(`Workflow timeout exceeded after ${String(this.definition.settings.timeout_seconds)} seconds`));
1401
- }, this.definition.settings.timeout_seconds * 1000);
1723
+ }, Math.max(0, delayMs));
1402
1724
  if (this.signal != null) {
1403
1725
  if (this.signal.aborted) {
1404
1726
  controller.abort(new WorkflowCancellationError());
@@ -1412,6 +1734,24 @@ export class WorkflowEngine {
1412
1734
  dispose: () => clearTimeout(timeout),
1413
1735
  };
1414
1736
  }
1737
+ /** Remaining workflow budget in ms; undefined when no deadline is installed. */
1738
+ workflowRemainingMs() {
1739
+ return this.workflowDeadlineAtMonoMs == null
1740
+ ? undefined
1741
+ : Math.floor(this.workflowDeadlineAtMonoMs - monotonicNowMs());
1742
+ }
1743
+ /**
1744
+ * True only while a workflow deadline is set AND already past. Pure: reads
1745
+ * the deadline the running `execute()` installed and the clock, nothing else.
1746
+ * `execute()` clears `workflowDeadlineAtMonoMs` in its `finally`, so this can
1747
+ * never report a stale deadline from a prior run. The reading is monotonic:
1748
+ * a wall-clock step (NTP correction, suspend/resume, manual change) can
1749
+ * neither manufacture nor erase an exhausted budget.
1750
+ */
1751
+ isWorkflowDeadlineExhausted() {
1752
+ const remainingMs = this.workflowRemainingMs();
1753
+ return remainingMs != null && remainingMs <= 0;
1754
+ }
1415
1755
  throwIfAborted(signal) {
1416
1756
  if (!signal.aborted) {
1417
1757
  return;
@@ -1421,7 +1761,11 @@ export class WorkflowEngine {
1421
1761
  throw reason;
1422
1762
  }
1423
1763
  if (reason instanceof Error && reason.message.includes("Workflow timeout exceeded")) {
1424
- throw new WorkflowError(reason.message);
1764
+ // Same environmental cause as WorkflowDispatchDeadlineError, so it
1765
+ // carries the same class. Kept as a bare WorkflowError: the two are now
1766
+ // handled identically by materializeUnexpectedFailure (both materialize
1767
+ // rather than rethrow), so the distinction buys nothing here.
1768
+ throw new WorkflowError(reason.message, "transient");
1425
1769
  }
1426
1770
  throw new WorkflowCancellationError();
1427
1771
  }
@@ -1447,16 +1791,7 @@ export class WorkflowEngine {
1447
1791
  `Known cli_name values: ${knownCliNames.join(", ")}`);
1448
1792
  }
1449
1793
  defaultAllowedRoots() {
1450
- const workspaceRoot = resolveWorkspaceRoot();
1451
- const stateDir = getStateDir();
1452
- const configuredArtifactsDir = getArtifactsDir();
1453
- return [
1454
- workspaceRoot,
1455
- stateDir,
1456
- configuredArtifactsDir,
1457
- this.runDir,
1458
- join(this.runDir, "artifacts"),
1459
- ];
1794
+ return defaultWorkflowAllowedRoots(this.runDir);
1460
1795
  }
1461
1796
  recordPlanReviewDeliveredAliases(callKey, call, absoluteFilePaths) {
1462
1797
  if (call.plan_review_anonymous_aliases == null)
@@ -1531,6 +1866,9 @@ export class WorkflowEngine {
1531
1866
  ];
1532
1867
  this.recordPlanReviewDeliveredAliases(callKey, call, contextArtifacts.absoluteFilePaths);
1533
1868
  const images = [...staticImages, ...dynamicImages];
1869
+ const reserveBytes = isCouncilExecutionWorkflow(this.definition.name)
1870
+ ? (this.promptReserveBytes ?? DEFAULT_RESERVE_BYTES) + COUNCIL_DEADLINE_GUIDANCE_RESERVE_BYTES
1871
+ : this.promptReserveBytes;
1534
1872
  const estimate = estimatePromptBudget({
1535
1873
  promptText: prompt,
1536
1874
  contextText: contextArtifacts.context,
@@ -1541,13 +1879,15 @@ export class WorkflowEngine {
1541
1879
  ],
1542
1880
  imageContributors: images.map((path) => createPathContributor(`image:${path}`, path, "image")),
1543
1881
  maxBytes: this.maxPromptBytes,
1544
- reserveBytes: this.promptReserveBytes,
1882
+ reserveBytes,
1545
1883
  });
1546
1884
  enforcePromptBudget(estimate);
1547
1885
  const input = {
1548
1886
  cli_name: call.cli_name,
1549
1887
  role: call.role,
1550
1888
  prompt,
1889
+ ...(call.model !== undefined ? { model: call.model } : {}),
1890
+ ...(call.effort !== undefined ? { effort: call.effort } : {}),
1551
1891
  };
1552
1892
  if (absoluteFilePaths.length > 0) {
1553
1893
  input["absolute_file_paths"] = absoluteFilePaths;
@@ -1572,26 +1912,59 @@ export class WorkflowEngine {
1572
1912
  }
1573
1913
  return input;
1574
1914
  }
1915
+ /**
1916
+ * Derive a per-call invocation audit from the materialized input.
1917
+ * Uses the dispatched cli_name/role (plan-review bindings may rewrite
1918
+ * cli_name on resume) and never mutates `input`.
1919
+ */
1920
+ maybeInvocationAudit(input) {
1921
+ const derive = this.deriveInvocationAudit;
1922
+ if (derive == null) {
1923
+ return undefined;
1924
+ }
1925
+ const cliName = input["cli_name"];
1926
+ const role = input["role"];
1927
+ if (typeof cliName !== "string" || typeof role !== "string") {
1928
+ return undefined;
1929
+ }
1930
+ const model = input["model"];
1931
+ const effort = input["effort"];
1932
+ return derive(cliName, role, {
1933
+ ...(typeof model === "string" ? { model } : {}),
1934
+ ...(typeof effort === "string" ? { effort } : {}),
1935
+ });
1936
+ }
1575
1937
  async materializeUnexpectedFailure(stage, call, error) {
1938
+ // Only cancellation escapes a parallel stage. Every other failure —
1939
+ // environmental (an exhausted dispatch deadline) as much as a child
1940
+ // failure — materializes into an ExecutedCall{ok:false} so already-settled
1941
+ // sibling results survive Promise.allSettled instead of being discarded by
1942
+ // a rethrow.
1576
1943
  if (error instanceof WorkflowCancellationError) {
1577
1944
  throw error;
1578
1945
  }
1579
1946
  const message = error instanceof Error ? error.message : String(error);
1580
1947
  this.artifactStore.noteFailedCall(stage.id, call.id, message);
1581
1948
  logger.error({ run_id: this.runId, stage_id: stage.id, call_id: call.id, err: message }, "parallel stage call rejected unexpectedly");
1582
- await this.emitCallStateChange({
1583
- runId: this.runId,
1584
- workflowName: this.definition.name,
1585
- stageId: stage.id,
1586
- callId: call.id,
1587
- state: "failed",
1588
- attempt: 1,
1589
- retryCount: 0,
1590
- completedAt: new Date().toISOString(),
1591
- error: message,
1592
- failureClass: classifyFailureClass(error),
1593
- childDebugMetadata: error instanceof CallExecutionError ? error.childDebugMetadata : undefined,
1594
- });
1949
+ // Only the genuinely un-terminalized failures need a state emit here.
1950
+ // `emitTerminalCallAbort` already wrote the real attempt/retryCount and
1951
+ // startedAt/durationMs, and onCallStateChange assigns those fields
1952
+ // unconditionally, so re-emitting would overwrite them with placeholders.
1953
+ if (!this.terminalizedCallKeys.has(`${stage.id}.${call.id}`)) {
1954
+ await this.emitCallStateChange({
1955
+ runId: this.runId,
1956
+ workflowName: this.definition.name,
1957
+ stageId: stage.id,
1958
+ callId: call.id,
1959
+ state: "failed",
1960
+ attempt: 1,
1961
+ retryCount: 0,
1962
+ completedAt: new Date().toISOString(),
1963
+ error: message,
1964
+ failureClass: classifyFailureClass(error),
1965
+ childDebugMetadata: error instanceof CallExecutionError ? error.childDebugMetadata : undefined,
1966
+ });
1967
+ }
1595
1968
  await this.emitProgress({
1596
1969
  runId: this.runId,
1597
1970
  workflowName: this.definition.name,
@@ -1829,18 +2202,41 @@ const CORRECTIVE_SUFFIX_FOOTER = "\nReturn ONLY a single JSON object conforming
1829
2202
  * is not one a corrective re-prompt could fix (transport death, timeout, …).
1830
2203
  */
1831
2204
  function correctiveFailureDetail(error) {
2205
+ if (error instanceof TypedResultFidelityError) {
2206
+ return error.message;
2207
+ }
1832
2208
  if (error instanceof SchemaValidationError) {
1833
2209
  return error.message;
1834
2210
  }
1835
2211
  if (error instanceof OutputSizeError) {
1836
2212
  return error.message;
1837
2213
  }
1838
- if (error instanceof CallExecutionError &&
1839
- error.childDebugMetadata?.rejection_reason === "invalid_json_output") {
1840
- return "output did not contain valid JSON";
2214
+ if (error instanceof CallExecutionError) {
2215
+ const debug = error.childDebugMetadata;
2216
+ if (debug?.rejection_reason === "invalid_json_output" ||
2217
+ debug?.semantic_failure_reason === "invalid_json_output" ||
2218
+ debug?.secondary_failure_reason === "invalid_json_output") {
2219
+ return "output did not contain valid JSON";
2220
+ }
1841
2221
  }
1842
2222
  return undefined;
1843
2223
  }
2224
+ /** Mirrors the expect_json derivation in buildCallInput. */
2225
+ function declaresJsonContract(call) {
2226
+ return call.response_contract === "json" || call.validator_schema_id != null;
2227
+ }
2228
+ /**
2229
+ * D1b: fold the workflow's `context_external_gap_policy` into an ENABLED
2230
+ * preflight delegate that does not choose one itself. Runtime-only: the
2231
+ * caller's `cb_delegate` variable (and so the inputs snapshot) is untouched,
2232
+ * and an explicit delegate `external_gap_policy` always wins.
2233
+ */
2234
+ function foldExternalGapPolicy(delegate, policy) {
2235
+ if (policy === undefined || delegate.enabled !== true || delegate.external_gap_policy !== undefined) {
2236
+ return delegate;
2237
+ }
2238
+ return { ...delegate, external_gap_policy: policy };
2239
+ }
1844
2240
  function buildCorrectiveSuffixText(detail, header = CORRECTIVE_SUFFIX_HEADER) {
1845
2241
  const budget = CORRECTIVE_SUFFIX_MAX_CHARS -
1846
2242
  header.length -