@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
@@ -22,19 +22,23 @@ export interface AuthoredStepContext {
22
22
  }
23
23
 
24
24
  /**
25
- * Shared by every `f.*` verb that lowers to one kernel step run in isolation:
26
- * find its `step.completed` entry, record it, and refuse anything but a
25
+ * Shared by every `f.*` verb that lowers to its own kernel run: find the
26
+ * operation's `step.completed` entry, record it, and refuse anything but a
27
27
  * clean success before handing the raw `output` back for verb-specific
28
28
  * extraction (a plain string for `f.run`, an `AgentResult` for `f.agent`).
29
29
  *
30
- * Callers are responsible for having already established that the RUN
31
- * reached a terminal, successful state before calling this — `f.run`'s
32
- * caller relies on `runStart`'s own immediate response (the kernel drives a
33
- * deterministic step to completion inline, no race); `f.agent`'s caller
34
- * relies on `classifyOutcome` (cli/run.ts) having already polled to a true
35
- * terminal state. Given that, a single read from the start of this run's
36
- * (small, single-step) journal is enough — no polling here, and no run
37
- * outcome ever needs re-checking.
30
+ * Callers are responsible for having already waited for the run to reach a
31
+ * terminal state — `f.run`'s caller relies on `runStart`'s own immediate
32
+ * response (the kernel drives a deterministic step to completion inline, no
33
+ * race); `f.agent`'s caller relies on `classifyOutcome` (cli/run.ts) having
34
+ * already polled to one. Given that, a single read from the start of this
35
+ * run's small journal is enough, and no polling happens here.
36
+ *
37
+ * What is NOT delegated to the caller is the run's verdict. A child spec has
38
+ * more than one step whenever the author declared a `.gate()`, and that gate
39
+ * step runs after the producer, so the producer's own `success` is not the
40
+ * run's. Both are checked here, and the run-level check is what makes a failed
41
+ * gate reject the authored operation.
38
42
  *
39
43
  * A non-success completion used to throw `journal step "run-1" completed with
40
44
  * retries_exhausted` and stop there, discarding the evidence it was holding:
@@ -63,26 +67,19 @@ export async function readCompletedStepOutput(
63
67
  const edges = context.stepEdges?.(stepId);
64
68
  journalSteps.push(Object.freeze({ id: stepId, runId, completionReason: reason, ...edges }));
65
69
  if (reason !== 'success') {
66
- let message = `journal step "${stepId}" completed with ${reason}`;
67
- let details: StepFailedDetails | undefined;
68
- try {
69
- details = await stepFailureDetails(journal, runId);
70
- } catch (error) {
71
- // Inspection must not erase the already known step failure; the two
72
- // failures stay separately visible, as in classifyOutcome.
73
- message += ` Could not inspect the failed step: ${errorMessage(error)}`;
74
- }
75
- if (details !== undefined) message += renderStepEvidence(details);
76
- // Appended whatever inspection found — including nothing. A failure shape
77
- // this reader does not recognise must still end with somewhere to go.
78
- const where = inspectionHint(runId, details?.stepId ?? stepId, context.dataDir);
79
- message += renderInspection(where);
80
- message += await alsoRecord(journal, context.rootRunId, {
81
- step: stepId, runId, state: 'completed', completionReason: reason,
82
- ...(details?.stepId === undefined ? {} : { kernelStep: details.stepId }),
83
- ...edges,
84
- });
85
- throw new AuthoredFlowExecutionError('step_failed', message, reason, runId, { ...details, ...where });
70
+ throw await stepFailure(journal, runId, stepId, reason, context, edges,
71
+ `journal step "${stepId}" completed with ${reason}`);
72
+ }
73
+ // An authored operation does not always lower to ONE kernel step. A declared
74
+ // `.gate()` becomes its own barrier step that runs AFTER the producer
75
+ // (`lowerNamedGates`), so the producer's own success is not the run's
76
+ // verdict. Reading only the producer's entry let a failed gate resolve as
77
+ // though the command had passed — the `|| true` invisibility this module's
78
+ // recording policy exists to remove, reappearing one layer up.
79
+ const runReason = entries.map(runCompletionReason).find((value) => value !== undefined);
80
+ if (runReason !== undefined && runReason !== 'success') {
81
+ throw await stepFailure(journal, runId, stepId, runReason, context, edges,
82
+ `journal run for step "${stepId}" completed with ${runReason}`);
86
83
  }
87
84
  await recordAuthoredChild(journal, context.rootRunId, {
88
85
  step: stepId, runId, state: 'completed', completionReason: reason, ...edges,
@@ -90,6 +87,42 @@ export async function readCompletedStepOutput(
90
87
  return completed.payload.output;
91
88
  }
92
89
 
90
+ /**
91
+ * The one `step_failed` grammar both the step-level and the run-level refusal
92
+ * report through: inspect the journal for the step that actually failed, render
93
+ * its evidence, name where to look, and index the child before throwing.
94
+ */
95
+ async function stepFailure(
96
+ journal: JournalClient,
97
+ runId: string,
98
+ stepId: string,
99
+ reason: ProtocolCompletionReason | ProtocolRunCompletionReason,
100
+ context: AuthoredStepContext,
101
+ edges: ReturnType<NonNullable<AuthoredStepContext['stepEdges']>>,
102
+ header: string,
103
+ ): Promise<AuthoredFlowExecutionError> {
104
+ let message = header;
105
+ let details: StepFailedDetails | undefined;
106
+ try {
107
+ details = await stepFailureDetails(journal, runId);
108
+ } catch (error) {
109
+ // Inspection must not erase the already known step failure; the two
110
+ // failures stay separately visible, as in classifyOutcome.
111
+ message += ` Could not inspect the failed step: ${errorMessage(error)}`;
112
+ }
113
+ if (details !== undefined) message += renderStepEvidence(details);
114
+ // Appended whatever inspection found — including nothing. A failure shape
115
+ // this reader does not recognise must still end with somewhere to go.
116
+ const where = inspectionHint(runId, details?.stepId ?? stepId, context.dataDir);
117
+ message += renderInspection(where);
118
+ message += await alsoRecord(journal, context.rootRunId, {
119
+ step: stepId, runId, state: 'completed', completionReason: reason,
120
+ ...(details?.stepId === undefined ? {} : { kernelStep: details.stepId }),
121
+ ...edges,
122
+ });
123
+ return new AuthoredFlowExecutionError('step_failed', message, reason, runId, { ...details, ...where });
124
+ }
125
+
93
126
  export async function readSuccessfulOutput(
94
127
  journal: JournalClient,
95
128
  outcome: RunOutcome,
@@ -98,24 +131,74 @@ export async function readSuccessfulOutput(
98
131
  context: AuthoredStepContext = {},
99
132
  ): Promise<string> {
100
133
  const output = await readCompletedStepOutput(journal, outcome.run_id, stepId, journalSteps, context)
101
- .catch(error => {
102
- if (error instanceof AuthoredFlowExecutionError
103
- && (error.completionReason === 'timeout' || error.completionReason === 'lease_expired')) {
104
- // A timeout is still a failed command, and the evidence extracted for
105
- // it is the same evidence. Re-labelling the error must not delete it.
106
- throw new AuthoredFlowExecutionError('lease_exceeded',
107
- `f.run step "${stepId}" exceeded its command timeout.`
108
- + ` ${error.message.slice(`${error.code}: `.length)}`,
109
- error.completionReason, error.runId, error.details);
110
- }
111
- throw error;
112
- });
134
+ .catch(relabelCommandTimeout(stepId));
113
135
  if (!isRecord(output) || typeof output['stdout_tail'] !== 'string') {
114
136
  throw protocolViolation(outcome.run_id, `step "${stepId}" has no string stdout_tail`);
115
137
  }
116
138
  return output['stdout_tail'];
117
139
  }
118
140
 
141
+ /** What `f.run` resolves to under `onNonZero: 'record'` (surface `RunResult`). */
142
+ export interface RecordedRunOutcome {
143
+ ok: boolean;
144
+ exitCode: number;
145
+ output: string;
146
+ stdout: string;
147
+ stderr: string;
148
+ }
149
+
150
+ /**
151
+ * The read side of `onNonZero: 'record'`.
152
+ *
153
+ * A recorded red exit is a SUCCESSFUL step, so this takes the same path as
154
+ * `readSuccessfulOutput` and every field comes from the journaled envelope —
155
+ * nothing is re-run and nothing is re-measured. What still throws is what the
156
+ * policy never covered: a timeout, a signal, a step that never ran, or a
157
+ * declared content or schema gate that failed. The `-1` the executor writes
158
+ * for "no exit status" is refused rather than handed back as a plausible
159
+ * code, mirroring the kernel's own rule, so a malformed envelope can never
160
+ * reach an author's `if (!result.ok)` as a real verdict.
161
+ */
162
+ export async function readRecordedOutcome(
163
+ journal: JournalClient,
164
+ outcome: RunOutcome,
165
+ stepId: string,
166
+ journalSteps: AuthoredFlowJournalStep[],
167
+ context: AuthoredStepContext = {},
168
+ ): Promise<RecordedRunOutcome> {
169
+ const envelope = await readCompletedStepOutput(journal, outcome.run_id, stepId, journalSteps, context)
170
+ .catch(relabelCommandTimeout(stepId));
171
+ const exitCode = isRecord(envelope) ? envelope['exit_code'] : undefined;
172
+ if (!isRecord(envelope) || typeof envelope['stdout_tail'] !== 'string'
173
+ || typeof envelope['stderr_tail'] !== 'string'
174
+ || typeof exitCode !== 'number' || !Number.isInteger(exitCode) || exitCode < 0) {
175
+ throw protocolViolation(outcome.run_id,
176
+ `step "${stepId}" recorded no {exit_code, stdout_tail, stderr_tail} to report`);
177
+ }
178
+ const stdout = envelope['stdout_tail'];
179
+ const stderr = envelope['stderr_tail'];
180
+ return {
181
+ ok: exitCode === 0, exitCode, stdout, stderr,
182
+ // Concatenated, not interleaved: the two tails were captured separately,
183
+ // so joining them is a reading convenience and is documented as one.
184
+ output: stdout !== '' && stderr !== '' ? `${stdout}\n${stderr}` : stdout + stderr,
185
+ };
186
+ }
187
+
188
+ /** A timeout is still a failed command; re-labelling it must not delete its evidence. */
189
+ function relabelCommandTimeout(stepId: string): (error: unknown) => never {
190
+ return (error) => {
191
+ if (error instanceof AuthoredFlowExecutionError
192
+ && (error.completionReason === 'timeout' || error.completionReason === 'lease_expired')) {
193
+ throw new AuthoredFlowExecutionError('lease_exceeded',
194
+ `f.run step "${stepId}" exceeded its command timeout.`
195
+ + ` ${error.message.slice(`${error.code}: `.length)}`,
196
+ error.completionReason, error.runId, error.details);
197
+ }
198
+ throw error;
199
+ };
200
+ }
201
+
119
202
  interface StepCompletedEntry {
120
203
  entry_type: 'step.completed';
121
204
  step_id: string;
@@ -135,6 +218,20 @@ function isStepCompleted(value: unknown, stepId: string): value is StepCompleted
135
218
  && 'output' in payload;
136
219
  }
137
220
 
221
+ /**
222
+ * The run's own terminal reason, when this entry is the one carrying it.
223
+ *
224
+ * Anything else — including a `run.completed` whose payload this reader does
225
+ * not recognise — yields `undefined` and leaves the step-level verdict
226
+ * untouched, so an unfamiliar journal shape cannot invent a failure.
227
+ */
228
+ function runCompletionReason(entry: unknown): ProtocolRunCompletionReason | undefined {
229
+ if (!isRecord(entry) || entry['entry_type'] !== 'run.completed') return undefined;
230
+ const payload = entry['payload'];
231
+ const reason = isRecord(payload) ? payload['completionReason'] : undefined;
232
+ return isSurfaceRunCompletionReason(reason) ? reason : undefined;
233
+ }
234
+
138
235
  export function isSurfaceCompletionReason(value: unknown): value is ProtocolCompletionReason {
139
236
  return typeof value === 'string'
140
237
  && (COMPLETION_REASONS as readonly string[]).includes(value);
@@ -0,0 +1,87 @@
1
+ // Project an authored flow's own verdict out of its root journal.
2
+ //
3
+ // `foldRunState` answers what the KERNEL recorded: the root step succeeded, so
4
+ // the run completed with `success`. That is true and must stay printed. It is
5
+ // also not what a reader asking "why did this fail?" needs when the body
6
+ // declared `step_failed` — a verdict the kernel's vocabulary cannot express on
7
+ // a step that succeeded, so it travels as output on the root's own
8
+ // `step.completed`.
9
+ //
10
+ // This is a separate, narrow projection rather than a branch inside the fold:
11
+ // the fold is generic over every run and has no business knowing what an
12
+ // authored root is, and the executor this vocabulary comes from must not
13
+ // become a dependency of `flows status`.
14
+
15
+ import {
16
+ isDurableCompletionDetail,
17
+ isLoweredCompletion,
18
+ type LoweredCompletionReason,
19
+ } from './authored-completion.js';
20
+ import type { JournalEvent } from './journal-reader.js';
21
+
22
+ /** The `instruction` discriminator `authored-root.ts` writes into the root step. */
23
+ export const AUTHORED_ROOT_KIND = 'relayflows.authored-root.v1';
24
+
25
+ export interface AuthoredVerdict {
26
+ readonly reason: LoweredCompletionReason;
27
+ /** Absent when the body called `done()` with one argument, as most do. */
28
+ readonly detail?: string;
29
+ }
30
+
31
+ /**
32
+ * The verdict an authored root attested, or `null` when this journal holds none.
33
+ *
34
+ * `null` is the answer for an ordinary run, for an authored run still in
35
+ * flight, and for one whose root output is malformed. The last of those
36
+ * matters: an ordinary flow may name a step `authored-root`, and a worker may
37
+ * journal anything into a step's output, so the step id alone attests nothing.
38
+ * A verdict is reported only when the run's own spec declares the authored
39
+ * root — carrying {@link AUTHORED_ROOT_KIND} metadata — and the root's last
40
+ * `step.completed` is a terminal kernel `success` whose output is a complete,
41
+ * in-bounds authored result. A retry or park completion is not terminal and a
42
+ * later attempt may still change the answer, so neither is promoted.
43
+ *
44
+ * This is a projection for a read-only view, so a malformed record yields no
45
+ * verdict rather than an error: refusing to render a whole status because one
46
+ * output field is the wrong type would replace the answer with a worse one.
47
+ */
48
+ export function authoredVerdictOf(events: readonly JournalEvent[]): AuthoredVerdict | null {
49
+ const spawned = events[0];
50
+ if (spawned === undefined || spawned.entry_type !== 'run.spawned') return null;
51
+ if (!declaresAuthoredRoot(spawned)) return null;
52
+
53
+ const completed = [...events].reverse().find((event) =>
54
+ event.entry_type === 'step.completed' && event.step_id === 'authored-root');
55
+ const payload = record(completed?.payload);
56
+ if (payload === null || payload['completionReason'] !== 'success') return null;
57
+ const disposition = payload['disposition'];
58
+ if (disposition !== undefined && disposition !== 'step_done') return null;
59
+
60
+ const output = record(payload['output']);
61
+ if (output === null || typeof output['name'] !== 'string') return null;
62
+ const reason = output['completionReason'];
63
+ if (!isLoweredCompletion(reason)) return null;
64
+ const detail = output['completionDetail'];
65
+ if (detail !== undefined && !isDurableCompletionDetail(detail)) return null;
66
+
67
+ return Object.freeze({ reason, ...(detail === undefined ? {} : { detail }) });
68
+ }
69
+
70
+ function declaresAuthoredRoot(spawned: JournalEvent): boolean {
71
+ const spec = record(record(spawned.payload)?.['spec']);
72
+ const steps = spec?.['steps'];
73
+ if (!Array.isArray(steps)) return false;
74
+ const step = record(steps[0]);
75
+ if (step === null || step['id'] !== 'authored-root') return false;
76
+ if (typeof step['instruction'] !== 'string') return false;
77
+ try {
78
+ return record(JSON.parse(step['instruction']))?.['kind'] === AUTHORED_ROOT_KIND;
79
+ } catch {
80
+ return false;
81
+ }
82
+ }
83
+
84
+ function record(value: unknown): Record<string, unknown> | null {
85
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
86
+ ? value as Record<string, unknown> : null;
87
+ }
@@ -1,15 +1,16 @@
1
- import { resolve } from 'node:path';
1
+ import { dirname, resolve, isAbsolute, relative, sep } from 'node:path';
2
2
  import type { AuthoredBudget } from './authored-budget.js';
3
3
  import { parseBudget } from './budget.js';
4
4
  import type { AgentOptions, AgentResult, LlmOptions, NamedGate } from '@relayflows/surface';
5
5
  import { compileSpec, toKernelSpec } from './compile.js';
6
- import { checkAuthoredFlow } from './cli/check.js';
6
+ import { authoredPreflight } from './authored-preflight.js';
7
7
  import { classifyOutcome, type RunLifecycleOptions, type RunReport } from './cli/run.js';
8
8
  import type { PreflightDiagnostic } from './preflight.js';
9
9
  import { AuthoredFlowExecutionError } from './authored-flow-error.js';
10
+ import { agentCwdDeclarationError, agentCwdTransportError } from './agent-cwd.js';
10
11
  import type { JournalClient } from './journal-client.js';
11
12
  import { SPEC_SCHEMA_VERSION, type FlowSpec, type PermissionsSpec, type StepSpec } from './spec.js';
12
- import { isSurfaceCompletionReason, readCompletedStepOutput, readSuccessfulOutput, type AuthoredStepContext } from './authored-step-output.js';
13
+ import { isSurfaceCompletionReason, readCompletedStepOutput, readRecordedOutcome, readSuccessfulOutput, type AuthoredStepContext, type RecordedRunOutcome } from './authored-step-output.js';
13
14
  import type { AuthoredFlowJournalStep } from './authored-flow-executor.js';
14
15
  import { snapshotJsonValue } from './json-value.js';
15
16
  import { authoredChildAdmissionKey } from './authored-admission.js';
@@ -30,6 +31,7 @@ export function authoredWorkerRunner(
30
31
  // slot instead of being admitted and parked for want of a free worker.
31
32
  const slots = workerCapacity === undefined ? undefined
32
33
  : { agent: new WorkerSlots(workerCapacity), llm: new WorkerSlots(workerCapacity) };
34
+ const check = authoredPreflight(flowPath);
33
35
  const context: AuthoredStepContext = {
34
36
  ...(rootRunId === undefined ? {} : { rootRunId }),
35
37
  ...(waitOptions.dataDir === undefined ? {} : { dataDir: waitOptions.dataDir }),
@@ -43,7 +45,7 @@ export function authoredWorkerRunner(
43
45
  // (cli/check.ts), searching for the nearest flows.json from `flowPath`
44
46
  // and real-probing auth/model readiness. An authored agent step gets
45
47
  // nothing for free just because it was declared in TS instead of YAML.
46
- const { report, flow: resolved } = checkAuthoredFlow(authoring, flowPath);
48
+ const { report, flow: resolved } = await check(authoring);
47
49
  if (!report.ok || resolved === undefined) {
48
50
  const refusal = report.diagnostics.find(
49
51
  (diagnostic): diagnostic is PreflightDiagnostic & { severity: 'refusal' } =>
@@ -77,7 +79,7 @@ export function authoredWorkerRunner(
77
79
  const execution = await classifyOutcome(journal, 'run', outcome, report, '', waitOptions);
78
80
  if (execution.exitCode === 3) {
79
81
  const parked = execution.report.parkedStep;
80
- throw new AuthoredFlowExecutionError(
82
+ const error = new AuthoredFlowExecutionError(
81
83
  step.type === 'llm' ? 'llm_parked' : 'agent_parked',
82
84
  execution.report.diagnostics.at(-1)?.message
83
85
  ?? `flow "${definition.name}" step "${id}" parked`
@@ -86,6 +88,14 @@ export function authoredWorkerRunner(
86
88
  undefined,
87
89
  outcome.run_id,
88
90
  );
91
+ // Both `agent_parked` and `llm_parked` cover two unrelated situations —
92
+ // nothing attached to run the step, and the kernel's `needs_human`
93
+ // recovery wait after a worker attempt failed — and only the first is
94
+ // fixed by attaching a worker. The child's classification already knows
95
+ // which; carry it so the CLI boundary can name a remedy without
96
+ // re-deriving one from this message's wording.
97
+ error.parkCause = execution.report.parkCause;
98
+ throw error;
89
99
  }
90
100
  if (execution.exitCode !== 0) {
91
101
  // `execution.report.completionReason` is the RUN's reason (normally
@@ -161,10 +171,27 @@ export function authoredWorkerRunner(
161
171
  `f.agent options.model must be a string when set (got ${typeof options.model}).`,
162
172
  );
163
173
  }
164
- if (options.cwd !== undefined && typeof options.cwd !== 'string') {
174
+ // The same lexical rule the kernel and `flows check` apply, raised here
175
+ // so an authored body is refused before a run exists rather than at
176
+ // dispatch. Whether the directory is there is the worker's question.
177
+ // An absolute cwd is honored when it names a directory inside the run
178
+ // root — it lowers to the same relative declaration — and refused when
179
+ // it escapes, because a declaration this contract cannot contain is
180
+ // worse than none.
181
+ const declaredCwd = options.cwd === undefined || !isAbsolute(options.cwd) ? options.cwd
182
+ : relative(process.cwd(), options.cwd) || undefined;
183
+ if (options.cwd !== undefined && isAbsolute(options.cwd) && declaredCwd !== undefined
184
+ && (declaredCwd === '..' || declaredCwd.startsWith(`..${sep}`) || isAbsolute(declaredCwd))) {
185
+ throw new AuthoredFlowExecutionError(
186
+ 'agent_cli_unresolved',
187
+ `f.agent options.cwd must name a directory inside the run root ${JSON.stringify(process.cwd())} (got ${JSON.stringify(options.cwd)}).`,
188
+ );
189
+ }
190
+ const cwdProblem = declaredCwd === undefined ? undefined : agentCwdDeclarationError(declaredCwd);
191
+ if (cwdProblem !== undefined) {
165
192
  throw new AuthoredFlowExecutionError(
166
193
  'agent_cli_unresolved',
167
- `f.agent options.cwd must be a string when set (got ${typeof options.cwd}).`,
194
+ `f.agent options.cwd: ${cwdProblem}.`,
168
195
  );
169
196
  }
170
197
  if (options.transport !== undefined && options.transport !== 'direct' && options.transport !== 'relay') {
@@ -173,6 +200,10 @@ export function authoredWorkerRunner(
173
200
  `f.agent options.transport must be 'direct' or 'relay' (got ${JSON.stringify(options.transport)}).`,
174
201
  );
175
202
  }
203
+ const cwdTransport = agentCwdTransportError(declaredCwd, options.transport);
204
+ if (cwdTransport !== undefined) {
205
+ throw new AuthoredFlowExecutionError('agent_cli_unresolved', `f.agent options.${cwdTransport}.`);
206
+ }
176
207
  const permissions = options.permissions;
177
208
  const permissionsSnapshot = permissions === undefined ? undefined
178
209
  : snapshotJsonValue(permissions, 'f.agent options.permissions') as unknown as PermissionsSpec;
@@ -183,8 +214,7 @@ export function authoredWorkerRunner(
183
214
  ...(options.workspace === undefined ? {} : { surfaces: { workspace: [{ surface: options.workspace }] } }),
184
215
  ...(options.cli === undefined ? {} : { cli: options.cli }),
185
216
  ...(options.model === undefined ? {} : { model: options.model }),
186
- // The kernel takes only an absolute cwd; a relative one means the runner's own directory.
187
- ...(options.cwd === undefined ? {} : { cwd: resolve(options.cwd) }),
217
+ ...(declaredCwd === undefined ? {} : { cwd: declaredCwd }),
188
218
  ...(options.transport === undefined ? {} : { transport: options.transport }),
189
219
  ...(verification === undefined ? {} : { verification }),
190
220
  });
@@ -234,26 +264,43 @@ export function authoredDeterministicRunner(
234
264
  context: AuthoredStepContext = {},
235
265
  ) {
236
266
  const rootRunId = context.rootRunId;
237
- return async (id: string, command: string, terminal = false, leaseMs?: number, verification?: NamedGate): Promise<string> => {
267
+ // One lowering, two readers. `record` changes what the child run's verdict
268
+ // MEANS, so the reader changes with it; everything before the read — spec,
269
+ // admission key, budget, child index — is deliberately shared, because a
270
+ // recorded red command is still an ordinary child run.
271
+ function lower<T>(
272
+ id: string, command: string, terminal: boolean, leaseMs: number | undefined,
273
+ verification: NamedGate | undefined, onNonZero: 'record' | undefined,
274
+ read: (outcome: import('./protocol.js').RunOutcome) => Promise<T>,
275
+ ): Promise<T> {
238
276
  const spec = toKernelSpec(compileSpec({
239
277
  version: SPEC_SCHEMA_VERSION,
240
278
  name: `${name}/${id}`,
241
279
  steps: [{
242
280
  id, type: 'deterministic', command,
243
281
  ...(leaseMs === undefined ? {} : { lease_ms: leaseMs }),
282
+ ...(onNonZero === undefined ? {} : { onNonZero }),
244
283
  ...(verification === undefined ? {} : { verification }),
245
284
  }],
246
285
  }));
247
286
  const admissionKey = authoredChildAdmissionKey(rootRunId, id);
248
- const consume = async (outcome: import('./protocol.js').RunOutcome): Promise<string> => {
287
+ const consume = async (outcome: import('./protocol.js').RunOutcome): Promise<T> => {
249
288
  await recordAuthoredChild(journal, rootRunId, {
250
289
  step: id, runId: outcome.run_id, state: 'admitted', ...context.stepEdges?.(id),
251
290
  });
252
- return readSuccessfulOutput(journal, outcome, id, journalSteps, context);
291
+ return read(outcome);
253
292
  };
254
- if (terminal) return consume(await journal.runStart(spec, undefined, admissionKey));
293
+ if (terminal) return journal.runStart(spec, undefined, admissionKey).then(consume);
255
294
  return budget.execute(journal, spec, consume, admissionKey);
256
- };
295
+ }
296
+ const lowerRun = (id: string, command: string, terminal = false, leaseMs?: number, verification?: NamedGate): Promise<string> =>
297
+ lower(id, command, terminal, leaseMs, verification, undefined,
298
+ outcome => readSuccessfulOutput(journal, outcome, id, journalSteps, context));
299
+ /** `f.run(..., { onNonZero: 'record' })`: the exit code is the result, not a throw. */
300
+ lowerRun.recording = (id: string, command: string, leaseMs?: number, verification?: NamedGate): Promise<RecordedRunOutcome> =>
301
+ lower(id, command, false, leaseMs, verification, 'record',
302
+ outcome => readRecordedOutcome(journal, outcome, id, journalSteps, context));
303
+ return lowerRun;
257
304
  }
258
305
 
259
306
  /**
@@ -284,6 +331,8 @@ function stepDetails(
284
331
  ...(found.stderrTail === undefined ? {} : { stderrTail: found.stderrTail }),
285
332
  ...(found.detail === undefined ? {} : { detail: found.detail }),
286
333
  ...(found.transcriptPath === undefined ? {} : { transcriptPath: found.transcriptPath }),
334
+ ...(found.attempts === undefined ? {} : { attempts: found.attempts }),
335
+ ...(found.attemptEvidence === undefined ? {} : { attemptEvidence: found.attemptEvidence }),
287
336
  ...(found.hint === undefined ? {} : { hint: found.hint }),
288
337
  ...(found.journalPath === undefined ? {} : { journalPath: found.journalPath }),
289
338
  };
@@ -0,0 +1,66 @@
1
+ import { compileSpec } from '../compile.js';
2
+ import type { FlowSpec } from '../spec.js';
3
+ import type { CheckWarningDiagnostic } from './check.js';
4
+
5
+ /** Enough ids to recognise the flow, not enough to bury the remedy. */
6
+ const NAMED_STEP_LIMIT = 3;
7
+
8
+ /**
9
+ * `agent` steps need a worker attached for step type `agent`, and nothing in a
10
+ * spec says whether one will be. `flows run` attaches the SDK's `AgentWorker`
11
+ * only under `--local-agent` (`cli/run.ts`); with no worker the run parks at
12
+ * the first agent step with `no worker is attached for step type "agent"`.
13
+ * `flows check` already walks these steps to print its `REQUIRES` line, so the
14
+ * fact is in hand — this spends it.
15
+ *
16
+ * Warning, never a refusal, and worded as a requirement rather than a
17
+ * prediction: worker attachment is *unknown* here, not absent. A worker can be
18
+ * attached out of band (an SDK `AgentWorker` against the same daemon, Cloud's
19
+ * workers for a hosted run), and `flows check` is daemon-free by construction,
20
+ * so it cannot see one either way.
21
+ *
22
+ * Scoped to `agent` steps. `--local-agent` attaches no `llm` worker on the
23
+ * YAML path, so naming it for `llm` steps would be false; those steps are not
24
+ * counted and not mentioned.
25
+ *
26
+ * Blind spot, shared with `permissions_unenforced`: an authored `.flow.ts`
27
+ * flow is checked through `checkMcpHeader`, which preflights a synthetic
28
+ * one-step header spec without executing the body. There are no compiled
29
+ * `agent` steps to count, so this diagnostic is silent for `.flow.ts`.
30
+ */
31
+ export function agentWorkerDiagnostics(authoring: FlowSpec): CheckWarningDiagnostic[] {
32
+ const steps = agentStepIds(authoring);
33
+ if (steps.length === 0) return [];
34
+ return [{ severity: 'warning', kind: 'agent_worker_unresolved', message: message(steps) }];
35
+ }
36
+
37
+ /**
38
+ * Compiled `agent` steps, by id. Compilation is the only walk that sees them
39
+ * all: a YAML helper step (`slack: { post: … }`) is an `agent` step carrying a
40
+ * helper envelope and needs the same SDK agent worker, and only the compiler
41
+ * lowers it into one. `requirements.harnesses` is the wrong source — it
42
+ * deduplicates by harness, counts `llm` use, and omits helper steps entirely.
43
+ *
44
+ * A spec that does not compile yields no diagnostic rather than a thrown
45
+ * check: the compile refusal is owned by the existing preflight path and is
46
+ * the whole report in that case.
47
+ */
48
+ function agentStepIds(authoring: FlowSpec): string[] {
49
+ try {
50
+ return compileSpec(authoring).steps.filter((step) => step.type === 'agent').map((step) => step.id);
51
+ } catch {
52
+ return [];
53
+ }
54
+ }
55
+
56
+ function message(steps: readonly string[]): string {
57
+ const named = steps.slice(0, NAMED_STEP_LIMIT).map((id) => `"${id}"`).join(', ');
58
+ const rest = steps.length - NAMED_STEP_LIMIT;
59
+ const list = rest > 0 ? `${named} and ${rest} more` : named;
60
+ const subject = steps.length === 1
61
+ ? `1 agent step (${list}) requires`
62
+ : `${steps.length} agent steps (${list}) require`;
63
+ return `${subject} an attached worker. For a local run, use \`flows run --local-agent <flow>\`, `
64
+ + 'unless you already attach an agent worker for this daemon. `flows check` does not verify worker '
65
+ + 'attachment: with no worker attached the run parks at the first agent step.';
66
+ }