@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
package/src/cli.ts CHANGED
@@ -7,6 +7,8 @@ import { describeFlowRequirements } from './flow-requirements.js';
7
7
  import type { CliModelSource } from './cli-adapter.js';
8
8
 
9
9
  import { renderProgress, type ProgressEvent } from './progress.js';
10
+ import type { JournalEvent } from './journal-reader.js';
11
+ import { createObserverSession } from './cli/observer-session.js';
10
12
  import { realpathSync } from 'node:fs';
11
13
  import { pathToFileURL } from 'node:url';
12
14
  import {
@@ -31,6 +33,7 @@ import {
31
33
  parseLogsArgs, parseRunsArgs, runCloudLogsCli, runCloudRunsCli, runCloudStatusCli,
32
34
  type LogsArgs, type RunsArgs,
33
35
  } from './cli/cloud-read.js';
36
+ import { runCloudLogsFollow, runCloudStatusWatch } from './cli/cloud-live.js';
34
37
  import { transcriptTailSource } from './transcript-tail.js';
35
38
  import { checkTypeScriptFlow } from './cli/check-typescript.js';
36
39
  import { runCloudCli } from './cli/cloud-run.js';
@@ -57,6 +60,16 @@ export type { CheckInputDiagnostic, CheckReport } from './cli/check.js';
57
60
  export interface CliIo {
58
61
  stdout(line: string): void;
59
62
  stderr(line: string): void;
63
+ /**
64
+ * True when stdout is an interactive terminal.
65
+ *
66
+ * Only the live views read it, and only to decide whether a redraw may use
67
+ * ANSI control sequences: `flows status --cloud --watch` clears the screen
68
+ * for a terminal and appends whole pages when its output is redirected or
69
+ * mounted in a host that renders text. Absent means "not a terminal", so an
70
+ * embedder that says nothing gets the safe form.
71
+ */
72
+ tty?: boolean;
60
73
  }
61
74
 
62
75
  type CliExitCode = 0 | 1 | 2 | 3;
@@ -123,9 +136,9 @@ const USAGE = [
123
136
  'flows answer [--json] [--no-spawn] [--data-dir <dir>] [--note <text>] [--by <identity>] <run-id> <wait-id> <yes|no>',
124
137
  'flows replay [--allow-human-influenced] [--json] [--data-dir <dir>] <run-id> [--at <step-id>]',
125
138
  'flows status [--json] [--data-dir <dir>] [--tail <n>] [<run-id>]',
126
- 'flows status --cloud [--json] <run-id>',
139
+ 'flows status --cloud [--json] [--watch] <run-id>',
127
140
  'flows runs [--limit <n>] [--json]',
128
- 'flows logs [--step <name>] [--raw] [--json] <run-id>',
141
+ 'flows logs [--step <name>] [--raw] [--json] [--follow] <run-id>',
129
142
  'flows observer [--data-dir <dir>]',
130
143
  'flows hn-monitor start [--data-dir <dir>] [--poll-interval-ms <n>] <spec.json>',
131
144
  ].join('\n');
@@ -143,6 +156,7 @@ function spawnAllowedByEnv(env: NodeJS.ProcessEnv = process.env): boolean {
143
156
  const PROCESS_IO: CliIo = {
144
157
  stdout: (line) => process.stdout.write(`${line}\n`),
145
158
  stderr: (line) => process.stderr.write(`${line}\n`),
159
+ tty: process.stdout.isTTY === true,
146
160
  };
147
161
 
148
162
  /** Optional knobs for an embedded caller. `bin/flows.js` passes none. */
@@ -222,13 +236,21 @@ export async function runCli(
222
236
  // works inside a step of a run whose daemon is gone (kernel/DAEMON-LIFECYCLE.md §4).
223
237
  if (parsed.command === 'status') {
224
238
  // One verb, two sources. `--cloud` never reaches `runStatus`, so the
225
- // offline reader stays offline (cli/status.ts).
226
- return parsed.cloud === true
227
- ? runCloudStatusCli(parsed, io)
228
- : runStatus(parsed, io, { tails: transcriptTailSource() });
239
+ // offline reader stays offline (cli/status.ts). Only `--watch` blocks, so
240
+ // only `--watch` takes a signal: a one-shot read keeps installing none.
241
+ if (parsed.cloud !== true) return runStatus(parsed, io, { tails: transcriptTailSource() });
242
+ const cloudStatus = parsed;
243
+ return cloudStatus.watch === true
244
+ ? withInterrupt(options.signal, (signal) => runCloudStatusWatch(cloudStatus, io, { signal }))
245
+ : runCloudStatusCli(cloudStatus, io);
229
246
  }
230
247
  if (parsed.command === 'runs') return runCloudRunsCli(parsed, io);
231
- if (parsed.command === 'logs') return runCloudLogsCli(parsed, io);
248
+ if (parsed.command === 'logs') {
249
+ const logs = parsed;
250
+ return logs.follow
251
+ ? withInterrupt(options.signal, (signal) => runCloudLogsFollow(logs, io, { signal }))
252
+ : runCloudLogsCli(logs, io);
253
+ }
232
254
  if (parsed.command === 'answer') {
233
255
  const execution = await answerFlow(parsed.runId, parsed.waitId, parsed.answer, parsed.dataDir, {
234
256
  ...(parsed.note === undefined ? {} : { note: parsed.note }),
@@ -250,8 +272,13 @@ export async function runCli(
250
272
  // refuses `--data-dir` on `check`, so there is no data dir to attach to.
251
273
  // `flows check` keeps working with no daemon, no relayflowd binary and no
252
274
  // data directory at all -- a property worth keeping, not an omission.
275
+ // Only this invocation opts into `agent_worker_unresolved`: `flows check`
276
+ // attaches no worker and, being daemon-free, cannot see one attached
277
+ // elsewhere. The authored `.flow.ts` path checks header declarations
278
+ // without compiling steps, so it has no agent steps to count.
253
279
  const checked = /\.(?:[cm]?[jt]s)$/.test(parsed.value)
254
- ? await checkAuthoredFlowComposed(parsed.value) : checkFlow(parsed.value);
280
+ ? await checkAuthoredFlowComposed(parsed.value)
281
+ : checkFlow(parsed.value, { warnUnresolvedAgentWorker: true });
255
282
  emitCheckReport(checked.report, parsed.json, io);
256
283
  return checked.report.ok ? 0 : 2;
257
284
  }
@@ -287,9 +314,11 @@ export async function runCli(
287
314
  // not here. Hoisting it above the dispatch would start a daemon as a side
288
315
  // effect of an invocation that is about to be refused for bad input.
289
316
  const startedSteps = new Map<string, number>();
317
+ const observer = parsed.noObserverLink ? undefined : createObserverSession(parsed.command, io);
290
318
  const showProgress = (event: ProgressEvent): void => {
291
319
  if (event.type === 'step.started') startedSteps.set(event.stepId, performance.now());
292
320
  if (!parsed.json) for (const line of renderProgress([event])) io.stderr(line);
321
+ observer?.onProgress(event);
293
322
  };
294
323
  const lifecycle = {
295
324
  ...(parsed.command === 'run' ? { bucket: parsed.bucket } : {}),
@@ -299,6 +328,10 @@ export async function runCli(
299
328
  localAgent: parsed.localAgent,
300
329
  ...(parsed.agentCapacity === undefined ? {} : { agentCapacity: parsed.agentCapacity }),
301
330
  onProgress: showProgress,
331
+ ...(observer === undefined ? {} : {
332
+ onJournalEntry: (entry: JournalEvent) => observer.onJournalEntry(entry),
333
+ onRunStarted: (run: { runId: string; flow: string; resumed?: boolean }) => observer.onRunStarted(run),
334
+ }),
302
335
  onWait: (progress: RunProgress) => {
303
336
  emitWait(progress, io);
304
337
  const now = performance.now();
@@ -308,16 +341,15 @@ export async function runCli(
308
341
  },
309
342
  daemon: { spawn: parsed.spawn && spawnAllowedByEnv() },
310
343
  };
311
- // Mint the observer token in parallel with the run so the mint round-trip
312
- // never adds to the RUN summary latency. The outcome is only consulted at
313
- // emit time; a rejected promise here can never fail the run (see
314
- // `observerUrlFrom`, which swallows every failure into `warning`).
315
- const observerMint = startObserverMint(parsed);
316
344
  const execution = parsed.command === 'run'
317
345
  ? isAuthoredFlowPath(parsed.value)
318
346
  ? await runDirectFlow(parsed.value, parsed.input, parsed.dataDir, lifecycle)
319
347
  : await runFlow(parsed.value, parsed.dataDir, lifecycle)
320
348
  : await resumeFlow(parsed.value, parsed.dataDir, lifecycle);
349
+ // The link is scoped to the run's channel, so it exists only once the run
350
+ // does. `finish` drains the projection and settles the mint, both bounded;
351
+ // it never rejects (see `createObserverSession`).
352
+ const observerMint = observer?.finish(execution.report);
321
353
  // In `--json` mode the report is a single machine-readable object that
322
354
  // MUST carry `observerUrl` when one is available, so a consumer sees one
323
355
  // authoritative signal. That justifies blocking up to `MINT_TIMEOUT_MS`
@@ -369,35 +401,6 @@ async function checkAuthoredFlowComposed(path: string): Promise<{ report: CheckR
369
401
  };
370
402
  }
371
403
 
372
- /**
373
- * Start the observer-token mint if the environment says one should happen.
374
- * Returns `undefined` when no attempt should be made — no workspace key
375
- * configured, or `FLOWS_NO_OBSERVER=1` / `--no-observer-link` — which is the
376
- * silent-skip branch. The returned promise always resolves; a rejection here
377
- * would slip past `observerUrlFrom` and could fail the run, which the feature
378
- * expressly forbids.
379
- */
380
- function startObserverMint(
381
- parsed: { command: 'run' | 'resume'; noObserverLink: boolean },
382
- env: NodeJS.ProcessEnv = process.env,
383
- mint: (options: MintObserverOptions) => Promise<{ observerUrl?: string; warning?: string }> = mintObserverUrl,
384
- ): Promise<{ observerUrl?: string; warning?: string }> | undefined {
385
- if (parsed.noObserverLink) return undefined;
386
- // `resolveObserverLinkEnv` (not `readObserverLinkEnv`) falls back to the
387
- // `agent-relay cloud login` workspace store (~/.agentworkforce/relay/
388
- // workspaces.json) when RELAYCAST_WORKSPACE_KEY is unset. Env wins if set;
389
- // FLOWS_NO_OBSERVER=1 still suppresses regardless of source.
390
- const link = resolveObserverLinkEnv(env);
391
- if (link.suppressed || link.workspaceKey === undefined) return undefined;
392
- return mint({
393
- workspaceKey: link.workspaceKey,
394
- ...(link.baseUrl !== undefined ? { baseUrl: link.baseUrl } : {}),
395
- ...(link.dashboardUrl !== undefined ? { dashboardUrl: link.dashboardUrl } : {}),
396
- }).catch((error) => ({
397
- warning: error instanceof Error ? error.message : 'unknown mint error',
398
- }));
399
- }
400
-
401
404
  /**
402
405
  * Resolve the pending observer-mint into a URL (or nothing), and route any
403
406
  * mint warning to stderr under a `[observer]` label. Silent-skip (`undefined`
@@ -941,6 +944,18 @@ function parseTickArgs(rest: readonly string[]): ParsedArgs | undefined {
941
944
  };
942
945
  }
943
946
 
947
+ /**
948
+ * `agent_worker_unresolved` reads as a footnote to `REQUIRES codex (step
949
+ * "implement"), …`: that line already names the steps that need an agent
950
+ * worker, and this says what has to be true for one to be attached. So the
951
+ * plain-text pass holds it back and emits it in that position, exactly once —
952
+ * the leading batch below skips it rather than printing it twice. JSON mode
953
+ * returns before any of this and keeps the single ordered diagnostics array.
954
+ */
955
+ function isWorkerSurfaceWarning(diagnostic: CheckReport['diagnostics'][number]): boolean {
956
+ return diagnostic.kind === 'agent_worker_unresolved';
957
+ }
958
+
944
959
  const MODEL_PROVENANCE: Readonly<Record<CliModelSource, string>> = {
945
960
  step: 'step',
946
961
  named: 'named agent',
@@ -953,16 +968,20 @@ function modelProvenance(source: CliModelSource | undefined): string {
953
968
  }
954
969
 
955
970
  function emitCheckReport(report: CheckReport, json: boolean, io: CliIo): void {
956
- emitDiagnostics(report.diagnostics, io);
957
971
  if (json) {
972
+ emitDiagnostics(report.diagnostics, io);
958
973
  io.stdout(JSON.stringify(report));
959
974
  return;
960
975
  }
976
+ const deferred = report.diagnostics.filter(isWorkerSurfaceWarning);
977
+ emitDiagnostics(report.diagnostics.filter((diagnostic) => !isWorkerSurfaceWarning(diagnostic)), io);
961
978
  for (const gate of report.gates) {
962
979
  // A gate that accepts every output is legal, but it must not read like a
963
980
  // gate that judges something.
964
981
  const vacuous = gate.acceptsAnyOutput === true ? ' [json_schema accepts any output]' : '';
965
- io.stdout(`GATE step "${gate.stepId}" ${gate.checks.join('+')} from data (kernel, journal-replayable)${vacuous}`);
982
+ // Red-but-complete is a declared shape, so it is a visible one.
983
+ const recorded = gate.recordsNonZeroExit === true ? ' [onNonZero: record]' : '';
984
+ io.stdout(`GATE step "${gate.stepId}" ${gate.checks.join('+')} from data (kernel, journal-replayable)${vacuous}${recorded}`);
966
985
  }
967
986
  for (const schedule of report.schedules ?? []) {
968
987
  const declared = schedule.cron !== undefined
@@ -1001,6 +1020,7 @@ function emitCheckReport(report: CheckReport, json: boolean, io: CliIo): void {
1001
1020
  // the hosted verbs check the same list against Cloud before submitting.
1002
1021
  const requires = report.requirements === undefined ? '' : describeFlowRequirements(report.requirements);
1003
1022
  if (requires) io.stdout(`REQUIRES ${requires}`);
1023
+ emitDiagnostics(deferred, io);
1004
1024
  if (report.ok) io.stdout(`CHECK PASSED ${report.path ?? ''}`.trimEnd());
1005
1025
  }
1006
1026
 
package/src/cloud-read.ts CHANGED
@@ -15,6 +15,7 @@
15
15
  // key is ignored, a route that drops one yields `null`, never a throw.
16
16
 
17
17
  import { CloudFlowError, cloudFetch, cloudRunId, isCloudRecord, type CloudConnectionOptions } from './cloud-http.js';
18
+ import { cloudRunState, type CloudRunState } from './cloud-run-record.js';
18
19
 
19
20
  /** One row of `GET /api/v1/workflows/runs`. */
20
21
  export interface CloudRunSummary {
@@ -334,8 +335,31 @@ export async function getCloudRunDetail(
334
335
  options: CloudConnectionOptions = {},
335
336
  ): Promise<CloudRunDetail> {
336
337
  const asked = callerRunId(runId);
337
- const body = record(await cloudFetch(`/api/v1/workflows/runs/${asked}`, options,
338
+ return runDetail(asked, await cloudFetch(`/api/v1/workflows/runs/${asked}`, options,
338
339
  { method: 'GET', detail: true }));
340
+ }
341
+
342
+ /**
343
+ * The run record twice over: what to render, and what it attests.
344
+ *
345
+ * `--watch` and `--follow` need both, from the same bytes. The rendered view
346
+ * is the permissive mapping below, which shows whatever Cloud said; the exit
347
+ * code comes from `cloudRunState`, the same validator `flows run --wait`
348
+ * blocks on, so a live read cannot exit 0 on a record the waiter refuses.
349
+ * One request, because a second GET per poll could observe a different run
350
+ * than the one that was drawn.
351
+ */
352
+ export async function getCloudRunDetailLive(
353
+ runId: string,
354
+ options: CloudConnectionOptions = {},
355
+ ): Promise<{ detail: CloudRunDetail; state: CloudRunState }> {
356
+ const asked = callerRunId(runId);
357
+ const answered = await cloudFetch(`/api/v1/workflows/runs/${asked}`, options, { method: 'GET', detail: true });
358
+ return { detail: runDetail(asked, answered), state: cloudRunState(answered, asked) };
359
+ }
360
+
361
+ function runDetail(asked: string, answeredBody: unknown): CloudRunDetail {
362
+ const body = record(answeredBody);
339
363
  if (body === null) throw new CloudFlowError('invalid_response', 'Cloud returned no run record.');
340
364
  // The record has to be the record that was asked for, and it has to say what
341
365
  // the run is doing. `getCloudFlowRun` (cloud-run.ts) already refuses a
@@ -0,0 +1,64 @@
1
+ // What a Cloud run record has to say before this client will act on it.
2
+ //
3
+ // One validator, two callers. `waitForCloudFlowRun` (cloud-run.ts) blocks on
4
+ // it to decide when a hosted `flows run --wait` is over, and the live read
5
+ // verbs (cli/cloud-live.ts, through `getCloudRunDetailLive`) block on it to
6
+ // decide when `--watch` and `--follow` stop and with which exit code. Both
7
+ // have to mean the same thing by "this run is over, and it ended well", or a
8
+ // watch would exit 0 on a record the waiter refuses.
9
+ //
10
+ // Deliberately strict and deliberately separate from the *permissive*
11
+ // `getCloudRunDetail` mapping, which exists to show a reader whatever Cloud
12
+ // said. Attesting an outcome is a different job to rendering one: a
13
+ // `completed` header with no valid `completionReason` attests nothing, and
14
+ // must not become exit 0.
15
+
16
+ import { RUN_COMPLETION_REASONS } from '@relayflows/surface';
17
+ import { CloudFlowError, isCloudRecord } from './cloud-http.js';
18
+ import type { RunCompletionReason } from './protocol.js';
19
+
20
+ export type CloudRunState =
21
+ | { runId: string; status: 'pending' | 'launching' | 'running' }
22
+ | { runId: string; status: 'completed' | 'failed' | 'cancelled'; completionReason: RunCompletionReason };
23
+
24
+ /** The statuses that mean the hosted run has not finished. */
25
+ const ACTIVE_RUN_STATUSES: readonly string[] = ['pending', 'launching', 'running'];
26
+
27
+ /**
28
+ * Is this run still going?
29
+ *
30
+ * The one vocabulary for the question, so `run --cloud --wait`,
31
+ * `status --cloud --watch` and `logs --follow` cannot disagree about what
32
+ * `launching` means. Note this is about the *run*: a `needs_human` step is
33
+ * not a run status and never reaches here.
34
+ */
35
+ export function isCloudRunActive(status: string): boolean {
36
+ return ACTIVE_RUN_STATUSES.includes(status);
37
+ }
38
+
39
+ /**
40
+ * Validate a raw run record into an execution outcome, or refuse.
41
+ *
42
+ * Takes the body as Cloud answered it so a caller that also wants the
43
+ * renderable projection can make one request and validate the same bytes it
44
+ * rendered, rather than issuing a second GET per poll.
45
+ */
46
+ export function cloudRunState(body: unknown, runId: string): CloudRunState {
47
+ if (!isCloudRecord(body) || body.runId !== runId || body.relayflowVersion !== 'v2'
48
+ || typeof body.status !== 'string'
49
+ || !['pending', 'launching', 'running', 'completed', 'failed', 'cancelled'].includes(body.status)) {
50
+ throw new CloudFlowError('invalid_response', 'Cloud returned an invalid v2 run record.');
51
+ }
52
+ if (isCloudRunActive(body.status)) {
53
+ return { runId, status: body.status as 'pending' | 'launching' | 'running' };
54
+ }
55
+ const report = body.result;
56
+ const reason = isCloudRecord(report) ? report.completionReason : undefined;
57
+ if (typeof reason !== 'string' || !(RUN_COMPLETION_REASONS as readonly string[]).includes(reason)
58
+ || (body.status === 'completed' && (reason !== 'success' || !isCloudRecord(report) || report.ok !== true || report.status !== 'completed'))
59
+ || (body.status === 'failed' && !['step_failed', 'budget_exceeded'].includes(reason))
60
+ || (body.status === 'cancelled' && reason !== 'canceled')) {
61
+ throw new CloudFlowError('invalid_response', 'Cloud terminal record lacks a valid, consistent run completionReason; no execution outcome is attested.');
62
+ }
63
+ return { runId, status: body.status as 'completed' | 'failed' | 'cancelled', completionReason: reason as RunCompletionReason };
64
+ }
package/src/cloud-run.ts CHANGED
@@ -1,5 +1,4 @@
1
- import { RUN_COMPLETION_REASONS, type ScheduleTriggerSource } from '@relayflows/surface';
2
- import type { RunCompletionReason } from './protocol.js';
1
+ import { type ScheduleTriggerSource } from '@relayflows/surface';
3
2
  import { readFile } from 'node:fs/promises';
4
3
  import { createHash } from 'node:crypto';
5
4
  import { setTimeout } from 'node:timers/promises';
@@ -14,6 +13,7 @@ import {
14
13
  CloudFlowError, cloudConnection, cloudFetch, cloudRequest, cloudRunId, isCloudRecord,
15
14
  type CloudConnectionOptions,
16
15
  } from './cloud-http.js';
16
+ import { cloudRunState, isCloudRunActive, type CloudRunState } from './cloud-run-record.js';
17
17
  import { packWorkingTree, prepareCloudSync, uploadCloudCode } from './cloud-sync.js';
18
18
 
19
19
  export type CloudFlowSource = FlowSpec | { path: string };
@@ -66,9 +66,7 @@ export function cloudSubmissionBody(submission: CloudSubmission): Record<string,
66
66
  };
67
67
  }
68
68
 
69
- export type CloudRunState =
70
- | { runId: string; status: 'pending' | 'launching' | 'running' }
71
- | { runId: string; status: 'completed' | 'failed' | 'cancelled'; completionReason: RunCompletionReason };
69
+ export type { CloudRunState };
72
70
 
73
71
  /**
74
72
  * Submit a declarative flow to Cloud's pinned v2 runtime. Compilation only
@@ -269,24 +267,7 @@ export async function getCloudFlowRun(
269
267
  options: CloudConnectionOptions = {},
270
268
  ): Promise<CloudRunState> {
271
269
  cloudRunId(runId);
272
- const result = await cloudRequest(`/api/v1/workflows/runs/${runId}`, options);
273
- if (!isCloudRecord(result) || result.runId !== runId || result.relayflowVersion !== 'v2'
274
- || typeof result.status !== 'string'
275
- || !['pending', 'launching', 'running', 'completed', 'failed', 'cancelled'].includes(result.status)) {
276
- throw new CloudFlowError('invalid_response', 'Cloud returned an invalid v2 run record.');
277
- }
278
- if (result.status === 'pending' || result.status === 'launching' || result.status === 'running') {
279
- return { runId, status: result.status };
280
- }
281
- const report = result.result;
282
- const reason = isCloudRecord(report) ? report.completionReason : undefined;
283
- if (typeof reason !== 'string' || !(RUN_COMPLETION_REASONS as readonly string[]).includes(reason)
284
- || (result.status === 'completed' && (reason !== 'success' || !isCloudRecord(report) || report.ok !== true || report.status !== 'completed'))
285
- || (result.status === 'failed' && !['step_failed', 'budget_exceeded'].includes(reason))
286
- || (result.status === 'cancelled' && reason !== 'canceled')) {
287
- throw new CloudFlowError('invalid_response', 'Cloud terminal record lacks a valid, consistent run completionReason; no execution outcome is attested.');
288
- }
289
- return { runId, status: result.status as 'completed' | 'failed' | 'cancelled', completionReason: reason as RunCompletionReason };
270
+ return cloudRunState(await cloudRequest(`/api/v1/workflows/runs/${runId}`, options), runId);
290
271
  }
291
272
 
292
273
  /** Wait without a fixed execution deadline. Abort stops observation, not the hosted run. */
@@ -304,7 +285,7 @@ export async function waitForCloudFlowRun(
304
285
  try {
305
286
  const run = await getCloudFlowRun(runId, options);
306
287
  failures = 0;
307
- if (run.status !== 'pending' && run.status !== 'launching' && run.status !== 'running') return run;
288
+ if (!isCloudRunActive(run.status)) return run;
308
289
  } catch (error) {
309
290
  options.signal?.throwIfAborted();
310
291
  const transient = error instanceof CloudFlowError && (error.code === 'transient_error'
@@ -1,3 +1,4 @@
1
+ import { onWorkerFailure } from '../worker-lease.js';
1
2
  import { randomUUID } from 'node:crypto';
2
3
  import { JournalClient } from '../journal-client.js';
3
4
  import { AgentWorker } from '../worker.js';
@@ -31,7 +32,7 @@ export async function attachCommunicationWorkers(spec: KernelRunSpec, socketPath
31
32
  requiredStreams: [channelName(step.id, '$receipts')],
32
33
  pins: { workspace: [], streams: step.surfaces?.streams?.map(({ stream }) => ({ stream, read_offset: 0 })) ?? [] } });
33
34
  workers.push({ client, worker });
34
- worker.on('error', error => { failure = error; client.close(); });
35
+ worker.on('error', onWorkerFailure('communication', error => { failure = error; client.close(); }));
35
36
  await client.connect();
36
37
  await client.hello('flows-communication');
37
38
  await worker.attach();
@@ -7,6 +7,7 @@ import type { KernelAgentStep } from '../spec.js';
7
7
  import { withWorkerLease } from '../worker-lease.js';
8
8
  import { workerInstruction } from '../worker-input.js';
9
9
  import { resolveCliModel } from '../cli-adapter.js';
10
+ import { resolveAgentCwd } from '../agent-cwd.js';
10
11
  import { channelName, type CommunicationInstruction } from './spec.js';
11
12
  import { acquireRelayRuntime, type RelayHandle } from './relay.js';
12
13
  import { CommunicationSession } from './session.js';
@@ -17,13 +18,13 @@ export function requireCommunicationCli(cli: string | undefined): void {
17
18
  if (!cli?.trim()) throw new Error('Agent communication requires a declared CLI executable');
18
19
  }
19
20
  export async function completeCommunicationDispatch(client: JournalClient, dispatch: StepDispatchEvent,
20
- instruction: CommunicationInstruction, dataDir: string): Promise<void> {
21
+ instruction: CommunicationInstruction, dataDir: string, runRoot?: string): Promise<void> {
21
22
  const spec = dispatch.spec as KernelAgentStep;
22
23
  requireCommunicationCli(spec.cli);
23
24
  let output: unknown;
24
25
  let completionReason: 'success' | 'worker_error' = 'success';
25
26
  try {
26
- output = await withWorkerLease(client, dispatch, signal => run(client, dispatch, instruction, spec, dataDir, signal));
27
+ output = await withWorkerLease(client, dispatch, signal => run(client, dispatch, instruction, spec, dataDir, signal, runRoot));
27
28
  } catch (error) {
28
29
  completionReason = 'worker_error';
29
30
  output = { error: error instanceof Error ? error.message : String(error) };
@@ -33,7 +34,14 @@ export async function completeCommunicationDispatch(client: JournalClient, dispa
33
34
  started_pins: dispatch.pins, end_pins: dispatch.pins });
34
35
  }
35
36
  async function run(client: JournalClient, dispatch: StepDispatchEvent, instruction: CommunicationInstruction,
36
- spec: KernelAgentStep, dataDir: string, lease: AbortSignal): Promise<unknown> {
37
+ spec: KernelAgentStep, dataDir: string, lease: AbortSignal, runRoot?: string): Promise<unknown> {
38
+ // Same contract as the CLI worker: a declared directory is resolved and held
39
+ // inside the same run root the CLI worker measures against, before anything
40
+ // is spawned. Thrown, not reported, because `completeCommunicationDispatch`
41
+ // already turns a throw here into a `worker_error` completion carrying the
42
+ // message.
43
+ const root = runRoot ?? process.cwd();
44
+ const directory = resolveAgentCwd(root, spec.cwd) ?? root;
37
45
  const controller = new AbortController();
38
46
  const signal = AbortSignal.any([lease, controller.signal, AbortSignal.timeout(instruction.timeoutMs)]);
39
47
  const relay = await acquireRelayRuntime(dataDir, dispatch.run_id);
@@ -70,9 +78,9 @@ async function run(client: JournalClient, dispatch: StepDispatchEvent, instructi
70
78
  });
71
79
  // Relay supplies each CLI's launch flags and injection behavior.
72
80
  handle = await relay.broker.spawnPty({ name, cli: basename(spec.cli!).replace(/\.exe$/i, ''), task: prompt, channels: [], skipRelayPrompt: true,
73
- model: resolveCliModel(spec.cli!, spec.model), cwd: spec.cwd ?? process.cwd(),
81
+ model: resolveCliModel(spec.cli!, spec.model), cwd: directory,
74
82
  harnessConfig: { runtime: 'pty', command: quote(spec.cli!), args: [],
75
- cwd: spec.cwd ?? process.cwd(), env: { ...agentEnvironment(spec.cli!),
83
+ cwd: directory, env: { ...agentEnvironment(spec.cli!),
76
84
  RELAYFLOW_COMMUNICATION_SOCKET: tools.path, RELAYFLOW_COMMUNICATION_TOKEN: tools.token },
77
85
  delivery: { mode: 'pty-injection', format: 'relay-block' } } });
78
86
  const ready = await handle.waitForReady(Math.min(instruction.timeoutMs, 90_000));
package/src/compile.ts CHANGED
@@ -19,6 +19,7 @@ import { parseBudget, toKernelBudget } from './budget.js';
19
19
  import { bindingDependencies } from './input-binding.js';
20
20
  import { namedGateFailure } from './named-gates.js';
21
21
  import { lowerNamedGates } from './named-gate-lowering.js';
22
+ import { lowerStepsGreenGates } from './steps-green.js';
22
23
  import type {
23
24
  AgentStepSpec,
24
25
  DeterministicStepSpec,
@@ -208,6 +209,10 @@ function compileStep(step: StepSpec): StepSpec {
208
209
  // their dispatch timeout. It must be spread HERE and nowhere in `base`.
209
210
  ...(s.timeoutMs !== undefined ? { timeoutMs: s.timeoutMs } : {}),
210
211
  ...(s.lease_ms !== undefined ? { lease_ms: parseStepTimeout(s.lease_ms) } : {}),
212
+ // Normalized away when it names the default, so an author who spells
213
+ // out `fail` gets the same canonical spec — and the same hash — as one
214
+ // who omits the field entirely.
215
+ ...(s.onNonZero !== undefined && s.onNonZero !== 'fail' ? { onNonZero: s.onNonZero } : {}),
211
216
  };
212
217
  }
213
218
  case 'llm': {
@@ -259,6 +264,11 @@ function outputGate(gate: VerificationSpec, stepId: string): OutputVerificationS
259
264
  `step "${stepId}": exit_code is supported only on deterministic steps`,
260
265
  ]);
261
266
  }
267
+ if (gate.type === 'steps_green') {
268
+ throw new CompileError([
269
+ `step "${stepId}": gate_host_unsupported: steps_green is supported only on deterministic steps`,
270
+ ], 'gate_host_unsupported');
271
+ }
262
272
  return gate;
263
273
  }
264
274
 
@@ -331,7 +341,13 @@ export function toKernelSpec(flow: FlowSpec): KernelRunSpec {
331
341
  // reverts the other, and `validateSpec` and `flows check` would both still
332
342
  // look correct. See ops/reviews/20260903-pr139-repair-0903.md section 10.
333
343
  ...(compiled.triggers?.length ? { triggers: compiled.triggers.map(toKernelTrigger) } : {}),
334
- steps: lowerNamedGates(compiled.steps).map((step) => toKernelStep(resolveNamedAgent(step, compiled.agents))),
344
+ // `steps_green` lowers FIRST: it rewrites the steps it reads to the
345
+ // permissive envelope schema its bindings need, and the named-gate pass
346
+ // then adds each source's own gate barrier to the generated step. The
347
+ // reverse order would hand the green gate a source whose barrier it does
348
+ // not wait for.
349
+ steps: lowerNamedGates(lowerStepsGreenGates(compiled.steps))
350
+ .map((step) => toKernelStep(resolveNamedAgent(step, compiled.agents))),
335
351
  ...(compiled.budget !== undefined ? { budget: toKernelBudget(compiled.budget) } : {}),
336
352
  };
337
353
  }
@@ -426,18 +442,18 @@ function kernelTriggerToAuthoring(value: unknown, at: string): unknown {
426
442
  function kernelStepToAuthoring(value: unknown, at: string): unknown {
427
443
  const unionKeys = [
428
444
  'id', 'type', 'depends_on', 'max_iterations', 'retry', 'verification', 'memory', 'requirements', 'input',
429
- 'command', 'timeout_ms', 'lease_ms', 'prompt', 'model', 'cli', 'instruction',
430
- 'recovery_mode', 'surfaces', 'permissions',
445
+ 'command', 'timeout_ms', 'lease_ms', 'on_non_zero', 'prompt', 'model', 'cli', 'instruction',
446
+ 'cwd', 'recovery_mode', 'surfaces', 'permissions',
431
447
  ] as const;
432
448
  const step = requireKernelObject(value, unionKeys, at);
433
449
  const type = step['type'];
434
450
  const commonKeys = ['id', 'type', 'depends_on', 'max_iterations', 'retry', 'verification', 'memory', 'requirements', 'input'] as const;
435
451
  const typeKeys = type === 'deterministic'
436
- ? ['command', 'timeout_ms', 'lease_ms'] as const
452
+ ? ['command', 'timeout_ms', 'lease_ms', 'on_non_zero'] as const
437
453
  : type === 'llm'
438
454
  ? ['prompt', 'model', 'cli'] as const
439
455
  : type === 'agent'
440
- ? ['instruction', 'cli', 'model', 'recovery_mode', 'surfaces', 'permissions'] as const
456
+ ? ['instruction', 'cli', 'model', 'cwd', 'recovery_mode', 'surfaces', 'permissions'] as const
441
457
  : [];
442
458
  assertKernelKeys(step, [...commonKeys, ...typeKeys], at);
443
459
  if (step['retry'] !== undefined) validateAuthoringRetryDefaults(step['retry'], `${at}.retry`);
@@ -460,6 +476,7 @@ function kernelStepToAuthoring(value: unknown, at: string): unknown {
460
476
  command: step['command'],
461
477
  ...(step['timeout_ms'] !== undefined ? { timeoutMs: step['timeout_ms'] } : {}),
462
478
  ...(step['lease_ms'] !== undefined ? { lease_ms: step['lease_ms'] } : {}),
479
+ ...(step['on_non_zero'] !== undefined ? { onNonZero: step['on_non_zero'] } : {}),
463
480
  };
464
481
  }
465
482
  if (type === 'llm') {
@@ -470,7 +487,7 @@ function kernelStepToAuthoring(value: unknown, at: string): unknown {
470
487
  ...common,
471
488
  instruction: step['instruction'],
472
489
  ...(step['recovery_mode'] !== undefined ? { recoveryMode: step['recovery_mode'] } : {}),
473
- ...copyDefined(step, ['cli', 'model', 'surfaces']),
490
+ ...copyDefined(step, ['cli', 'model', 'cwd', 'surfaces']),
474
491
  ...(step['permissions'] !== undefined
475
492
  ? { permissions: kernelPermissionsToAuthoring(step['permissions'], `${at}.permissions`) }
476
493
  : {}),
@@ -610,6 +627,11 @@ function toKernelStep(step: StepSpec): KernelStepSpec {
610
627
  command: step.command,
611
628
  ...(step.timeoutMs !== undefined ? { timeout_ms: step.timeoutMs } : {}),
612
629
  ...(step.lease_ms !== undefined ? { lease_ms: parseStepTimeout(step.lease_ms) } : {}),
630
+ // Omitted for the default, exactly as the kernel skips serializing it:
631
+ // a spec written before this field existed keeps its canonical bytes
632
+ // and therefore its spec hash.
633
+ ...(step.onNonZero !== undefined && step.onNonZero !== 'fail'
634
+ ? { on_non_zero: step.onNonZero } : {}),
613
635
  };
614
636
  case 'llm': {
615
637
  return {
@@ -39,7 +39,7 @@ export function parseDirectInput(argument: string | undefined): unknown {
39
39
  }
40
40
  } catch (error) {
41
41
  if (error instanceof DirectInputError) throw error;
42
- if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
42
+ if (!namesNoFile(error)) {
43
43
  throw new DirectInputError('input_unreadable', `Input file "${argument}" could not be inspected.`);
44
44
  }
45
45
  }
@@ -56,6 +56,27 @@ export function parseDirectInput(argument: string | undefined): unknown {
56
56
  }
57
57
  }
58
58
 
59
+ /**
60
+ * Whether the `stat` failed because the argument names no file at all, rather
61
+ * than because a file exists and could not be inspected.
62
+ *
63
+ * `ENOENT` is the ordinary "nothing there". `ENAMETOOLONG` is the same answer
64
+ * for a longer argument: no path component may exceed the filesystem's limit
65
+ * (255 bytes on ext4 and on APFS), so an inline JSON object of a few hundred
66
+ * bytes cannot be a filename on any filesystem this runs on. Calling that
67
+ * `input_unreadable` refused every inline input longer than a filename — the
68
+ * recorded-input recovery commands `cli/local-agent-remedy.ts` prints for a
69
+ * parked run among them, which is how it was found.
70
+ *
71
+ * Anything else — a permission error, an I/O error — still refuses, because it
72
+ * means a path is there and this process could not look at it. Guessing that
73
+ * such an argument was inline JSON would parse a filename as a document.
74
+ */
75
+ function namesNoFile(error: unknown): boolean {
76
+ const code = (error as NodeJS.ErrnoException).code;
77
+ return code === 'ENOENT' || code === 'ENAMETOOLONG';
78
+ }
79
+
59
80
  function tooLarge(argument: string, fromFile: boolean): DirectInputError {
60
81
  const sourceKind = fromFile ? `Input file "${argument}"` : 'Inline input';
61
82
  return new DirectInputError(