@relayflows/sdk 2.0.30 → 2.0.32

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 (347) 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 +54 -47
  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-preflight.d.ts +4 -0
  38. package/dist/authored-preflight.d.ts.map +1 -0
  39. package/dist/authored-preflight.js +50 -0
  40. package/dist/authored-preflight.js.map +1 -0
  41. package/dist/authored-root.d.ts +5 -0
  42. package/dist/authored-root.d.ts.map +1 -1
  43. package/dist/authored-root.js +20 -2
  44. package/dist/authored-root.js.map +1 -1
  45. package/dist/authored-step-output.d.ts +35 -10
  46. package/dist/authored-step-output.d.ts.map +1 -1
  47. package/dist/authored-step-output.js +108 -40
  48. package/dist/authored-step-output.js.map +1 -1
  49. package/dist/authored-verdict.d.ts +28 -0
  50. package/dist/authored-verdict.d.ts.map +1 -0
  51. package/dist/authored-verdict.js +79 -0
  52. package/dist/authored-verdict.js.map +1 -0
  53. package/dist/authored-worker-step.d.ts +5 -2
  54. package/dist/authored-worker-step.d.ts.map +1 -1
  55. package/dist/authored-worker-step.js +51 -13
  56. package/dist/authored-worker-step.js.map +1 -1
  57. package/dist/cli/check-worker-surface.d.ts +27 -0
  58. package/dist/cli/check-worker-surface.d.ts.map +1 -0
  59. package/dist/cli/check-worker-surface.js +63 -0
  60. package/dist/cli/check-worker-surface.js.map +1 -0
  61. package/dist/cli/check.d.ts +28 -3
  62. package/dist/cli/check.d.ts.map +1 -1
  63. package/dist/cli/check.js +27 -140
  64. package/dist/cli/check.js.map +1 -1
  65. package/dist/cli/cli-probe.d.ts +20 -0
  66. package/dist/cli/cli-probe.d.ts.map +1 -0
  67. package/dist/cli/cli-probe.js +166 -0
  68. package/dist/cli/cli-probe.js.map +1 -0
  69. package/dist/cli/cloud-format.d.ts +9 -0
  70. package/dist/cli/cloud-format.d.ts.map +1 -0
  71. package/dist/cli/cloud-format.js +77 -0
  72. package/dist/cli/cloud-format.js.map +1 -0
  73. package/dist/cli/cloud-live.d.ts +39 -0
  74. package/dist/cli/cloud-live.d.ts.map +1 -0
  75. package/dist/cli/cloud-live.js +325 -0
  76. package/dist/cli/cloud-live.js.map +1 -0
  77. package/dist/cli/cloud-mirror-session.d.ts +97 -0
  78. package/dist/cli/cloud-mirror-session.d.ts.map +1 -0
  79. package/dist/cli/cloud-mirror-session.js +264 -0
  80. package/dist/cli/cloud-mirror-session.js.map +1 -0
  81. package/dist/cli/cloud-read.d.ts +3 -7
  82. package/dist/cli/cloud-read.d.ts.map +1 -1
  83. package/dist/cli/cloud-read.js +14 -266
  84. package/dist/cli/cloud-read.js.map +1 -1
  85. package/dist/cli/cloud-refusal.d.ts +39 -0
  86. package/dist/cli/cloud-refusal.d.ts.map +1 -0
  87. package/dist/cli/cloud-refusal.js +95 -0
  88. package/dist/cli/cloud-refusal.js.map +1 -0
  89. package/dist/cli/cloud-status-view.d.ts +7 -0
  90. package/dist/cli/cloud-status-view.d.ts.map +1 -0
  91. package/dist/cli/cloud-status-view.js +214 -0
  92. package/dist/cli/cloud-status-view.js.map +1 -0
  93. package/dist/cli/direct-run.d.ts.map +1 -1
  94. package/dist/cli/direct-run.js +16 -2
  95. package/dist/cli/direct-run.js.map +1 -1
  96. package/dist/cli/hn-monitor.d.ts +0 -13
  97. package/dist/cli/hn-monitor.d.ts.map +1 -1
  98. package/dist/cli/hn-monitor.js +2 -1
  99. package/dist/cli/hn-monitor.js.map +1 -1
  100. package/dist/cli/local-agent-remedy.d.ts +80 -0
  101. package/dist/cli/local-agent-remedy.d.ts.map +1 -0
  102. package/dist/cli/local-agent-remedy.js +116 -0
  103. package/dist/cli/local-agent-remedy.js.map +1 -0
  104. package/dist/cli/observer-session.d.ts +41 -0
  105. package/dist/cli/observer-session.d.ts.map +1 -0
  106. package/dist/cli/observer-session.js +78 -0
  107. package/dist/cli/observer-session.js.map +1 -0
  108. package/dist/cli/run.d.ts +30 -2
  109. package/dist/cli/run.d.ts.map +1 -1
  110. package/dist/cli/run.js +186 -24
  111. package/dist/cli/run.js.map +1 -1
  112. package/dist/cli/status.d.ts +9 -0
  113. package/dist/cli/status.d.ts.map +1 -1
  114. package/dist/cli/status.js +31 -2
  115. package/dist/cli/status.js.map +1 -1
  116. package/dist/cli/step-evidence.d.ts +90 -0
  117. package/dist/cli/step-evidence.d.ts.map +1 -0
  118. package/dist/cli/step-evidence.js +259 -0
  119. package/dist/cli/step-evidence.js.map +1 -0
  120. package/dist/cli/step-failure.d.ts +14 -4
  121. package/dist/cli/step-failure.d.ts.map +1 -1
  122. package/dist/cli/step-failure.js +41 -76
  123. package/dist/cli/step-failure.js.map +1 -1
  124. package/dist/cli-commands.d.ts +12 -0
  125. package/dist/cli-commands.d.ts.map +1 -1
  126. package/dist/cli-commands.js +3 -0
  127. package/dist/cli-commands.js.map +1 -1
  128. package/dist/cli.d.ts +17 -0
  129. package/dist/cli.d.ts.map +1 -1
  130. package/dist/cli.js +134 -57
  131. package/dist/cli.js.map +1 -1
  132. package/dist/cloud-deploy.d.ts +1 -1
  133. package/dist/cloud-deploy.d.ts.map +1 -1
  134. package/dist/cloud-deploy.js +24 -8
  135. package/dist/cloud-deploy.js.map +1 -1
  136. package/dist/cloud-mirror-ledger.d.ts +40 -0
  137. package/dist/cloud-mirror-ledger.d.ts.map +1 -0
  138. package/dist/cloud-mirror-ledger.js +142 -0
  139. package/dist/cloud-mirror-ledger.js.map +1 -0
  140. package/dist/cloud-mirror-step.d.ts +168 -0
  141. package/dist/cloud-mirror-step.d.ts.map +1 -0
  142. package/dist/cloud-mirror-step.js +504 -0
  143. package/dist/cloud-mirror-step.js.map +1 -0
  144. package/dist/cloud-mirror-transport.d.ts +94 -0
  145. package/dist/cloud-mirror-transport.d.ts.map +1 -0
  146. package/dist/cloud-mirror-transport.js +163 -0
  147. package/dist/cloud-mirror-transport.js.map +1 -0
  148. package/dist/cloud-mirror.d.ts +132 -0
  149. package/dist/cloud-mirror.d.ts.map +1 -0
  150. package/dist/cloud-mirror.js +556 -0
  151. package/dist/cloud-mirror.js.map +1 -0
  152. package/dist/cloud-read.d.ts +15 -0
  153. package/dist/cloud-read.d.ts.map +1 -1
  154. package/dist/cloud-read.js +20 -1
  155. package/dist/cloud-read.js.map +1 -1
  156. package/dist/cloud-run-record.d.ts +27 -0
  157. package/dist/cloud-run-record.d.ts.map +1 -0
  158. package/dist/cloud-run-record.js +56 -0
  159. package/dist/cloud-run-record.js.map +1 -0
  160. package/dist/cloud-run.d.ts +2 -9
  161. package/dist/cloud-run.d.ts.map +1 -1
  162. package/dist/cloud-run.js +3 -20
  163. package/dist/cloud-run.js.map +1 -1
  164. package/dist/communication/local.d.ts.map +1 -1
  165. package/dist/communication/local.js +2 -1
  166. package/dist/communication/local.js.map +1 -1
  167. package/dist/communication/worker.d.ts +1 -1
  168. package/dist/communication/worker.d.ts.map +1 -1
  169. package/dist/communication/worker.js +13 -5
  170. package/dist/communication/worker.js.map +1 -1
  171. package/dist/compile.d.ts.map +1 -1
  172. package/dist/compile.js +28 -6
  173. package/dist/compile.js.map +1 -1
  174. package/dist/direct-input.d.ts.map +1 -1
  175. package/dist/direct-input.js +21 -1
  176. package/dist/direct-input.js.map +1 -1
  177. package/dist/failure-kinds.d.ts +96 -6
  178. package/dist/failure-kinds.d.ts.map +1 -1
  179. package/dist/failure-kinds.js +29 -2
  180. package/dist/failure-kinds.js.map +1 -1
  181. package/dist/flow-requirements.d.ts.map +1 -1
  182. package/dist/flow-requirements.js +24 -9
  183. package/dist/flow-requirements.js.map +1 -1
  184. package/dist/gate-contract.d.ts +7 -0
  185. package/dist/gate-contract.d.ts.map +1 -1
  186. package/dist/gate-contract.js +6 -1
  187. package/dist/gate-contract.js.map +1 -1
  188. package/dist/helper-preflight.d.ts.map +1 -1
  189. package/dist/helper-preflight.js +10 -3
  190. package/dist/helper-preflight.js.map +1 -1
  191. package/dist/helper-reference.d.ts +25 -0
  192. package/dist/helper-reference.d.ts.map +1 -0
  193. package/dist/helper-reference.js +129 -0
  194. package/dist/helper-reference.js.map +1 -0
  195. package/dist/hosted-extension-runtime.js +1 -1
  196. package/dist/index.d.ts +1 -1
  197. package/dist/index.d.ts.map +1 -1
  198. package/dist/index.js.map +1 -1
  199. package/dist/journal-client.d.ts +3 -1
  200. package/dist/journal-client.d.ts.map +1 -1
  201. package/dist/journal-client.js +17 -2
  202. package/dist/journal-client.js.map +1 -1
  203. package/dist/journal-projection.d.ts +16 -0
  204. package/dist/journal-projection.d.ts.map +1 -0
  205. package/dist/journal-projection.js +108 -0
  206. package/dist/journal-projection.js.map +1 -0
  207. package/dist/llm-worker.js +1 -1
  208. package/dist/llm-worker.js.map +1 -1
  209. package/dist/local-agent.d.ts +1 -1
  210. package/dist/local-agent.d.ts.map +1 -1
  211. package/dist/local-agent.js +4 -3
  212. package/dist/local-agent.js.map +1 -1
  213. package/dist/named-gate-lowering.js +35 -5
  214. package/dist/named-gate-lowering.js.map +1 -1
  215. package/dist/named-gate-preflight.d.ts +27 -0
  216. package/dist/named-gate-preflight.d.ts.map +1 -0
  217. package/dist/named-gate-preflight.js +47 -0
  218. package/dist/named-gate-preflight.js.map +1 -0
  219. package/dist/named-gates.d.ts +1 -1
  220. package/dist/named-gates.d.ts.map +1 -1
  221. package/dist/named-gates.js +3 -0
  222. package/dist/named-gates.js.map +1 -1
  223. package/dist/observer-link.d.ts +7 -0
  224. package/dist/observer-link.d.ts.map +1 -1
  225. package/dist/observer-link.js +3 -1
  226. package/dist/observer-link.js.map +1 -1
  227. package/dist/package-version.d.ts +10 -0
  228. package/dist/package-version.d.ts.map +1 -0
  229. package/dist/package-version.js +22 -0
  230. package/dist/package-version.js.map +1 -0
  231. package/dist/preflight.d.ts +14 -0
  232. package/dist/preflight.d.ts.map +1 -1
  233. package/dist/preflight.js +25 -3
  234. package/dist/preflight.js.map +1 -1
  235. package/dist/protocol.d.ts +2 -0
  236. package/dist/protocol.d.ts.map +1 -1
  237. package/dist/redact.d.ts +42 -0
  238. package/dist/redact.d.ts.map +1 -1
  239. package/dist/redact.js +128 -0
  240. package/dist/redact.js.map +1 -1
  241. package/dist/relay-cli.d.ts.map +1 -1
  242. package/dist/relay-cli.js +1 -21
  243. package/dist/relay-cli.js.map +1 -1
  244. package/dist/run-projection.d.ts +83 -0
  245. package/dist/run-projection.d.ts.map +1 -0
  246. package/dist/run-projection.js +199 -0
  247. package/dist/run-projection.js.map +1 -0
  248. package/dist/shell-word.d.ts +19 -0
  249. package/dist/shell-word.d.ts.map +1 -0
  250. package/dist/shell-word.js +23 -0
  251. package/dist/shell-word.js.map +1 -0
  252. package/dist/spec.d.ts +52 -3
  253. package/dist/spec.d.ts.map +1 -1
  254. package/dist/spec.js.map +1 -1
  255. package/dist/step-fields.d.ts +1 -1
  256. package/dist/step-fields.js +1 -1
  257. package/dist/step-fields.js.map +1 -1
  258. package/dist/steps-green.d.ts +27 -0
  259. package/dist/steps-green.d.ts.map +1 -0
  260. package/dist/steps-green.js +164 -0
  261. package/dist/steps-green.js.map +1 -0
  262. package/dist/validate.d.ts.map +1 -1
  263. package/dist/validate.js +39 -8
  264. package/dist/validate.js.map +1 -1
  265. package/dist/worker-lease.d.ts +8 -1
  266. package/dist/worker-lease.d.ts.map +1 -1
  267. package/dist/worker-lease.js +34 -4
  268. package/dist/worker-lease.js.map +1 -1
  269. package/dist/worker.d.ts +8 -0
  270. package/dist/worker.d.ts.map +1 -1
  271. package/dist/worker.js +17 -9
  272. package/dist/worker.js.map +1 -1
  273. package/package.json +3 -7
  274. package/src/agent-artifacts.ts +2 -10
  275. package/src/agent-cwd.ts +139 -0
  276. package/src/artifact-scan-policy.ts +51 -0
  277. package/src/authored-completion-record.ts +119 -0
  278. package/src/authored-completion.ts +203 -0
  279. package/src/authored-flow-error.ts +13 -1
  280. package/src/authored-flow-executor.ts +91 -58
  281. package/src/authored-human.ts +1 -4
  282. package/src/authored-node-entry.ts +4 -1
  283. package/src/authored-node-runner.ts +96 -22
  284. package/src/authored-preflight.ts +46 -0
  285. package/src/authored-root.ts +20 -2
  286. package/src/authored-step-output.ts +139 -42
  287. package/src/authored-verdict.ts +87 -0
  288. package/src/authored-worker-step.ts +63 -14
  289. package/src/cli/check-worker-surface.ts +66 -0
  290. package/src/cli/check.ts +59 -166
  291. package/src/cli/cli-probe.ts +194 -0
  292. package/src/cli/cloud-format.ts +82 -0
  293. package/src/cli/cloud-live.ts +366 -0
  294. package/src/cli/cloud-mirror-session.ts +329 -0
  295. package/src/cli/cloud-read.ts +13 -282
  296. package/src/cli/cloud-refusal.ts +113 -0
  297. package/src/cli/cloud-status-view.ts +213 -0
  298. package/src/cli/direct-run.ts +19 -2
  299. package/src/cli/hn-monitor.ts +2 -1
  300. package/src/cli/local-agent-remedy.ts +160 -0
  301. package/src/cli/observer-session.ts +107 -0
  302. package/src/cli/run.ts +233 -28
  303. package/src/cli/status.ts +59 -3
  304. package/src/cli/step-evidence.ts +310 -0
  305. package/src/cli/step-failure.ts +57 -81
  306. package/src/cli-commands.ts +3 -0
  307. package/src/cli.ts +155 -60
  308. package/src/cloud-deploy.ts +26 -8
  309. package/src/cloud-mirror-ledger.ts +161 -0
  310. package/src/cloud-mirror-step.ts +618 -0
  311. package/src/cloud-mirror-transport.ts +261 -0
  312. package/src/cloud-mirror.ts +633 -0
  313. package/src/cloud-read.ts +25 -1
  314. package/src/cloud-run-record.ts +64 -0
  315. package/src/cloud-run.ts +5 -24
  316. package/src/communication/local.ts +2 -1
  317. package/src/communication/worker.ts +13 -5
  318. package/src/compile.ts +28 -6
  319. package/src/direct-input.ts +22 -1
  320. package/src/failure-kinds.ts +104 -2
  321. package/src/flow-requirements.ts +24 -9
  322. package/src/gate-contract.ts +13 -1
  323. package/src/helper-preflight.ts +10 -3
  324. package/src/helper-reference.ts +130 -0
  325. package/src/hosted-extension-runtime.ts +1 -1
  326. package/src/index.ts +2 -0
  327. package/src/journal-client.ts +19 -2
  328. package/src/journal-projection.ts +106 -0
  329. package/src/llm-worker.ts +1 -1
  330. package/src/local-agent.ts +4 -2
  331. package/src/named-gate-lowering.ts +35 -5
  332. package/src/named-gate-preflight.ts +47 -0
  333. package/src/named-gates.ts +3 -0
  334. package/src/observer-link.ts +10 -1
  335. package/src/package-version.ts +23 -0
  336. package/src/preflight.ts +38 -7
  337. package/src/protocol.ts +2 -0
  338. package/src/redact.ts +111 -0
  339. package/src/relay-cli.ts +1 -24
  340. package/src/run-projection.ts +247 -0
  341. package/src/shell-word.ts +24 -0
  342. package/src/spec.ts +54 -3
  343. package/src/step-fields.ts +1 -1
  344. package/src/steps-green.ts +168 -0
  345. package/src/validate.ts +35 -8
  346. package/src/worker-lease.ts +35 -5
  347. package/src/worker.ts +27 -11
@@ -0,0 +1,130 @@
1
+ import { parse, parseExpressionAt } from 'acorn';
2
+
3
+ /**
4
+ * Which `f.<namespace>` helpers a flow body actually USES.
5
+ *
6
+ * Read from a parse, not from the text. A helper merely NAMED in a string, a
7
+ * comment, a template quasi or a regex is not used, and declaring a
8
+ * requirement from one refuses a flow for a mount it never touches:
9
+ * `f.run("echo f.gitlab")`, an agent prompt naming `f.slack`, or a PR title in
10
+ * a commit message were each enough to demand a GitLab mount.
11
+ *
12
+ * This was first written as a lexer that blanked comments, strings and
13
+ * regexes. That is the wrong tool: `/` is a regex or a division depending on
14
+ * whether the preceding token ends an expression, which is not decidable
15
+ * without parse context, and the two shapes that ambiguity broke —
16
+ * interpolations (`${await f.gitlab…}`) and a quote inside a regex (`/'/`) —
17
+ * both HID real helper use, deploying a flow without its mount to fail at
18
+ * runtime. A parser settles every one of those by construction.
19
+ *
20
+ * What a parser still cannot see is indirection: `const p = 'gitlab';
21
+ * f[p].issues…` is invisible to any static reading, because it needs value
22
+ * tracking. This scan is therefore a convenience for the literal case, never
23
+ * the authority — `header.tools[namespace] === true` is the declaration that
24
+ * is, and it short-circuits this entirely.
25
+ */
26
+ export function helperNamespacesUsed(body: string, root: string): ReadonlySet<string> {
27
+ const program = parseFlowBody(body);
28
+ if (program === null) return textFallback(body, root);
29
+ const used = new Set<string>();
30
+ walk(program, (node) => {
31
+ if (node.type !== 'MemberExpression') return;
32
+ const object = node.object as AstNode | undefined;
33
+ if (object?.type !== 'Identifier' || object.name !== root) return;
34
+ const namespace = memberName(node);
35
+ if (namespace !== undefined) used.add(namespace);
36
+ });
37
+ return used;
38
+ }
39
+
40
+ /** `f.slack`, `f["slack"]`, `f?.slack` — but not `f[variable]`, which is unknowable. */
41
+ function memberName(node: AstNode): string | undefined {
42
+ const property = node.property as AstNode | undefined;
43
+ if (property === undefined) return undefined;
44
+ if (node.computed !== true) return property.type === 'Identifier' ? property.name : undefined;
45
+ return property.type === 'Literal' && typeof property.value === 'string' ? property.value : undefined;
46
+ }
47
+
48
+ /**
49
+ * Parse whatever `Function.prototype.toString()` produced.
50
+ *
51
+ * It can be an arrow, a function expression, an async method shorthand from an
52
+ * object literal, or a declaration — only some of which are expressions, so
53
+ * each shape gets a try. Types are already stripped by the time a body reaches
54
+ * here (Node strips before evaluating, and bundlers transpile), so this is
55
+ * plain JavaScript.
56
+ */
57
+ function parseFlowBody(body: string): AstNode | null {
58
+ const options = { ecmaVersion: 'latest' as const, allowAwaitOutsideFunction: true, allowReturnOutsideFunction: true };
59
+ // `parseExpressionAt` stops at the end of the first expression and does not
60
+ // object to what follows, so `async post(f) { … }` parses as the identifier
61
+ // `async` and reports success having read three characters. Every attempt
62
+ // must therefore consume the whole source, or a method body would be walked
63
+ // as its own name and declare no helpers at all.
64
+ const whole = (source: string, node: AstNode): AstNode => {
65
+ const end = typeof node.end === 'number' ? node.end : -1;
66
+ if (end < 0 || source.slice(end).trim() !== '') throw new SyntaxError('unconsumed input');
67
+ return node;
68
+ };
69
+ for (const attempt of [
70
+ () => whole(body, parseExpressionAt(body, 0, options) as unknown as AstNode),
71
+ () => whole(`(${body})`, parseExpressionAt(`(${body})`, 0, options) as unknown as AstNode),
72
+ () => parse(body, options) as unknown as AstNode,
73
+ // An object-literal method (`async post(f) { … }`) is neither expression
74
+ // nor statement on its own; it only parses inside an object.
75
+ () => whole(`({${body}})`, parseExpressionAt(`({${body}})`, 0, options) as unknown as AstNode),
76
+ ]) {
77
+ try {
78
+ return attempt();
79
+ } catch {
80
+ continue;
81
+ }
82
+ }
83
+ return null;
84
+ }
85
+
86
+ /**
87
+ * Last resort when nothing parses: match the text.
88
+ *
89
+ * Deliberately the permissive direction. An unparseable body is a shape this
90
+ * code does not understand, and under-reporting would deploy a flow without a
91
+ * mount it needs and fail at the call; over-reporting only asks for a mount
92
+ * that may go unused. Reaching here at all is a bug worth hearing about.
93
+ */
94
+ function textFallback(body: string, root: string): ReadonlySet<string> {
95
+ const used = new Set<string>();
96
+ const escaped = root.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
97
+ const pattern = new RegExp(`(?:^|[^\\w$.])${escaped}\\s*(?:\\.\\s*([\\w$]+)|\\[\\s*['"]([^'"]+)['"]\\s*\\])`, 'gu');
98
+ for (const match of body.matchAll(pattern)) {
99
+ const namespace = match[1] ?? match[2];
100
+ if (namespace !== undefined) used.add(namespace);
101
+ }
102
+ return used;
103
+ }
104
+
105
+ type AstNode = {
106
+ type: string;
107
+ end?: number;
108
+ name?: string;
109
+ value?: unknown;
110
+ computed?: boolean;
111
+ [key: string]: unknown;
112
+ };
113
+
114
+ /** Depth-first over every child node, without pulling in a second package. */
115
+ function walk(node: AstNode, visit: (node: AstNode) => void): void {
116
+ visit(node);
117
+ for (const key of Object.keys(node)) {
118
+ if (key === 'type' || key === 'start' || key === 'end' || key === 'loc') continue;
119
+ const child = node[key];
120
+ if (Array.isArray(child)) {
121
+ for (const entry of child) if (isNode(entry)) walk(entry, visit);
122
+ } else if (isNode(child)) {
123
+ walk(child, visit);
124
+ }
125
+ }
126
+ }
127
+
128
+ function isNode(value: unknown): value is AstNode {
129
+ return typeof value === 'object' && value !== null && typeof (value as { type?: unknown }).type === 'string';
130
+ }
@@ -26,7 +26,7 @@ const REALPATH = realpath;
26
26
  const PATH_DIRNAME = dirname;
27
27
  const PATH_JOIN = join;
28
28
  const PATH_RESOLVE = resolve;
29
- const SOFTWARE_FACTORY_SHA256 = '49c993220b9c34fab2d4b0e51911656f62b8b657f534d988691960d45bb9d9b6';
29
+ const SOFTWARE_FACTORY_SHA256 = 'b97a3466c2affabb61afa655d4b2f726942d753c0466740b64ba26a78da359c6';
30
30
  const ARRAY_IS_ARRAY = Array.isArray;
31
31
  const OBJECT_FREEZE = Object.freeze;
32
32
  const WEAK_MAP_GET = Function.prototype.call.bind(WeakMap.prototype.get) as <K extends object, V>(
package/src/index.ts CHANGED
@@ -34,6 +34,8 @@ export type {
34
34
  LlmStepSpec,
35
35
  NamedAgentSpec,
36
36
  NamedDataGate,
37
+ OnNonZero,
38
+ StepsGreenGate,
37
39
  ReferencesInputGate,
38
40
  SubprocessGate,
39
41
  WordCountBoundsGate,
@@ -51,6 +51,7 @@ interface Pending {
51
51
  /** A structured rejection returned by relayflowd over the journal protocol. */
52
52
  export class JournalProtocolError extends Error {
53
53
  readonly code: string;
54
+ verb?: string;
54
55
 
55
56
  constructor(code: string, message: string) {
56
57
  super(`${code}: ${message}`);
@@ -223,12 +224,19 @@ export class JournalClient extends EventEmitter {
223
224
  * Validate a compiled kernel-dialect spec (zero-agent flows legal), create
224
225
  * the run file, and append `run.spawned`. Authoring specs must be compiled
225
226
  * with `toKernelSpec` before crossing this journal-protocol boundary.
227
+ * `watch` streams the run's entries as `'entry'` events while it runs.
226
228
  */
227
- runStart(spec: KernelRunSpec, reuseFromRunId?: string, admissionKey?: string): Promise<VerbContract['run.start']['result']> {
229
+ runStart(
230
+ spec: KernelRunSpec,
231
+ reuseFromRunId?: string,
232
+ admissionKey?: string,
233
+ watch = false,
234
+ ): Promise<VerbContract['run.start']['result']> {
228
235
  return this.request('run.start', {
229
236
  spec,
230
237
  ...(reuseFromRunId === undefined ? {} : { reuse_from_run_id: reuseFromRunId }),
231
238
  ...(admissionKey === undefined ? {} : { admission_key: admissionKey }),
239
+ ...(watch ? { watch: true } : {}),
232
240
  }, null);
233
241
  }
234
242
 
@@ -370,6 +378,9 @@ export class JournalClient extends EventEmitter {
370
378
  step_id: stepId,
371
379
  attempt,
372
380
  lease_id: leaseId,
381
+ }).catch(error => {
382
+ if (error instanceof JournalProtocolError) error.verb = 'step.heartbeat';
383
+ throw error;
373
384
  });
374
385
  }
375
386
 
@@ -401,7 +412,10 @@ export class JournalClient extends EventEmitter {
401
412
  idempotency_key: idempotencyKey,
402
413
  completionReason,
403
414
  ...extra,
404
- }, null);
415
+ }, null).catch(error => {
416
+ if (error instanceof JournalProtocolError) error.verb = 'step.complete';
417
+ throw error;
418
+ });
405
419
  }
406
420
 
407
421
  /** Satisfy `wait.event`; a human response arrives here too. */
@@ -422,6 +436,9 @@ export class JournalClient extends EventEmitter {
422
436
  attempt,
423
437
  idempotency_key: idempotencyKey,
424
438
  ...wait,
439
+ }).catch(error => {
440
+ if (error instanceof JournalProtocolError) error.verb = 'step.wait';
441
+ throw error;
425
442
  });
426
443
  }
427
444
 
@@ -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/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
  };
@@ -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
 
@@ -0,0 +1,23 @@
1
+ import { readFileSync } from 'node:fs';
2
+
3
+ /**
4
+ * Read the installed SDK package version from the manifest beside this module.
5
+ *
6
+ * The same relative URL works from `src/` during development and `dist/` in a
7
+ * built or npm-installed package. Reading the manifest at runtime keeps the
8
+ * CLI output tied to the bytes that are actually installed instead of a
9
+ * hardcoded release value that can drift between package files.
10
+ */
11
+ export function packageVersion(): string {
12
+ try {
13
+ const manifest: unknown = JSON.parse(
14
+ readFileSync(new URL('../package.json', import.meta.url), 'utf8'),
15
+ );
16
+ const version = (manifest as { version?: unknown }).version;
17
+ return typeof version === 'string' && version.length > 0 ? version : '0.0.0';
18
+ } catch {
19
+ // Version display is cosmetic. Keep the CLI usable if a host strips the
20
+ // manifest from an otherwise runnable package artifact.
21
+ return '0.0.0';
22
+ }
23
+ }