@relayflows/sdk 2.0.29 → 2.0.31

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 (393) hide show
  1. package/dist/agent-artifacts.d.ts.map +1 -1
  2. package/dist/agent-artifacts.js +2 -9
  3. package/dist/agent-artifacts.js.map +1 -1
  4. package/dist/agent-cwd.d.ts +69 -0
  5. package/dist/agent-cwd.d.ts.map +1 -0
  6. package/dist/agent-cwd.js +140 -0
  7. package/dist/agent-cwd.js.map +1 -0
  8. package/dist/artifact-scan-policy.d.ts +35 -0
  9. package/dist/artifact-scan-policy.d.ts.map +1 -0
  10. package/dist/artifact-scan-policy.js +49 -0
  11. package/dist/artifact-scan-policy.js.map +1 -0
  12. package/dist/authored-completion-record.d.ts +35 -0
  13. package/dist/authored-completion-record.d.ts.map +1 -0
  14. package/dist/authored-completion-record.js +92 -0
  15. package/dist/authored-completion-record.js.map +1 -0
  16. package/dist/authored-completion.d.ts +99 -0
  17. package/dist/authored-completion.d.ts.map +1 -0
  18. package/dist/authored-completion.js +184 -0
  19. package/dist/authored-completion.js.map +1 -0
  20. package/dist/authored-flow-error.d.ts +13 -1
  21. package/dist/authored-flow-error.d.ts.map +1 -1
  22. package/dist/authored-flow-error.js +12 -0
  23. package/dist/authored-flow-error.js.map +1 -1
  24. package/dist/authored-flow-executor.d.ts +7 -30
  25. package/dist/authored-flow-executor.d.ts.map +1 -1
  26. package/dist/authored-flow-executor.js +64 -48
  27. package/dist/authored-flow-executor.js.map +1 -1
  28. package/dist/authored-human.d.ts.map +1 -1
  29. package/dist/authored-human.js +1 -3
  30. package/dist/authored-human.js.map +1 -1
  31. package/dist/authored-node-entry.js +4 -1
  32. package/dist/authored-node-entry.js.map +1 -1
  33. package/dist/authored-node-runner.d.ts +9 -1
  34. package/dist/authored-node-runner.d.ts.map +1 -1
  35. package/dist/authored-node-runner.js +92 -17
  36. package/dist/authored-node-runner.js.map +1 -1
  37. package/dist/authored-root.d.ts +5 -0
  38. package/dist/authored-root.d.ts.map +1 -1
  39. package/dist/authored-root.js +20 -2
  40. package/dist/authored-root.js.map +1 -1
  41. package/dist/authored-step-output.d.ts +35 -10
  42. package/dist/authored-step-output.d.ts.map +1 -1
  43. package/dist/authored-step-output.js +108 -40
  44. package/dist/authored-step-output.js.map +1 -1
  45. package/dist/authored-verdict.d.ts +28 -0
  46. package/dist/authored-verdict.d.ts.map +1 -0
  47. package/dist/authored-verdict.js +79 -0
  48. package/dist/authored-verdict.js.map +1 -0
  49. package/dist/authored-worker-step.d.ts +5 -2
  50. package/dist/authored-worker-step.d.ts.map +1 -1
  51. package/dist/authored-worker-step.js +48 -11
  52. package/dist/authored-worker-step.js.map +1 -1
  53. package/dist/bundle-extensions.d.ts.map +1 -1
  54. package/dist/bundle-extensions.js +4 -7
  55. package/dist/bundle-extensions.js.map +1 -1
  56. package/dist/bundle.d.ts.map +1 -1
  57. package/dist/bundle.js +41 -7
  58. package/dist/bundle.js.map +1 -1
  59. package/dist/canonical.d.ts.map +1 -1
  60. package/dist/canonical.js +18 -6
  61. package/dist/canonical.js.map +1 -1
  62. package/dist/cli/check-worker-surface.d.ts +27 -0
  63. package/dist/cli/check-worker-surface.d.ts.map +1 -0
  64. package/dist/cli/check-worker-surface.js +63 -0
  65. package/dist/cli/check-worker-surface.js.map +1 -0
  66. package/dist/cli/check.d.ts +19 -2
  67. package/dist/cli/check.d.ts.map +1 -1
  68. package/dist/cli/check.js +12 -4
  69. package/dist/cli/check.js.map +1 -1
  70. package/dist/cli/cloud-format.d.ts +9 -0
  71. package/dist/cli/cloud-format.d.ts.map +1 -0
  72. package/dist/cli/cloud-format.js +77 -0
  73. package/dist/cli/cloud-format.js.map +1 -0
  74. package/dist/cli/cloud-live.d.ts +39 -0
  75. package/dist/cli/cloud-live.d.ts.map +1 -0
  76. package/dist/cli/cloud-live.js +325 -0
  77. package/dist/cli/cloud-live.js.map +1 -0
  78. package/dist/cli/cloud-read.d.ts +3 -7
  79. package/dist/cli/cloud-read.d.ts.map +1 -1
  80. package/dist/cli/cloud-read.js +14 -266
  81. package/dist/cli/cloud-read.js.map +1 -1
  82. package/dist/cli/cloud-refusal.d.ts +39 -0
  83. package/dist/cli/cloud-refusal.d.ts.map +1 -0
  84. package/dist/cli/cloud-refusal.js +95 -0
  85. package/dist/cli/cloud-refusal.js.map +1 -0
  86. package/dist/cli/cloud-status-view.d.ts +7 -0
  87. package/dist/cli/cloud-status-view.d.ts.map +1 -0
  88. package/dist/cli/cloud-status-view.js +214 -0
  89. package/dist/cli/cloud-status-view.js.map +1 -0
  90. package/dist/cli/direct-run.d.ts.map +1 -1
  91. package/dist/cli/direct-run.js +16 -2
  92. package/dist/cli/direct-run.js.map +1 -1
  93. package/dist/cli/hn-monitor.d.ts +0 -13
  94. package/dist/cli/hn-monitor.d.ts.map +1 -1
  95. package/dist/cli/hn-monitor.js +2 -1
  96. package/dist/cli/hn-monitor.js.map +1 -1
  97. package/dist/cli/local-agent-remedy.d.ts +80 -0
  98. package/dist/cli/local-agent-remedy.d.ts.map +1 -0
  99. package/dist/cli/local-agent-remedy.js +116 -0
  100. package/dist/cli/local-agent-remedy.js.map +1 -0
  101. package/dist/cli/observer-session.d.ts +41 -0
  102. package/dist/cli/observer-session.d.ts.map +1 -0
  103. package/dist/cli/observer-session.js +78 -0
  104. package/dist/cli/observer-session.js.map +1 -0
  105. package/dist/cli/run.d.ts +30 -2
  106. package/dist/cli/run.d.ts.map +1 -1
  107. package/dist/cli/run.js +186 -24
  108. package/dist/cli/run.js.map +1 -1
  109. package/dist/cli/status.d.ts +9 -0
  110. package/dist/cli/status.d.ts.map +1 -1
  111. package/dist/cli/status.js +31 -2
  112. package/dist/cli/status.js.map +1 -1
  113. package/dist/cli/step-evidence.d.ts +90 -0
  114. package/dist/cli/step-evidence.d.ts.map +1 -0
  115. package/dist/cli/step-evidence.js +259 -0
  116. package/dist/cli/step-evidence.js.map +1 -0
  117. package/dist/cli/step-failure.d.ts +14 -4
  118. package/dist/cli/step-failure.d.ts.map +1 -1
  119. package/dist/cli/step-failure.js +41 -76
  120. package/dist/cli/step-failure.js.map +1 -1
  121. package/dist/cli-commands.d.ts +6 -0
  122. package/dist/cli-commands.d.ts.map +1 -1
  123. package/dist/cli-commands.js +2 -0
  124. package/dist/cli-commands.js.map +1 -1
  125. package/dist/cli.d.ts +10 -0
  126. package/dist/cli.d.ts.map +1 -1
  127. package/dist/cli.js +53 -42
  128. package/dist/cli.js.map +1 -1
  129. package/dist/cloud-read.d.ts +15 -0
  130. package/dist/cloud-read.d.ts.map +1 -1
  131. package/dist/cloud-read.js +20 -1
  132. package/dist/cloud-read.js.map +1 -1
  133. package/dist/cloud-run-record.d.ts +27 -0
  134. package/dist/cloud-run-record.d.ts.map +1 -0
  135. package/dist/cloud-run-record.js +56 -0
  136. package/dist/cloud-run-record.js.map +1 -0
  137. package/dist/cloud-run.d.ts +2 -9
  138. package/dist/cloud-run.d.ts.map +1 -1
  139. package/dist/cloud-run.js +3 -20
  140. package/dist/cloud-run.js.map +1 -1
  141. package/dist/communication/local.d.ts.map +1 -1
  142. package/dist/communication/local.js +2 -1
  143. package/dist/communication/local.js.map +1 -1
  144. package/dist/communication/worker.d.ts +1 -1
  145. package/dist/communication/worker.d.ts.map +1 -1
  146. package/dist/communication/worker.js +13 -5
  147. package/dist/communication/worker.js.map +1 -1
  148. package/dist/compile.d.ts.map +1 -1
  149. package/dist/compile.js +28 -6
  150. package/dist/compile.js.map +1 -1
  151. package/dist/direct-input.d.ts.map +1 -1
  152. package/dist/direct-input.js +21 -1
  153. package/dist/direct-input.js.map +1 -1
  154. package/dist/failure-kinds.d.ts +96 -6
  155. package/dist/failure-kinds.d.ts.map +1 -1
  156. package/dist/failure-kinds.js +29 -2
  157. package/dist/failure-kinds.js.map +1 -1
  158. package/dist/flow-extension-compat.d.ts.map +1 -1
  159. package/dist/flow-extension-compat.js +26 -8
  160. package/dist/flow-extension-compat.js.map +1 -1
  161. package/dist/flow-extension-loader.d.ts +7 -0
  162. package/dist/flow-extension-loader.d.ts.map +1 -1
  163. package/dist/flow-extension-loader.js +48 -22
  164. package/dist/flow-extension-loader.js.map +1 -1
  165. package/dist/flow-requirements.d.ts.map +1 -1
  166. package/dist/flow-requirements.js +24 -9
  167. package/dist/flow-requirements.js.map +1 -1
  168. package/dist/fs-descriptor.d.ts +17 -0
  169. package/dist/fs-descriptor.d.ts.map +1 -0
  170. package/dist/fs-descriptor.js +96 -0
  171. package/dist/fs-descriptor.js.map +1 -0
  172. package/dist/gate-contract.d.ts +7 -0
  173. package/dist/gate-contract.d.ts.map +1 -1
  174. package/dist/gate-contract.js +6 -1
  175. package/dist/gate-contract.js.map +1 -1
  176. package/dist/helper-preflight.d.ts.map +1 -1
  177. package/dist/helper-preflight.js +10 -3
  178. package/dist/helper-preflight.js.map +1 -1
  179. package/dist/helper-reference.d.ts +25 -0
  180. package/dist/helper-reference.d.ts.map +1 -0
  181. package/dist/helper-reference.js +129 -0
  182. package/dist/helper-reference.js.map +1 -0
  183. package/dist/hosted-base-snapshot.d.ts +25 -0
  184. package/dist/hosted-base-snapshot.d.ts.map +1 -0
  185. package/dist/hosted-base-snapshot.js +278 -0
  186. package/dist/hosted-base-snapshot.js.map +1 -0
  187. package/dist/hosted-extension-declarations.d.ts +10 -0
  188. package/dist/hosted-extension-declarations.d.ts.map +1 -0
  189. package/dist/hosted-extension-declarations.js +272 -0
  190. package/dist/hosted-extension-declarations.js.map +1 -0
  191. package/dist/hosted-extension-isolation.d.ts +82 -0
  192. package/dist/hosted-extension-isolation.d.ts.map +1 -0
  193. package/dist/hosted-extension-isolation.js +322 -0
  194. package/dist/hosted-extension-isolation.js.map +1 -0
  195. package/dist/hosted-extension-manifest.d.ts +9 -0
  196. package/dist/hosted-extension-manifest.d.ts.map +1 -0
  197. package/dist/hosted-extension-manifest.js +235 -0
  198. package/dist/hosted-extension-manifest.js.map +1 -0
  199. package/dist/hosted-extension-protocol.d.ts +16 -0
  200. package/dist/hosted-extension-protocol.d.ts.map +1 -0
  201. package/dist/hosted-extension-protocol.js +224 -0
  202. package/dist/hosted-extension-protocol.js.map +1 -0
  203. package/dist/hosted-extension-runtime.d.ts +30 -0
  204. package/dist/hosted-extension-runtime.d.ts.map +1 -0
  205. package/dist/hosted-extension-runtime.js +167 -0
  206. package/dist/hosted-extension-runtime.js.map +1 -0
  207. package/dist/hosted-extension-sandbox-source.d.ts +9 -0
  208. package/dist/hosted-extension-sandbox-source.d.ts.map +1 -0
  209. package/dist/hosted-extension-sandbox-source.js +95 -0
  210. package/dist/hosted-extension-sandbox-source.js.map +1 -0
  211. package/dist/hosted-extension-sandbox.d.ts +39 -0
  212. package/dist/hosted-extension-sandbox.d.ts.map +1 -0
  213. package/dist/hosted-extension-sandbox.js +418 -0
  214. package/dist/hosted-extension-sandbox.js.map +1 -0
  215. package/dist/hosted-project.d.ts +3 -0
  216. package/dist/hosted-project.d.ts.map +1 -0
  217. package/dist/hosted-project.js +19 -0
  218. package/dist/hosted-project.js.map +1 -0
  219. package/dist/hosted-promise-safety.d.ts +12 -0
  220. package/dist/hosted-promise-safety.d.ts.map +1 -0
  221. package/dist/hosted-promise-safety.js +63 -0
  222. package/dist/hosted-promise-safety.js.map +1 -0
  223. package/dist/index.d.ts +3 -1
  224. package/dist/index.d.ts.map +1 -1
  225. package/dist/index.js +2 -0
  226. package/dist/index.js.map +1 -1
  227. package/dist/intrinsic-array.d.ts +3 -0
  228. package/dist/intrinsic-array.d.ts.map +1 -0
  229. package/dist/intrinsic-array.js +14 -0
  230. package/dist/intrinsic-array.js.map +1 -0
  231. package/dist/journal-client.d.ts +3 -1
  232. package/dist/journal-client.d.ts.map +1 -1
  233. package/dist/journal-client.js +17 -2
  234. package/dist/journal-client.js.map +1 -1
  235. package/dist/journal-projection.d.ts +16 -0
  236. package/dist/journal-projection.d.ts.map +1 -0
  237. package/dist/journal-projection.js +108 -0
  238. package/dist/journal-projection.js.map +1 -0
  239. package/dist/json-value.d.ts +6 -1
  240. package/dist/json-value.d.ts.map +1 -1
  241. package/dist/json-value.js +155 -36
  242. package/dist/json-value.js.map +1 -1
  243. package/dist/llm-worker.d.ts +9 -0
  244. package/dist/llm-worker.d.ts.map +1 -1
  245. package/dist/llm-worker.js +16 -2
  246. package/dist/llm-worker.js.map +1 -1
  247. package/dist/local-agent.d.ts +1 -1
  248. package/dist/local-agent.d.ts.map +1 -1
  249. package/dist/local-agent.js +4 -3
  250. package/dist/local-agent.js.map +1 -1
  251. package/dist/named-gate-lowering.js +35 -5
  252. package/dist/named-gate-lowering.js.map +1 -1
  253. package/dist/named-gate-preflight.d.ts +27 -0
  254. package/dist/named-gate-preflight.d.ts.map +1 -0
  255. package/dist/named-gate-preflight.js +47 -0
  256. package/dist/named-gate-preflight.js.map +1 -0
  257. package/dist/named-gates.d.ts +1 -1
  258. package/dist/named-gates.d.ts.map +1 -1
  259. package/dist/named-gates.js +3 -0
  260. package/dist/named-gates.js.map +1 -1
  261. package/dist/observer-link.d.ts +7 -0
  262. package/dist/observer-link.d.ts.map +1 -1
  263. package/dist/observer-link.js +3 -1
  264. package/dist/observer-link.js.map +1 -1
  265. package/dist/plugin-store.d.ts +8 -2
  266. package/dist/plugin-store.d.ts.map +1 -1
  267. package/dist/plugin-store.js +233 -56
  268. package/dist/plugin-store.js.map +1 -1
  269. package/dist/preflight.d.ts.map +1 -1
  270. package/dist/preflight.js +7 -0
  271. package/dist/preflight.js.map +1 -1
  272. package/dist/protocol.d.ts +2 -0
  273. package/dist/protocol.d.ts.map +1 -1
  274. package/dist/redact.d.ts +42 -0
  275. package/dist/redact.d.ts.map +1 -1
  276. package/dist/redact.js +128 -0
  277. package/dist/redact.js.map +1 -1
  278. package/dist/run-projection.d.ts +83 -0
  279. package/dist/run-projection.d.ts.map +1 -0
  280. package/dist/run-projection.js +199 -0
  281. package/dist/run-projection.js.map +1 -0
  282. package/dist/semver-range.d.ts.map +1 -1
  283. package/dist/semver-range.js +27 -15
  284. package/dist/semver-range.js.map +1 -1
  285. package/dist/shell-word.d.ts +19 -0
  286. package/dist/shell-word.d.ts.map +1 -0
  287. package/dist/shell-word.js +23 -0
  288. package/dist/shell-word.js.map +1 -0
  289. package/dist/spec.d.ts +52 -3
  290. package/dist/spec.d.ts.map +1 -1
  291. package/dist/spec.js.map +1 -1
  292. package/dist/step-fields.d.ts +1 -1
  293. package/dist/step-fields.js +1 -1
  294. package/dist/step-fields.js.map +1 -1
  295. package/dist/steps-green.d.ts +27 -0
  296. package/dist/steps-green.d.ts.map +1 -0
  297. package/dist/steps-green.js +164 -0
  298. package/dist/steps-green.js.map +1 -0
  299. package/dist/validate.d.ts.map +1 -1
  300. package/dist/validate.js +39 -8
  301. package/dist/validate.js.map +1 -1
  302. package/dist/worker-lease.d.ts +8 -1
  303. package/dist/worker-lease.d.ts.map +1 -1
  304. package/dist/worker-lease.js +34 -4
  305. package/dist/worker-lease.js.map +1 -1
  306. package/dist/worker.d.ts +8 -0
  307. package/dist/worker.d.ts.map +1 -1
  308. package/dist/worker.js +17 -9
  309. package/dist/worker.js.map +1 -1
  310. package/package.json +3 -7
  311. package/src/agent-artifacts.ts +2 -10
  312. package/src/agent-cwd.ts +139 -0
  313. package/src/artifact-scan-policy.ts +51 -0
  314. package/src/authored-completion-record.ts +119 -0
  315. package/src/authored-completion.ts +203 -0
  316. package/src/authored-flow-error.ts +13 -1
  317. package/src/authored-flow-executor.ts +101 -59
  318. package/src/authored-human.ts +1 -4
  319. package/src/authored-node-entry.ts +4 -1
  320. package/src/authored-node-runner.ts +96 -22
  321. package/src/authored-root.ts +20 -2
  322. package/src/authored-step-output.ts +139 -42
  323. package/src/authored-verdict.ts +87 -0
  324. package/src/authored-worker-step.ts +60 -12
  325. package/src/bundle-extensions.ts +6 -5
  326. package/src/bundle.ts +52 -7
  327. package/src/canonical.ts +20 -6
  328. package/src/cli/check-worker-surface.ts +66 -0
  329. package/src/cli/check.ts +35 -4
  330. package/src/cli/cloud-format.ts +82 -0
  331. package/src/cli/cloud-live.ts +366 -0
  332. package/src/cli/cloud-read.ts +13 -282
  333. package/src/cli/cloud-refusal.ts +113 -0
  334. package/src/cli/cloud-status-view.ts +213 -0
  335. package/src/cli/direct-run.ts +19 -2
  336. package/src/cli/hn-monitor.ts +2 -1
  337. package/src/cli/local-agent-remedy.ts +160 -0
  338. package/src/cli/observer-session.ts +107 -0
  339. package/src/cli/run.ts +233 -28
  340. package/src/cli/status.ts +59 -3
  341. package/src/cli/step-evidence.ts +310 -0
  342. package/src/cli/step-failure.ts +57 -81
  343. package/src/cli-commands.ts +2 -0
  344. package/src/cli.ts +64 -44
  345. package/src/cloud-read.ts +25 -1
  346. package/src/cloud-run-record.ts +64 -0
  347. package/src/cloud-run.ts +5 -24
  348. package/src/communication/local.ts +2 -1
  349. package/src/communication/worker.ts +13 -5
  350. package/src/compile.ts +28 -6
  351. package/src/direct-input.ts +22 -1
  352. package/src/failure-kinds.ts +104 -2
  353. package/src/flow-extension-compat.ts +32 -7
  354. package/src/flow-extension-loader.ts +56 -21
  355. package/src/flow-requirements.ts +24 -9
  356. package/src/fs-descriptor.ts +127 -0
  357. package/src/gate-contract.ts +13 -1
  358. package/src/helper-preflight.ts +10 -3
  359. package/src/helper-reference.ts +130 -0
  360. package/src/hosted-base-snapshot.ts +351 -0
  361. package/src/hosted-extension-declarations.ts +387 -0
  362. package/src/hosted-extension-isolation.ts +457 -0
  363. package/src/hosted-extension-manifest.ts +249 -0
  364. package/src/hosted-extension-protocol.ts +262 -0
  365. package/src/hosted-extension-runtime.ts +266 -0
  366. package/src/hosted-extension-sandbox-source.ts +94 -0
  367. package/src/hosted-extension-sandbox.ts +489 -0
  368. package/src/hosted-project.ts +18 -0
  369. package/src/hosted-promise-safety.ts +68 -0
  370. package/src/index.ts +20 -0
  371. package/src/intrinsic-array.ts +14 -0
  372. package/src/journal-client.ts +19 -2
  373. package/src/journal-projection.ts +106 -0
  374. package/src/json-value.ts +184 -32
  375. package/src/llm-worker.ts +16 -2
  376. package/src/local-agent.ts +4 -2
  377. package/src/named-gate-lowering.ts +35 -5
  378. package/src/named-gate-preflight.ts +47 -0
  379. package/src/named-gates.ts +3 -0
  380. package/src/observer-link.ts +10 -1
  381. package/src/plugin-store.ts +323 -56
  382. package/src/preflight.ts +7 -0
  383. package/src/protocol.ts +2 -0
  384. package/src/redact.ts +111 -0
  385. package/src/run-projection.ts +247 -0
  386. package/src/semver-range.ts +39 -14
  387. package/src/shell-word.ts +24 -0
  388. package/src/spec.ts +54 -3
  389. package/src/step-fields.ts +1 -1
  390. package/src/steps-green.ts +168 -0
  391. package/src/validate.ts +35 -8
  392. package/src/worker-lease.ts +35 -5
  393. package/src/worker.ts +27 -11
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Fold a run's journal entries, as `run.start {watch}` or `run.watch` pushes
3
+ * them, into a `RunProjection`: `run.spawned` opens it with the declared step
4
+ * graph, attempt starts and completions become step transitions, and
5
+ * `run.completed` closes it. Entries journaled before `liveSinceMs` (a resume
6
+ * replaying history) update the snapshot without publishing a message each.
7
+ */
8
+
9
+ import type { JournalEvent } from './journal-reader.js';
10
+ import type { ProgressEvent } from './progress.js';
11
+ import type { DeclaredStep, RunProjection, RunSnapshot } from './run-projection.js';
12
+ import type { StepType } from './spec.js';
13
+
14
+ export type OpenProjection = (run: { runId: string; flow: string; steps: DeclaredStep[] }) => RunProjection;
15
+
16
+ export function createJournalProjector(open: OpenProjection, liveSinceMs = 0): (entry: JournalEvent) => void {
17
+ let projection: RunProjection | undefined;
18
+ const types = new Map<string, StepType>();
19
+ const started = new Map<string, number>();
20
+ const parked = new Set<string>();
21
+
22
+ return entry => {
23
+ const payload = (entry.payload ?? {}) as Record<string, unknown>;
24
+ const live = entry.at_ms >= liveSinceMs;
25
+ if (entry.entry_type === 'run.spawned') {
26
+ if (projection !== undefined) return;
27
+ const spec = (payload['spec'] ?? {}) as { name?: unknown; steps?: unknown };
28
+ const steps = (Array.isArray(spec.steps) ? spec.steps : []).flatMap(declaredStep);
29
+ for (const step of steps) types.set(step.id, step.type);
30
+ projection = open({ runId: entry.run_id, flow: typeof spec.name === 'string' ? spec.name : 'flow', steps });
31
+ return;
32
+ }
33
+ if (projection === undefined) return;
34
+ if (entry.entry_type === 'epoch.summary') {
35
+ started.clear();
36
+ parked.clear();
37
+ const done = (payload['steps_done'] ?? {}) as Record<string, { completionReason?: string }>;
38
+ const pending = (payload['steps_open'] ?? {}) as Record<string, { state?: string; attempt?: number }>;
39
+ projection.epoch([
40
+ ...Object.entries(done).map(([id, value]) => ({ id,
41
+ state: value.completionReason === 'success' ? 'completed' as const : 'failed' as const,
42
+ completionReason: value.completionReason })),
43
+ ...Object.entries(pending).map(([id, value]) => ({ id, attempt: value.attempt,
44
+ state: value.state === 'running' ? 'running' as const : value.state === 'needs_human' ? 'parked' as const : 'pending' as const })),
45
+ ], live);
46
+ return;
47
+ }
48
+ if (entry.step_id === null && entry.entry_type !== 'run.completed') return;
49
+ const stepId = entry.step_id ?? '';
50
+ const stepType = types.get(stepId) ?? 'deterministic';
51
+ const attempt = entry.attempt ?? undefined;
52
+ const key = `${stepId}#${attempt ?? 0}`;
53
+ const event = (type: ProgressEvent['type'], completionReason?: string): void => {
54
+ const elapsedMs = type === 'step.started' ? 0 : entry.at_ms - (started.get(key) ?? entry.at_ms);
55
+ projection!.step({
56
+ type, stepId, stepType, elapsedMs,
57
+ ...(attempt === undefined ? {} : { attempt }),
58
+ ...(completionReason === undefined ? {} : { completionReason: completionReason as never }),
59
+ }, live);
60
+ };
61
+ switch (entry.entry_type) {
62
+ case 'step.attempt.started':
63
+ parked.delete(key);
64
+ started.set(key, entry.at_ms);
65
+ event('step.started');
66
+ return;
67
+ case 'wait.human':
68
+ if (parked.has(key)) return;
69
+ parked.add(key);
70
+ event('step.parked');
71
+ return;
72
+ case 'step.completed': {
73
+ const reason = typeof payload['completionReason'] === 'string' ? payload['completionReason'] : undefined;
74
+ const isParked = payload['disposition'] === 'park';
75
+ if (isParked && parked.has(key)) return;
76
+ if (isParked) parked.add(key);
77
+ event(isParked ? 'step.parked' : reason === 'success' ? 'step.completed' : 'step.failed', reason);
78
+ return;
79
+ }
80
+ case 'run.completed': {
81
+ // Historical outcomes belong to earlier epochs. The current resume's
82
+ // report closes the projection if no new terminal entry is appended.
83
+ if (!live) return;
84
+ const reason = typeof payload['completionReason'] === 'string' ? payload['completionReason'] : undefined;
85
+ projection.finish({ status: runStatus(reason), ...(reason === undefined ? {} : { completionReason: reason }) });
86
+ return;
87
+ }
88
+ }
89
+ };
90
+ }
91
+
92
+ function declaredStep(value: unknown): DeclaredStep[] {
93
+ if (typeof value !== 'object' || value === null) return [];
94
+ const step = value as { id?: unknown; type?: unknown; depends_on?: unknown };
95
+ if (typeof step.id !== 'string') return [];
96
+ const type = step.type === 'llm' || step.type === 'agent' ? step.type : 'deterministic';
97
+ const dependsOn = Array.isArray(step.depends_on)
98
+ ? step.depends_on.filter((id): id is string => typeof id === 'string') : [];
99
+ return [{ id: step.id, type, dependsOn }];
100
+ }
101
+
102
+ function runStatus(reason: string | undefined): RunSnapshot['status'] {
103
+ if (reason === 'success') return 'completed';
104
+ if (reason === 'canceled') return 'canceled';
105
+ return 'failed';
106
+ }
package/src/json-value.ts CHANGED
@@ -1,4 +1,38 @@
1
1
  import { isProxy } from 'node:util/types';
2
+ import { appendIntrinsicArray } from './intrinsic-array.js';
3
+
4
+ const ARRAY_IS_ARRAY = Array.isArray;
5
+ const ARRAY_PROTOTYPE = Array.prototype;
6
+ const ERROR = Error;
7
+ const IS_PROXY = isProxy;
8
+ const JSON_STRINGIFY = JSON.stringify;
9
+ const NUMBER = Number;
10
+ const NUMBER_IS_FINITE = Number.isFinite;
11
+ const NUMBER_IS_INTEGER = Number.isInteger;
12
+ const OBJECT_CREATE = Object.create;
13
+ const OBJECT_DEFINE_PROPERTY = Object.defineProperty;
14
+ const OBJECT_FREEZE = Object.freeze;
15
+ const OBJECT_GET_OWN_PROPERTY_DESCRIPTOR = Object.getOwnPropertyDescriptor;
16
+ const OBJECT_GET_PROTOTYPE_OF = Object.getPrototypeOf;
17
+ const OBJECT_HAS_OWN = Object.hasOwn;
18
+ const OBJECT_PROTOTYPE = Object.prototype;
19
+ const REGEXP_TEST = Function.prototype.call.bind(RegExp.prototype.test) as (
20
+ regexp: RegExp, value: string,
21
+ ) => boolean;
22
+ const STRING = String;
23
+ const STRING_CHAR_CODE_AT = Function.prototype.call.bind(String.prototype.charCodeAt) as (
24
+ value: string, index: number,
25
+ ) => number;
26
+ const WEAK_SET = WeakSet;
27
+ const WEAK_SET_ADD = Function.prototype.call.bind(WeakSet.prototype.add) as <T extends object>(
28
+ set: WeakSet<T>, value: T,
29
+ ) => WeakSet<T>;
30
+ const WEAK_SET_DELETE = Function.prototype.call.bind(WeakSet.prototype.delete) as <T extends object>(
31
+ set: WeakSet<T>, value: T,
32
+ ) => boolean;
33
+ const WEAK_SET_HAS = Function.prototype.call.bind(WeakSet.prototype.has) as <T extends object>(
34
+ set: WeakSet<T>, value: T,
35
+ ) => boolean;
2
36
 
3
37
  export type JsonValue =
4
38
  | null
@@ -8,15 +42,45 @@ export type JsonValue =
8
42
  | JsonValue[]
9
43
  | { [key: string]: JsonValue };
10
44
 
45
+ export interface JsonSnapshotLimits {
46
+ readonly maxDepth: number;
47
+ readonly maxNodes: number;
48
+ readonly maxBytes: number;
49
+ }
50
+
51
+ interface SnapshotBudget {
52
+ readonly limits?: JsonSnapshotLimits;
53
+ nodes: number;
54
+ properties: number;
55
+ bytes: number;
56
+ }
57
+
11
58
  /** Copy runtime input into frozen, behavior-free JSON data. */
12
- export function snapshotJsonValue(value: unknown, at: string): JsonValue {
13
- return snapshot(value, at, new WeakSet<object>());
59
+ export function snapshotJsonValue(value: unknown, at: string, limits?: JsonSnapshotLimits): JsonValue {
60
+ return snapshot(value, at, new WEAK_SET<object>(), { limits, nodes: 0, properties: 0, bytes: 0 }, 0);
14
61
  }
15
62
 
16
- function snapshot(value: unknown, at: string, ancestors: WeakSet<object>): JsonValue {
17
- if (value === null || typeof value === 'string' || typeof value === 'boolean') return value;
63
+ function snapshot(
64
+ value: unknown,
65
+ at: string,
66
+ ancestors: WeakSet<object>,
67
+ budget: SnapshotBudget,
68
+ depth: number,
69
+ ): JsonValue {
70
+ consumeNode(budget, at, depth);
71
+ if (typeof value === 'string') {
72
+ consumeStringBytes(budget, value, at);
73
+ return value;
74
+ }
75
+ if (value === null || typeof value === 'boolean') {
76
+ consumeBytes(budget, value === null ? 4 : value ? 4 : 5, at);
77
+ return value;
78
+ }
18
79
  if (typeof value === 'number') {
19
- if (Number.isFinite(value)) return value;
80
+ if (NUMBER_IS_FINITE(value)) {
81
+ consumeBytes(budget, STRING(value).length, at);
82
+ return value;
83
+ }
20
84
  throw nonJson(at, 'numbers must be finite');
21
85
  }
22
86
  if (typeof value !== 'object') {
@@ -25,15 +89,15 @@ function snapshot(value: unknown, at: string, ancestors: WeakSet<object>): JsonV
25
89
  // Every ordinary reflective operation on a Proxy can execute author code.
26
90
  // Node and Bun expose this trap-free brand check, so reject before touching
27
91
  // its prototype, keys, descriptors, or identity collection.
28
- if (isProxy(value)) throw nonJson(at, 'Proxy objects are not allowed');
29
- if (ancestors.has(value)) throw nonJson(at, 'cycles are not allowed');
30
- ancestors.add(value);
92
+ if (IS_PROXY(value)) throw nonJson(at, 'Proxy objects are not allowed');
93
+ if (WEAK_SET_HAS(ancestors, value)) throw nonJson(at, 'cycles are not allowed');
94
+ WEAK_SET_ADD(ancestors, value);
31
95
  try {
32
- return Array.isArray(value)
33
- ? snapshotArray(value, at, ancestors)
34
- : snapshotObject(value, at, ancestors);
96
+ return ARRAY_IS_ARRAY(value)
97
+ ? snapshotArray(value, at, ancestors, budget, depth)
98
+ : snapshotObject(value, at, ancestors, budget, depth);
35
99
  } finally {
36
- ancestors.delete(value);
100
+ WEAK_SET_DELETE(ancestors, value);
37
101
  }
38
102
  }
39
103
 
@@ -41,53 +105,88 @@ function snapshotArray(
41
105
  value: unknown[],
42
106
  at: string,
43
107
  ancestors: WeakSet<object>,
108
+ budget: SnapshotBudget,
109
+ depth: number,
44
110
  ): JsonValue[] {
45
- const keys = Reflect.ownKeys(value);
46
- for (const key of keys) {
47
- if (key === 'length') continue;
48
- if (typeof key !== 'string' || !isArrayIndex(key, value.length)) {
111
+ if (OBJECT_GET_PROTOTYPE_OF(value) !== ARRAY_PROTOTYPE
112
+ || OBJECT_GET_PROTOTYPE_OF(ARRAY_PROTOTYPE) !== OBJECT_PROTOTYPE) {
113
+ throw nonJson(at, 'only arrays with the intrinsic prototype are allowed');
114
+ }
115
+ consumeBytes(budget, 2, at);
116
+ if (budget.limits !== undefined && value.length > budget.limits.maxNodes - budget.nodes) {
117
+ throw nonJson(at, 'snapshot depth or node limit exceeded');
118
+ }
119
+ // `Reflect.ownKeys` allocates the complete key list before a caller can
120
+ // enforce cardinality. Enumerate JSON-visible keys one at a time instead.
121
+ // Symbols and non-enumerable properties are deliberately ignored, matching
122
+ // JSON.stringify; the copied value has neither, so they cannot affect later
123
+ // validation or serialization.
124
+ for (const key in value) {
125
+ consumeProperty(budget, at);
126
+ if (!OBJECT_HAS_OWN(value, key)) continue;
127
+ if (!isArrayIndex(key, value.length)) {
49
128
  throw nonJson(at, 'arrays may contain only indexed data');
50
129
  }
51
130
  }
52
131
  const out: JsonValue[] = [];
53
132
  for (let index = 0; index < value.length; index += 1) {
54
- const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
133
+ if (index > 0) consumeBytes(budget, 1, at);
134
+ const descriptor = OBJECT_GET_OWN_PROPERTY_DESCRIPTOR(value, STRING(index));
55
135
  if (descriptor === undefined) throw nonJson(`${at}[${index}]`, 'array holes are not allowed');
56
- out.push(snapshotDescriptor(descriptor, `${at}[${index}]`, ancestors));
136
+ appendIntrinsicArray(out, snapshotDescriptor(descriptor, `${at}[${index}]`, ancestors, budget, depth + 1));
57
137
  }
58
- return Object.freeze(out) as unknown as JsonValue[];
138
+ // JSON.stringify consults `toJSON` before applying array semantics. Shadow
139
+ // any poisoned Array.prototype hook with inert, non-JSON-visible data while
140
+ // retaining the intrinsic prototype expected by downstream array consumers.
141
+ OBJECT_DEFINE_PROPERTY(out, 'toJSON', { value: undefined });
142
+ return OBJECT_FREEZE(out) as unknown as JsonValue[];
59
143
  }
60
144
 
61
145
  function snapshotObject(
62
146
  value: object,
63
147
  at: string,
64
148
  ancestors: WeakSet<object>,
149
+ budget: SnapshotBudget,
150
+ depth: number,
65
151
  ): { [key: string]: JsonValue } {
66
- const prototype = Object.getPrototypeOf(value);
67
- if (prototype !== Object.prototype && prototype !== null) {
152
+ const prototype = OBJECT_GET_PROTOTYPE_OF(value);
153
+ if (prototype !== OBJECT_PROTOTYPE && prototype !== null) {
68
154
  throw nonJson(at, 'only plain objects are allowed');
69
155
  }
70
- const out = Object.create(null) as { [key: string]: JsonValue };
71
- for (const key of Reflect.ownKeys(value)) {
72
- if (typeof key !== 'string') throw nonJson(at, 'symbol keys are not allowed');
156
+ consumeBytes(budget, 2, at);
157
+ const out = OBJECT_CREATE(null) as { [key: string]: JsonValue };
158
+ let included = 0;
159
+ // Avoid materializing an attacker-sized key array in the unsandboxed
160
+ // parent. This visits only JSON-visible own string properties; symbols and
161
+ // non-enumerable properties are omitted exactly as JSON.stringify omits
162
+ // them, and cannot influence the null-prototype copy.
163
+ for (const key in value) {
164
+ consumeProperty(budget, at);
165
+ if (!OBJECT_HAS_OWN(value, key)) continue;
73
166
  const childAt = propertyPath(at, key);
74
- const descriptor = Object.getOwnPropertyDescriptor(value, key);
167
+ const descriptor = OBJECT_GET_OWN_PROPERTY_DESCRIPTOR(value, key);
75
168
  if (descriptor === undefined) throw nonJson(childAt, 'missing property descriptor');
76
169
  const child = descriptorValue(descriptor, childAt);
77
170
  // JSON.stringify and the pre-existing compiler omit undefined object
78
171
  // optionals. Arrays remain strict because undefined there becomes null.
79
172
  if (child === undefined) continue;
80
- out[key] = snapshot(child, childAt, ancestors);
173
+ if (included > 0) consumeBytes(budget, 1, at);
174
+ consumeStringBytes(budget, key, childAt);
175
+ consumeBytes(budget, 1, childAt);
176
+ out[key] = snapshot(child, childAt, ancestors, budget, depth + 1);
177
+ included += 1;
81
178
  }
82
- return Object.freeze(out);
179
+ return OBJECT_FREEZE(out);
83
180
  }
84
181
 
85
182
  function snapshotDescriptor(
86
183
  descriptor: PropertyDescriptor,
87
184
  at: string,
88
185
  ancestors: WeakSet<object>,
186
+ budget: SnapshotBudget,
187
+ depth: number,
89
188
  ): JsonValue {
90
- return snapshot(descriptorValue(descriptor, at), at, ancestors);
189
+ return snapshot(descriptorValue(descriptor, at), at, ancestors, budget, depth);
91
190
  }
92
191
 
93
192
  function descriptorValue(descriptor: PropertyDescriptor, at: string): unknown {
@@ -97,14 +196,67 @@ function descriptorValue(descriptor: PropertyDescriptor, at: string): unknown {
97
196
  }
98
197
 
99
198
  function isArrayIndex(key: string, length: number): boolean {
100
- const index = Number(key);
101
- return Number.isInteger(index) && index >= 0 && index < length && String(index) === key;
199
+ const index = NUMBER(key);
200
+ return NUMBER_IS_INTEGER(index) && index >= 0 && index < length && STRING(index) === key;
102
201
  }
103
202
 
104
203
  function propertyPath(at: string, key: string): string {
105
- return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? `${at}.${key}` : `${at}[${JSON.stringify(key)}]`;
204
+ if (key.length > 100) return `${at}[long property]`;
205
+ return REGEXP_TEST(/^[A-Za-z_$][A-Za-z0-9_$]*$/, key)
206
+ ? `${at}.${key}`
207
+ : `${at}[${JSON_STRINGIFY(key)}]`;
208
+ }
209
+
210
+ function consumeNode(budget: SnapshotBudget, at: string, depth: number): void {
211
+ budget.nodes += 1;
212
+ if (budget.limits !== undefined
213
+ && (depth > budget.limits.maxDepth || budget.nodes > budget.limits.maxNodes)) {
214
+ throw nonJson(at, 'snapshot depth or node limit exceeded');
215
+ }
216
+ }
217
+
218
+ function consumeProperty(budget: SnapshotBudget, at: string): void {
219
+ if (budget.limits === undefined) return;
220
+ budget.properties += 1;
221
+ if (budget.properties > budget.limits.maxNodes) {
222
+ throw nonJson(at, 'snapshot depth or node limit exceeded');
223
+ }
224
+ }
225
+
226
+ /** Count JSON's UTF-8 string encoding without allocating the escaped value. */
227
+ function consumeStringBytes(budget: SnapshotBudget, value: string, at: string): void {
228
+ consumeBytes(budget, 2, at);
229
+ if (budget.limits !== undefined && value.length > budget.limits.maxBytes - budget.bytes) {
230
+ throw nonJson(at, 'snapshot byte limit exceeded');
231
+ }
232
+ for (let index = 0; index < value.length; index += 1) {
233
+ const code = STRING_CHAR_CODE_AT(value, index);
234
+ if (code === 0x22 || code === 0x5c || code === 0x08 || code === 0x09
235
+ || code === 0x0a || code === 0x0c || code === 0x0d) {
236
+ consumeBytes(budget, 2, at);
237
+ } else if (code < 0x20 || (code >= 0xd800 && code <= 0xdfff)) {
238
+ if (code >= 0xd800 && code <= 0xdbff && index + 1 < value.length) {
239
+ const low = STRING_CHAR_CODE_AT(value, index + 1);
240
+ if (low >= 0xdc00 && low <= 0xdfff) {
241
+ consumeBytes(budget, 4, at);
242
+ index += 1;
243
+ continue;
244
+ }
245
+ }
246
+ consumeBytes(budget, 6, at);
247
+ } else if (code < 0x80) consumeBytes(budget, 1, at);
248
+ else if (code < 0x800) consumeBytes(budget, 2, at);
249
+ else consumeBytes(budget, 3, at);
250
+ }
251
+ }
252
+
253
+ function consumeBytes(budget: SnapshotBudget, bytes: number, at: string): void {
254
+ budget.bytes += bytes;
255
+ if (budget.limits !== undefined && budget.bytes > budget.limits.maxBytes) {
256
+ throw nonJson(at, 'snapshot byte limit exceeded');
257
+ }
106
258
  }
107
259
 
108
260
  function nonJson(at: string, detail: string): Error {
109
- return new Error(`${at}: expected JSON-compatible data; ${detail}`);
261
+ return new ERROR(`${at}: expected JSON-compatible data; ${detail}`);
110
262
  }
package/src/llm-worker.ts CHANGED
@@ -46,7 +46,7 @@ export class LlmWorker extends EventEmitter {
46
46
 
47
47
  private readonly onDispatch = (dispatch: StepDispatchEvent): void => {
48
48
  if (this.closing || dispatch.step_type !== 'llm') return;
49
- const running = this.execute(dispatch).catch(error => { this.emit('error', error); });
49
+ const running = this.execute(dispatch).catch(error => { this.emit('error', error, dispatch); });
50
50
  this.inFlight.add(running);
51
51
  void running.finally(() => { this.inFlight.delete(running); });
52
52
  };
@@ -67,7 +67,7 @@ export class LlmWorker extends EventEmitter {
67
67
  let detail = result.stderr_tail;
68
68
  if (reason === 'success' && schema !== undefined) {
69
69
  try {
70
- output = JSON.parse(result.stdout_tail);
70
+ output = JSON.parse(unfenced(result.stdout_tail));
71
71
  const invalid = jsonSchemaOutputError(schema, output);
72
72
  if (invalid !== undefined) {
73
73
  reason = 'verification_failed';
@@ -98,3 +98,17 @@ export class LlmWorker extends EventEmitter {
98
98
  });
99
99
  }
100
100
  }
101
+
102
+ /**
103
+ * A reply that is exactly one markdown code fence around a value, reduced to
104
+ * that value. Models add the fence despite the instruction not to; the schema
105
+ * still judges what is inside it. A fence opens with three or more backticks
106
+ * or tildes and closes with a run of the same character at least as long.
107
+ * Anything else (prose around the JSON, two fences) is returned unchanged and
108
+ * fails the parse as before.
109
+ */
110
+ export function unfenced(text: string): string {
111
+ const fence = /^\s*(?:(`{3,})[^`\n]*\n([\s\S]*?)\n?\1`*|(~{3,})[^\n]*\n([\s\S]*?)\n?\3~*)\s*$/.exec(text);
112
+ if (fence === null) return text;
113
+ return fence[2] ?? fence[4]!;
114
+ }
@@ -1,3 +1,4 @@
1
+ import { onWorkerFailure } from './worker-lease.js';
1
2
  import { randomUUID } from 'node:crypto';
2
3
  import type { JournalClient } from './journal-client.js';
3
4
  import { AgentWorker } from './worker.js';
@@ -10,6 +11,7 @@ export async function attachLocalAgent(
10
11
  onPtyReady?: (path: string) => void,
11
12
  requestedStream?: string,
12
13
  capacity: number = DEFAULT_LOCAL_AGENT_CAPACITY,
14
+ runRoot?: string,
13
15
  ): Promise<{
14
16
  stream: string;
15
17
  readonly failure: unknown;
@@ -24,12 +26,12 @@ export async function attachLocalAgent(
24
26
  // second parking behind the first. Authored bodies size their admission to
25
27
  // this same number (worker-slots.ts), so they never ask for more.
26
28
  capacity,
27
- dataDir, onPtyReady,
29
+ dataDir, onPtyReady, runRoot,
28
30
  pins: { workspace: [], streams: [{ stream, read_offset: 0 }] },
29
31
  });
30
32
  let failure: unknown;
31
33
  // The cause travels with the close, so the flow's next request names it.
32
- worker.on('error', error => { failure = error; client.close(error); });
34
+ worker.on('error', onWorkerFailure('local-agent', error => { failure = error; client.close(error); }));
33
35
  await worker.attach();
34
36
  return {
35
37
  stream,
@@ -71,19 +71,33 @@ process.exit(artifacts.includes(${JSON.stringify(gate.path)})?0:1);`)}`;
71
71
  }
72
72
  const path = gate.type === 'subprocess_gate' ? gate.from_output
73
73
  : gate.type === 'word_count_bounds' ? undefined : gate.in_output_at;
74
+ const pathKey = gate.type === 'subprocess_gate' ? 'from_output' : 'in_output_at';
74
75
  // Only compiler-owned code is serialized. Author strings are JSON literals;
75
76
  // upstream output travels exclusively through FLOWS_INPUT, never shell text.
77
+ //
78
+ // `fail` answers a gate that exits 1 having said nothing. Every exit that is
79
+ // NOT the author's own predicate or command verdict goes through it: a
80
+ // selection that missed, input the child cannot carry, a child that never
81
+ // started, a child killed by a signal. It writes ONE bounded line to fd 2
82
+ // and nothing to fd 1 — `references_input` and `word_count_bounds` verify
83
+ // against stdout, so a stray byte there would change a verdict.
84
+ //
85
+ // `writeSync` rather than `process.stderr.write`: on a pipe the latter is
86
+ // asynchronous, and the `process.exit` on the same line would drop the
87
+ // diagnostic exactly when it is the only account of the failure there is.
76
88
  const setup = `const cp=require('node:child_process');
89
+ const fail=m=>{require('node:fs').writeSync(2,${JSON.stringify(`${gate.type}: `)}+m.replace(/[\\r\\n]+/g,' ').slice(0,400)+'\\n');process.exit(1);};
77
90
  const input=JSON.parse(process.env.FLOWS_INPUT);
78
91
  let value=input.output;
79
92
  const path=${JSON.stringify(path ?? null)};
80
93
  if(path!==null){for(const key of path){
81
94
  if(value===null||typeof value!=='object'||!Object.hasOwn(value,key)||
82
- (typeof key==='number'?!Array.isArray(value):Array.isArray(value)))process.exit(1);
95
+ (typeof key==='number'?!Array.isArray(value):Array.isArray(value)))
96
+ fail(${JSON.stringify(`${pathKey} ${JSON.stringify(path ?? null)} selected nothing: no `)}+JSON.stringify(key)+' at that position in the producer output');
83
97
  value=value[key];
84
98
  }}else if(${deterministic})value=value.stdout_tail;
85
99
  const text=typeof value==='string'?value:JSON.stringify(value);
86
- if(typeof text!=='string')process.exit(1);
100
+ if(typeof text!=='string')fail('the selected value could not be read as text');
87
101
  `;
88
102
  let body: string;
89
103
  switch (gate.type) {
@@ -93,16 +107,32 @@ if(typeof reference!=='string'||reference.length===0||!text.includes(reference))
93
107
  process.stdout.write('references_input:pass');`;
94
108
  break;
95
109
  case 'subprocess_gate':
96
- body = `if(text.includes('\\0'))process.exit(1);
110
+ // `stdio: 'inherit'` hands the child the gate step's own stdout/stderr,
111
+ // which ARE the kernel's capture pipes (relayflowd/src/exec_det.rs:78),
112
+ // so the command's output is journaled as it is produced — including
113
+ // whatever it wrote before a timeout killed the process group. Buffering
114
+ // it for a post-wait flush would lose exactly that. What was missing is
115
+ // below: `status` is null for every outcome that is not an exit, and all
116
+ // of them used to collapse into an indistinguishable bare `exit 1`.
117
+ body = `if(text.includes('\\0'))fail('the selected text contains a NUL byte and cannot be passed to the gate command');
97
118
  const result=cp.spawnSync('/bin/sh',['-c',${JSON.stringify(gate.command)}],{
98
119
  env:{...process.env,INPUT:text},stdio:'inherit'});
120
+ if(result.error)fail('could not run the gate command: '+(result.error.code||result.error.message)+' (the selected input was '+Buffer.byteLength(text)+' bytes)');
121
+ if(result.signal)fail('the gate command was terminated by '+result.signal);
99
122
  process.exit(result.status===0?0:1);`;
100
123
  break;
101
124
  case 'word_count_bounds':
125
+ // The other gate that spawns a child. Its `wc` stderr was piped and then
126
+ // discarded, so a broken or absent `wc` was reported as a word count out
127
+ // of bounds. A bounded suffix of what it said travels with the cause.
102
128
  body = `const result=cp.spawnSync('wc',['-w'],{input:text,encoding:'utf8',env:{...process.env,LC_ALL:'C'}});
103
- if(result.status!==0)process.exit(1);
129
+ const noise=(result.stderr||'').trim().slice(-200);
130
+ const said=noise===''?'':': '+noise;
131
+ if(result.error)fail('could not run wc -w: '+(result.error.code||result.error.message)+said);
132
+ if(result.signal)fail('wc -w was terminated by '+result.signal+said);
133
+ if(result.status!==0)fail('wc -w exited '+result.status+said);
104
134
  const count=result.stdout.trim();
105
- if(!/^[0-9]+$/.test(count))process.exit(1);
135
+ if(!/^[0-9]+$/.test(count))fail('wc -w printed '+JSON.stringify(count.slice(0,80))+' instead of a word count'+said);
106
136
  process.stdout.write(BigInt(count).toString());`;
107
137
  break;
108
138
  case 'regex_match':
@@ -0,0 +1,47 @@
1
+ import { unscannedArtifactPrefix } from './artifact-scan-policy.js';
2
+ import { isNamedGate } from './named-gates.js';
3
+ import type { StepSpec } from './spec.js';
4
+ import type { PreflightWarning } from './preflight.js';
5
+
6
+ /**
7
+ * Static named-gate scan coverage, decided from the compiled snapshot alone.
8
+ *
9
+ * An `artifact_exists` gate lowers to a deterministic step that asks whether
10
+ * the producer's journaled `output.artifacts` list contains the literal path
11
+ * (`named-gate-lowering.ts`). One writer of that list is the bundled agent
12
+ * worker's filesystem scan, which walks the step's cwd under the policy in
13
+ * `artifact-scan-policy.ts` and records nothing under a dot-named or
14
+ * `node_modules` entry. A path inside one of those prefixes can never come
15
+ * from that scan, on any host, in any environment.
16
+ *
17
+ * A warning rather than a refusal, because the scan is not the only writer.
18
+ * The same bundled worker promotes object-shaped JSON stdout — and a completed
19
+ * Relay task's output — straight to the journaled `output` (`worker.ts`), so
20
+ * an agent that reports its own `artifacts` array makes a hidden path pass;
21
+ * the journal protocol lets a custom worker do the same. Which of those an
22
+ * `agent` step will take is not decidable from the spec, so this reports the
23
+ * scan's limitation and leaves the verdict to the run rather than declaring a
24
+ * satisfiable gate impossible.
25
+ *
26
+ * Temporary: this describes #513. When the scan stops excluding these paths,
27
+ * delete this module and its call site.
28
+ */
29
+ export function namedGateScanCoverageDiagnostics(step: StepSpec): PreflightWarning[] {
30
+ const gate = step.verification;
31
+ if (!isNamedGate(gate) || gate.type !== 'artifact_exists') return [];
32
+ // A malformed path is already `gate_path_invalid` from compilation; only
33
+ // validated relative POSIX paths reach here.
34
+ const prefix = unscannedArtifactPrefix(gate.path);
35
+ if (prefix === undefined) return [];
36
+ return [{
37
+ severity: 'warning',
38
+ kind: 'gate_path_unscanned',
39
+ stepId: step.id,
40
+ message: `Step "${step.id}" gates on artifact_exists path "${gate.path}", but the bundled agent worker's `
41
+ + `artifact scan records nothing under "${prefix}": it skips entries named `
42
+ + '".git", ".relayflowd" and "node_modules". The gate reads the journaled output.artifacts and never the disk, so '
43
+ + 'writing the file is not enough: the step has to report the path itself — as object-shaped JSON stdout '
44
+ + 'carrying its own "artifacts" array, as a completed Relay task output, or from a custom worker. If the '
45
+ + 'gate is meant to rest on the scan, write the artifact to a path the scan records.',
46
+ }];
47
+ }
@@ -3,6 +3,9 @@ import type { NamedDataGate, VerificationSpec } from './spec.js';
3
3
 
4
4
  export const NAMED_GATE_FAILURE_KINDS = [
5
5
  'unknown_gate_kind', 'gate_pattern_invalid', 'gate_command_missing', 'gate_bound_invalid', 'gate_path_invalid',
6
+ // steps_green refusals: the gate is declarative like the named gates above,
7
+ // so it reports through the same kind channel a caller already switches on.
8
+ 'gate_host_unsupported', 'gate_ids_invalid', 'gate_source_unsupported',
6
9
  ] as const;
7
10
  export type NamedGateFailureKind = typeof NAMED_GATE_FAILURE_KINDS[number];
8
11
 
@@ -238,6 +238,13 @@ export interface MintObserverOptions {
238
238
  * (default `https://agentrelay.com`). Separate axis from `baseUrl` — the
239
239
  * mint API lives on a different subdomain from the dashboard. */
240
240
  dashboardUrl?: string;
241
+ /**
242
+ * Scope the token to this one channel, so the dashboard opens on it — the
243
+ * run's `wf-<runId>` channel — rather than on whichever channel the
244
+ * workspace lists first. DMs are excluded: a run projects its
245
+ * conversation into that channel. Omitted: the whole workspace.
246
+ */
247
+ channel?: string;
241
248
  fetch?: ObserverFetch;
242
249
  now?: () => number;
243
250
  /** Called for the token's uniquely-suffixed name; injectable for tests. */
@@ -299,7 +306,9 @@ export async function mintObserverUrl(
299
306
  name: `flows-run-${uuid}`,
300
307
  description: 'Auto-minted by `flows run` for the observer dashboard link.',
301
308
  scopes: OBSERVER_SCOPES,
302
- filters: { include_dms: true },
309
+ filters: options.channel === undefined
310
+ ? { include_dms: true }
311
+ : { channel_names: [options.channel], include_dms: false },
303
312
  expires_at: new Date(now() + OBSERVER_TOKEN_TTL_MS).toISOString(),
304
313
  };
305
314