@provos/ironcurtain 0.9.1 → 0.11.0

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 (502) hide show
  1. package/README.md +69 -10
  2. package/dist/auth/oauth-provider.js +1 -4
  3. package/dist/auth/oauth-provider.js.map +1 -1
  4. package/dist/cli.js +40 -2
  5. package/dist/cli.js.map +1 -1
  6. package/dist/config/config-command.js +174 -10
  7. package/dist/config/config-command.js.map +1 -1
  8. package/dist/config/first-start.js +61 -0
  9. package/dist/config/first-start.js.map +1 -1
  10. package/dist/config/generated/compiled-policy.json +528 -161
  11. package/dist/config/generated/test-scenarios.json +4810 -170
  12. package/dist/config/generated/tool-annotations.json +34 -47
  13. package/dist/config/index.d.ts +58 -0
  14. package/dist/config/index.js +97 -15
  15. package/dist/config/index.js.map +1 -1
  16. package/dist/config/mcp-servers.json +2 -2
  17. package/dist/config/model-provider.d.ts +8 -2
  18. package/dist/config/model-provider.js +52 -9
  19. package/dist/config/model-provider.js.map +1 -1
  20. package/dist/config/paths.d.ts +181 -0
  21. package/dist/config/paths.js +326 -23
  22. package/dist/config/paths.js.map +1 -1
  23. package/dist/config/types.d.ts +7 -1
  24. package/dist/config/user-config.d.ts +61 -4
  25. package/dist/config/user-config.js +108 -21
  26. package/dist/config/user-config.js.map +1 -1
  27. package/dist/config/validate-policy-dir.d.ts +10 -0
  28. package/dist/config/validate-policy-dir.js +59 -0
  29. package/dist/config/validate-policy-dir.js.map +1 -0
  30. package/dist/cron/compile-task-policy.js +1 -0
  31. package/dist/cron/compile-task-policy.js.map +1 -1
  32. package/dist/cron/job-commands.js +22 -2
  33. package/dist/cron/job-commands.js.map +1 -1
  34. package/dist/cron/types.d.ts +16 -2
  35. package/dist/cron/types.js +1 -3
  36. package/dist/cron/types.js.map +1 -1
  37. package/dist/daemon/daemon-command.js +16 -2
  38. package/dist/daemon/daemon-command.js.map +1 -1
  39. package/dist/daemon/ironcurtain-daemon.d.ts +44 -1
  40. package/dist/daemon/ironcurtain-daemon.js +210 -8
  41. package/dist/daemon/ironcurtain-daemon.js.map +1 -1
  42. package/dist/docker/adapters/claude-code.d.ts +2 -1
  43. package/dist/docker/adapters/claude-code.js +320 -131
  44. package/dist/docker/adapters/claude-code.js.map +1 -1
  45. package/dist/docker/adapters/goose.d.ts +12 -11
  46. package/dist/docker/adapters/goose.js +80 -27
  47. package/dist/docker/adapters/goose.js.map +1 -1
  48. package/dist/docker/agent-adapter.d.ts +178 -4
  49. package/dist/docker/agent-adapter.js +14 -0
  50. package/dist/docker/agent-adapter.js.map +1 -1
  51. package/dist/docker/agent-registry.js +3 -2
  52. package/dist/docker/agent-registry.js.map +1 -1
  53. package/dist/docker/code-mode-proxy.d.ts +27 -0
  54. package/dist/docker/code-mode-proxy.js +3 -0
  55. package/dist/docker/code-mode-proxy.js.map +1 -1
  56. package/dist/docker/container-lifecycle.d.ts +15 -0
  57. package/dist/docker/container-lifecycle.js +32 -0
  58. package/dist/docker/container-lifecycle.js.map +1 -0
  59. package/dist/docker/docker-agent-session.d.ts +113 -62
  60. package/dist/docker/docker-agent-session.js +241 -300
  61. package/dist/docker/docker-agent-session.js.map +1 -1
  62. package/dist/docker/docker-infrastructure.d.ts +256 -21
  63. package/dist/docker/docker-infrastructure.js +585 -30
  64. package/dist/docker/docker-infrastructure.js.map +1 -1
  65. package/dist/docker/docker-manager.d.ts +36 -1
  66. package/dist/docker/docker-manager.js +168 -26
  67. package/dist/docker/docker-manager.js.map +1 -1
  68. package/dist/docker/docker-progress-sink.d.ts +43 -0
  69. package/dist/docker/docker-progress-sink.js +299 -0
  70. package/dist/docker/docker-progress-sink.js.map +1 -0
  71. package/dist/docker/mitm-proxy.d.ts +90 -1
  72. package/dist/docker/mitm-proxy.js +720 -40
  73. package/dist/docker/mitm-proxy.js.map +1 -1
  74. package/dist/docker/oauth-credentials.d.ts +46 -3
  75. package/dist/docker/oauth-credentials.js +56 -14
  76. package/dist/docker/oauth-credentials.js.map +1 -1
  77. package/dist/docker/oauth-token-manager.js +2 -2
  78. package/dist/docker/oauth-token-manager.js.map +1 -1
  79. package/dist/docker/orientation.d.ts +1 -1
  80. package/dist/docker/provider-config.d.ts +81 -2
  81. package/dist/docker/provider-config.js +222 -28
  82. package/dist/docker/provider-config.js.map +1 -1
  83. package/dist/docker/proxy-tools.d.ts +0 -2
  84. package/dist/docker/proxy-tools.js +2 -7
  85. package/dist/docker/proxy-tools.js.map +1 -1
  86. package/dist/docker/pty-session.d.ts +49 -1
  87. package/dist/docker/pty-session.js +212 -90
  88. package/dist/docker/pty-session.js.map +1 -1
  89. package/dist/docker/pty-types.d.ts +0 -2
  90. package/dist/docker/pty-types.js +0 -2
  91. package/dist/docker/pty-types.js.map +1 -1
  92. package/dist/docker/resource-limits.d.ts +125 -0
  93. package/dist/docker/resource-limits.js +200 -0
  94. package/dist/docker/resource-limits.js.map +1 -0
  95. package/dist/docker/spawn-with-idle-timeout.d.ts +39 -0
  96. package/dist/docker/spawn-with-idle-timeout.js +152 -0
  97. package/dist/docker/spawn-with-idle-timeout.js.map +1 -0
  98. package/dist/docker/sse-extractor.d.ts +51 -0
  99. package/dist/docker/sse-extractor.js +303 -0
  100. package/dist/docker/sse-extractor.js.map +1 -0
  101. package/dist/docker/token-stream-bus.d.ts +83 -0
  102. package/dist/docker/token-stream-bus.js +98 -0
  103. package/dist/docker/token-stream-bus.js.map +1 -0
  104. package/dist/docker/token-stream-types.d.ts +71 -0
  105. package/dist/docker/token-stream-types.js +8 -0
  106. package/dist/docker/token-stream-types.js.map +1 -0
  107. package/dist/docker/types.d.ts +61 -6
  108. package/dist/doctor/checks.d.ts +138 -0
  109. package/dist/doctor/checks.js +511 -0
  110. package/dist/doctor/checks.js.map +1 -0
  111. package/dist/doctor/doctor-command.d.ts +29 -0
  112. package/dist/doctor/doctor-command.js +160 -0
  113. package/dist/doctor/doctor-command.js.map +1 -0
  114. package/dist/doctor/mcp-liveness.d.ts +28 -0
  115. package/dist/doctor/mcp-liveness.js +84 -0
  116. package/dist/doctor/mcp-liveness.js.map +1 -0
  117. package/dist/doctor/oauth-checks.d.ts +23 -0
  118. package/dist/doctor/oauth-checks.js +157 -0
  119. package/dist/doctor/oauth-checks.js.map +1 -0
  120. package/dist/doctor/output.d.ts +26 -0
  121. package/dist/doctor/output.js +82 -0
  122. package/dist/doctor/output.js.map +1 -0
  123. package/dist/escalation/listener-command.js +0 -3
  124. package/dist/escalation/listener-command.js.map +1 -1
  125. package/dist/event-bus/typed-event-bus.d.ts +26 -0
  126. package/dist/event-bus/typed-event-bus.js +35 -0
  127. package/dist/event-bus/typed-event-bus.js.map +1 -0
  128. package/dist/index.js +32 -9
  129. package/dist/index.js.map +1 -1
  130. package/dist/logger.js +31 -2
  131. package/dist/logger.js.map +1 -1
  132. package/dist/memory/auto-save.d.ts +18 -4
  133. package/dist/memory/auto-save.js +46 -6
  134. package/dist/memory/auto-save.js.map +1 -1
  135. package/dist/memory/memory-policy.d.ts +41 -0
  136. package/dist/memory/memory-policy.js +36 -0
  137. package/dist/memory/memory-policy.js.map +1 -0
  138. package/dist/mux/mux-app.d.ts +2 -0
  139. package/dist/mux/mux-app.js +30 -34
  140. package/dist/mux/mux-app.js.map +1 -1
  141. package/dist/mux/mux-command.d.ts +10 -1
  142. package/dist/mux/mux-command.js +62 -35
  143. package/dist/mux/mux-command.js.map +1 -1
  144. package/dist/mux/mux-renderer.js +22 -17
  145. package/dist/mux/mux-renderer.js.map +1 -1
  146. package/dist/mux/pty-bridge.d.ts +2 -0
  147. package/dist/mux/pty-bridge.js +3 -0
  148. package/dist/mux/pty-bridge.js.map +1 -1
  149. package/dist/mux/session-scanner.d.ts +7 -0
  150. package/dist/mux/session-scanner.js +21 -2
  151. package/dist/mux/session-scanner.js.map +1 -1
  152. package/dist/mux/types.d.ts +0 -4
  153. package/dist/mux/types.js.map +1 -1
  154. package/dist/{pipeline → observability}/llm-logger.d.ts +9 -3
  155. package/dist/{pipeline → observability}/llm-logger.js +16 -4
  156. package/dist/observability/llm-logger.js.map +1 -0
  157. package/dist/observe/observe-command.d.ts +12 -0
  158. package/dist/observe/observe-command.js +298 -0
  159. package/dist/observe/observe-command.js.map +1 -0
  160. package/dist/observe/observe-renderer.d.ts +24 -0
  161. package/dist/observe/observe-renderer.js +87 -0
  162. package/dist/observe/observe-renderer.js.map +1 -0
  163. package/dist/observe/observe-tui-rain.d.ts +42 -0
  164. package/dist/observe/observe-tui-rain.js +531 -0
  165. package/dist/observe/observe-tui-rain.js.map +1 -0
  166. package/dist/observe/observe-tui-text-panel.d.ts +46 -0
  167. package/dist/observe/observe-tui-text-panel.js +525 -0
  168. package/dist/observe/observe-tui-text-panel.js.map +1 -0
  169. package/dist/observe/observe-tui-types.d.ts +270 -0
  170. package/dist/observe/observe-tui-types.js +200 -0
  171. package/dist/observe/observe-tui-types.js.map +1 -0
  172. package/dist/observe/observe-tui-word-scorer.d.ts +143 -0
  173. package/dist/observe/observe-tui-word-scorer.js +572 -0
  174. package/dist/observe/observe-tui-word-scorer.js.map +1 -0
  175. package/dist/observe/observe-tui.d.ts +59 -0
  176. package/dist/observe/observe-tui.js +592 -0
  177. package/dist/observe/observe-tui.js.map +1 -0
  178. package/dist/persona/compile-persona-policy.js +1 -0
  179. package/dist/persona/compile-persona-policy.js.map +1 -1
  180. package/dist/persona/memory-gate.d.ts +19 -0
  181. package/dist/persona/memory-gate.js +33 -0
  182. package/dist/persona/memory-gate.js.map +1 -0
  183. package/dist/persona/persona-command.js +41 -6
  184. package/dist/persona/persona-command.js.map +1 -1
  185. package/dist/persona/resolve.d.ts +22 -0
  186. package/dist/persona/resolve.js +45 -2
  187. package/dist/persona/resolve.js.map +1 -1
  188. package/dist/persona/types.d.ts +16 -0
  189. package/dist/pipeline/annotate.d.ts +7 -1
  190. package/dist/pipeline/annotate.js +92 -2
  191. package/dist/pipeline/annotate.js.map +1 -1
  192. package/dist/pipeline/compile.js +2 -0
  193. package/dist/pipeline/compile.js.map +1 -1
  194. package/dist/pipeline/constitution-compiler.d.ts +25 -14
  195. package/dist/pipeline/constitution-compiler.js +107 -38
  196. package/dist/pipeline/constitution-compiler.js.map +1 -1
  197. package/dist/pipeline/dynamic-list-types.d.ts +3 -6
  198. package/dist/pipeline/dynamic-list-types.js +3 -16
  199. package/dist/pipeline/dynamic-list-types.js.map +1 -1
  200. package/dist/pipeline/parallel-progress.d.ts +59 -0
  201. package/dist/pipeline/parallel-progress.js +263 -0
  202. package/dist/pipeline/parallel-progress.js.map +1 -0
  203. package/dist/pipeline/pipeline-runner.d.ts +49 -8
  204. package/dist/pipeline/pipeline-runner.js +419 -231
  205. package/dist/pipeline/pipeline-runner.js.map +1 -1
  206. package/dist/pipeline/pipeline-shared.d.ts +59 -1
  207. package/dist/pipeline/pipeline-shared.js +127 -12
  208. package/dist/pipeline/pipeline-shared.js.map +1 -1
  209. package/dist/pipeline/policy-verifier.js +16 -3
  210. package/dist/pipeline/policy-verifier.js.map +1 -1
  211. package/dist/pipeline/proxy-mcp-connections.js +17 -2
  212. package/dist/pipeline/proxy-mcp-connections.js.map +1 -1
  213. package/dist/pipeline/scenario-generator.d.ts +4 -8
  214. package/dist/pipeline/scenario-generator.js +13 -46
  215. package/dist/pipeline/scenario-generator.js.map +1 -1
  216. package/dist/pipeline/server-prefilter.d.ts +31 -0
  217. package/dist/pipeline/server-prefilter.js +115 -0
  218. package/dist/pipeline/server-prefilter.js.map +1 -0
  219. package/dist/pipeline/types.d.ts +5 -0
  220. package/dist/sandbox/index.d.ts +14 -0
  221. package/dist/sandbox/index.js +335 -116
  222. package/dist/sandbox/index.js.map +1 -1
  223. package/dist/sandbox/ironcurtain-protocol.d.ts +52 -0
  224. package/dist/sandbox/ironcurtain-protocol.js +185 -0
  225. package/dist/sandbox/ironcurtain-protocol.js.map +1 -0
  226. package/dist/session/agent-session.d.ts +6 -0
  227. package/dist/session/agent-session.js +16 -5
  228. package/dist/session/agent-session.js.map +1 -1
  229. package/dist/session/cli-transport.js +0 -6
  230. package/dist/session/cli-transport.js.map +1 -1
  231. package/dist/session/index.d.ts +35 -14
  232. package/dist/session/index.js +324 -130
  233. package/dist/session/index.js.map +1 -1
  234. package/dist/session/preflight.d.ts +50 -13
  235. package/dist/session/preflight.js +252 -80
  236. package/dist/session/preflight.js.map +1 -1
  237. package/dist/session/prompts.d.ts +2 -5
  238. package/dist/session/prompts.js.map +1 -1
  239. package/dist/session/session-manager.d.ts +61 -6
  240. package/dist/session/session-manager.js +72 -10
  241. package/dist/session/session-manager.js.map +1 -1
  242. package/dist/session/session-metadata.d.ts +21 -5
  243. package/dist/session/session-metadata.js +34 -12
  244. package/dist/session/session-metadata.js.map +1 -1
  245. package/dist/session/types.d.ts +311 -5
  246. package/dist/session/types.js +57 -1
  247. package/dist/session/types.js.map +1 -1
  248. package/dist/signal/setup-signal.js +13 -21
  249. package/dist/signal/setup-signal.js.map +1 -1
  250. package/dist/signal/signal-bot-daemon.d.ts +16 -1
  251. package/dist/signal/signal-bot-daemon.js +39 -27
  252. package/dist/signal/signal-bot-daemon.js.map +1 -1
  253. package/dist/signal/signal-container.js +7 -1
  254. package/dist/signal/signal-container.js.map +1 -1
  255. package/dist/skills/discovery.d.ts +77 -0
  256. package/dist/skills/discovery.js +206 -0
  257. package/dist/skills/discovery.js.map +1 -0
  258. package/dist/skills/index.d.ts +4 -0
  259. package/dist/skills/index.js +3 -0
  260. package/dist/skills/index.js.map +1 -0
  261. package/dist/skills/staging.d.ts +28 -0
  262. package/dist/skills/staging.js +68 -0
  263. package/dist/skills/staging.js.map +1 -0
  264. package/dist/skills/types.d.ts +46 -0
  265. package/dist/skills/types.js +2 -0
  266. package/dist/skills/types.js.map +1 -0
  267. package/dist/trusted-process/approval-whitelist.d.ts +0 -2
  268. package/dist/trusted-process/approval-whitelist.js +17 -17
  269. package/dist/trusted-process/approval-whitelist.js.map +1 -1
  270. package/dist/trusted-process/async-mutex.d.ts +32 -0
  271. package/dist/trusted-process/async-mutex.js +50 -0
  272. package/dist/trusted-process/async-mutex.js.map +1 -0
  273. package/dist/trusted-process/audit-log.d.ts +38 -1
  274. package/dist/trusted-process/audit-log.js +82 -13
  275. package/dist/trusted-process/audit-log.js.map +1 -1
  276. package/dist/trusted-process/call-circuit-breaker.js +2 -1
  277. package/dist/trusted-process/call-circuit-breaker.js.map +1 -1
  278. package/dist/trusted-process/control-server.d.ts +101 -0
  279. package/dist/trusted-process/control-server.js +282 -0
  280. package/dist/trusted-process/control-server.js.map +1 -0
  281. package/dist/trusted-process/domain-utils.d.ts +6 -6
  282. package/dist/trusted-process/domain-utils.js +7 -23
  283. package/dist/trusted-process/domain-utils.js.map +1 -1
  284. package/dist/trusted-process/error-prefixes.d.ts +16 -0
  285. package/dist/trusted-process/error-prefixes.js +17 -0
  286. package/dist/trusted-process/error-prefixes.js.map +1 -0
  287. package/dist/trusted-process/index.d.ts +40 -23
  288. package/dist/trusted-process/index.js +131 -225
  289. package/dist/trusted-process/index.js.map +1 -1
  290. package/dist/trusted-process/list-matcher.d.ts +17 -0
  291. package/dist/trusted-process/list-matcher.js +31 -0
  292. package/dist/trusted-process/list-matcher.js.map +1 -0
  293. package/dist/trusted-process/mcp-client-manager.d.ts +35 -6
  294. package/dist/trusted-process/mcp-client-manager.js +40 -45
  295. package/dist/trusted-process/mcp-client-manager.js.map +1 -1
  296. package/dist/trusted-process/mcp-proxy-server.d.ts +31 -120
  297. package/dist/trusted-process/mcp-proxy-server.js +166 -657
  298. package/dist/trusted-process/mcp-proxy-server.js.map +1 -1
  299. package/dist/trusted-process/policy-engine.d.ts +16 -4
  300. package/dist/trusted-process/policy-engine.js +90 -45
  301. package/dist/trusted-process/policy-engine.js.map +1 -1
  302. package/dist/trusted-process/policy-roots.d.ts +6 -0
  303. package/dist/trusted-process/policy-roots.js +20 -0
  304. package/dist/trusted-process/policy-roots.js.map +1 -1
  305. package/dist/trusted-process/sandbox-integration.d.ts +12 -0
  306. package/dist/trusted-process/sandbox-integration.js +26 -0
  307. package/dist/trusted-process/sandbox-integration.js.map +1 -1
  308. package/dist/trusted-process/tool-call-coordinator.d.ts +269 -0
  309. package/dist/trusted-process/tool-call-coordinator.js +508 -0
  310. package/dist/trusted-process/tool-call-coordinator.js.map +1 -0
  311. package/dist/trusted-process/tool-call-pipeline.d.ts +205 -0
  312. package/dist/trusted-process/tool-call-pipeline.js +798 -0
  313. package/dist/trusted-process/tool-call-pipeline.js.map +1 -0
  314. package/dist/trusted-process/tool-description-hints.d.ts +1 -1
  315. package/dist/types/argument-roles.d.ts +1 -1
  316. package/dist/types/argument-roles.js +14 -1
  317. package/dist/types/argument-roles.js.map +1 -1
  318. package/dist/types/audit.d.ts +9 -0
  319. package/dist/{session → types}/errors.d.ts +8 -2
  320. package/dist/{session → types}/errors.js +8 -2
  321. package/dist/types/errors.js.map +1 -0
  322. package/dist/types/reserved-servers.d.ts +11 -0
  323. package/dist/types/reserved-servers.js +12 -0
  324. package/dist/types/reserved-servers.js.map +1 -0
  325. package/dist/types/server-listing.d.ts +14 -0
  326. package/dist/types/server-listing.js +2 -0
  327. package/dist/types/server-listing.js.map +1 -0
  328. package/dist/types/slug.d.ts +2 -0
  329. package/dist/types/slug.js +2 -0
  330. package/dist/types/slug.js.map +1 -1
  331. package/dist/types/url-normalize.d.ts +16 -0
  332. package/dist/types/url-normalize.js +33 -0
  333. package/dist/types/url-normalize.js.map +1 -0
  334. package/dist/utils/error-message.d.ts +8 -0
  335. package/dist/utils/error-message.js +11 -0
  336. package/dist/utils/error-message.js.map +1 -0
  337. package/dist/utils/exec-error.d.ts +19 -0
  338. package/dist/utils/exec-error.js +16 -0
  339. package/dist/utils/exec-error.js.map +1 -0
  340. package/dist/utils/is-plain-object.d.ts +7 -0
  341. package/dist/utils/is-plain-object.js +12 -0
  342. package/dist/utils/is-plain-object.js.map +1 -0
  343. package/dist/utils/preflight-checks.d.ts +16 -0
  344. package/dist/utils/preflight-checks.js +178 -0
  345. package/dist/utils/preflight-checks.js.map +1 -0
  346. package/dist/web-ui/__tests__/json-rpc-dispatch.test.d.ts +7 -0
  347. package/dist/web-ui/__tests__/json-rpc-dispatch.test.js +725 -0
  348. package/dist/web-ui/__tests__/json-rpc-dispatch.test.js.map +1 -0
  349. package/dist/web-ui/dispatch/escalation-dispatch.d.ts +7 -0
  350. package/dist/web-ui/dispatch/escalation-dispatch.js +52 -0
  351. package/dist/web-ui/dispatch/escalation-dispatch.js.map +1 -0
  352. package/dist/web-ui/dispatch/job-dispatch.d.ts +8 -0
  353. package/dist/web-ui/dispatch/job-dispatch.js +83 -0
  354. package/dist/web-ui/dispatch/job-dispatch.js.map +1 -0
  355. package/dist/web-ui/dispatch/persona-dispatch.d.ts +6 -0
  356. package/dist/web-ui/dispatch/persona-dispatch.js +116 -0
  357. package/dist/web-ui/dispatch/persona-dispatch.js.map +1 -0
  358. package/dist/web-ui/dispatch/session-dispatch.d.ts +9 -0
  359. package/dist/web-ui/dispatch/session-dispatch.js +201 -0
  360. package/dist/web-ui/dispatch/session-dispatch.js.map +1 -0
  361. package/dist/web-ui/dispatch/token-stream-dispatch.d.ts +15 -0
  362. package/dist/web-ui/dispatch/token-stream-dispatch.js +55 -0
  363. package/dist/web-ui/dispatch/token-stream-dispatch.js.map +1 -0
  364. package/dist/web-ui/dispatch/types.d.ts +31 -0
  365. package/dist/web-ui/dispatch/types.js +68 -0
  366. package/dist/web-ui/dispatch/types.js.map +1 -0
  367. package/dist/web-ui/dispatch/workflow-dispatch.d.ts +103 -0
  368. package/dist/web-ui/dispatch/workflow-dispatch.js +925 -0
  369. package/dist/web-ui/dispatch/workflow-dispatch.js.map +1 -0
  370. package/dist/web-ui/json-rpc-dispatch.d.ts +12 -0
  371. package/dist/web-ui/json-rpc-dispatch.js +34 -0
  372. package/dist/web-ui/json-rpc-dispatch.js.map +1 -0
  373. package/dist/web-ui/state-graph.d.ts +13 -0
  374. package/dist/web-ui/state-graph.js +82 -0
  375. package/dist/web-ui/state-graph.js.map +1 -0
  376. package/dist/web-ui/token-stream-bridge.d.ts +76 -0
  377. package/dist/web-ui/token-stream-bridge.js +265 -0
  378. package/dist/web-ui/token-stream-bridge.js.map +1 -0
  379. package/dist/web-ui/web-event-bus.d.ts +152 -0
  380. package/dist/web-ui/web-event-bus.js +19 -0
  381. package/dist/web-ui/web-event-bus.js.map +1 -0
  382. package/dist/web-ui/web-session-transport.d.ts +31 -0
  383. package/dist/web-ui/web-session-transport.js +109 -0
  384. package/dist/web-ui/web-session-transport.js.map +1 -0
  385. package/dist/web-ui/web-ui-server.d.ts +76 -0
  386. package/dist/web-ui/web-ui-server.js +461 -0
  387. package/dist/web-ui/web-ui-server.js.map +1 -0
  388. package/dist/web-ui/web-ui-types.d.ts +290 -0
  389. package/dist/web-ui/web-ui-types.js +32 -0
  390. package/dist/web-ui/web-ui-types.js.map +1 -0
  391. package/dist/web-ui/ws-utils.d.ts +5 -0
  392. package/dist/web-ui/ws-utils.js +12 -0
  393. package/dist/web-ui/ws-utils.js.map +1 -0
  394. package/dist/web-ui-static/assets/index-BFO5z1js.js +76 -0
  395. package/dist/web-ui-static/assets/index-CCUwrkIL.css +1 -0
  396. package/dist/web-ui-static/assets/orbitron-latin-700-normal-4jsRXGGJ.woff2 +0 -0
  397. package/dist/web-ui-static/assets/orbitron-latin-700-normal-DmYOh-W9.woff +0 -0
  398. package/dist/web-ui-static/index.html +14 -0
  399. package/dist/workflow/artifacts.d.ts +59 -0
  400. package/dist/workflow/artifacts.js +188 -0
  401. package/dist/workflow/artifacts.js.map +1 -0
  402. package/dist/workflow/checkpoint-selection.d.ts +34 -0
  403. package/dist/workflow/checkpoint-selection.js +48 -0
  404. package/dist/workflow/checkpoint-selection.js.map +1 -0
  405. package/dist/workflow/checkpoint.d.ts +35 -0
  406. package/dist/workflow/checkpoint.js +84 -0
  407. package/dist/workflow/checkpoint.js.map +1 -0
  408. package/dist/workflow/cli-support.d.ts +92 -0
  409. package/dist/workflow/cli-support.js +423 -0
  410. package/dist/workflow/cli-support.js.map +1 -0
  411. package/dist/workflow/definition-loader.d.ts +32 -0
  412. package/dist/workflow/definition-loader.js +56 -0
  413. package/dist/workflow/definition-loader.js.map +1 -0
  414. package/dist/workflow/discovery.d.ts +78 -0
  415. package/dist/workflow/discovery.js +203 -0
  416. package/dist/workflow/discovery.js.map +1 -0
  417. package/dist/workflow/errors.d.ts +66 -0
  418. package/dist/workflow/errors.js +49 -0
  419. package/dist/workflow/errors.js.map +1 -0
  420. package/dist/workflow/guards.d.ts +8 -0
  421. package/dist/workflow/guards.js +38 -0
  422. package/dist/workflow/guards.js.map +1 -0
  423. package/dist/workflow/lint-integration.d.ts +64 -0
  424. package/dist/workflow/lint-integration.js +56 -0
  425. package/dist/workflow/lint-integration.js.map +1 -0
  426. package/dist/workflow/lint.d.ts +66 -0
  427. package/dist/workflow/lint.js +410 -0
  428. package/dist/workflow/lint.js.map +1 -0
  429. package/dist/workflow/machine-builder.d.ts +90 -0
  430. package/dist/workflow/machine-builder.js +485 -0
  431. package/dist/workflow/machine-builder.js.map +1 -0
  432. package/dist/workflow/message-log.d.ts +85 -0
  433. package/dist/workflow/message-log.js +40 -0
  434. package/dist/workflow/message-log.js.map +1 -0
  435. package/dist/workflow/orchestrator.d.ts +450 -0
  436. package/dist/workflow/orchestrator.js +1941 -0
  437. package/dist/workflow/orchestrator.js.map +1 -0
  438. package/dist/workflow/prompt-builder.d.ts +39 -0
  439. package/dist/workflow/prompt-builder.js +242 -0
  440. package/dist/workflow/prompt-builder.js.map +1 -0
  441. package/dist/workflow/run-state-command.d.ts +9 -0
  442. package/dist/workflow/run-state-command.js +414 -0
  443. package/dist/workflow/run-state-command.js.map +1 -0
  444. package/dist/workflow/run-state-debug-capture.d.ts +40 -0
  445. package/dist/workflow/run-state-debug-capture.js +101 -0
  446. package/dist/workflow/run-state-debug-capture.js.map +1 -0
  447. package/dist/workflow/status-parser.d.ts +68 -0
  448. package/dist/workflow/status-parser.js +259 -0
  449. package/dist/workflow/status-parser.js.map +1 -0
  450. package/dist/workflow/types.d.ts +498 -0
  451. package/dist/workflow/types.js +83 -0
  452. package/dist/workflow/types.js.map +1 -0
  453. package/dist/workflow/validate.d.ts +58 -0
  454. package/dist/workflow/validate.js +499 -0
  455. package/dist/workflow/validate.js.map +1 -0
  456. package/dist/workflow/workflow-command.d.ts +26 -0
  457. package/dist/workflow/workflow-command.js +648 -0
  458. package/dist/workflow/workflow-command.js.map +1 -0
  459. package/dist/workflow/workflow-discovery.d.ts +25 -0
  460. package/dist/workflow/workflow-discovery.js +100 -0
  461. package/dist/workflow/workflow-discovery.js.map +1 -0
  462. package/dist/workflow/workflow-manager.d.ts +122 -0
  463. package/dist/workflow/workflow-manager.js +308 -0
  464. package/dist/workflow/workflow-manager.js.map +1 -0
  465. package/dist/workflow/workflows/design-and-code/workflow.yaml +169 -0
  466. package/dist/workflow/workflows/test-email-summary/skills/email-formatting/SKILL.md +55 -0
  467. package/dist/workflow/workflows/test-email-summary/skills/gmail-query-syntax/SKILL.md +63 -0
  468. package/dist/workflow/workflows/test-email-summary/skills/json-schema-emails/SKILL.md +41 -0
  469. package/dist/workflow/workflows/test-email-summary/skills/json-schema-emails/schema.json +34 -0
  470. package/dist/workflow/workflows/test-email-summary/skills/summary-style-guide/SKILL.md +51 -0
  471. package/dist/workflow/workflows/test-email-summary/workflow.yaml +89 -0
  472. package/dist/workflow/workflows/vuln-discovery/skills/harness-design-fuzzing/SKILL.md +201 -0
  473. package/dist/workflow/workflows/vuln-discovery/skills/memory-safety-c-cpp/SKILL.md +166 -0
  474. package/dist/workflow/workflows/vuln-discovery/skills/vulnerability-triage/SKILL.md +185 -0
  475. package/dist/workflow/workflows/vuln-discovery/workflow.yaml +920 -0
  476. package/dist/workflow/worktree.d.ts +43 -0
  477. package/dist/workflow/worktree.js +106 -0
  478. package/dist/workflow/worktree.js.map +1 -0
  479. package/docker/Dockerfile.base +12 -1
  480. package/docker/Dockerfile.base.arm64 +17 -1
  481. package/docker/entrypoint-claude-code.sh +46 -0
  482. package/docker/entrypoint-goose.sh +25 -0
  483. package/package.json +22 -8
  484. package/packages/web-ui/README.md +113 -0
  485. package/packages/web-ui/docs/workflow-state-machine.png +0 -0
  486. package/src/config/generated/compiled-policy.json +528 -161
  487. package/src/config/generated/test-scenarios.json +4810 -170
  488. package/src/config/generated/tool-annotations.json +34 -47
  489. package/src/config/mcp-servers.json +2 -2
  490. package/src/workflow/workflows/design-and-code/workflow.yaml +169 -0
  491. package/src/workflow/workflows/test-email-summary/skills/email-formatting/SKILL.md +55 -0
  492. package/src/workflow/workflows/test-email-summary/skills/gmail-query-syntax/SKILL.md +63 -0
  493. package/src/workflow/workflows/test-email-summary/skills/json-schema-emails/SKILL.md +41 -0
  494. package/src/workflow/workflows/test-email-summary/skills/json-schema-emails/schema.json +34 -0
  495. package/src/workflow/workflows/test-email-summary/skills/summary-style-guide/SKILL.md +51 -0
  496. package/src/workflow/workflows/test-email-summary/workflow.yaml +89 -0
  497. package/src/workflow/workflows/vuln-discovery/skills/harness-design-fuzzing/SKILL.md +201 -0
  498. package/src/workflow/workflows/vuln-discovery/skills/memory-safety-c-cpp/SKILL.md +166 -0
  499. package/src/workflow/workflows/vuln-discovery/skills/vulnerability-triage/SKILL.md +185 -0
  500. package/src/workflow/workflows/vuln-discovery/workflow.yaml +920 -0
  501. package/dist/pipeline/llm-logger.js.map +0 -1
  502. package/dist/session/errors.js.map +0 -1
@@ -0,0 +1,925 @@
1
+ /**
2
+ * Workflow-related JSON-RPC method dispatch.
3
+ *
4
+ * Handles all `workflows.*` methods: list, get, start, resume,
5
+ * abort, resolveGate, inspect, fileTree, fileContent, artifacts.
6
+ */
7
+ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
8
+ import { resolve, extname, normalize } from 'node:path';
9
+ import { z } from 'zod';
10
+ import { validateParams } from './types.js';
11
+ import { RpcError, MethodNotFoundError, } from '../web-ui-types.js';
12
+ import { toHumanGateRequestDto, } from '../../workflow/types.js';
13
+ import { MessageLog } from '../../workflow/message-log.js';
14
+ import { discoverWorkflowRuns, discoverWorkspacePathFromContainers, summaryForId, } from '../../workflow/workflow-discovery.js';
15
+ import { extractStateGraph } from '../state-graph.js';
16
+ import { isWithinDirectory } from '../../types/argument-roles.js';
17
+ import { runPreflight } from '../../workflow/lint-integration.js';
18
+ import * as logger from '../../logger.js';
19
+ // ---------------------------------------------------------------------------
20
+ // State graph cache (definition never changes during execution)
21
+ // ---------------------------------------------------------------------------
22
+ const stateGraphCache = new Map();
23
+ const PAST_RUN_PHASES = new Set(['completed', 'aborted', 'failed', 'waiting_human', 'interrupted']);
24
+ function isPastRunPhase(value) {
25
+ return value !== undefined && PAST_RUN_PHASES.has(value);
26
+ }
27
+ /** Map a terminal state's name to a phase using the orchestrator's name convention. */
28
+ function phaseFromTerminalStateName(name) {
29
+ return name === 'aborted' || name.toLowerCase().includes('abort') ? 'aborted' : 'completed';
30
+ }
31
+ /** Returns the entry with the largest `ts` among `state_transition` entries, or undefined. */
32
+ function findLastStateTransition(entries) {
33
+ let last;
34
+ for (const entry of entries) {
35
+ if (entry.type !== 'state_transition')
36
+ continue;
37
+ if (last === undefined || entry.ts > last.ts)
38
+ last = entry;
39
+ }
40
+ return last;
41
+ }
42
+ /** Defensive state lookup (runtime-safe; the index signature lies about presence). */
43
+ function getStateDef(definition, name) {
44
+ return Object.prototype.hasOwnProperty.call(definition.states, name) ? definition.states[name] : undefined;
45
+ }
46
+ // ---------------------------------------------------------------------------
47
+ // Param validation schemas
48
+ // ---------------------------------------------------------------------------
49
+ const workflowIdSchema = z.object({ workflowId: z.string().min(1) });
50
+ const workflowStartSchema = z.object({
51
+ definitionPath: z.string().min(1),
52
+ taskDescription: z.string().min(1),
53
+ workspacePath: z.string().min(1).optional(),
54
+ });
55
+ // FORCE_REVISION and REPLAN require non-empty feedback: the orchestrator
56
+ // injects this into the next agent's prompt, and an empty string produces
57
+ // an incoherent re-entry prompt. Orchestrator validates too; this is the
58
+ // first line of defense for external (JSON-RPC) callers.
59
+ const workflowIdField = { workflowId: z.string().min(1) };
60
+ const workflowResolveGateSchema = z.discriminatedUnion('event', [
61
+ z.object({ ...workflowIdField, event: z.literal('APPROVE'), prompt: z.string().optional() }),
62
+ z.object({ ...workflowIdField, event: z.literal('ABORT'), prompt: z.string().optional() }),
63
+ z.object({
64
+ ...workflowIdField,
65
+ event: z.literal('FORCE_REVISION'),
66
+ prompt: z.string().trim().min(1, 'Feedback is required for FORCE_REVISION events'),
67
+ }),
68
+ z.object({
69
+ ...workflowIdField,
70
+ event: z.literal('REPLAN'),
71
+ prompt: z.string().trim().min(1, 'Feedback is required for REPLAN events'),
72
+ }),
73
+ ]);
74
+ const workflowFileTreeSchema = z.object({
75
+ workflowId: z.string().min(1),
76
+ path: z.string().optional(),
77
+ });
78
+ const workflowFileContentSchema = z.object({
79
+ workflowId: z.string().min(1),
80
+ path: z.string().min(1),
81
+ });
82
+ const workflowArtifactSchema = z.object({
83
+ workflowId: z.string().min(1),
84
+ artifactName: z.string().min(1),
85
+ });
86
+ // Cursor-pagination params for `workflows.messageLog`. The 2000-entry hard
87
+ // cap is a DoS guardrail: each entry is read+parsed in process, and a single
88
+ // pathologically-large request shouldn't be able to stall the dispatch loop.
89
+ // The default of 200 (applied in the handler when `limit` is omitted) matches
90
+ // the page size the frontend's "Load older" UX is designed around.
91
+ const workflowMessageLogSchema = z.object({
92
+ workflowId: z.string().min(1),
93
+ before: z.string().optional(),
94
+ limit: z.number().int().positive().max(2000).optional(),
95
+ });
96
+ const DEFAULT_MESSAGE_LOG_LIMIT = 200;
97
+ // ---------------------------------------------------------------------------
98
+ // Lint pre-flight (daemon)
99
+ // ---------------------------------------------------------------------------
100
+ /**
101
+ * Parses + validates the definition file, then runs the shared
102
+ * `runPreflight()` helper in `warn` mode. Errors abort via `LINT_FAILED`;
103
+ * warnings are logged and ignored.
104
+ *
105
+ * The `warn` mode is the right default for the daemon: warnings are
106
+ * advisory and should not block a workflow start from the web UI,
107
+ * whereas true errors indicate a workflow that will not run correctly.
108
+ */
109
+ function preflightLintForDaemon(definitionPath) {
110
+ const result = runPreflight(definitionPath, 'warn');
111
+ if (result.ok) {
112
+ if (result.warnings > 0) {
113
+ logger.warn(`[workflow-dispatch] Lint warnings for ${definitionPath}: ${result.diagnostics
114
+ .map((d) => `[${d.code}] ${d.message}`)
115
+ .join(' | ')}`);
116
+ }
117
+ return;
118
+ }
119
+ if (result.kind === 'load') {
120
+ // `loadDefinition` builds the message in the same format the previous
121
+ // inline implementation used: validate failures begin with "Workflow
122
+ // validation failed: ..."; parse failures are wrapped here with the
123
+ // legacy "Failed to load workflow definition:" prefix to preserve the
124
+ // existing wording for daemon consumers.
125
+ if (result.loadKind === 'validate') {
126
+ throw new RpcError('INVALID_PARAMS', result.message);
127
+ }
128
+ throw new RpcError('INVALID_PARAMS', `Failed to load workflow definition: ${result.message}`);
129
+ }
130
+ // Lint failure (load succeeded).
131
+ throw new RpcError('LINT_FAILED', `Workflow lint failed: ${result.errors} error(s), ${result.warnings} warning(s).`, {
132
+ diagnostics: result.diagnostics,
133
+ });
134
+ }
135
+ // ---------------------------------------------------------------------------
136
+ // Workflow dispatch
137
+ // ---------------------------------------------------------------------------
138
+ export async function workflowDispatch(ctx, method, params) {
139
+ // workflows.listDefinitions does not need a running workflow manager
140
+ if (method === 'workflows.listDefinitions') {
141
+ const { discoverWorkflows } = await import('../../workflow/discovery.js');
142
+ return discoverWorkflows();
143
+ }
144
+ if (!ctx.workflowManager) {
145
+ throw new RpcError('INTERNAL_ERROR', 'Workflow system not available');
146
+ }
147
+ const manager = ctx.workflowManager;
148
+ const controller = manager.getOrchestrator();
149
+ switch (method) {
150
+ case 'workflows.list': {
151
+ const activeIds = controller.listActive();
152
+ const summaries = [];
153
+ for (const id of activeIds) {
154
+ const status = controller.getStatus(id);
155
+ if (status) {
156
+ const detail = controller.getDetail(id);
157
+ summaries.push(buildSummaryDto(id, status, detail));
158
+ }
159
+ }
160
+ return summaries;
161
+ }
162
+ case 'workflows.get': {
163
+ const { workflowId } = validateParams(workflowIdSchema, params);
164
+ const id = workflowId;
165
+ const status = controller.getStatus(id);
166
+ if (status) {
167
+ const detail = controller.getDetail(id);
168
+ return buildDetailDto(id, status, detail);
169
+ }
170
+ // Live miss -- fall back to the on-disk past-run loader (D6).
171
+ const result = manager.loadPastRun(id);
172
+ if ('error' in result) {
173
+ if (result.error === 'not_found') {
174
+ throw new RpcError('WORKFLOW_NOT_FOUND', `Workflow ${workflowId} not found`);
175
+ }
176
+ // 'corrupted' -- preserve the loader's diagnostic message for the UI.
177
+ throw new RpcError('WORKFLOW_CORRUPTED', result.message ?? `Workflow ${workflowId} checkpoint is corrupted`);
178
+ }
179
+ // `buildDetailFromPastRun` only consults `summary.mtime` in the
180
+ // checkpoint-less branch, so a synthetic now-dated summary is safe in
181
+ // the rare race where the directory vanished between load and lookup.
182
+ const summary = summaryForId(manager.getBaseDir(), id) ?? {
183
+ workflowId: id,
184
+ directoryPath: resolve(manager.getBaseDir(), id),
185
+ hasCheckpoint: false,
186
+ hasDefinition: false,
187
+ hasMessageLog: false,
188
+ mtime: new Date(),
189
+ };
190
+ return buildDetailFromPastRun(id, result, summary);
191
+ }
192
+ case 'workflows.start': {
193
+ const { definitionPath, taskDescription, workspacePath } = validateParams(workflowStartSchema, params);
194
+ // Pre-flight lint: runs in `warn` mode (errors abort, warnings log and pass).
195
+ // Controller.start() re-parses and re-validates internally; this extra
196
+ // parse is the cheapest way to reach a validated WorkflowDefinition.
197
+ preflightLintForDaemon(definitionPath);
198
+ const workflowId = await controller.start(definitionPath, taskDescription, workspacePath);
199
+ return { workflowId };
200
+ }
201
+ case 'workflows.listResumable': {
202
+ // Despite the legacy name (kept per design decision D4), this method
203
+ // returns *all* past runs on disk -- terminal (completed/failed/aborted),
204
+ // gate-paused (`waiting_human`), and `interrupted` (no live orchestrator
205
+ // and no `finalStatus`). Not just runs the user could literally resume.
206
+ return buildResumableList(manager);
207
+ }
208
+ case 'workflows.import': {
209
+ const schema = z.object({
210
+ baseDir: z.string().min(1),
211
+ workflowId: z.string().min(1).optional(),
212
+ });
213
+ const { baseDir, workflowId } = validateParams(schema, params);
214
+ if (!existsSync(baseDir)) {
215
+ throw new RpcError('INVALID_PARAMS', `Directory does not exist: ${baseDir}`);
216
+ }
217
+ if (!statSync(baseDir).isDirectory()) {
218
+ throw new RpcError('INVALID_PARAMS', `Path is not a directory: ${baseDir}`);
219
+ }
220
+ const importedId = manager.importExternalCheckpoint(baseDir, workflowId);
221
+ return { workflowId: importedId };
222
+ }
223
+ case 'workflows.resume': {
224
+ const schema = z.object({
225
+ workflowId: z.string().min(1).optional(),
226
+ baseDir: z.string().min(1).optional(),
227
+ });
228
+ const { workflowId, baseDir } = validateParams(schema, params);
229
+ let resolvedId;
230
+ if (baseDir) {
231
+ if (!existsSync(baseDir)) {
232
+ throw new RpcError('INVALID_PARAMS', `Directory does not exist: ${baseDir}`);
233
+ }
234
+ if (!statSync(baseDir).isDirectory()) {
235
+ throw new RpcError('INVALID_PARAMS', `Path is not a directory: ${baseDir}`);
236
+ }
237
+ resolvedId = manager.importExternalCheckpoint(baseDir, workflowId);
238
+ }
239
+ else if (workflowId) {
240
+ resolvedId = workflowId;
241
+ }
242
+ else {
243
+ throw new RpcError('INVALID_PARAMS', 'Either workflowId or baseDir must be provided');
244
+ }
245
+ await controller.resume(resolvedId);
246
+ return { accepted: true, workflowId: resolvedId };
247
+ }
248
+ case 'workflows.abort': {
249
+ const { workflowId } = validateParams(workflowIdSchema, params);
250
+ const id = workflowId;
251
+ const status = controller.getStatus(id);
252
+ if (!status) {
253
+ throw new RpcError('WORKFLOW_NOT_FOUND', `Workflow ${workflowId} not found`);
254
+ }
255
+ await controller.abort(id);
256
+ return;
257
+ }
258
+ case 'workflows.resolveGate': {
259
+ const { workflowId, event, prompt } = validateParams(workflowResolveGateSchema, params);
260
+ const id = workflowId;
261
+ const status = controller.getStatus(id);
262
+ if (!status) {
263
+ throw new RpcError('WORKFLOW_NOT_FOUND', `Workflow ${workflowId} not found`);
264
+ }
265
+ if (status.phase !== 'waiting_human') {
266
+ throw new RpcError('WORKFLOW_NOT_AT_GATE', `Workflow ${workflowId} is not waiting at a gate`);
267
+ }
268
+ controller.resolveGate(id, { type: event, prompt });
269
+ return;
270
+ }
271
+ case 'workflows.inspect': {
272
+ const { workflowId } = validateParams(workflowIdSchema, params);
273
+ const id = workflowId;
274
+ const status = controller.getStatus(id);
275
+ if (!status) {
276
+ throw new RpcError('WORKFLOW_NOT_FOUND', `Workflow ${workflowId} not found`);
277
+ }
278
+ return { status };
279
+ }
280
+ case 'workflows.fileTree': {
281
+ const { workflowId, path: relPath } = validateParams(workflowFileTreeSchema, params);
282
+ const workspacePath = getWorkspacePath(controller, manager, workflowId);
283
+ const targetDir = resolveAndContain(workspacePath, relPath ?? '');
284
+ return listDirectory(targetDir);
285
+ }
286
+ case 'workflows.fileContent': {
287
+ const { workflowId, path: relPath } = validateParams(workflowFileContentSchema, params);
288
+ const workspacePath = getWorkspacePath(controller, manager, workflowId);
289
+ const targetFile = resolveAndContain(workspacePath, relPath);
290
+ return readWorkspaceFile(targetFile);
291
+ }
292
+ case 'workflows.artifacts': {
293
+ const { workflowId, artifactName } = validateParams(workflowArtifactSchema, params);
294
+ const workspacePath = getWorkspacePath(controller, manager, workflowId);
295
+ return readArtifact(workspacePath, artifactName);
296
+ }
297
+ case 'workflows.messageLog': {
298
+ const { workflowId, before, limit } = validateParams(workflowMessageLogSchema, params);
299
+ return readMessageLogPage(manager, workflowId, {
300
+ before,
301
+ limit: limit ?? DEFAULT_MESSAGE_LOG_LIMIT,
302
+ });
303
+ }
304
+ default:
305
+ throw new MethodNotFoundError(method);
306
+ }
307
+ }
308
+ // ---------------------------------------------------------------------------
309
+ // DTO builders
310
+ // ---------------------------------------------------------------------------
311
+ function getCurrentState(status) {
312
+ switch (status.phase) {
313
+ case 'running':
314
+ return status.currentState;
315
+ case 'waiting_human':
316
+ return status.gate.stateName;
317
+ case 'completed':
318
+ return 'completed';
319
+ case 'failed':
320
+ return status.lastState;
321
+ case 'aborted':
322
+ return 'aborted';
323
+ }
324
+ }
325
+ function buildSummaryDto(id, status, detail) {
326
+ // `name` and `startedAt` are not available from the orchestrator's runtime
327
+ // state alone. We use the workflow id as a name fallback and the current
328
+ // wall-clock time as a stand-in start time -- the frontend's event handler
329
+ // overwrites both with authoritative values from `workflow.started` events.
330
+ return {
331
+ workflowId: id,
332
+ name: detail?.definition.name ?? id,
333
+ phase: status.phase,
334
+ currentState: getCurrentState(status),
335
+ startedAt: new Date().toISOString(),
336
+ taskDescription: detail?.context.taskDescription ?? '',
337
+ round: detail?.context.round ?? 0,
338
+ maxRounds: detail?.context.maxRounds ?? 0,
339
+ totalTokens: detail?.context.totalTokens ?? 0,
340
+ latestVerdict: extractLatestVerdictFromTransitions(),
341
+ error: status.phase === 'failed' ? status.error : undefined,
342
+ };
343
+ }
344
+ function buildDetailDto(id, status, detail) {
345
+ const base = buildSummaryDto(id, status, detail);
346
+ const stateGraph = detail ? getCachedStateGraph(id, detail.definition) : { states: [], transitions: [] };
347
+ const transitionHistory = mapTransitionRecords(detail?.transitionHistory);
348
+ return {
349
+ ...base,
350
+ description: detail?.context.taskDescription ?? '',
351
+ stateGraph,
352
+ transitionHistory,
353
+ context: detail
354
+ ? {
355
+ taskDescription: detail.context.taskDescription,
356
+ round: detail.context.round,
357
+ maxRounds: detail.context.maxRounds,
358
+ totalTokens: detail.context.totalTokens,
359
+ visitCounts: detail.context.visitCounts,
360
+ }
361
+ : { taskDescription: '', round: 0, maxRounds: 0, totalTokens: 0, visitCounts: {} },
362
+ gate: status.phase === 'waiting_human' ? toHumanGateRequestDto(status.gate) : undefined,
363
+ workspacePath: detail?.workspacePath ?? '',
364
+ };
365
+ }
366
+ // ---------------------------------------------------------------------------
367
+ // Past-run helpers (shared by B3 `workflows.get` and B4 `workflows.listResumable`)
368
+ // ---------------------------------------------------------------------------
369
+ /**
370
+ * Synthesizes a phase value for a checkpoint loaded from disk.
371
+ *
372
+ * Requires a defined `WorkflowCheckpoint`. For checkpoint-less past-run rows
373
+ * (i.e. directories where `checkpoint.json` is absent — e.g. runs completed
374
+ * before B3b persisted `finalStatus`), callers must use
375
+ * `synthesizePhaseFromMessageLog` instead.
376
+ *
377
+ * Resolution order:
378
+ * - If `isLive` is true, returns `'running'`.
379
+ * - Post-B3b checkpoints carry a canonical `finalStatus.phase`. When present
380
+ * and valid (matches a `PastRunPhase` value), return it directly —
381
+ * this short-circuits the legacy state-name heuristic.
382
+ * - Legacy pre-B3b checkpoints fall through to the state-name heuristic:
383
+ * - Terminal state in the definition: infer `'aborted'` from the
384
+ * conventional name pattern (`'aborted'` or contains `'abort'`);
385
+ * otherwise `'completed'`. Matches `handleWorkflowComplete` in the
386
+ * orchestrator.
387
+ * - Human-gate state: `'waiting_human'`.
388
+ * - Otherwise: `'interrupted'` (mid-run state with no live instance —
389
+ * typical of a daemon crash).
390
+ */
391
+ export function computePastRunPhase(checkpoint, definition, isLive) {
392
+ if (isLive)
393
+ return 'running';
394
+ const finalPhase = checkpoint.finalStatus?.phase;
395
+ if (isPastRunPhase(finalPhase))
396
+ return finalPhase;
397
+ // Legacy pre-B3b path: derive from the state name.
398
+ const stateName = extractLastState(checkpoint.machineState);
399
+ const stateDef = getStateDef(definition, stateName);
400
+ if (stateDef?.type === 'terminal')
401
+ return phaseFromTerminalStateName(stateName);
402
+ if (stateDef?.type === 'human_gate')
403
+ return 'waiting_human';
404
+ return 'interrupted';
405
+ }
406
+ /**
407
+ * Synthesize a `PastRunPhase` for a checkpoint-less directory from its message log.
408
+ * `entries` must be chronological (oldest-first), matching `MessageLog.readAll()`.
409
+ *
410
+ * Per D6: the destination state of the last `state_transition` lives in the
411
+ * `event` field (orchestrator.handleTransition writes it there). For non-terminal,
412
+ * non-gate destinations, post-transition `quota_exhausted` → 'aborted',
413
+ * `transient_failure` → 'aborted', `error` → 'failed', none → 'interrupted'.
414
+ */
415
+ export function synthesizePhaseFromMessageLog(entries, definition) {
416
+ // Single pass: track lastTransition AND post-transition signals together.
417
+ // When a newer transition arrives, the post-transition accumulators reset.
418
+ let lastTransition;
419
+ let sawQuotaExhausted = false;
420
+ let sawTransientFailure = false;
421
+ let sawError = false;
422
+ for (const entry of entries) {
423
+ if (entry.type === 'state_transition') {
424
+ if (lastTransition === undefined || entry.ts > lastTransition.ts) {
425
+ lastTransition = entry;
426
+ sawQuotaExhausted = false;
427
+ sawTransientFailure = false;
428
+ sawError = false;
429
+ }
430
+ continue;
431
+ }
432
+ if (lastTransition === undefined || entry.ts <= lastTransition.ts)
433
+ continue;
434
+ if (entry.type === 'quota_exhausted')
435
+ sawQuotaExhausted = true;
436
+ else if (entry.type === 'transient_failure')
437
+ sawTransientFailure = true;
438
+ else if (entry.type === 'error')
439
+ sawError = true;
440
+ }
441
+ if (lastTransition === undefined)
442
+ return 'interrupted';
443
+ const stateDef = getStateDef(definition, lastTransition.event);
444
+ if (stateDef?.type === 'terminal')
445
+ return phaseFromTerminalStateName(lastTransition.event);
446
+ if (stateDef?.type === 'human_gate')
447
+ return 'waiting_human';
448
+ if (sawQuotaExhausted)
449
+ return 'aborted';
450
+ if (sawTransientFailure)
451
+ return 'aborted';
452
+ if (sawError)
453
+ return 'failed';
454
+ return 'interrupted';
455
+ }
456
+ /**
457
+ * `TransitionRecord` doesn't carry a structured verdict; the frontend populates
458
+ * `latestVerdict` from live `workflow.agent_completed` events. Past-run callers
459
+ * receive `undefined` until checkpoints capture verdict/confidence first-class.
460
+ */
461
+ export function extractLatestVerdictFromTransitions() {
462
+ return undefined;
463
+ }
464
+ /**
465
+ * Extracts the failure error message for a past-run checkpoint, when present.
466
+ *
467
+ * Past-run checkpoints don't persist `finalStatus`, but they do persist the
468
+ * workflow context, which carries `lastError` for failed/aborted runs. This
469
+ * is the closest thing to an authoritative error message from disk.
470
+ */
471
+ function extractPastRunError(checkpoint, phase) {
472
+ if (phase === 'completed')
473
+ return undefined;
474
+ return checkpoint.context.lastError ?? undefined;
475
+ }
476
+ /** Returns the cached state graph for a workflow, computing it on first access. */
477
+ function getCachedStateGraph(id, definition) {
478
+ let cached = stateGraphCache.get(id);
479
+ if (!cached) {
480
+ cached = extractStateGraph(definition);
481
+ stateGraphCache.set(id, cached);
482
+ }
483
+ return cached;
484
+ }
485
+ /** Maps domain `TransitionRecord[]` to DTO shape (renames `duration_ms` → `durationMs`). */
486
+ function mapTransitionRecords(history) {
487
+ return (history ?? []).map((t) => ({
488
+ from: t.from,
489
+ to: t.to,
490
+ event: t.event,
491
+ timestamp: t.timestamp,
492
+ durationMs: t.duration_ms,
493
+ agentMessage: t.agentMessage,
494
+ }));
495
+ }
496
+ /** Destination state of the latest `state_transition`, or `definition.initial`. */
497
+ function deriveLastStateFromEntries(entries, definition) {
498
+ return findLastStateTransition(entries)?.event ?? definition.initial;
499
+ }
500
+ /** Assembles a `WorkflowDetailDto` from on-disk past-run objects (checkpoint or message-log fallback). */
501
+ export function buildDetailFromPastRun(id, load, summary) {
502
+ const { checkpoint, definition, isLive } = load;
503
+ const stateGraph = getCachedStateGraph(id, definition);
504
+ if (checkpoint) {
505
+ const phase = computePastRunPhase(checkpoint, definition, isLive);
506
+ const currentState = extractLastState(checkpoint.machineState);
507
+ return {
508
+ workflowId: id,
509
+ name: definition.name,
510
+ phase,
511
+ currentState,
512
+ // Checkpoint timestamp is a best-effort stand-in for startedAt.
513
+ startedAt: checkpoint.timestamp,
514
+ taskDescription: checkpoint.context.taskDescription,
515
+ round: checkpoint.context.round,
516
+ maxRounds: checkpoint.context.maxRounds,
517
+ totalTokens: checkpoint.context.totalTokens,
518
+ latestVerdict: extractLatestVerdictFromTransitions(),
519
+ error: extractPastRunError(checkpoint, phase),
520
+ description: checkpoint.context.taskDescription,
521
+ stateGraph,
522
+ transitionHistory: mapTransitionRecords(checkpoint.transitionHistory),
523
+ context: {
524
+ taskDescription: checkpoint.context.taskDescription,
525
+ round: checkpoint.context.round,
526
+ maxRounds: checkpoint.context.maxRounds,
527
+ totalTokens: checkpoint.context.totalTokens,
528
+ visitCounts: { ...checkpoint.context.visitCounts },
529
+ },
530
+ gate: undefined,
531
+ workspacePath: checkpoint.workspacePath ?? '',
532
+ };
533
+ }
534
+ const entries = new MessageLog(load.messageLogPath).readAll();
535
+ const widePhase = isLive
536
+ ? 'running'
537
+ : synthesizePhaseFromMessageLog(entries, definition);
538
+ const currentState = deriveLastStateFromEntries(entries, definition);
539
+ const taskDescription = definition.description;
540
+ const startedAt = summary.mtime.toISOString();
541
+ const recoveredWorkspace = discoverWorkspacePathFromContainers(summary.directoryPath) ?? '';
542
+ return {
543
+ workflowId: id,
544
+ name: definition.name,
545
+ phase: widePhase,
546
+ currentState,
547
+ startedAt,
548
+ taskDescription,
549
+ round: 0,
550
+ maxRounds: 0,
551
+ totalTokens: 0,
552
+ latestVerdict: undefined,
553
+ error: undefined,
554
+ description: taskDescription,
555
+ stateGraph,
556
+ // state_transition entries from the message log are surfaced via
557
+ // workflows.messageLog instead, to avoid double-rendering.
558
+ transitionHistory: [],
559
+ context: { taskDescription, round: 0, maxRounds: 0, totalTokens: 0, visitCounts: {} },
560
+ gate: undefined,
561
+ workspacePath: recoveredWorkspace,
562
+ };
563
+ }
564
+ // ---------------------------------------------------------------------------
565
+ // File browser helpers
566
+ // ---------------------------------------------------------------------------
567
+ /** Directories excluded from listings by default. */
568
+ const EXCLUDED_DIRS = new Set(['.git', 'node_modules']);
569
+ /** Maximum file size (bytes) for content display. */
570
+ const MAX_FILE_SIZE = 1_048_576; // 1MB
571
+ /** Number of bytes to check for binary content. */
572
+ const BINARY_CHECK_BYTES = 8192;
573
+ /** Extension-to-language mapping for syntax highlighting in the file viewer. */
574
+ const LANG_MAP = {
575
+ '.ts': 'typescript',
576
+ '.tsx': 'typescript',
577
+ '.js': 'javascript',
578
+ '.jsx': 'javascript',
579
+ '.json': 'json',
580
+ '.md': 'markdown',
581
+ '.yaml': 'yaml',
582
+ '.yml': 'yaml',
583
+ '.css': 'css',
584
+ '.html': 'html',
585
+ '.py': 'python',
586
+ '.rs': 'rust',
587
+ '.go': 'go',
588
+ '.sh': 'shell',
589
+ '.bash': 'shell',
590
+ '.toml': 'toml',
591
+ '.xml': 'xml',
592
+ '.svg': 'xml',
593
+ '.sql': 'sql',
594
+ '.dockerfile': 'dockerfile',
595
+ '.txt': 'text',
596
+ };
597
+ /** Map file extensions to language identifiers for syntax highlighting. */
598
+ export function inferLanguage(filePath) {
599
+ const ext = extname(filePath).toLowerCase();
600
+ return LANG_MAP[ext] ?? 'text';
601
+ }
602
+ /**
603
+ * Resolves a workflow's workspace path: live detail → past-run checkpoint →
604
+ * recovered from container session-metadata. Throws `WORKFLOW_NOT_FOUND`
605
+ * when all three sources are empty. The returned path is not guaranteed to
606
+ * exist on disk (ephemeral workspaces may be torn down post-completion);
607
+ * `listDirectory` handles ENOENT gracefully.
608
+ */
609
+ function getWorkspacePath(controller, manager, id) {
610
+ const liveDetail = controller.getDetail(id);
611
+ if (liveDetail)
612
+ return liveDetail.workspacePath;
613
+ const result = manager.loadPastRun(id);
614
+ if ('error' in result) {
615
+ throw new RpcError('WORKFLOW_NOT_FOUND', `Workflow ${id} not found`);
616
+ }
617
+ const pastWorkspace = result.checkpoint?.workspacePath;
618
+ if (pastWorkspace)
619
+ return pastWorkspace;
620
+ const recovered = discoverWorkspacePathFromContainers(resolve(manager.getBaseDir(), id));
621
+ if (recovered)
622
+ return recovered;
623
+ throw new RpcError('WORKFLOW_NOT_FOUND', `Workflow ${id} has no recorded workspace`);
624
+ }
625
+ /**
626
+ * Resolves a relative path within a workspace and validates containment.
627
+ * Prevents path traversal attacks.
628
+ */
629
+ export function resolveAndContain(workspacePath, relPath) {
630
+ const normalized = normalize(relPath);
631
+ if (normalized.startsWith('..') || normalized.startsWith('/')) {
632
+ throw new RpcError('INVALID_PARAMS', 'Path must be relative and within the workspace');
633
+ }
634
+ const target = resolve(workspacePath, normalized);
635
+ if (!isWithinDirectory(target, workspacePath)) {
636
+ throw new RpcError('INVALID_PARAMS', 'Path escapes workspace boundary');
637
+ }
638
+ return target;
639
+ }
640
+ function listDirectory(dirPath) {
641
+ let rawEntries;
642
+ try {
643
+ rawEntries = readdirSync(dirPath, { withFileTypes: true });
644
+ }
645
+ catch (err) {
646
+ if (err instanceof Error && 'code' in err && err.code === 'ENOENT') {
647
+ return { entries: [] };
648
+ }
649
+ throw err;
650
+ }
651
+ const entries = [];
652
+ for (const entry of rawEntries) {
653
+ if (EXCLUDED_DIRS.has(entry.name))
654
+ continue;
655
+ const fullPath = resolve(dirPath, entry.name);
656
+ if (entry.isDirectory()) {
657
+ entries.push({ name: entry.name, type: 'directory' });
658
+ }
659
+ else if (entry.isFile()) {
660
+ try {
661
+ const s = statSync(fullPath);
662
+ entries.push({ name: entry.name, type: 'file', size: s.size });
663
+ }
664
+ catch {
665
+ entries.push({ name: entry.name, type: 'file' });
666
+ }
667
+ }
668
+ }
669
+ // Sort: directories first, then files, both alphabetical
670
+ entries.sort((a, b) => {
671
+ if (a.type !== b.type)
672
+ return a.type === 'directory' ? -1 : 1;
673
+ return a.name.localeCompare(b.name);
674
+ });
675
+ return { entries };
676
+ }
677
+ function readWorkspaceFile(filePath) {
678
+ let s;
679
+ try {
680
+ s = statSync(filePath);
681
+ }
682
+ catch (err) {
683
+ if (err instanceof Error && 'code' in err && err.code === 'ENOENT') {
684
+ throw new RpcError('INVALID_PARAMS', 'File not found');
685
+ }
686
+ throw err;
687
+ }
688
+ if (s.isDirectory()) {
689
+ throw new RpcError('INVALID_PARAMS', 'Path is a directory, not a file');
690
+ }
691
+ if (s.size > MAX_FILE_SIZE) {
692
+ return { error: 'File too large to display (>1MB)' };
693
+ }
694
+ const buffer = readFileSync(filePath);
695
+ // Null bytes reliably indicate binary content; checking only the header avoids scanning large files
696
+ const checkLen = Math.min(buffer.length, BINARY_CHECK_BYTES);
697
+ for (let i = 0; i < checkLen; i++) {
698
+ if (buffer[i] === 0) {
699
+ return { binary: true };
700
+ }
701
+ }
702
+ return {
703
+ content: buffer.toString('utf-8'),
704
+ language: inferLanguage(filePath),
705
+ };
706
+ }
707
+ function readArtifact(workspacePath, artifactName) {
708
+ // Validate artifact name: no path separators or traversal
709
+ if (artifactName.includes('/') || artifactName.includes('\\') || artifactName === '..' || artifactName === '.') {
710
+ throw new RpcError('INVALID_PARAMS', 'Invalid artifact name');
711
+ }
712
+ // Artifacts live in .workflow/{artifactName}/ inside the workspace
713
+ const artifactDir = resolve(workspacePath, '.workflow', artifactName);
714
+ const workflowDir = resolve(workspacePath, '.workflow');
715
+ if (!isWithinDirectory(artifactDir, workflowDir)) {
716
+ throw new RpcError('INVALID_PARAMS', 'Artifact path escapes workflow directory');
717
+ }
718
+ let entries;
719
+ try {
720
+ entries = readdirSync(artifactDir, { withFileTypes: true });
721
+ }
722
+ catch (err) {
723
+ if (err instanceof Error && 'code' in err && err.code === 'ENOENT') {
724
+ throw new RpcError('ARTIFACT_NOT_FOUND', `Artifact "${artifactName}" not found`);
725
+ }
726
+ throw err;
727
+ }
728
+ const files = [];
729
+ collectArtifactFilesFromEntries(artifactDir, '', entries, files);
730
+ return { files };
731
+ }
732
+ // ---------------------------------------------------------------------------
733
+ // Resumable workflow helpers
734
+ // ---------------------------------------------------------------------------
735
+ /**
736
+ * Returns every past-run directory as `PastRunDto[]` for `workflows.listResumable`
737
+ * (RPC name kept per D4; payload covers terminal, gate-paused, and interrupted
738
+ * rows). Live workflows are skipped (they belong to `workflows.list`); corrupted
739
+ * rows are warned-and-skipped; not_found rows are silently skipped.
740
+ */
741
+ function buildResumableList(manager) {
742
+ const controller = manager.getOrchestrator();
743
+ const baseDir = manager.getBaseDir();
744
+ const runs = discoverWorkflowRuns(baseDir);
745
+ const activeIds = new Set(controller.listActive());
746
+ const dtos = [];
747
+ for (const run of runs) {
748
+ if (activeIds.has(run.workflowId))
749
+ continue;
750
+ const result = manager.loadPastRun(run.workflowId, activeIds);
751
+ if ('error' in result) {
752
+ if (result.error === 'corrupted') {
753
+ logger.warn(`[workflow-dispatch] skipping corrupted checkpoint ${run.workflowId}: ${result.message ?? 'unknown'}`);
754
+ }
755
+ continue;
756
+ }
757
+ dtos.push(buildPastRunDto(run.workflowId, result, run));
758
+ }
759
+ dtos.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
760
+ return dtos;
761
+ }
762
+ /** Assembles a `PastRunDto` from on-disk past-run objects (checkpoint or message-log fallback). */
763
+ export function buildPastRunDto(id, load, summary) {
764
+ const { checkpoint, definition, isLive } = load;
765
+ if (checkpoint) {
766
+ const widePhase = computePastRunPhase(checkpoint, definition, isLive);
767
+ // PastRunDto.phase excludes 'running'; coerce the rare live-race row.
768
+ const phase = widePhase === 'running' ? 'interrupted' : widePhase;
769
+ const lastState = extractLastState(checkpoint.machineState);
770
+ return {
771
+ workflowId: id,
772
+ name: definition.name,
773
+ phase,
774
+ currentState: lastState,
775
+ lastState,
776
+ taskDescription: checkpoint.context.taskDescription,
777
+ round: checkpoint.context.round,
778
+ maxRounds: checkpoint.context.maxRounds,
779
+ totalTokens: checkpoint.context.totalTokens,
780
+ latestVerdict: extractLatestVerdictFromTransitions(),
781
+ error: extractPastRunError(checkpoint, phase),
782
+ timestamp: checkpoint.timestamp,
783
+ durationMs: undefined,
784
+ workspacePath: checkpoint.workspacePath,
785
+ };
786
+ }
787
+ const entries = new MessageLog(load.messageLogPath).readAll();
788
+ const phase = synthesizePhaseFromMessageLog(entries, definition);
789
+ const lastState = deriveLastStateFromEntries(entries, definition);
790
+ return {
791
+ workflowId: id,
792
+ name: definition.name,
793
+ phase,
794
+ currentState: lastState,
795
+ lastState,
796
+ taskDescription: definition.description,
797
+ round: 0,
798
+ maxRounds: 0,
799
+ totalTokens: 0,
800
+ latestVerdict: undefined,
801
+ error: undefined,
802
+ timestamp: summary.mtime.toISOString(),
803
+ durationMs: undefined,
804
+ workspacePath: undefined,
805
+ };
806
+ }
807
+ /**
808
+ * Reads a page of {@link MessageLogEntry} records for a workflow with cursor
809
+ * pagination (per design decision D5).
810
+ *
811
+ * Existence semantics: the workflow must be either currently active in the
812
+ * orchestrator or recoverable from disk via {@link WorkflowManager.loadPastRun}.
813
+ * Workflows write their checkpoint+definition immediately on `start()`, so
814
+ * `loadPastRun` succeeds for live workflows too — this single check covers
815
+ * both the live and past-run cases. Throws `WORKFLOW_NOT_FOUND` when neither
816
+ * source has the id and `WORKFLOW_CORRUPTED` when the checkpoint or definition
817
+ * file is present but unparseable.
818
+ *
819
+ * Note: `MessageLog.readAll()` silently skips malformed JSONL lines (it's
820
+ * append-only and tolerant of truncated writes from crashes), so we cannot
821
+ * surface log-file corruption distinctly here. The corruption error class
822
+ * therefore reflects only checkpoint/definition issues, not log issues.
823
+ *
824
+ * Pagination shape: `entries` is filtered to `ts < before` when `before` is
825
+ * set, sorted descending by `ts` (newest first), then sliced to `limit`.
826
+ * `hasMore` is true iff `entries.length === limit` AND at least one entry
827
+ * with `ts < entries[entries.length - 1].ts` exists in the original log.
828
+ *
829
+ * Cost: v1 re-reads and re-parses the whole log on every page request. This
830
+ * is acceptable given typical message-log sizes (hundreds of entries); a
831
+ * future `MessageLog.readRange(opts)` API can stream from disk in reverse
832
+ * order if logs grow pathologically large. The `MessageLogResponseDto` shape
833
+ * is stable across that change.
834
+ */
835
+ export function readMessageLogPage(manager, workflowId, opts) {
836
+ // Validate workflow existence via the past-run loader. Live workflows write
837
+ // their checkpoint immediately on start, so this single lookup serves both
838
+ // live and past-run cases without duplicating the live/disk fan-out from
839
+ // `workflows.get`.
840
+ const result = manager.loadPastRun(workflowId);
841
+ if ('error' in result) {
842
+ if (result.error === 'not_found') {
843
+ throw new RpcError('WORKFLOW_NOT_FOUND', `Workflow ${workflowId} not found`);
844
+ }
845
+ throw new RpcError('WORKFLOW_CORRUPTED', result.message ?? `Workflow ${workflowId} checkpoint is corrupted`);
846
+ }
847
+ const logPath = resolve(manager.getBaseDir(), workflowId, 'messages.jsonl');
848
+ const log = new MessageLog(logPath);
849
+ // Tolerant of malformed JSONL by design (skips bad lines silently).
850
+ const all = log.readAll();
851
+ const filtered = opts.before === undefined ? all : all.filter((e) => e.ts < opts.before);
852
+ // Newest-first ordering. Stable for entries with equal `ts`.
853
+ filtered.sort(compareEntriesDesc);
854
+ const page = filtered.slice(0, opts.limit);
855
+ const hasMore = computeHasMore(filtered, page, opts.limit);
856
+ return { entries: page, hasMore };
857
+ }
858
+ /** Descending comparator on `ts` (lexicographic on ISO-8601 strings == chronological). */
859
+ function compareEntriesDesc(a, b) {
860
+ if (a.ts === b.ts)
861
+ return 0;
862
+ return a.ts < b.ts ? 1 : -1;
863
+ }
864
+ /**
865
+ * `hasMore` is true iff the page is full (`page.length === limit`) AND at
866
+ * least one entry strictly older than the last entry on the page exists in
867
+ * the filtered set. The strict-less-than check matches the cursor semantics:
868
+ * the next request would set `before = page.last.ts` and exclude entries
869
+ * with that exact timestamp, so equal-`ts` entries don't count as "more".
870
+ */
871
+ function computeHasMore(filtered, page, limit) {
872
+ if (page.length < limit)
873
+ return false;
874
+ const cursor = page[page.length - 1].ts;
875
+ for (const e of filtered) {
876
+ if (e.ts < cursor)
877
+ return true;
878
+ }
879
+ return false;
880
+ }
881
+ /**
882
+ * Extracts a human-readable state name from an XState machine state snapshot.
883
+ * The machineState can be a string (simple state) or a nested object (compound state).
884
+ */
885
+ export function extractLastState(machineState) {
886
+ if (typeof machineState === 'string')
887
+ return machineState;
888
+ if (machineState && typeof machineState === 'object') {
889
+ // Nested XState value: { parentState: "childState" }
890
+ const keys = Object.keys(machineState);
891
+ if (keys.length > 0)
892
+ return keys[0];
893
+ }
894
+ return 'unknown';
895
+ }
896
+ function collectArtifactFilesFromEntries(dir, prefix, dirEntries, files) {
897
+ for (const entry of dirEntries) {
898
+ const relPath = prefix ? `${prefix}/${entry.name}` : entry.name;
899
+ const fullPath = resolve(dir, entry.name);
900
+ if (entry.isDirectory()) {
901
+ try {
902
+ const childEntries = readdirSync(fullPath, { withFileTypes: true });
903
+ collectArtifactFilesFromEntries(fullPath, relPath, childEntries, files);
904
+ }
905
+ catch {
906
+ // Skip unreadable directories
907
+ }
908
+ }
909
+ else if (entry.isFile()) {
910
+ try {
911
+ const s = statSync(fullPath);
912
+ if (s.size > MAX_FILE_SIZE) {
913
+ files.push({ path: relPath, content: `[File too large to display (${s.size} bytes)]` });
914
+ continue;
915
+ }
916
+ const content = readFileSync(fullPath, 'utf-8');
917
+ files.push({ path: relPath, content });
918
+ }
919
+ catch {
920
+ // Skip unreadable files
921
+ }
922
+ }
923
+ }
924
+ }
925
+ //# sourceMappingURL=workflow-dispatch.js.map