@relayflows/sdk 2.0.29 → 2.0.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (393) hide show
  1. package/dist/agent-artifacts.d.ts.map +1 -1
  2. package/dist/agent-artifacts.js +2 -9
  3. package/dist/agent-artifacts.js.map +1 -1
  4. package/dist/agent-cwd.d.ts +69 -0
  5. package/dist/agent-cwd.d.ts.map +1 -0
  6. package/dist/agent-cwd.js +140 -0
  7. package/dist/agent-cwd.js.map +1 -0
  8. package/dist/artifact-scan-policy.d.ts +35 -0
  9. package/dist/artifact-scan-policy.d.ts.map +1 -0
  10. package/dist/artifact-scan-policy.js +49 -0
  11. package/dist/artifact-scan-policy.js.map +1 -0
  12. package/dist/authored-completion-record.d.ts +35 -0
  13. package/dist/authored-completion-record.d.ts.map +1 -0
  14. package/dist/authored-completion-record.js +92 -0
  15. package/dist/authored-completion-record.js.map +1 -0
  16. package/dist/authored-completion.d.ts +99 -0
  17. package/dist/authored-completion.d.ts.map +1 -0
  18. package/dist/authored-completion.js +184 -0
  19. package/dist/authored-completion.js.map +1 -0
  20. package/dist/authored-flow-error.d.ts +13 -1
  21. package/dist/authored-flow-error.d.ts.map +1 -1
  22. package/dist/authored-flow-error.js +12 -0
  23. package/dist/authored-flow-error.js.map +1 -1
  24. package/dist/authored-flow-executor.d.ts +7 -30
  25. package/dist/authored-flow-executor.d.ts.map +1 -1
  26. package/dist/authored-flow-executor.js +64 -48
  27. package/dist/authored-flow-executor.js.map +1 -1
  28. package/dist/authored-human.d.ts.map +1 -1
  29. package/dist/authored-human.js +1 -3
  30. package/dist/authored-human.js.map +1 -1
  31. package/dist/authored-node-entry.js +4 -1
  32. package/dist/authored-node-entry.js.map +1 -1
  33. package/dist/authored-node-runner.d.ts +9 -1
  34. package/dist/authored-node-runner.d.ts.map +1 -1
  35. package/dist/authored-node-runner.js +92 -17
  36. package/dist/authored-node-runner.js.map +1 -1
  37. package/dist/authored-root.d.ts +5 -0
  38. package/dist/authored-root.d.ts.map +1 -1
  39. package/dist/authored-root.js +20 -2
  40. package/dist/authored-root.js.map +1 -1
  41. package/dist/authored-step-output.d.ts +35 -10
  42. package/dist/authored-step-output.d.ts.map +1 -1
  43. package/dist/authored-step-output.js +108 -40
  44. package/dist/authored-step-output.js.map +1 -1
  45. package/dist/authored-verdict.d.ts +28 -0
  46. package/dist/authored-verdict.d.ts.map +1 -0
  47. package/dist/authored-verdict.js +79 -0
  48. package/dist/authored-verdict.js.map +1 -0
  49. package/dist/authored-worker-step.d.ts +5 -2
  50. package/dist/authored-worker-step.d.ts.map +1 -1
  51. package/dist/authored-worker-step.js +48 -11
  52. package/dist/authored-worker-step.js.map +1 -1
  53. package/dist/bundle-extensions.d.ts.map +1 -1
  54. package/dist/bundle-extensions.js +4 -7
  55. package/dist/bundle-extensions.js.map +1 -1
  56. package/dist/bundle.d.ts.map +1 -1
  57. package/dist/bundle.js +41 -7
  58. package/dist/bundle.js.map +1 -1
  59. package/dist/canonical.d.ts.map +1 -1
  60. package/dist/canonical.js +18 -6
  61. package/dist/canonical.js.map +1 -1
  62. package/dist/cli/check-worker-surface.d.ts +27 -0
  63. package/dist/cli/check-worker-surface.d.ts.map +1 -0
  64. package/dist/cli/check-worker-surface.js +63 -0
  65. package/dist/cli/check-worker-surface.js.map +1 -0
  66. package/dist/cli/check.d.ts +19 -2
  67. package/dist/cli/check.d.ts.map +1 -1
  68. package/dist/cli/check.js +12 -4
  69. package/dist/cli/check.js.map +1 -1
  70. package/dist/cli/cloud-format.d.ts +9 -0
  71. package/dist/cli/cloud-format.d.ts.map +1 -0
  72. package/dist/cli/cloud-format.js +77 -0
  73. package/dist/cli/cloud-format.js.map +1 -0
  74. package/dist/cli/cloud-live.d.ts +39 -0
  75. package/dist/cli/cloud-live.d.ts.map +1 -0
  76. package/dist/cli/cloud-live.js +325 -0
  77. package/dist/cli/cloud-live.js.map +1 -0
  78. package/dist/cli/cloud-read.d.ts +3 -7
  79. package/dist/cli/cloud-read.d.ts.map +1 -1
  80. package/dist/cli/cloud-read.js +14 -266
  81. package/dist/cli/cloud-read.js.map +1 -1
  82. package/dist/cli/cloud-refusal.d.ts +39 -0
  83. package/dist/cli/cloud-refusal.d.ts.map +1 -0
  84. package/dist/cli/cloud-refusal.js +95 -0
  85. package/dist/cli/cloud-refusal.js.map +1 -0
  86. package/dist/cli/cloud-status-view.d.ts +7 -0
  87. package/dist/cli/cloud-status-view.d.ts.map +1 -0
  88. package/dist/cli/cloud-status-view.js +214 -0
  89. package/dist/cli/cloud-status-view.js.map +1 -0
  90. package/dist/cli/direct-run.d.ts.map +1 -1
  91. package/dist/cli/direct-run.js +16 -2
  92. package/dist/cli/direct-run.js.map +1 -1
  93. package/dist/cli/hn-monitor.d.ts +0 -13
  94. package/dist/cli/hn-monitor.d.ts.map +1 -1
  95. package/dist/cli/hn-monitor.js +2 -1
  96. package/dist/cli/hn-monitor.js.map +1 -1
  97. package/dist/cli/local-agent-remedy.d.ts +80 -0
  98. package/dist/cli/local-agent-remedy.d.ts.map +1 -0
  99. package/dist/cli/local-agent-remedy.js +116 -0
  100. package/dist/cli/local-agent-remedy.js.map +1 -0
  101. package/dist/cli/observer-session.d.ts +41 -0
  102. package/dist/cli/observer-session.d.ts.map +1 -0
  103. package/dist/cli/observer-session.js +78 -0
  104. package/dist/cli/observer-session.js.map +1 -0
  105. package/dist/cli/run.d.ts +30 -2
  106. package/dist/cli/run.d.ts.map +1 -1
  107. package/dist/cli/run.js +186 -24
  108. package/dist/cli/run.js.map +1 -1
  109. package/dist/cli/status.d.ts +9 -0
  110. package/dist/cli/status.d.ts.map +1 -1
  111. package/dist/cli/status.js +31 -2
  112. package/dist/cli/status.js.map +1 -1
  113. package/dist/cli/step-evidence.d.ts +90 -0
  114. package/dist/cli/step-evidence.d.ts.map +1 -0
  115. package/dist/cli/step-evidence.js +259 -0
  116. package/dist/cli/step-evidence.js.map +1 -0
  117. package/dist/cli/step-failure.d.ts +14 -4
  118. package/dist/cli/step-failure.d.ts.map +1 -1
  119. package/dist/cli/step-failure.js +41 -76
  120. package/dist/cli/step-failure.js.map +1 -1
  121. package/dist/cli-commands.d.ts +6 -0
  122. package/dist/cli-commands.d.ts.map +1 -1
  123. package/dist/cli-commands.js +2 -0
  124. package/dist/cli-commands.js.map +1 -1
  125. package/dist/cli.d.ts +10 -0
  126. package/dist/cli.d.ts.map +1 -1
  127. package/dist/cli.js +53 -42
  128. package/dist/cli.js.map +1 -1
  129. package/dist/cloud-read.d.ts +15 -0
  130. package/dist/cloud-read.d.ts.map +1 -1
  131. package/dist/cloud-read.js +20 -1
  132. package/dist/cloud-read.js.map +1 -1
  133. package/dist/cloud-run-record.d.ts +27 -0
  134. package/dist/cloud-run-record.d.ts.map +1 -0
  135. package/dist/cloud-run-record.js +56 -0
  136. package/dist/cloud-run-record.js.map +1 -0
  137. package/dist/cloud-run.d.ts +2 -9
  138. package/dist/cloud-run.d.ts.map +1 -1
  139. package/dist/cloud-run.js +3 -20
  140. package/dist/cloud-run.js.map +1 -1
  141. package/dist/communication/local.d.ts.map +1 -1
  142. package/dist/communication/local.js +2 -1
  143. package/dist/communication/local.js.map +1 -1
  144. package/dist/communication/worker.d.ts +1 -1
  145. package/dist/communication/worker.d.ts.map +1 -1
  146. package/dist/communication/worker.js +13 -5
  147. package/dist/communication/worker.js.map +1 -1
  148. package/dist/compile.d.ts.map +1 -1
  149. package/dist/compile.js +28 -6
  150. package/dist/compile.js.map +1 -1
  151. package/dist/direct-input.d.ts.map +1 -1
  152. package/dist/direct-input.js +21 -1
  153. package/dist/direct-input.js.map +1 -1
  154. package/dist/failure-kinds.d.ts +96 -6
  155. package/dist/failure-kinds.d.ts.map +1 -1
  156. package/dist/failure-kinds.js +29 -2
  157. package/dist/failure-kinds.js.map +1 -1
  158. package/dist/flow-extension-compat.d.ts.map +1 -1
  159. package/dist/flow-extension-compat.js +26 -8
  160. package/dist/flow-extension-compat.js.map +1 -1
  161. package/dist/flow-extension-loader.d.ts +7 -0
  162. package/dist/flow-extension-loader.d.ts.map +1 -1
  163. package/dist/flow-extension-loader.js +48 -22
  164. package/dist/flow-extension-loader.js.map +1 -1
  165. package/dist/flow-requirements.d.ts.map +1 -1
  166. package/dist/flow-requirements.js +24 -9
  167. package/dist/flow-requirements.js.map +1 -1
  168. package/dist/fs-descriptor.d.ts +17 -0
  169. package/dist/fs-descriptor.d.ts.map +1 -0
  170. package/dist/fs-descriptor.js +96 -0
  171. package/dist/fs-descriptor.js.map +1 -0
  172. package/dist/gate-contract.d.ts +7 -0
  173. package/dist/gate-contract.d.ts.map +1 -1
  174. package/dist/gate-contract.js +6 -1
  175. package/dist/gate-contract.js.map +1 -1
  176. package/dist/helper-preflight.d.ts.map +1 -1
  177. package/dist/helper-preflight.js +10 -3
  178. package/dist/helper-preflight.js.map +1 -1
  179. package/dist/helper-reference.d.ts +25 -0
  180. package/dist/helper-reference.d.ts.map +1 -0
  181. package/dist/helper-reference.js +129 -0
  182. package/dist/helper-reference.js.map +1 -0
  183. package/dist/hosted-base-snapshot.d.ts +25 -0
  184. package/dist/hosted-base-snapshot.d.ts.map +1 -0
  185. package/dist/hosted-base-snapshot.js +278 -0
  186. package/dist/hosted-base-snapshot.js.map +1 -0
  187. package/dist/hosted-extension-declarations.d.ts +10 -0
  188. package/dist/hosted-extension-declarations.d.ts.map +1 -0
  189. package/dist/hosted-extension-declarations.js +272 -0
  190. package/dist/hosted-extension-declarations.js.map +1 -0
  191. package/dist/hosted-extension-isolation.d.ts +82 -0
  192. package/dist/hosted-extension-isolation.d.ts.map +1 -0
  193. package/dist/hosted-extension-isolation.js +322 -0
  194. package/dist/hosted-extension-isolation.js.map +1 -0
  195. package/dist/hosted-extension-manifest.d.ts +9 -0
  196. package/dist/hosted-extension-manifest.d.ts.map +1 -0
  197. package/dist/hosted-extension-manifest.js +235 -0
  198. package/dist/hosted-extension-manifest.js.map +1 -0
  199. package/dist/hosted-extension-protocol.d.ts +16 -0
  200. package/dist/hosted-extension-protocol.d.ts.map +1 -0
  201. package/dist/hosted-extension-protocol.js +224 -0
  202. package/dist/hosted-extension-protocol.js.map +1 -0
  203. package/dist/hosted-extension-runtime.d.ts +30 -0
  204. package/dist/hosted-extension-runtime.d.ts.map +1 -0
  205. package/dist/hosted-extension-runtime.js +167 -0
  206. package/dist/hosted-extension-runtime.js.map +1 -0
  207. package/dist/hosted-extension-sandbox-source.d.ts +9 -0
  208. package/dist/hosted-extension-sandbox-source.d.ts.map +1 -0
  209. package/dist/hosted-extension-sandbox-source.js +95 -0
  210. package/dist/hosted-extension-sandbox-source.js.map +1 -0
  211. package/dist/hosted-extension-sandbox.d.ts +39 -0
  212. package/dist/hosted-extension-sandbox.d.ts.map +1 -0
  213. package/dist/hosted-extension-sandbox.js +418 -0
  214. package/dist/hosted-extension-sandbox.js.map +1 -0
  215. package/dist/hosted-project.d.ts +3 -0
  216. package/dist/hosted-project.d.ts.map +1 -0
  217. package/dist/hosted-project.js +19 -0
  218. package/dist/hosted-project.js.map +1 -0
  219. package/dist/hosted-promise-safety.d.ts +12 -0
  220. package/dist/hosted-promise-safety.d.ts.map +1 -0
  221. package/dist/hosted-promise-safety.js +63 -0
  222. package/dist/hosted-promise-safety.js.map +1 -0
  223. package/dist/index.d.ts +3 -1
  224. package/dist/index.d.ts.map +1 -1
  225. package/dist/index.js +2 -0
  226. package/dist/index.js.map +1 -1
  227. package/dist/intrinsic-array.d.ts +3 -0
  228. package/dist/intrinsic-array.d.ts.map +1 -0
  229. package/dist/intrinsic-array.js +14 -0
  230. package/dist/intrinsic-array.js.map +1 -0
  231. package/dist/journal-client.d.ts +3 -1
  232. package/dist/journal-client.d.ts.map +1 -1
  233. package/dist/journal-client.js +17 -2
  234. package/dist/journal-client.js.map +1 -1
  235. package/dist/journal-projection.d.ts +16 -0
  236. package/dist/journal-projection.d.ts.map +1 -0
  237. package/dist/journal-projection.js +108 -0
  238. package/dist/journal-projection.js.map +1 -0
  239. package/dist/json-value.d.ts +6 -1
  240. package/dist/json-value.d.ts.map +1 -1
  241. package/dist/json-value.js +155 -36
  242. package/dist/json-value.js.map +1 -1
  243. package/dist/llm-worker.d.ts +9 -0
  244. package/dist/llm-worker.d.ts.map +1 -1
  245. package/dist/llm-worker.js +16 -2
  246. package/dist/llm-worker.js.map +1 -1
  247. package/dist/local-agent.d.ts +1 -1
  248. package/dist/local-agent.d.ts.map +1 -1
  249. package/dist/local-agent.js +4 -3
  250. package/dist/local-agent.js.map +1 -1
  251. package/dist/named-gate-lowering.js +35 -5
  252. package/dist/named-gate-lowering.js.map +1 -1
  253. package/dist/named-gate-preflight.d.ts +27 -0
  254. package/dist/named-gate-preflight.d.ts.map +1 -0
  255. package/dist/named-gate-preflight.js +47 -0
  256. package/dist/named-gate-preflight.js.map +1 -0
  257. package/dist/named-gates.d.ts +1 -1
  258. package/dist/named-gates.d.ts.map +1 -1
  259. package/dist/named-gates.js +3 -0
  260. package/dist/named-gates.js.map +1 -1
  261. package/dist/observer-link.d.ts +7 -0
  262. package/dist/observer-link.d.ts.map +1 -1
  263. package/dist/observer-link.js +3 -1
  264. package/dist/observer-link.js.map +1 -1
  265. package/dist/plugin-store.d.ts +8 -2
  266. package/dist/plugin-store.d.ts.map +1 -1
  267. package/dist/plugin-store.js +233 -56
  268. package/dist/plugin-store.js.map +1 -1
  269. package/dist/preflight.d.ts.map +1 -1
  270. package/dist/preflight.js +7 -0
  271. package/dist/preflight.js.map +1 -1
  272. package/dist/protocol.d.ts +2 -0
  273. package/dist/protocol.d.ts.map +1 -1
  274. package/dist/redact.d.ts +42 -0
  275. package/dist/redact.d.ts.map +1 -1
  276. package/dist/redact.js +128 -0
  277. package/dist/redact.js.map +1 -1
  278. package/dist/run-projection.d.ts +83 -0
  279. package/dist/run-projection.d.ts.map +1 -0
  280. package/dist/run-projection.js +199 -0
  281. package/dist/run-projection.js.map +1 -0
  282. package/dist/semver-range.d.ts.map +1 -1
  283. package/dist/semver-range.js +27 -15
  284. package/dist/semver-range.js.map +1 -1
  285. package/dist/shell-word.d.ts +19 -0
  286. package/dist/shell-word.d.ts.map +1 -0
  287. package/dist/shell-word.js +23 -0
  288. package/dist/shell-word.js.map +1 -0
  289. package/dist/spec.d.ts +52 -3
  290. package/dist/spec.d.ts.map +1 -1
  291. package/dist/spec.js.map +1 -1
  292. package/dist/step-fields.d.ts +1 -1
  293. package/dist/step-fields.js +1 -1
  294. package/dist/step-fields.js.map +1 -1
  295. package/dist/steps-green.d.ts +27 -0
  296. package/dist/steps-green.d.ts.map +1 -0
  297. package/dist/steps-green.js +164 -0
  298. package/dist/steps-green.js.map +1 -0
  299. package/dist/validate.d.ts.map +1 -1
  300. package/dist/validate.js +39 -8
  301. package/dist/validate.js.map +1 -1
  302. package/dist/worker-lease.d.ts +8 -1
  303. package/dist/worker-lease.d.ts.map +1 -1
  304. package/dist/worker-lease.js +34 -4
  305. package/dist/worker-lease.js.map +1 -1
  306. package/dist/worker.d.ts +8 -0
  307. package/dist/worker.d.ts.map +1 -1
  308. package/dist/worker.js +17 -9
  309. package/dist/worker.js.map +1 -1
  310. package/package.json +3 -7
  311. package/src/agent-artifacts.ts +2 -10
  312. package/src/agent-cwd.ts +139 -0
  313. package/src/artifact-scan-policy.ts +51 -0
  314. package/src/authored-completion-record.ts +119 -0
  315. package/src/authored-completion.ts +203 -0
  316. package/src/authored-flow-error.ts +13 -1
  317. package/src/authored-flow-executor.ts +101 -59
  318. package/src/authored-human.ts +1 -4
  319. package/src/authored-node-entry.ts +4 -1
  320. package/src/authored-node-runner.ts +96 -22
  321. package/src/authored-root.ts +20 -2
  322. package/src/authored-step-output.ts +139 -42
  323. package/src/authored-verdict.ts +87 -0
  324. package/src/authored-worker-step.ts +60 -12
  325. package/src/bundle-extensions.ts +6 -5
  326. package/src/bundle.ts +52 -7
  327. package/src/canonical.ts +20 -6
  328. package/src/cli/check-worker-surface.ts +66 -0
  329. package/src/cli/check.ts +35 -4
  330. package/src/cli/cloud-format.ts +82 -0
  331. package/src/cli/cloud-live.ts +366 -0
  332. package/src/cli/cloud-read.ts +13 -282
  333. package/src/cli/cloud-refusal.ts +113 -0
  334. package/src/cli/cloud-status-view.ts +213 -0
  335. package/src/cli/direct-run.ts +19 -2
  336. package/src/cli/hn-monitor.ts +2 -1
  337. package/src/cli/local-agent-remedy.ts +160 -0
  338. package/src/cli/observer-session.ts +107 -0
  339. package/src/cli/run.ts +233 -28
  340. package/src/cli/status.ts +59 -3
  341. package/src/cli/step-evidence.ts +310 -0
  342. package/src/cli/step-failure.ts +57 -81
  343. package/src/cli-commands.ts +2 -0
  344. package/src/cli.ts +64 -44
  345. package/src/cloud-read.ts +25 -1
  346. package/src/cloud-run-record.ts +64 -0
  347. package/src/cloud-run.ts +5 -24
  348. package/src/communication/local.ts +2 -1
  349. package/src/communication/worker.ts +13 -5
  350. package/src/compile.ts +28 -6
  351. package/src/direct-input.ts +22 -1
  352. package/src/failure-kinds.ts +104 -2
  353. package/src/flow-extension-compat.ts +32 -7
  354. package/src/flow-extension-loader.ts +56 -21
  355. package/src/flow-requirements.ts +24 -9
  356. package/src/fs-descriptor.ts +127 -0
  357. package/src/gate-contract.ts +13 -1
  358. package/src/helper-preflight.ts +10 -3
  359. package/src/helper-reference.ts +130 -0
  360. package/src/hosted-base-snapshot.ts +351 -0
  361. package/src/hosted-extension-declarations.ts +387 -0
  362. package/src/hosted-extension-isolation.ts +457 -0
  363. package/src/hosted-extension-manifest.ts +249 -0
  364. package/src/hosted-extension-protocol.ts +262 -0
  365. package/src/hosted-extension-runtime.ts +266 -0
  366. package/src/hosted-extension-sandbox-source.ts +94 -0
  367. package/src/hosted-extension-sandbox.ts +489 -0
  368. package/src/hosted-project.ts +18 -0
  369. package/src/hosted-promise-safety.ts +68 -0
  370. package/src/index.ts +20 -0
  371. package/src/intrinsic-array.ts +14 -0
  372. package/src/journal-client.ts +19 -2
  373. package/src/journal-projection.ts +106 -0
  374. package/src/json-value.ts +184 -32
  375. package/src/llm-worker.ts +16 -2
  376. package/src/local-agent.ts +4 -2
  377. package/src/named-gate-lowering.ts +35 -5
  378. package/src/named-gate-preflight.ts +47 -0
  379. package/src/named-gates.ts +3 -0
  380. package/src/observer-link.ts +10 -1
  381. package/src/plugin-store.ts +323 -56
  382. package/src/preflight.ts +7 -0
  383. package/src/protocol.ts +2 -0
  384. package/src/redact.ts +111 -0
  385. package/src/run-projection.ts +247 -0
  386. package/src/semver-range.ts +39 -14
  387. package/src/shell-word.ts +24 -0
  388. package/src/spec.ts +54 -3
  389. package/src/step-fields.ts +1 -1
  390. package/src/steps-green.ts +168 -0
  391. package/src/validate.ts +35 -8
  392. package/src/worker-lease.ts +35 -5
  393. package/src/worker.ts +27 -11
@@ -0,0 +1,310 @@
1
+ import { redact } from '../redact.js';
2
+ import { formatStepExcerpt } from './step-excerpt.js';
3
+ import type {
4
+ AttemptEvidenceComparison,
5
+ StepAttemptFailure,
6
+ StepFailedDetails,
7
+ } from '../failure-kinds.js';
8
+
9
+ /** Per-attempt history is context beside that account, not a second copy of it. */
10
+ export const ATTEMPT_TAIL_BYTES = 256;
11
+
12
+ /**
13
+ * What the daemon appends when it cut a render before journaling it
14
+ * (`worker_failure_detail`, relayflowd/src/engine/remote.rs). Evidence that
15
+ * arrived already truncated cannot establish that two attempts failed
16
+ * identically, however equal the surviving bytes are.
17
+ */
18
+ const PRODUCER_TRUNCATED = /…\s*\((?:render bounded|[\d,]+ bytes truncated)\)$/u;
19
+
20
+ /**
21
+ * The evidence fields a step failure reports, as the journal held them.
22
+ *
23
+ * Extraction and display bounds are separated deliberately. The terminal
24
+ * attempt renders at 1,024 bytes and each historical attempt at 256, but the
25
+ * COMPARISON that decides whether a retry failed differently runs on the
26
+ * unbounded record (`failureCause`): two different errors can share a 256-byte
27
+ * suffix, and calling those the same failure is the bug this reader exists to
28
+ * avoid.
29
+ */
30
+ export interface SelectedEvidence {
31
+ exitCode?: number;
32
+ /** Present only when nonempty. */
33
+ stdoutTail?: string;
34
+ /** Present whenever the journal carried one, including the empty string. */
35
+ stderrTail?: string;
36
+ detail?: string;
37
+ transcriptPath?: string;
38
+ }
39
+
40
+ /**
41
+ * Pull the process-shaped fields out of whichever field carried them.
42
+ *
43
+ * `verification.detail` is last because it is the daemon's own render rather
44
+ * than the worker's structured report — but for an agent step it is the only
45
+ * thing that survives, so it is parsed when it parses and kept verbatim when
46
+ * it does not. A truncated render (the daemon caps at 2,000 chars and appends
47
+ * a truncation note) will not parse; that falls through to the raw string,
48
+ * which is still the account of what went wrong.
49
+ *
50
+ * An agent or llm completion now also carries `trajectory_tail.transcript`
51
+ * (agent-transcript.ts) on every attempt. That object is not process-shaped,
52
+ * so it must not shadow the render that is: the first candidate that carries
53
+ * an exit code or a tail wins. The digest contributes what only it has — the
54
+ * failure excerpt the worker picked out of the provider's frames, and the
55
+ * path of the transcript file.
56
+ */
57
+ export function selectEvidence(payload: Record<string, unknown>): SelectedEvidence {
58
+ const detail = record(payload['verification'])?.['detail'];
59
+ const rendered = typeof detail === 'string' ? parsed(detail) : undefined;
60
+ const candidates = [
61
+ record(payload['output']),
62
+ record(payload['trajectory_tail']),
63
+ rendered,
64
+ ].filter((candidate): candidate is Record<string, unknown> => candidate !== undefined);
65
+ const structured = candidates.find(processShaped) ?? candidates[0];
66
+ const exitCode = structured?.['exit_code'];
67
+ const stdout = structured?.['stdout_tail'];
68
+ const stderr = structured?.['stderr_tail'];
69
+ const transcript = record(record(payload['trajectory_tail'])?.['transcript']);
70
+ const failure = record(transcript?.['failure']);
71
+ const excerpt = failure?.['excerpt'];
72
+ const transcriptPath = record(transcript?.['file'])?.['path'];
73
+ // The excerpt is the worker's own pick of the failure; a `stderr` excerpt
74
+ // is the same bytes as `stderrTail`, so it is not printed twice.
75
+ const excerptDetail = typeof excerpt === 'string' && excerpt.length > 0
76
+ && !(failure?.['kind'] === 'stderr' && typeof stderr === 'string')
77
+ ? excerpt : undefined;
78
+ return {
79
+ ...(typeof exitCode === 'number' && Number.isSafeInteger(exitCode) ? { exitCode } : {}),
80
+ ...(typeof stdout === 'string' && stdout.length > 0 ? { stdoutTail: stdout } : {}),
81
+ ...(typeof stderr === 'string' ? { stderrTail: stderr } : {}),
82
+ ...(excerptDetail !== undefined ? { detail: excerptDetail }
83
+ : typeof detail === 'string' && detail.length > 0 && !duplicates(detail, rendered, exitCode)
84
+ ? { detail } : {}),
85
+ ...(typeof transcriptPath === 'string' && transcriptPath.length > 0 ? { transcriptPath } : {}),
86
+ };
87
+ }
88
+
89
+ /**
90
+ * Whether printing `verification.detail` would print bytes already printed.
91
+ *
92
+ * Only two shapes do. The daemon's render of a worker failure IS the
93
+ * `{exit_code, stdout_tail, stderr_tail}` report (`worker_failure_detail`,
94
+ * relayflowd/src/engine/remote.rs), which a deterministic step journals
95
+ * alongside its preserved `output`; and the `exit_code` gate's verdict on its
96
+ * own is the exit code that is already reported beside it (`exit code was 1`,
97
+ * relayflowd-core/src/verify.rs).
98
+ *
99
+ * Every other verdict is the ONLY account of its failure. `output did not
100
+ * contain "READY"` accompanies exit 0 and empty tails, so suppressing the
101
+ * detail whenever some field happened to be process-shaped left the attempt
102
+ * saying `exit=0 — no failure evidence recorded` while the journal held the
103
+ * gate error — the first attempt hidden again, inside the diagnostic that
104
+ * exists to surface it.
105
+ */
106
+ function duplicates(
107
+ detail: string,
108
+ rendered: Record<string, unknown> | undefined,
109
+ exitCode: unknown,
110
+ ): boolean {
111
+ if (rendered !== undefined && processShaped(rendered)) return true;
112
+ return typeof exitCode === 'number' && detail === `exit code was ${exitCode}`;
113
+ }
114
+
115
+ /** The `StepFailedDetails` scalars for one completion, as a failure excerpt. */
116
+ export function terminalEvidence(payload: Record<string, unknown>): Partial<StepFailedDetails> {
117
+ const selected = selectEvidence(payload);
118
+ return {
119
+ ...(selected.exitCode === undefined ? {} : { exitCode: selected.exitCode }),
120
+ ...(selected.stdoutTail === undefined ? {} : { stdoutTail: formatStepExcerpt(selected.stdoutTail) }),
121
+ ...(selected.stderrTail === undefined ? {} : { stderrTail: formatStepExcerpt(selected.stderrTail) }),
122
+ ...(selected.detail === undefined ? {} : { detail: formatStepExcerpt(selected.detail) }),
123
+ ...(selected.transcriptPath === undefined ? {} : { transcriptPath: selected.transcriptPath }),
124
+ };
125
+ }
126
+
127
+ /**
128
+ * One historical attempt's record, redacted and then bounded.
129
+ *
130
+ * Redaction runs first so a credential cannot survive by sitting outside the
131
+ * excerpt window on one attempt and inside it on another. Control-character
132
+ * replacement alone is not redaction: an agent that echoed `rk_live_...` into
133
+ * its stderr would otherwise reach a terminal through this new field.
134
+ */
135
+ export function attemptFailure(
136
+ attempt: unknown,
137
+ completionReason: string,
138
+ disposition: unknown,
139
+ payload: Record<string, unknown>,
140
+ ): StepAttemptFailure {
141
+ const selected = selectEvidence(payload);
142
+ let truncated = false;
143
+ const bound = (value: string): string => {
144
+ const redacted = redact(value);
145
+ if (Buffer.byteLength(redacted, 'utf8') > ATTEMPT_TAIL_BYTES) truncated = true;
146
+ return tail(redacted, ATTEMPT_TAIL_BYTES);
147
+ };
148
+ const record_: StepAttemptFailure = {
149
+ ...(typeof attempt === 'number' && Number.isSafeInteger(attempt) && attempt > 0
150
+ ? { attempt } : {}),
151
+ completionReason,
152
+ ...(typeof disposition === 'string' && disposition.length > 0 ? { disposition } : {}),
153
+ ...(selected.exitCode === undefined ? {} : { exitCode: selected.exitCode }),
154
+ ...(selected.stdoutTail === undefined ? {} : { stdoutTail: bound(selected.stdoutTail) }),
155
+ ...(selected.stderrTail === undefined ? {} : { stderrTail: bound(selected.stderrTail) }),
156
+ ...(selected.detail === undefined ? {} : { detail: bound(selected.detail) }),
157
+ };
158
+ return truncated ? { ...record_, truncated } : record_;
159
+ }
160
+
161
+ /**
162
+ * Everything about a completion that bears on WHY it failed, unbounded.
163
+ *
164
+ * Deliberately not the display selection: `selectEvidence` suppresses the
165
+ * daemon's render when process-shaped output exists, and prefers a transcript
166
+ * excerpt over both, so two attempts can show identical excerpts over
167
+ * different gate verdicts. Everything the journal recorded about the failure
168
+ * is read here instead, before any bound is applied.
169
+ *
170
+ * Timestamps, transcript paths, dispositions and attempt numbers are NOT
171
+ * causes and are excluded: they differ on every attempt by construction.
172
+ */
173
+ export function failureCause(
174
+ completionReason: string,
175
+ payload: Record<string, unknown>,
176
+ ): AttemptCause {
177
+ const verification = record(payload['verification']);
178
+ const output = record(payload['output']);
179
+ const trajectory = record(payload['trajectory_tail']);
180
+ const failure = record(record(trajectory?.['transcript'])?.['failure']);
181
+ const verificationDetail = text(verification?.['detail']);
182
+ const accounts = [
183
+ verificationDetail,
184
+ text(output?.['stdout_tail']), text(output?.['stderr_tail']),
185
+ text(trajectory?.['stdout_tail']), text(trajectory?.['stderr_tail']),
186
+ text(failure?.['excerpt']),
187
+ ];
188
+ const exitCodes = [output?.['exit_code'], trajectory?.['exit_code']]
189
+ .filter(value => typeof value === 'number');
190
+ return {
191
+ key: JSON.stringify([
192
+ // `verification_failed` on a retry and `retries_exhausted` at the budget
193
+ // limit are the SAME fallback over the same failure: `completion_actions`
194
+ // picks the label from the branch it took, not from the cause. Comparing
195
+ // them literally would report "the retry failed differently" on every
196
+ // ordinary repeated failure. No other pair of labels is collapsed.
197
+ completionReason === 'verification_failed' || completionReason === 'retries_exhausted'
198
+ ? 'kernel_rejected' : completionReason,
199
+ text(verification?.['gate']), text(verification?.['verdict']), verificationDetail,
200
+ ...exitCodes, ...accounts, text(failure?.['kind']),
201
+ ]),
202
+ // An attempt that journaled nothing about itself cannot agree with
203
+ // another one; it can only fail to disagree.
204
+ recorded: exitCodes.length > 0 || accounts.some(account => account !== undefined),
205
+ producerTruncated: (verificationDetail !== undefined
206
+ && PRODUCER_TRUNCATED.test(verificationDetail))
207
+ // The transcript digest's own flag (`boundTranscriptDigest`,
208
+ // agent-transcript.ts): the worker already cut this excerpt, so equal
209
+ // survivors cannot establish identical failures either.
210
+ || failure?.['truncated'] === true,
211
+ };
212
+ }
213
+
214
+ export interface AttemptCause {
215
+ readonly key: string;
216
+ readonly recorded: boolean;
217
+ readonly producerTruncated: boolean;
218
+ }
219
+
220
+ /**
221
+ * Only attempts that recorded something can establish a difference.
222
+ *
223
+ * An attempt that journaled no account of itself differs from every other one
224
+ * on paper while saying nothing about why it failed, and reporting that as
225
+ * `differs` would put "an earlier attempt may have had side effects" on a
226
+ * crash that is evidence of nothing. Truncation is not the same case: two
227
+ * renders the daemon already cut can only look MORE alike than they were, so
228
+ * surviving bytes that disagree still prove the causes disagreed.
229
+ */
230
+ export function compareAttempts(causes: readonly AttemptCause[]): AttemptEvidenceComparison {
231
+ const recorded = causes.filter(cause => cause.recorded);
232
+ if (new Set(recorded.map(cause => cause.key)).size > 1) return 'differs';
233
+ return recorded.length === causes.length && causes.every(cause => !cause.producerTruncated)
234
+ ? 'unchanged' : 'unknown';
235
+ }
236
+
237
+ const COMPARISON_CLAUSE: Record<AttemptEvidenceComparison, string> = {
238
+ differs: 'recorded failure evidence differs. An earlier attempt may have had side effects.',
239
+ unchanged: 'recorded failure evidence is unchanged across them.',
240
+ unknown: 'recorded failure evidence is incomplete, so whether the causes differ is unknown.',
241
+ };
242
+
243
+ /**
244
+ * Every failed attempt, oldest first, under one line saying whether they
245
+ * agree. Rendered in full rather than elided: `flows logs` prints the runner
246
+ * log without eliding it, so an attempt dropped here is an attempt that
247
+ * reaches no reader at all.
248
+ */
249
+ export function renderAttemptHistory(details: StepFailedDetails): string {
250
+ const attempts = details.attempts;
251
+ if (attempts === undefined || attempts.length < 2) return '';
252
+ const comparison = COMPARISON_CLAUSE[details.attemptEvidence ?? 'unknown'];
253
+ return [`\nAttempts: ${attempts.length} failed; ${comparison}`, ...attempts.map(renderAttempt)]
254
+ .join('\n');
255
+ }
256
+
257
+ function renderAttempt(attempt: StepAttemptFailure): string {
258
+ const head = ` attempt ${attempt.attempt ?? '?'}: ${attempt.completionReason ?? 'unknown'}`
259
+ + (attempt.exitCode === undefined ? '' : ` exit=${attempt.exitCode}`)
260
+ + (attempt.truncated ? ' (excerpt truncated)' : '');
261
+ // An empty `stderrTail` is a journaled fact but not an account of anything,
262
+ // so it never displaces useful stdout the way `detail ?? stderr ?? stdout`
263
+ // would. Both are kept when both exist.
264
+ const accounts: Array<[string, string]> = [
265
+ ['detail', attempt.detail], ['stderr', attempt.stderrTail], ['stdout', attempt.stdoutTail],
266
+ ].filter((pair): pair is [string, string] => typeof pair[1] === 'string' && pair[1].length > 0);
267
+ if (accounts.length === 0) return `${head} — no failure evidence recorded`;
268
+ const [only] = accounts;
269
+ if (accounts.length === 1 && !only![1].includes('\n')) return `${head} — ${only![0]}: ${only![1]}`;
270
+ return [head, ...accounts.map(([label, value]) => indent(`${label}: ${value}`))].join('\n');
271
+ }
272
+
273
+ /** Continuation lines are indented past the label so the list stays readable. */
274
+ function indent(block: string): string {
275
+ const [first, ...rest] = block.split('\n');
276
+ return [` ${first}`, ...rest.map(line => ` ${line}`)].join('\n');
277
+ }
278
+
279
+ function text(value: unknown): string | undefined {
280
+ return typeof value === 'string' && value.length > 0 ? value : undefined;
281
+ }
282
+
283
+ function processShaped(value: Record<string, unknown>): boolean {
284
+ return typeof value['exit_code'] === 'number'
285
+ || typeof value['stdout_tail'] === 'string' || typeof value['stderr_tail'] === 'string';
286
+ }
287
+
288
+ function parsed(value: string): Record<string, unknown> | undefined {
289
+ try {
290
+ return record(JSON.parse(value));
291
+ } catch {
292
+ return undefined;
293
+ }
294
+ }
295
+
296
+ export function record(value: unknown): Record<string, unknown> | undefined {
297
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
298
+ ? value as Record<string, unknown> : undefined;
299
+ }
300
+
301
+ export function tail(value: string, limit: number): string {
302
+ const bytes = Buffer.from(value, 'utf8');
303
+ let start = Math.max(0, bytes.length - limit);
304
+ // Drop a partial leading code point, avoiding replacement-byte expansion.
305
+ while (start < bytes.length && (bytes[start]! & 0xc0) === 0x80) start += 1;
306
+ // Preserve tabs/newlines; replace binary controls (including ESC and CR),
307
+ // C1 controls and Unicode formatting controls without growing the excerpt.
308
+ return bytes.subarray(start).toString('utf8')
309
+ .replace(/[\p{Cc}\p{Cf}]/gu, character => character === '\n' || character === '\t' ? character : '?');
310
+ }
@@ -1,8 +1,22 @@
1
1
  import { join } from 'node:path';
2
2
  import { DEFAULT_DATA_DIR } from '../daemon-connection.js';
3
- import type { StepFailedDetails } from '../failure-kinds.js';
3
+ import type { StepAttemptFailure, StepFailedDetails } from '../failure-kinds.js';
4
4
  import type { JournalClient } from '../journal-client.js';
5
- import { formatStepExcerpt } from './step-excerpt.js';
5
+ import {
6
+ attemptFailure,
7
+ compareAttempts,
8
+ failureCause,
9
+ record,
10
+ renderAttemptHistory,
11
+ terminalEvidence,
12
+ type AttemptCause,
13
+ } from './step-evidence.js';
14
+
15
+ /** Every failed attempt of one step, in journal order, with its causes. */
16
+ interface AttemptHistory {
17
+ records: StepAttemptFailure[];
18
+ causes: AttemptCause[];
19
+ }
6
20
 
7
21
  /**
8
22
  * Read what a failed step left in the journal — for any step type.
@@ -21,10 +35,16 @@ import { formatStepExcerpt } from './step-excerpt.js';
21
35
  * agent or llm step the worker's `{exit_code, stdout_tail, stderr_tail}` is
22
36
  * nulled out of `output` and survives only as the bounded render the daemon
23
37
  * captured into `verification.detail` (`worker_failure_detail`,
24
- * relayflowd/src/engine/remote.rs). Both are read, in that order, and the
25
- * daemon's render is re-parsed when it carries that same shape: an exit code
26
- * the daemon stringified on its way into the journal is still an exit code,
27
- * and printing it as one is the difference between a diagnosis and a blob.
38
+ * relayflowd/src/engine/remote.rs). Both are read by `selectEvidence`, in that
39
+ * order, and the daemon's render is re-parsed when it carries that same shape.
40
+ *
41
+ * EVERY failed attempt is collected, not only the terminal one. The kernel
42
+ * appends a `step.completed` per attempt, so a retried step's first failure is
43
+ * in the journal — but `retries_exhausted` reported only the last attempt, and
44
+ * a retry that fails differently (because the first attempt already had a side
45
+ * effect) is exactly when the last attempt is the least informative record
46
+ * there is. The scalar fields still describe the terminal attempt; `attempts`
47
+ * is additive.
28
48
  */
29
49
  export async function stepFailureDetails(
30
50
  client: JournalClient,
@@ -40,6 +60,9 @@ export async function stepFailureDetails(
40
60
  // enforcing (relayflowd-core/src/entry.rs `AttemptStartedPayload`). They are
41
61
  // collected on the same walk and reported only when the journal held them.
42
62
  const budgets = new Map<string, number>();
63
+ // Keyed by step, so interleaved steps never pool their attempts and a page
64
+ // boundary never splits one step's history.
65
+ const histories = new Map<string, AttemptHistory>();
43
66
  while (true) {
44
67
  const { entries } = await client.journalRead(runId, fromSeq, 100);
45
68
  if (entries.length === 0) break;
@@ -63,15 +86,27 @@ export async function stepFailureDetails(
63
86
  // A later completion supersedes an earlier failed attempt.
64
87
  failures.delete(stepId);
65
88
  const payload = record(entry['payload']);
66
- if (payload === undefined) continue;
67
- const completionReason = payload['completionReason'];
89
+ const completionReason = payload?.['completionReason'];
90
+ // A success — or a completion this reader cannot read — ends the failure
91
+ // history too: what a step that eventually succeeded printed on the way
92
+ // is not the diagnosis of a later, different failure. The journal still
93
+ // holds every entry; only this diagnostic's candidate is cleared.
94
+ if (payload === undefined || typeof completionReason !== 'string' || completionReason === 'success') {
95
+ histories.delete(stepId);
96
+ continue;
97
+ }
98
+ const history = histories.get(stepId) ?? { records: [], causes: [] };
99
+ // Recorded for `retry` and `park` alike: the kernel's disposition says
100
+ // what it did next, not whether the attempt failed.
101
+ history.records.push(attemptFailure(entry['attempt'], completionReason, payload['disposition'], payload));
102
+ history.causes.push(failureCause(completionReason, payload));
103
+ histories.set(stepId, history);
68
104
  // A terminal completion that is not a success is the failure, whatever
69
105
  // its step type. The old predicate also demanded a non-zero `exit_code`,
70
106
  // which no agent completion carries and which a deterministic step that
71
107
  // exits 0 and then fails its gate does not carry either — both were
72
108
  // silently skipped.
73
- if (payload['disposition'] !== 'step_done'
74
- || typeof completionReason !== 'string' || completionReason === 'success') continue;
109
+ if (payload['disposition'] !== 'step_done') continue;
75
110
  const stepType = snapshot.steps[stepId]?.type;
76
111
  const attempt = entry['attempt'];
77
112
  const maxIterations = budgets.get(stepId);
@@ -81,7 +116,13 @@ export async function stepFailureDetails(
81
116
  ...(stepType === undefined ? {} : { stepType }),
82
117
  ...(typeof attempt === 'number' && Number.isSafeInteger(attempt) && attempt > 0 ? { attempt } : {}),
83
118
  ...(maxIterations === undefined ? {} : { maxIterations }),
84
- ...evidence(payload),
119
+ ...terminalEvidence(payload),
120
+ // A single failed attempt is already fully described by the scalars
121
+ // above; repeating it as a one-element history would add a clause to
122
+ // every ordinary failure report without adding a fact.
123
+ ...(history.records.length > 1
124
+ ? { attempts: history.records, attemptEvidence: compareAttempts(history.causes) }
125
+ : {}),
85
126
  });
86
127
  }
87
128
  }
@@ -104,6 +145,10 @@ export async function stepFailureDetails(
104
145
  * stops it being misread. Unknown values are omitted rather than defaulted —
105
146
  * an absent budget must not be reported as one attempt.
106
147
  *
148
+ * The attempt history follows that line and precedes the terminal attempt's
149
+ * own clauses, so the first error is visible without scrolling past the last
150
+ * one's output tails.
151
+ *
107
152
  * Lives next to `StepFailedDetails`'s extractor rather than in `cli/run.ts` so
108
153
  * the authored `f.run` path can render the same grammar without importing the
109
154
  * run lifecycle (which would be a cycle).
@@ -115,6 +160,7 @@ export function renderStepEvidence(details: StepFailedDetails): string {
115
160
  + renderAttempt(details)
116
161
  + (details.exitCode === undefined ? '' : ` exit=${details.exitCode}`)
117
162
  + '.'
163
+ + renderAttemptHistory(details)
118
164
  + (details.detail === undefined ? '' : `\nDetail: ${details.detail}`)
119
165
  + (details.stdoutTail ? `\nStdout (captured excerpt):\n${details.stdoutTail}` : '')
120
166
  + (details.stderrTail ? `\nStderr (captured excerpt):\n${details.stderrTail}` : '')
@@ -163,76 +209,6 @@ export function inspectionHint(
163
209
  };
164
210
  }
165
211
 
166
- /**
167
- * Pull the process-shaped fields out of whichever field carried them.
168
- *
169
- * `verification.detail` is last because it is the daemon's own render rather
170
- * than the worker's structured report — but for an agent step it is the only
171
- * thing that survives, so it is parsed when it parses and kept verbatim when
172
- * it does not. A truncated render (the daemon caps at 2,000 chars and appends
173
- * a truncation note) will not parse; that falls through to the raw string,
174
- * which is still the account of what went wrong.
175
- *
176
- * An agent or llm completion now also carries `trajectory_tail.transcript`
177
- * (agent-transcript.ts) on every attempt. That object is not process-shaped,
178
- * so it must not shadow the render that is: the first candidate that carries
179
- * an exit code or a tail wins. The digest contributes what only it has — the
180
- * failure excerpt the worker picked out of the provider's frames, and the
181
- * path of the transcript file.
182
- */
183
- function evidence(payload: Record<string, unknown>): Partial<StepFailedDetails> {
184
- const detail = record(payload['verification'])?.['detail'];
185
- const candidates = [
186
- record(payload['output']),
187
- record(payload['trajectory_tail']),
188
- typeof detail === 'string' ? parsed(detail) : undefined,
189
- ].filter((candidate): candidate is Record<string, unknown> => candidate !== undefined);
190
- const structured = candidates.find(processShaped) ?? candidates[0];
191
- const exitCode = structured?.['exit_code'];
192
- const stdout = structured?.['stdout_tail'];
193
- const stderr = structured?.['stderr_tail'];
194
- const structuredShape = structured !== undefined && processShaped(structured);
195
- const transcript = record(record(payload['trajectory_tail'])?.['transcript']);
196
- const failure = record(transcript?.['failure']);
197
- const failureExcerpt = failure?.['excerpt'];
198
- const transcriptPath = record(transcript?.['file'])?.['path'];
199
- // The excerpt is the worker's own pick of the failure; a `stderr` excerpt
200
- // is the same bytes as `stderrTail`, so it is not printed twice.
201
- const excerptDetail = typeof failureExcerpt === 'string' && failureExcerpt.length > 0
202
- && !(failure?.['kind'] === 'stderr' && typeof stderr === 'string')
203
- ? formatStepExcerpt(failureExcerpt) : undefined;
204
- return {
205
- ...(typeof exitCode === 'number' && Number.isSafeInteger(exitCode) ? { exitCode } : {}),
206
- ...(typeof stdout === 'string' && stdout.length > 0 ? { stdoutTail: formatStepExcerpt(stdout) } : {}),
207
- ...(typeof stderr === 'string' ? { stderrTail: formatStepExcerpt(stderr) } : {}),
208
- // Keep the daemon's account only when it was NOT just a render of the
209
- // fields above — otherwise the same bytes print twice.
210
- ...(excerptDetail !== undefined ? { detail: excerptDetail }
211
- : typeof detail === 'string' && detail.length > 0 && !structuredShape
212
- ? { detail: formatStepExcerpt(detail) } : {}),
213
- ...(typeof transcriptPath === 'string' && transcriptPath.length > 0 ? { transcriptPath } : {}),
214
- };
215
- }
216
-
217
- function processShaped(value: Record<string, unknown>): boolean {
218
- return typeof value['exit_code'] === 'number'
219
- || typeof value['stdout_tail'] === 'string' || typeof value['stderr_tail'] === 'string';
220
- }
221
-
222
- function parsed(value: string): Record<string, unknown> | undefined {
223
- try {
224
- return record(JSON.parse(value));
225
- } catch {
226
- return undefined;
227
- }
228
- }
229
-
230
- function record(value: unknown): Record<string, unknown> | undefined {
231
- return value !== null && typeof value === 'object' && !Array.isArray(value)
232
- ? value as Record<string, unknown> : undefined;
233
- }
234
-
235
-
236
212
  function shellQuote(value: string): string {
237
213
  return /^[A-Za-z0-9_-]+$/.test(value) && !value.startsWith('-')
238
214
  ? value : `'${value.replace(/'/g, "'\\''")}'`;
@@ -196,6 +196,7 @@ export const CLI_VERBS = [
196
196
  options: [
197
197
  { flags: '--step <name>', description: 'Show that agent step’s transcript instead of the runner log' },
198
198
  { flags: '--raw', description: 'Print the transcript JSONL unrendered (still redacted)' },
199
+ { flags: '--follow', description: 'Append new runner output until the run ends; exits with the run’s outcome' },
199
200
  JSON_OPTION,
200
201
  ],
201
202
  variants: ['logs'],
@@ -331,6 +332,7 @@ export const CLI_VERBS = [
331
332
  DATA_DIR_OPTION,
332
333
  { flags: '--tail <n>', description: 'Lines of each agent attempt’s transcript tail to show' },
333
334
  { flags: '--cloud', description: 'Read the run from Cloud instead of a local journal; needs the run id' },
335
+ { flags: '--watch', description: 'With --cloud: redraw until the run ends, then exit with its outcome' },
334
336
  ],
335
337
  variants: ['status'],
336
338
  },