@xdelivered/emberflow 0.2.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 (414) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +153 -0
  3. package/bin/commands.test.ts +185 -0
  4. package/bin/commands.ts +306 -0
  5. package/bin/emberflow.mjs +38 -0
  6. package/bin/init.test.ts +165 -0
  7. package/bin/init.ts +297 -0
  8. package/bin/runtime.mjs +105 -0
  9. package/bin/runtime.test.ts +167 -0
  10. package/bin/skillTargets.test.ts +108 -0
  11. package/bin/skillTargets.ts +36 -0
  12. package/bin/skillTemplates.test.ts +37 -0
  13. package/bin/tsconfig.json +23 -0
  14. package/dist/bin/commands.d.ts +24 -0
  15. package/dist/bin/commands.js +252 -0
  16. package/dist/bin/init.d.ts +17 -0
  17. package/dist/bin/init.js +241 -0
  18. package/dist/bin/runtime.d.mts +39 -0
  19. package/dist/bin/runtime.mjs +96 -0
  20. package/dist/bin/skillTargets.d.ts +13 -0
  21. package/dist/bin/skillTargets.js +22 -0
  22. package/dist/server/agents/claudeAdapter.d.ts +8 -0
  23. package/dist/server/agents/claudeAdapter.js +190 -0
  24. package/dist/server/agents/claudeParse.d.ts +2 -0
  25. package/dist/server/agents/claudeParse.js +162 -0
  26. package/dist/server/agents/codexAdapter.d.ts +13 -0
  27. package/dist/server/agents/codexAdapter.js +174 -0
  28. package/dist/server/agents/codexParse.d.ts +2 -0
  29. package/dist/server/agents/codexParse.js +74 -0
  30. package/dist/server/agents/detect.d.ts +23 -0
  31. package/dist/server/agents/detect.js +35 -0
  32. package/dist/server/agents/gitScope.d.ts +24 -0
  33. package/dist/server/agents/gitScope.js +95 -0
  34. package/dist/server/agents/modelRejectionHint.d.ts +8 -0
  35. package/dist/server/agents/modelRejectionHint.js +12 -0
  36. package/dist/server/agents/prompt.d.ts +79 -0
  37. package/dist/server/agents/prompt.js +364 -0
  38. package/dist/server/agents/runManager.d.ts +90 -0
  39. package/dist/server/agents/runManager.js +259 -0
  40. package/dist/server/agents/types.d.ts +15 -0
  41. package/dist/server/agents/types.js +1 -0
  42. package/dist/server/apiStore.d.ts +60 -0
  43. package/dist/server/apiStore.js +249 -0
  44. package/dist/server/auth/enforce.d.ts +1 -0
  45. package/dist/server/auth/enforce.js +6 -0
  46. package/dist/server/auth/types.d.ts +1 -0
  47. package/dist/server/auth/types.js +1 -0
  48. package/dist/server/auth/verifiers.d.ts +1 -0
  49. package/dist/server/auth/verifiers.js +4 -0
  50. package/dist/server/authAttach.d.ts +19 -0
  51. package/dist/server/authAttach.js +79 -0
  52. package/dist/server/cli.d.ts +7 -0
  53. package/dist/server/cli.js +804 -0
  54. package/dist/server/client.d.ts +130 -0
  55. package/dist/server/client.js +247 -0
  56. package/dist/server/environments.d.ts +103 -0
  57. package/dist/server/environments.js +486 -0
  58. package/dist/server/flowStore.d.ts +37 -0
  59. package/dist/server/flowStore.js +117 -0
  60. package/dist/server/httpOperations.d.ts +32 -0
  61. package/dist/server/httpOperations.js +62 -0
  62. package/dist/server/index.d.ts +1 -0
  63. package/dist/server/index.js +1022 -0
  64. package/dist/server/infrastructure.d.ts +51 -0
  65. package/dist/server/infrastructure.js +116 -0
  66. package/dist/server/login.d.ts +21 -0
  67. package/dist/server/login.js +44 -0
  68. package/dist/server/mcp.d.ts +1 -0
  69. package/dist/server/mcp.js +182 -0
  70. package/dist/server/migrateFlows.d.ts +8 -0
  71. package/dist/server/migrateFlows.js +25 -0
  72. package/dist/server/mockHandler.d.ts +15 -0
  73. package/dist/server/mockHandler.js +35 -0
  74. package/dist/server/nodesPayload.d.ts +14 -0
  75. package/dist/server/nodesPayload.js +14 -0
  76. package/dist/server/normalizeFlow.d.ts +21 -0
  77. package/dist/server/normalizeFlow.js +48 -0
  78. package/dist/server/openBrowser.d.ts +8 -0
  79. package/dist/server/openBrowser.js +16 -0
  80. package/dist/server/operationResult.d.ts +12 -0
  81. package/dist/server/operationResult.js +26 -0
  82. package/dist/server/pathGuard.d.ts +1 -0
  83. package/dist/server/pathGuard.js +16 -0
  84. package/dist/server/pathSafety.d.ts +20 -0
  85. package/dist/server/pathSafety.js +35 -0
  86. package/dist/server/projectConfig.d.ts +47 -0
  87. package/dist/server/projectConfig.js +61 -0
  88. package/dist/server/projectMode.d.ts +26 -0
  89. package/dist/server/projectMode.js +44 -0
  90. package/dist/server/redact.d.ts +6 -0
  91. package/dist/server/redact.js +37 -0
  92. package/dist/server/runRegistry.d.ts +84 -0
  93. package/dist/server/runRegistry.js +200 -0
  94. package/dist/server/runtime/RuntimeAdapter.d.ts +22 -0
  95. package/dist/server/runtime/RuntimeAdapter.js +1 -0
  96. package/dist/server/runtime/expressAdapter.d.ts +19 -0
  97. package/dist/server/runtime/expressAdapter.js +56 -0
  98. package/dist/server/scenarioTest.d.ts +14 -0
  99. package/dist/server/scenarioTest.js +66 -0
  100. package/dist/server/subflowRunner.d.ts +32 -0
  101. package/dist/server/subflowRunner.js +72 -0
  102. package/dist/server/testRunner.d.ts +70 -0
  103. package/dist/server/testRunner.js +151 -0
  104. package/dist/src/config.d.ts +3 -0
  105. package/dist/src/config.js +2 -0
  106. package/dist/src/engine/authVerify.d.ts +33 -0
  107. package/dist/src/engine/authVerify.js +63 -0
  108. package/dist/src/engine/diagnostics.d.ts +62 -0
  109. package/dist/src/engine/diagnostics.js +148 -0
  110. package/dist/src/engine/executor.d.ts +151 -0
  111. package/dist/src/engine/executor.js +684 -0
  112. package/dist/src/engine/httpError.d.ts +7 -0
  113. package/dist/src/engine/httpError.js +14 -0
  114. package/dist/src/engine/index.d.ts +11 -0
  115. package/dist/src/engine/index.js +11 -0
  116. package/dist/src/engine/output.d.ts +11 -0
  117. package/dist/src/engine/output.js +20 -0
  118. package/dist/src/engine/publish.d.ts +30 -0
  119. package/dist/src/engine/publish.js +95 -0
  120. package/dist/src/engine/registry.d.ts +34 -0
  121. package/dist/src/engine/registry.js +67 -0
  122. package/dist/src/engine/scenarios.d.ts +12 -0
  123. package/dist/src/engine/scenarios.js +19 -0
  124. package/dist/src/engine/schemaCheck.d.ts +4 -0
  125. package/dist/src/engine/schemaCheck.js +34 -0
  126. package/dist/src/engine/trace.d.ts +12 -0
  127. package/dist/src/engine/trace.js +12 -0
  128. package/dist/src/engine/types.d.ts +308 -0
  129. package/dist/src/engine/types.js +1 -0
  130. package/dist/src/engine/validation.d.ts +26 -0
  131. package/dist/src/engine/validation.js +318 -0
  132. package/dist/src/flows/anomaly-flows.d.ts +23 -0
  133. package/dist/src/flows/anomaly-flows.js +151 -0
  134. package/dist/src/flows/city-sweep-flow.d.ts +10 -0
  135. package/dist/src/flows/city-sweep-flow.js +127 -0
  136. package/dist/src/flows/login-flow.d.ts +13 -0
  137. package/dist/src/flows/login-flow.js +145 -0
  138. package/dist/src/flows/pradar-flows.d.ts +5 -0
  139. package/dist/src/flows/pradar-flows.js +375 -0
  140. package/dist/src/flows/weather-flow.d.ts +12 -0
  141. package/dist/src/flows/weather-flow.js +150 -0
  142. package/dist/src/lib/pathParams.d.ts +3 -0
  143. package/dist/src/lib/pathParams.js +6 -0
  144. package/dist/src/nodes/anomaly.d.ts +2 -0
  145. package/dist/src/nodes/anomaly.js +556 -0
  146. package/dist/src/nodes/flow-control.d.ts +7 -0
  147. package/dist/src/nodes/flow-control.js +270 -0
  148. package/dist/src/nodes/index.d.ts +3 -0
  149. package/dist/src/nodes/index.js +20 -0
  150. package/dist/src/nodes/login.d.ts +7 -0
  151. package/dist/src/nodes/login.js +158 -0
  152. package/dist/src/nodes/loops.d.ts +11 -0
  153. package/dist/src/nodes/loops.js +60 -0
  154. package/dist/src/nodes/pradar/index.d.ts +7 -0
  155. package/dist/src/nodes/pradar/index.js +9 -0
  156. package/dist/src/nodes/pradar/logic.d.ts +189 -0
  157. package/dist/src/nodes/pradar/logic.js +370 -0
  158. package/dist/src/nodes/pradar/nodes.d.ts +42 -0
  159. package/dist/src/nodes/pradar/nodes.js +801 -0
  160. package/dist/src/nodes/requireAuth.d.ts +20 -0
  161. package/dist/src/nodes/requireAuth.js +48 -0
  162. package/dist/src/nodes/response.d.ts +8 -0
  163. package/dist/src/nodes/response.js +33 -0
  164. package/dist/src/nodes/weather.d.ts +33 -0
  165. package/dist/src/nodes/weather.js +260 -0
  166. package/package.json +92 -0
  167. package/server/agentRoute.test.ts +157 -0
  168. package/server/agents/__fixtures__/fake-claude-writer.mjs +23 -0
  169. package/server/agents/__fixtures__/fake-claude.mjs +55 -0
  170. package/server/agents/__fixtures__/fake-codex-writer.mjs +27 -0
  171. package/server/agents/__fixtures__/fake-codex.mjs +52 -0
  172. package/server/agents/__fixtures__/fake-version-empty.mjs +3 -0
  173. package/server/agents/__fixtures__/fake-version-fail.mjs +4 -0
  174. package/server/agents/__fixtures__/fake-version-garbage.mjs +4 -0
  175. package/server/agents/__fixtures__/fake-version-good.mjs +4 -0
  176. package/server/agents/__fixtures__/fake-version-hang.mjs +3 -0
  177. package/server/agents/claudeAdapter.test.ts +127 -0
  178. package/server/agents/claudeAdapter.ts +212 -0
  179. package/server/agents/claudeParse.ts +193 -0
  180. package/server/agents/codexAdapter.test.ts +125 -0
  181. package/server/agents/codexAdapter.ts +198 -0
  182. package/server/agents/codexParse.test.ts +58 -0
  183. package/server/agents/codexParse.ts +126 -0
  184. package/server/agents/detect.test.ts +65 -0
  185. package/server/agents/detect.ts +43 -0
  186. package/server/agents/gitScope.test.ts +129 -0
  187. package/server/agents/gitScope.ts +110 -0
  188. package/server/agents/modelRejectionHint.test.ts +30 -0
  189. package/server/agents/modelRejectionHint.ts +14 -0
  190. package/server/agents/prompt.test.ts +613 -0
  191. package/server/agents/prompt.ts +527 -0
  192. package/server/agents/runManager.test.ts +251 -0
  193. package/server/agents/runManager.ts +286 -0
  194. package/server/agents/types.ts +17 -0
  195. package/server/apiMount.test.ts +591 -0
  196. package/server/apiStore.test.ts +324 -0
  197. package/server/apiStore.ts +241 -0
  198. package/server/auth/enforce.test.ts +61 -0
  199. package/server/auth/enforce.ts +6 -0
  200. package/server/auth/types.ts +5 -0
  201. package/server/auth/verifiers.test.ts +37 -0
  202. package/server/auth/verifiers.ts +9 -0
  203. package/server/authAttach.test.ts +175 -0
  204. package/server/authAttach.ts +99 -0
  205. package/server/cli.ts +847 -0
  206. package/server/cliAuth.test.ts +229 -0
  207. package/server/cliCreate.test.ts +131 -0
  208. package/server/cliDoctor.test.ts +326 -0
  209. package/server/cliTest.test.ts +735 -0
  210. package/server/client.ts +314 -0
  211. package/server/demoProject.test.ts +29 -0
  212. package/server/diagnosticsRoute.test.ts +169 -0
  213. package/server/environments.test.ts +620 -0
  214. package/server/environments.ts +603 -0
  215. package/server/environmentsRoute.test.ts +385 -0
  216. package/server/errorWorkflow.test.ts +205 -0
  217. package/server/flowStore.test.ts +84 -0
  218. package/server/flowStore.ts +125 -0
  219. package/server/httpOperations.test.ts +377 -0
  220. package/server/httpOperations.ts +96 -0
  221. package/server/index.ts +1111 -0
  222. package/server/infrastructure.test.ts +154 -0
  223. package/server/infrastructure.ts +161 -0
  224. package/server/infrastructureRoute.test.ts +105 -0
  225. package/server/login.test.ts +153 -0
  226. package/server/login.ts +67 -0
  227. package/server/mcp.ts +278 -0
  228. package/server/migrateFlows.test.ts +55 -0
  229. package/server/migrateFlows.ts +23 -0
  230. package/server/mockHandler.test.ts +138 -0
  231. package/server/mockHandler.ts +52 -0
  232. package/server/mockRun.test.ts +367 -0
  233. package/server/mockServe.test.ts +467 -0
  234. package/server/nodesPayload.test.ts +29 -0
  235. package/server/nodesPayload.ts +26 -0
  236. package/server/normalizeFlow.test.ts +80 -0
  237. package/server/normalizeFlow.ts +59 -0
  238. package/server/openBrowser.test.ts +20 -0
  239. package/server/openBrowser.ts +22 -0
  240. package/server/operationResult.test.ts +67 -0
  241. package/server/operationResult.ts +27 -0
  242. package/server/pathGuard.test.ts +19 -0
  243. package/server/pathGuard.ts +12 -0
  244. package/server/pathSafety.test.ts +38 -0
  245. package/server/pathSafety.ts +33 -0
  246. package/server/projectConfig.test.ts +67 -0
  247. package/server/projectConfig.ts +101 -0
  248. package/server/projectMode.test.ts +106 -0
  249. package/server/projectMode.ts +59 -0
  250. package/server/redact.test.ts +88 -0
  251. package/server/redact.ts +35 -0
  252. package/server/redactEgress.test.ts +281 -0
  253. package/server/runRegistry.ts +270 -0
  254. package/server/runsAuth.test.ts +119 -0
  255. package/server/runtime/RuntimeAdapter.ts +24 -0
  256. package/server/runtime/expressAdapter.test.ts +38 -0
  257. package/server/runtime/expressAdapter.ts +70 -0
  258. package/server/scenarioTest.test.ts +148 -0
  259. package/server/scenarioTest.ts +88 -0
  260. package/server/setupStatus.test.ts +127 -0
  261. package/server/subflowRunner.ts +104 -0
  262. package/server/testRunner.ts +216 -0
  263. package/server/tsconfig.json +20 -0
  264. package/server/workflowTestRoute.test.ts +297 -0
  265. package/src/App.tsx +233 -0
  266. package/src/components/AgentConsole.tsx +461 -0
  267. package/src/components/BranchRulesEditor.tsx +156 -0
  268. package/src/components/CreateModal.tsx +275 -0
  269. package/src/components/Dock.tsx +487 -0
  270. package/src/components/EnvironmentDialog.tsx +569 -0
  271. package/src/components/EnvironmentPicker.tsx +357 -0
  272. package/src/components/EnvironmentsDialog.tsx +144 -0
  273. package/src/components/ExecutionPager.tsx +67 -0
  274. package/src/components/InfraPanel.test.tsx +103 -0
  275. package/src/components/InfraPanel.tsx +171 -0
  276. package/src/components/Inspector.tsx +1182 -0
  277. package/src/components/Json.tsx +50 -0
  278. package/src/components/JsonModal.tsx +52 -0
  279. package/src/components/LogMessage.tsx +61 -0
  280. package/src/components/NodeRunModal.tsx +213 -0
  281. package/src/components/RunLogPanel.tsx +397 -0
  282. package/src/components/RunbookView.tsx +1085 -0
  283. package/src/components/Scenarios.tsx +476 -0
  284. package/src/components/SettingsDialog.tsx +229 -0
  285. package/src/components/Sidebar.tsx +436 -0
  286. package/src/components/StatusBar.tsx +302 -0
  287. package/src/components/Toolbar.tsx +663 -0
  288. package/src/components/WelcomeDialog.test.tsx +107 -0
  289. package/src/components/WelcomeDialog.tsx +412 -0
  290. package/src/components/agentMarkdown.test.tsx +91 -0
  291. package/src/components/agentMarkdown.tsx +158 -0
  292. package/src/components/ui/badge.tsx +31 -0
  293. package/src/components/ui/button.tsx +46 -0
  294. package/src/components/ui/combobox.tsx +114 -0
  295. package/src/components/ui/command.tsx +82 -0
  296. package/src/components/ui/dialog.tsx +73 -0
  297. package/src/components/ui/input.tsx +20 -0
  298. package/src/components/ui/popover.tsx +32 -0
  299. package/src/components/ui/resizable.tsx +44 -0
  300. package/src/components/ui/tabs.tsx +36 -0
  301. package/src/config.ts +4 -0
  302. package/src/engine/authVerify.ts +81 -0
  303. package/src/engine/diagnostics.test.ts +437 -0
  304. package/src/engine/diagnostics.ts +211 -0
  305. package/src/engine/executor.test.ts +1339 -0
  306. package/src/engine/executor.ts +840 -0
  307. package/src/engine/httpError.test.ts +12 -0
  308. package/src/engine/httpError.ts +16 -0
  309. package/src/engine/index.ts +11 -0
  310. package/src/engine/output.ts +21 -0
  311. package/src/engine/publish.test.ts +176 -0
  312. package/src/engine/publish.ts +127 -0
  313. package/src/engine/registry.test.ts +64 -0
  314. package/src/engine/registry.ts +80 -0
  315. package/src/engine/scenarios.test.ts +55 -0
  316. package/src/engine/scenarios.ts +29 -0
  317. package/src/engine/schemaCheck.test.ts +21 -0
  318. package/src/engine/schemaCheck.ts +35 -0
  319. package/src/engine/trace.test.ts +19 -0
  320. package/src/engine/trace.ts +23 -0
  321. package/src/engine/types.test.ts +22 -0
  322. package/src/engine/types.ts +316 -0
  323. package/src/engine/validation.test.ts +314 -0
  324. package/src/engine/validation.ts +354 -0
  325. package/src/flows/anomaly-flows.test.ts +81 -0
  326. package/src/flows/anomaly-flows.ts +202 -0
  327. package/src/flows/city-sweep-flow.ts +131 -0
  328. package/src/flows/flows.test.ts +30 -0
  329. package/src/flows/login-flow.ts +150 -0
  330. package/src/flows/pradar-flows.test.ts +204 -0
  331. package/src/flows/pradar-flows.ts +419 -0
  332. package/src/flows/weather-flow.ts +154 -0
  333. package/src/globals.css +147 -0
  334. package/src/lib/pathParams.test.ts +24 -0
  335. package/src/lib/pathParams.ts +6 -0
  336. package/src/lib/registerLens.test.ts +79 -0
  337. package/src/lib/registerLens.ts +59 -0
  338. package/src/lib/runbookModel.test.ts +61 -0
  339. package/src/lib/runbookModel.ts +290 -0
  340. package/src/lib/runbookProjection.test.ts +417 -0
  341. package/src/lib/runbookProjection.ts +254 -0
  342. package/src/lib/useTailAnchor.ts +53 -0
  343. package/src/lib/utils.ts +7 -0
  344. package/src/main.tsx +13 -0
  345. package/src/nodes/anomaly.test.ts +98 -0
  346. package/src/nodes/anomaly.ts +637 -0
  347. package/src/nodes/effects-scan.test.ts +44 -0
  348. package/src/nodes/flow-control.test.ts +280 -0
  349. package/src/nodes/flow-control.ts +305 -0
  350. package/src/nodes/index.ts +22 -0
  351. package/src/nodes/login.test.ts +81 -0
  352. package/src/nodes/login.ts +186 -0
  353. package/src/nodes/loops.ts +69 -0
  354. package/src/nodes/pradar/index.ts +11 -0
  355. package/src/nodes/pradar/logic.test.ts +288 -0
  356. package/src/nodes/pradar/logic.ts +582 -0
  357. package/src/nodes/pradar/nodes.test.ts +146 -0
  358. package/src/nodes/pradar/nodes.ts +955 -0
  359. package/src/nodes/requireAuth.test.ts +75 -0
  360. package/src/nodes/requireAuth.ts +55 -0
  361. package/src/nodes/response.test.ts +33 -0
  362. package/src/nodes/response.ts +39 -0
  363. package/src/nodes/traceDetail.test.ts +39 -0
  364. package/src/nodes/weather.test.ts +65 -0
  365. package/src/nodes/weather.ts +306 -0
  366. package/src/store/agentClient.test.ts +103 -0
  367. package/src/store/agentClient.ts +131 -0
  368. package/src/store/apiTree.test.ts +17 -0
  369. package/src/store/apiTree.ts +73 -0
  370. package/src/store/builderStore.test.ts +1234 -0
  371. package/src/store/builderStore.ts +1879 -0
  372. package/src/store/infraClient.ts +63 -0
  373. package/src/store/nodeMeta.test.ts +24 -0
  374. package/src/store/nodeMeta.ts +21 -0
  375. package/src/store/persistence.test.ts +15 -0
  376. package/src/store/persistence.ts +54 -0
  377. package/src/store/serverRunner.ts +381 -0
  378. package/src/store/setupClient.ts +57 -0
  379. package/studio-dist/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
  380. package/studio-dist/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
  381. package/studio-dist/assets/ibm-plex-mono-cyrillic-500-normal-Ael50iVv.woff +0 -0
  382. package/studio-dist/assets/ibm-plex-mono-cyrillic-500-normal-Bq9vWWag.woff2 +0 -0
  383. package/studio-dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
  384. package/studio-dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
  385. package/studio-dist/assets/ibm-plex-mono-cyrillic-ext-500-normal-BIfNGwUT.woff +0 -0
  386. package/studio-dist/assets/ibm-plex-mono-cyrillic-ext-500-normal-BqneJy0T.woff2 +0 -0
  387. package/studio-dist/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
  388. package/studio-dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
  389. package/studio-dist/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
  390. package/studio-dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
  391. package/studio-dist/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
  392. package/studio-dist/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
  393. package/studio-dist/assets/ibm-plex-mono-latin-ext-500-normal-CAhNIIs5.woff2 +0 -0
  394. package/studio-dist/assets/ibm-plex-mono-latin-ext-500-normal-CZ70TYgx.woff +0 -0
  395. package/studio-dist/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
  396. package/studio-dist/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
  397. package/studio-dist/assets/ibm-plex-mono-vietnamese-500-normal-C8zxqsMH.woff +0 -0
  398. package/studio-dist/assets/ibm-plex-mono-vietnamese-500-normal-DZ4AoWbu.woff2 +0 -0
  399. package/studio-dist/assets/index-DNJwW-hM.css +1 -0
  400. package/studio-dist/assets/index-O26dKRjW.js +65 -0
  401. package/studio-dist/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  402. package/studio-dist/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  403. package/studio-dist/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  404. package/studio-dist/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  405. package/studio-dist/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  406. package/studio-dist/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  407. package/studio-dist/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  408. package/studio-dist/favicon.svg +1 -0
  409. package/studio-dist/icons.svg +24 -0
  410. package/studio-dist/index.html +14 -0
  411. package/templates/skills/emberflow-basics/SKILL.md +316 -0
  412. package/templates/skills/emberflow-model-process/SKILL.md +209 -0
  413. package/templates/skills/emberflow-new-workflow/SKILL.md +211 -0
  414. package/templates/skills/emberflow-review-workflow/SKILL.md +148 -0
@@ -0,0 +1,840 @@
1
+ import type {
2
+ ExecutionRecord, LogLevel, LogLine, NodeRunState, SubflowResult,
3
+ WorkflowDefinition, WorkflowNode, WorkflowRun,
4
+ } from './types';
5
+ import type { NodeRegistry } from './registry';
6
+ import type { TraceSink } from './trace';
7
+ import { computeLoopRegions, topoSort, validateFlow } from './validation';
8
+ import type { LoopRegion } from './validation';
9
+
10
+ export function getByPath(obj: unknown, path: string): unknown {
11
+ if (path === '$') return obj;
12
+ return path.split('.').reduce<unknown>(
13
+ (acc, key) =>
14
+ acc !== null && typeof acc === 'object' ? (acc as Record<string, unknown>)[key] : undefined,
15
+ obj,
16
+ );
17
+ }
18
+
19
+ // Matches the V8 shapes ("Cannot read properties of undefined/null (reading 'x')")
20
+ // and the older/Safari-style shape ("undefined is not an object (evaluating 'a.x')")
21
+ // for reading a property off undefined or null.
22
+ const UNDEFINED_READ_PATTERN =
23
+ /^(Cannot read properties of (?:undefined|null) \(reading '[^']*'\)|undefined is not an object \(evaluating '[^']*'\))$/;
24
+
25
+ /**
26
+ * Enriches a "reading property of undefined/null" TypeError message with the
27
+ * node's resolved input keys, so the recorded error points at what the node
28
+ * actually received instead of just the raw JS engine message. Every other
29
+ * message passes through byte-identical.
30
+ */
31
+ export function explainUndefinedRead(message: string, input: Record<string, unknown>): string {
32
+ if (!UNDEFINED_READ_PATTERN.test(message)) return message;
33
+ // A field mapped in `inputMap` whose source produced nothing still appears as
34
+ // a key, holding `undefined` — and that is almost always the culprit here.
35
+ // Listing it alongside the fields that actually carry a value is what turns
36
+ // this from a raw JS message into a pointer at the broken mapping.
37
+ const present = Object.keys(input).filter((k) => input[k] !== undefined).sort();
38
+ const undef = Object.keys(input).filter((k) => input[k] === undefined).sort();
39
+ const got = present.length > 0 ? `[${present.join(', ')}]` : '(none)';
40
+ const missing =
41
+ undef.length > 0
42
+ ? ` These mapped fields resolved to undefined: ${undef.join(', ')}.`
43
+ : '';
44
+ return `${message} — this node received ${got}.${missing} Check the node's inputMap and the run input.`;
45
+ }
46
+
47
+ /** A `{"$env": "NAME"}` reference resolves to `vars["NAME"]`. */
48
+ function envRefName(value: unknown): string | undefined {
49
+ if (
50
+ value !== null &&
51
+ typeof value === 'object' &&
52
+ !Array.isArray(value) &&
53
+ typeof (value as Record<string, unknown>).$env === 'string'
54
+ ) {
55
+ return (value as { $env: string }).$env;
56
+ }
57
+ return undefined;
58
+ }
59
+
60
+ function resolveEnvRef(name: string, vars: Record<string, string>): string {
61
+ if (!(name in vars)) {
62
+ // The most common cause is not a missing key but a run with NO environment
63
+ // at all: the in-tab engine has no vars, so flows seeded with $env refs
64
+ // need the server runner. Say so, instead of a bare "missing".
65
+ const hint =
66
+ Object.keys(vars).length === 0
67
+ ? ' — this run has no environment variables (in-tab/browser runs never do; make sure the runner is online and the engine is set to Server or Auto)'
68
+ : '';
69
+ throw new Error(`Missing environment variable: ${name}${hint}`);
70
+ }
71
+ return vars[name];
72
+ }
73
+
74
+ /**
75
+ * Recursively replaces every `{"$env": "NAME"}` reference in a value (objects
76
+ * and arrays walked) with its resolved var. A missing var throws
77
+ * `Missing environment variable: NAME`. Non-ref values pass through untouched.
78
+ */
79
+ function resolveEnvDeep(value: unknown, vars: Record<string, string>): unknown {
80
+ const name = envRefName(value);
81
+ if (name !== undefined) return resolveEnvRef(name, vars);
82
+ if (Array.isArray(value)) return value.map((v) => resolveEnvDeep(v, vars));
83
+ if (value !== null && typeof value === 'object') {
84
+ const out: Record<string, unknown> = {};
85
+ for (const [key, v] of Object.entries(value as Record<string, unknown>)) {
86
+ out[key] = resolveEnvDeep(v, vars);
87
+ }
88
+ return out;
89
+ }
90
+ return value;
91
+ }
92
+
93
+ /**
94
+ * Appends one ExecutionRecord to a loop-body (or ForEach) node's
95
+ * accumulated per-iteration history. Pure — callers must read the prior
96
+ * array themselves, from whatever point in the node's state lifecycle it is
97
+ * still intact (setNodeState replaces the state object wholesale on every
98
+ * call, so an intermediate 'running'/'skipped' write can otherwise wipe it
99
+ * out before the caller gets a chance to read it back).
100
+ */
101
+ function appendExecution(
102
+ prior: ExecutionRecord[] | undefined,
103
+ record: ExecutionRecord,
104
+ ): ExecutionRecord[] {
105
+ return [...(prior ?? []), record];
106
+ }
107
+
108
+ export interface ExecutorEvents {
109
+ onNodeStateChange?: (nodeId: string, state: NodeRunState) => void;
110
+ onLog?: (line: LogLine) => void;
111
+ onRunFinished?: (run: WorkflowRun) => void;
112
+ }
113
+
114
+ /** Mutable state for the loop region currently being driven by the cursor. */
115
+ interface ActiveLoopState {
116
+ region: LoopRegion;
117
+ forEachNode: WorkflowNode;
118
+ collectNode: WorkflowNode;
119
+ /** Body node ids in region topo order (main topo order filtered to the region). */
120
+ bodyOrder: string[];
121
+ /** The full `items` array resolved once, at loop start. */
122
+ items: unknown[];
123
+ /** One entry per iteration: the element (batchSize 1) or the chunk array. */
124
+ chunks: unknown[];
125
+ /** Iteration currently in flight (or about to start once the body position resets). */
126
+ iterIndex: number;
127
+ /** Position within bodyOrder for the current iteration. */
128
+ bodyCursor: number;
129
+ /** Collect's mapped value, one per completed iteration. */
130
+ results: unknown[];
131
+ }
132
+
133
+ export interface StartRunOptions {
134
+ flow: WorkflowDefinition;
135
+ registry: NodeRegistry;
136
+ trace?: TraceSink;
137
+ events?: ExecutorEvents;
138
+ now?: () => string;
139
+ newId?: () => string;
140
+ secrets?: Record<string, string>;
141
+ /** Non-secret environment values, exposed as ctx.vars and used to resolve {"$env": NAME}. */
142
+ vars?: Record<string, string>;
143
+ /** When true, mutation nodes dry-run their side effects; recorded on the run. */
144
+ safeMode?: boolean;
145
+ /** The environment name this run points at; recorded on the run. */
146
+ environment?: string;
147
+ /** nodeId -> pinned output. Pinned nodes skip execution and tracing. */
148
+ pins?: Record<string, unknown>;
149
+ /**
150
+ * When true, runs in Mock mode: nodes with a canned output in `mocks` return
151
+ * it verbatim without invoking their implementation or the retry loop; nodes
152
+ * that would touch infrastructure (`traceKind` 'db'/'http'/'llm') and have no
153
+ * mock fail loud (a normal node failure — respects `optional`, halts like any
154
+ * error). Pure/compute nodes (`traceKind` 'compute' or absent) always execute
155
+ * their real implementation, mocked or not. Defaults false; when false,
156
+ * `mocks` is ignored entirely (Live behavior is byte-identical to today).
157
+ */
158
+ mockRun?: boolean;
159
+ /** nodeId -> canned output, consulted only when `mockRun` is true. See `mockRun`. */
160
+ mocks?: Record<string, unknown>;
161
+ /** The invocation payload, exposed to nodes as ctx.runInput. */
162
+ input?: Record<string, unknown>;
163
+ /**
164
+ * Host-provided runner for Subflow nodes: given a workflow id + input, runs
165
+ * that child flow to completion and returns its collected output. The
166
+ * executor injects the calling node's id (for log prefixing / cycle context);
167
+ * the host owns workflow lookup and the recursion/cycle guard. Absent when
168
+ * the host can't resolve other workflows.
169
+ */
170
+ subflowRunner?: (
171
+ workflowId: string,
172
+ input: Record<string, unknown>,
173
+ callerNodeId: string,
174
+ ) => Promise<SubflowResult>;
175
+ }
176
+
177
+ export class FlowRun {
178
+ readonly run: WorkflowRun;
179
+ private readonly flow: WorkflowDefinition;
180
+ private readonly registry: NodeRegistry;
181
+ private readonly trace?: TraceSink;
182
+ private readonly events: ExecutorEvents;
183
+ private readonly now: () => string;
184
+ private readonly newId: () => string;
185
+ private readonly secrets: Record<string, string>;
186
+ private readonly vars: Record<string, string>;
187
+ private readonly environment?: string;
188
+ private readonly safeMode: boolean;
189
+ private readonly runInput: Record<string, unknown>;
190
+ private readonly pins: Record<string, unknown>;
191
+ private readonly mockRun: boolean;
192
+ private readonly mocks: Record<string, unknown>;
193
+ private readonly subflowRunner?: StartRunOptions['subflowRunner'];
194
+ private readonly order: string[];
195
+ private readonly outputs = new Map<string, unknown>();
196
+ private readonly skippedNodes = new Set<string>();
197
+ private readonly branchTaken = new Map<string, string>();
198
+ private readonly regionByForEach: Map<string, LoopRegion>;
199
+ /** Nodes a completed loop region already executed; the main cursor consumes them silently. */
200
+ private readonly loopExecuted = new Set<string>();
201
+ private activeLoop?: ActiveLoopState;
202
+ private cursor = 0;
203
+ /** Run-wide execution counter — numbers every node execution in the log. */
204
+ private execSeq = 0;
205
+
206
+ constructor(opts: StartRunOptions) {
207
+ this.flow = opts.flow;
208
+ this.registry = opts.registry;
209
+ this.trace = opts.trace;
210
+ this.events = opts.events ?? {};
211
+ this.now = opts.now ?? (() => new Date().toISOString());
212
+ this.newId = opts.newId ?? (() => crypto.randomUUID());
213
+ this.secrets = opts.secrets ?? {};
214
+ this.vars = opts.vars ?? {};
215
+ this.environment = opts.environment;
216
+ this.safeMode = opts.safeMode ?? false;
217
+ // Resolve {"$env": NAME} anywhere in the run-input payload up front, so
218
+ // nodes read concrete values from ctx.runInput and a missing var fails the
219
+ // run at start rather than deep inside a node.
220
+ this.runInput = resolveEnvDeep(opts.input ?? {}, this.vars) as Record<string, unknown>;
221
+ this.pins = opts.pins ?? {};
222
+ this.mockRun = opts.mockRun ?? false;
223
+ this.mocks = opts.mocks ?? {};
224
+ this.subflowRunner = opts.subflowRunner;
225
+ this.order = topoSort(this.flow);
226
+ this.regionByForEach = new Map(computeLoopRegions(this.flow).map((r) => [r.forEachId, r]));
227
+ this.run = {
228
+ id: this.newId(),
229
+ workflowId: this.flow.id,
230
+ status: 'running',
231
+ startedAt: this.now(),
232
+ ...(opts.environment !== undefined ? { environment: opts.environment } : {}),
233
+ ...(opts.safeMode !== undefined ? { safeMode: opts.safeMode } : {}),
234
+ nodeStates: {},
235
+ };
236
+ // No events from the constructor: subscribers' closures may not be
237
+ // initialized yet. Initial states are readable via handle.run.
238
+ for (const node of this.flow.nodes) {
239
+ this.run.nodeStates[node.id] = { status: 'queued' };
240
+ }
241
+ }
242
+
243
+ async step(): Promise<boolean> {
244
+ if (this.run.status !== 'running' || this.cursor >= this.order.length) return false;
245
+
246
+ if (this.activeLoop) return this.stepLoop();
247
+
248
+ // Consume nodes a finished loop region already executed (topo order may
249
+ // interleave them with parallel non-region nodes), and nodes whose every
250
+ // incoming edge is dead (untaken route branch or skipped ancestor) —
251
+ // the latter are marked skipped, not executed.
252
+ while (this.cursor < this.order.length) {
253
+ const id = this.order[this.cursor];
254
+ if (this.loopExecuted.has(id)) {
255
+ this.cursor += 1;
256
+ continue;
257
+ }
258
+ if (!this.isReachable(id)) {
259
+ this.cursor += 1;
260
+ this.skippedNodes.add(id);
261
+ this.setNodeState(id, { status: 'skipped' });
262
+ continue;
263
+ }
264
+ break;
265
+ }
266
+ if (this.cursor >= this.order.length) {
267
+ this.finish('succeeded');
268
+ return false;
269
+ }
270
+
271
+ const nodeId = this.order[this.cursor];
272
+ this.cursor += 1;
273
+
274
+ // A reachable ForEach hands execution to the loop machinery, which owns
275
+ // the whole region (ForEach + body + Collect) until the Collect
276
+ // completes; the main cursor then resumes right after it.
277
+ const region = this.regionByForEach.get(nodeId);
278
+ if (region) return this.startLoop(region);
279
+
280
+ const ok = await this.executeNode(nodeId);
281
+ if (!ok) return false;
282
+ return this.epilogue();
283
+ }
284
+
285
+ /**
286
+ * Runs one node's implementation (or reuses its pin) end to end: resolve
287
+ * config/input, execute, record state + trace, handle failure. Shared by
288
+ * the main cursor and by loop-body execution, which additionally tags the
289
+ * resulting NodeRunState with the current iteration.
290
+ */
291
+ private async executeNode(
292
+ nodeId: string,
293
+ iteration?: { index: number; total: number },
294
+ ): Promise<boolean> {
295
+ const node = this.flow.nodes.find((n) => n.id === nodeId)!;
296
+ const iterationState = iteration ? { iteration } : {};
297
+ // Ordered execution receipt: every execution gets a run-wide sequence
298
+ // number, so the log is proof of exactly what ran, in what order — and
299
+ // that non-loop nodes run once (only loop iterations repeat a label).
300
+ this.execSeq += 1;
301
+ this.emitLog(
302
+ 'debug',
303
+ `#${this.execSeq} ▶ execute${iteration ? ` (iteration ${iteration.index + 1}/${iteration.total})` : ''}`,
304
+ node,
305
+ );
306
+ // Captured up front: the 'running' state set below replaces the node's
307
+ // state object wholesale, which would otherwise wipe out `executions`
308
+ // before the completion branch gets a chance to read it back out.
309
+ const priorExecutions = iteration ? this.run.nodeStates[nodeId]?.executions : undefined;
310
+
311
+ if (Object.prototype.hasOwnProperty.call(this.pins, nodeId)) {
312
+ const output = this.pins[nodeId];
313
+ const at = this.now();
314
+ this.outputs.set(nodeId, output);
315
+ this.captureBranch(nodeId, output);
316
+ const executions = iteration
317
+ ? appendExecution(priorExecutions, { iteration, output, status: 'succeeded' })
318
+ : undefined;
319
+ this.setNodeState(nodeId, {
320
+ status: 'succeeded', output, pinned: true, startedAt: at, completedAt: at, ...iterationState,
321
+ ...(executions ? { executions } : {}),
322
+ });
323
+ this.emitLog('info', `${node.label}: using pinned output`, node);
324
+ return true;
325
+ }
326
+
327
+ const { definition, implementation } = this.registry.get(node.type);
328
+ const startedAt = this.now();
329
+ let input: Record<string, unknown> = {};
330
+ // Hoisted above the try so both the success path and the shared catch
331
+ // below can read the final attempt count. Stays at 1 (so `attempts` is
332
+ // never stamped) unless the retry loop actually runs more than once —
333
+ // config/input-resolution failures above the loop never touch it.
334
+ let attempt = 1;
335
+
336
+ // In a mock run, a mocked node's canned output and an infra-no-mock
337
+ // failure both apply BEFORE config resolution: the node's real
338
+ // implementation never runs either way, so a config value it would
339
+ // otherwise need (e.g. a `$secret` ref with no secret configured) must
340
+ // never fail the node first. Only the inputMap-derived fields (other
341
+ // nodes' outputs — never secrets) are resolved for `state.input` here;
342
+ // config-sourced input fields are skipped (lenient), since resolving them
343
+ // is exactly the thing that could throw.
344
+ const isMocked = this.mockRun && Object.prototype.hasOwnProperty.call(this.mocks, nodeId);
345
+ const isInfraNoMock =
346
+ this.mockRun &&
347
+ !isMocked &&
348
+ (definition.traceKind === 'db' || definition.traceKind === 'http' || definition.traceKind === 'llm');
349
+
350
+ try {
351
+ if (isMocked) {
352
+ // Canned output: short-circuits before config resolution and the
353
+ // retry loop — the implementation is never invoked, so no retry
354
+ // machinery applies and a `$secret` config ref never throws.
355
+ input = this.resolveInputLenient(node);
356
+ this.setNodeState(nodeId, { status: 'running', input, startedAt, ...iterationState });
357
+ const output = this.mocks[nodeId];
358
+ const completedAt = this.now();
359
+ this.outputs.set(nodeId, output);
360
+ this.captureBranch(nodeId, output);
361
+ const executions = iteration
362
+ ? appendExecution(priorExecutions, { iteration, input, output, status: 'succeeded' })
363
+ : undefined;
364
+ this.setNodeState(nodeId, {
365
+ status: 'succeeded', input, output, startedAt, completedAt, mocked: true, ...iterationState,
366
+ ...(executions ? { executions } : {}),
367
+ });
368
+ this.recordSample(node, input, startedAt, completedAt, 'succeeded', output);
369
+ return true;
370
+ }
371
+ if (isInfraNoMock) {
372
+ // Fail-loud infrastructure boundary: thrown before config resolution
373
+ // (a `$secret` ref must not out-race this with "Missing secret"), as
374
+ // a normal implementation error (caught below), so `optional`
375
+ // fail-soft, halting semantics, and the error-workflow path all apply
376
+ // unchanged.
377
+ input = this.resolveInputLenient(node);
378
+ this.setNodeState(nodeId, { status: 'running', input, startedAt, ...iterationState });
379
+ throw new Error(
380
+ `"${node.label}" would touch real infrastructure (${definition.traceKind}) and has no mock — ` +
381
+ `in Mock, infrastructure nodes need a canned output. Add one under this scenario's mocks ` +
382
+ `(Cover with AI writes them), or go Live to run it for real.`,
383
+ );
384
+ }
385
+
386
+ const config = this.resolveConfig(node);
387
+ input = this.resolveInput(node, config);
388
+ this.setNodeState(nodeId, { status: 'running', input, startedAt, ...iterationState });
389
+
390
+ const rawRetry = node.retry;
391
+ const maxTries =
392
+ rawRetry && typeof rawRetry === 'object' && Number.isFinite(rawRetry.maxTries) && rawRetry.maxTries >= 1
393
+ ? rawRetry.maxTries
394
+ : 1;
395
+ const waitMs = rawRetry?.waitMs ?? 0;
396
+ const callImplementation = () =>
397
+ implementation({
398
+ input,
399
+ config,
400
+ secrets: this.secrets,
401
+ vars: this.vars,
402
+ environment: this.environment,
403
+ safeMode: this.safeMode,
404
+ runInput: this.runInput,
405
+ log: (level: LogLevel, message: string) => this.emitLog(level, message, node),
406
+ // Inject the calling node's id so the host can prefix child logs and
407
+ // report cycle context; the node's ctx.runSubflow stays (id, input).
408
+ ...(this.subflowRunner
409
+ ? {
410
+ runSubflow: (workflowId: string, subInput: Record<string, unknown>) =>
411
+ this.subflowRunner!(workflowId, subInput, node.id),
412
+ }
413
+ : {}),
414
+ });
415
+ let output: unknown;
416
+ for (attempt = 1; ; attempt++) {
417
+ try {
418
+ output = await callImplementation();
419
+ break;
420
+ } catch (err) {
421
+ if (attempt >= maxTries) throw err;
422
+ const msg = err instanceof Error ? err.message : String(err);
423
+ this.emitLog('warn', `retry ${attempt}/${maxTries} after error: ${msg}`, node);
424
+ await new Promise((r) => setTimeout(r, waitMs));
425
+ }
426
+ }
427
+ const completedAt = this.now();
428
+ this.outputs.set(nodeId, output);
429
+ this.captureBranch(nodeId, output);
430
+ const executions = iteration
431
+ ? appendExecution(priorExecutions, { iteration, input, output, status: 'succeeded' })
432
+ : undefined;
433
+ const mutationBlocked = this.safeMode && definition.effects === 'mutation';
434
+ this.setNodeState(nodeId, {
435
+ status: 'succeeded', input, output, startedAt, completedAt, ...iterationState,
436
+ ...(mutationBlocked ? { mutationBlocked: true } : {}),
437
+ ...(executions ? { executions } : {}),
438
+ ...(attempt > 1 ? { attempts: attempt } : {}),
439
+ });
440
+ this.recordSample(node, input, startedAt, completedAt, 'succeeded', output);
441
+ return true;
442
+ } catch (err) {
443
+ const completedAt = this.now();
444
+ const rawMessage = err instanceof Error ? err.message : String(err);
445
+ const error =
446
+ err instanceof TypeError ? explainUndefinedRead(rawMessage, input) : rawMessage;
447
+ const executions = iteration
448
+ ? appendExecution(priorExecutions, { iteration, input, error, status: 'failed' })
449
+ : undefined;
450
+ this.setNodeState(nodeId, {
451
+ status: 'failed', input, error, startedAt, completedAt, ...iterationState,
452
+ ...(executions ? { executions } : {}),
453
+ ...(attempt > 1 ? { attempts: attempt } : {}),
454
+ });
455
+ this.recordSample(node, input, startedAt, completedAt, 'failed');
456
+ this.emitLog('error', error, node);
457
+ // Fail-soft: an `optional` node's failure is recorded (state stays
458
+ // 'failed', visible in the runbook) but does not abort the run. It set no
459
+ // output, so its downstream edges are dead (isReachable) and dependent
460
+ // nodes skip, while independent branches keep running. The cursor was
461
+ // already advanced past this node (step()), so returning true resumes at
462
+ // the next node instead of finishing the run.
463
+ if (node.optional) {
464
+ return true;
465
+ }
466
+ this.finish('failed');
467
+ return false;
468
+ }
469
+ }
470
+
471
+ // ── ForEach/Collect loop region execution ───────────────────────────
472
+ //
473
+ // A region is driven across many step() calls, one node-execution each:
474
+ // the ForEach "executes" once per iteration (producing {item, index,
475
+ // total}), then each body node runs once per iteration in region topo
476
+ // order (with branch-skip state reset per iteration), then once all
477
+ // iterations are done the Collect executes exactly once. The main cursor
478
+ // is parked at the position right after the Collect for the duration.
479
+
480
+ /** First step on a reachable ForEach: resolve items, chunk, and start (or short-circuit) the region. */
481
+ private async startLoop(region: LoopRegion): Promise<boolean> {
482
+ const forEachNode = this.flow.nodes.find((n) => n.id === region.forEachId)!;
483
+ const collectNode = this.flow.nodes.find((n) => n.id === region.collectId)!;
484
+ const bodySet = new Set(region.bodyIds);
485
+ const bodyOrder = this.order.filter((id) => bodySet.has(id));
486
+
487
+ const startedAt = this.now();
488
+ let input: Record<string, unknown> = {};
489
+ try {
490
+ const config = this.resolveConfig(forEachNode);
491
+ input = this.resolveInput(forEachNode, config);
492
+ const items = input.items;
493
+ if (!Array.isArray(items)) {
494
+ throw new Error(`ForEach "${forEachNode.label}": "items" input is not an array`);
495
+ }
496
+
497
+ const rawBatchSize = Number(config.batchSize);
498
+ const batchSize = Number.isFinite(rawBatchSize) && rawBatchSize > 0 ? Math.floor(rawBatchSize) : 1;
499
+ let chunks: unknown[] = [];
500
+ for (let i = 0; i < items.length; i += batchSize) {
501
+ chunks.push(batchSize === 1 ? items[i] : items.slice(i, i + batchSize));
502
+ }
503
+
504
+ const rawMax = config.maxIterations;
505
+ const maxIterations =
506
+ rawMax === undefined || rawMax === null || rawMax === '' ? undefined : Number(rawMax);
507
+ if (maxIterations !== undefined && Number.isFinite(maxIterations) && chunks.length > maxIterations) {
508
+ const truncated = chunks.length - maxIterations;
509
+ this.emitLog(
510
+ 'warn',
511
+ `maxIterations ${maxIterations} reached, truncating ${truncated} iterations`,
512
+ forEachNode,
513
+ );
514
+ chunks = chunks.slice(0, maxIterations);
515
+ }
516
+
517
+ this.activeLoop = {
518
+ region, forEachNode, collectNode, bodyOrder,
519
+ items, chunks, iterIndex: 0, bodyCursor: 0, results: [],
520
+ };
521
+
522
+ if (chunks.length === 0) {
523
+ const completedAt = this.now();
524
+ const output = { item: undefined, index: 0, total: 0 };
525
+ this.outputs.set(forEachNode.id, output);
526
+ this.setNodeState(forEachNode.id, {
527
+ status: 'succeeded', input, output, startedAt, completedAt, iteration: { index: 0, total: 0 },
528
+ });
529
+ this.recordSample(forEachNode, output, startedAt, completedAt, 'succeeded', output);
530
+ for (const id of bodyOrder) {
531
+ this.skippedNodes.add(id);
532
+ this.setNodeState(id, { status: 'skipped', iteration: { index: 0, total: 0 } });
533
+ }
534
+ return true;
535
+ }
536
+
537
+ this.beginIteration(this.activeLoop);
538
+ return true;
539
+ } catch (err) {
540
+ const completedAt = this.now();
541
+ const error = err instanceof Error ? err.message : String(err);
542
+ this.setNodeState(forEachNode.id, { status: 'failed', input, error, startedAt, completedAt });
543
+ this.recordSample(forEachNode, input, startedAt, completedAt, 'failed');
544
+ this.emitLog('error', error, forEachNode);
545
+ this.finish('failed');
546
+ return false;
547
+ }
548
+ }
549
+
550
+ /**
551
+ * Sets the ForEach's per-iteration output and resets body branch-skip
552
+ * state. Also clears each body id's prior-iteration output: without this,
553
+ * a body node that a branch skips this iteration would still resolve
554
+ * downstream inputMap reads to whatever it produced last iteration, since
555
+ * `outputs` is otherwise only ever overwritten (never cleared) by execution.
556
+ */
557
+ private beginIteration(loop: ActiveLoopState): void {
558
+ const total = loop.chunks.length;
559
+ const chunk = loop.chunks[loop.iterIndex];
560
+ const output = { item: chunk, index: loop.iterIndex, total };
561
+ const at = this.now();
562
+ const iteration = { index: loop.iterIndex, total };
563
+ this.outputs.set(loop.forEachNode.id, output);
564
+ const executions = appendExecution(
565
+ this.run.nodeStates[loop.forEachNode.id]?.executions,
566
+ { iteration, output, status: 'succeeded' },
567
+ );
568
+ this.setNodeState(loop.forEachNode.id, {
569
+ status: 'succeeded',
570
+ input: { items: loop.items },
571
+ output,
572
+ startedAt: at,
573
+ completedAt: at,
574
+ iteration,
575
+ executions,
576
+ });
577
+ this.recordSample(loop.forEachNode, chunk, at, at, 'succeeded', output);
578
+
579
+ for (const id of loop.bodyOrder) {
580
+ this.skippedNodes.delete(id);
581
+ this.branchTaken.delete(id);
582
+ this.outputs.delete(id);
583
+ }
584
+ loop.bodyCursor = 0;
585
+ }
586
+
587
+ /** One step of an in-progress region: a body node, an iteration advance, or the final Collect. */
588
+ private async stepLoop(): Promise<boolean> {
589
+ const loop = this.activeLoop!;
590
+
591
+ if (loop.chunks.length === 0) {
592
+ this.finishLoop(loop);
593
+ return this.epilogue();
594
+ }
595
+
596
+ while (
597
+ loop.bodyCursor < loop.bodyOrder.length &&
598
+ !this.isReachable(loop.bodyOrder[loop.bodyCursor])
599
+ ) {
600
+ const deadId = loop.bodyOrder[loop.bodyCursor];
601
+ loop.bodyCursor += 1;
602
+ this.skippedNodes.add(deadId);
603
+ // A skip is a status, not an execution: no ExecutionRecord is appended
604
+ // for this iteration. But setNodeState replaces the state object
605
+ // wholesale, so any executions accumulated in earlier iterations (this
606
+ // branch may have been taken before, or will be again) must be carried
607
+ // forward explicitly or they'd be lost.
608
+ const priorExecutions = this.run.nodeStates[deadId]?.executions;
609
+ this.setNodeState(deadId, {
610
+ status: 'skipped',
611
+ iteration: { index: loop.iterIndex, total: loop.chunks.length },
612
+ ...(priorExecutions ? { executions: priorExecutions } : {}),
613
+ });
614
+ }
615
+
616
+ if (loop.bodyCursor < loop.bodyOrder.length) {
617
+ const nodeId = loop.bodyOrder[loop.bodyCursor];
618
+ loop.bodyCursor += 1;
619
+ const ok = await this.executeNode(nodeId, { index: loop.iterIndex, total: loop.chunks.length });
620
+ return ok;
621
+ }
622
+
623
+ // This iteration's body is exhausted: gather Collect's mapped value and advance.
624
+ loop.results.push(this.resolveCollectValue(loop.collectNode));
625
+ loop.iterIndex += 1;
626
+
627
+ if (loop.iterIndex < loop.chunks.length) {
628
+ this.beginIteration(loop);
629
+ return true;
630
+ }
631
+
632
+ this.finishLoop(loop);
633
+ return this.epilogue();
634
+ }
635
+
636
+ /** Resolves Collect's `value` input against the current iteration's body outputs. */
637
+ private resolveCollectValue(collectNode: WorkflowNode): unknown {
638
+ const config = this.resolveConfig(collectNode);
639
+ const input = this.resolveInput(collectNode, config);
640
+ return input.value;
641
+ }
642
+
643
+ /**
644
+ * Executes Collect once with the accumulated results and hands the region's
645
+ * nodes to `loopExecuted` so the main cursor consumes them silently — it
646
+ * must NOT jump past the Collect, because topo order can interleave
647
+ * parallel non-region nodes between the ForEach and the Collect.
648
+ */
649
+ private finishLoop(loop: ActiveLoopState): void {
650
+ const at = this.now();
651
+ const output = { items: loop.results, count: loop.results.length };
652
+ const lastValue = loop.results.length > 0 ? loop.results[loop.results.length - 1] : undefined;
653
+ this.outputs.set(loop.collectNode.id, output);
654
+ this.setNodeState(loop.collectNode.id, {
655
+ status: 'succeeded', input: { value: lastValue }, output, startedAt: at, completedAt: at,
656
+ });
657
+ this.recordSample(loop.collectNode, { value: lastValue }, at, at, 'succeeded', output);
658
+ for (const id of [...loop.region.bodyIds, loop.region.collectId]) {
659
+ this.loopExecuted.add(id);
660
+ }
661
+ this.activeLoop = undefined;
662
+ }
663
+
664
+ /** Remember a node's taken branch when its output carries `$branch`. */
665
+ private captureBranch(nodeId: string, output: unknown): void {
666
+ if (output !== null && typeof output === 'object') {
667
+ const branch = (output as Record<string, unknown>).$branch;
668
+ if (typeof branch === 'string') this.branchTaken.set(nodeId, branch);
669
+ }
670
+ }
671
+
672
+ /**
673
+ * A node is reachable if it has no incoming edges, or at least one incoming
674
+ * edge from a non-skipped, executed source whose branch (if the edge names
675
+ * one via sourceHandle) was taken.
676
+ */
677
+ private isReachable(nodeId: string): boolean {
678
+ const incoming = this.flow.edges.filter((e) => e.target === nodeId);
679
+ if (incoming.length === 0) return true;
680
+ const live = (edge: (typeof incoming)[number]): boolean => {
681
+ if (this.skippedNodes.has(edge.source)) return false;
682
+ if (!this.outputs.has(edge.source)) return false;
683
+ if (!edge.sourceHandle) return true;
684
+ return this.branchTaken.get(edge.source) === edge.sourceHandle;
685
+ };
686
+ // Branch edges are guards: when a node sits on named branches, only those
687
+ // edges decide reachability — a plain data edge from an always-executed
688
+ // ancestor must not smuggle the node past a dead gate. Nodes with no
689
+ // branch edges keep join semantics (any live data edge suffices).
690
+ const branchEdges = incoming.filter((e) => e.sourceHandle);
691
+ if (branchEdges.length > 0) return branchEdges.some(live);
692
+ return incoming.some(live);
693
+ }
694
+
695
+ private epilogue(): boolean {
696
+ if (this.cursor >= this.order.length) {
697
+ this.finish('succeeded');
698
+ return false;
699
+ }
700
+ return true;
701
+ }
702
+
703
+ async runToEnd(): Promise<WorkflowRun> {
704
+ while (await this.step()) {
705
+ // step until done
706
+ }
707
+ return this.run;
708
+ }
709
+
710
+ cancel(): void {
711
+ if (this.run.status !== 'running') return;
712
+ this.finish('cancelled');
713
+ }
714
+
715
+ /**
716
+ * Resolve {"$secret": NAME} and {"$env": NAME} config values; a missing
717
+ * secret/var fails the node. `$secret` is only recognized at a config
718
+ * value's top level (matching today's behavior — secrets are never nested
719
+ * inside larger config structures). `$env` is resolved deeply, the same
720
+ * walk used for the run-input payload, so refs nested in objects/arrays
721
+ * (e.g. an Input node's `config.defaults`) resolve too.
722
+ */
723
+ private resolveConfig(node: WorkflowNode): Record<string, unknown> {
724
+ const config: Record<string, unknown> = {};
725
+ for (const [key, value] of Object.entries(node.config)) {
726
+ if (
727
+ value !== null &&
728
+ typeof value === 'object' &&
729
+ !Array.isArray(value) &&
730
+ typeof (value as Record<string, unknown>).$secret === 'string'
731
+ ) {
732
+ const name = (value as { $secret: string }).$secret;
733
+ if (!(name in this.secrets)) throw new Error(`Missing secret: ${name}`);
734
+ config[key] = this.secrets[name];
735
+ } else {
736
+ config[key] = resolveEnvDeep(value, this.vars);
737
+ }
738
+ }
739
+ return config;
740
+ }
741
+
742
+ private resolveInput(
743
+ node: WorkflowNode,
744
+ config: Record<string, unknown>,
745
+ ): Record<string, unknown> {
746
+ const input: Record<string, unknown> = {};
747
+ const inputSchema = this.registry.get(node.type).definition.inputSchema;
748
+ for (const field of inputSchema?.fields ?? []) {
749
+ if (config[field.name] !== undefined) input[field.name] = config[field.name];
750
+ }
751
+ for (const [field, mapping] of Object.entries(node.inputMap ?? {})) {
752
+ input[field] = getByPath(this.outputs.get(mapping.sourceNodeId), mapping.sourceField);
753
+ }
754
+ return input;
755
+ }
756
+
757
+ /**
758
+ * A lenient variant of `resolveInput` for nodes whose implementation never
759
+ * runs (mocked, or an infra node with no mock, in a mock run): resolves
760
+ * only `inputMap`-sourced fields (other nodes' already-computed outputs,
761
+ * which can never carry a `$secret`/`$env` ref). Skips the config-sourced
762
+ * fields `resolveInput` also merges in — those come from `resolveConfig`,
763
+ * which this path deliberately never calls, since that is exactly what
764
+ * could throw (e.g. `Missing secret`) for a node this run will never
765
+ * actually execute.
766
+ */
767
+ private resolveInputLenient(node: WorkflowNode): Record<string, unknown> {
768
+ const input: Record<string, unknown> = {};
769
+ for (const [field, mapping] of Object.entries(node.inputMap ?? {})) {
770
+ input[field] = getByPath(this.outputs.get(mapping.sourceNodeId), mapping.sourceField);
771
+ }
772
+ return input;
773
+ }
774
+
775
+ private finish(status: WorkflowRun['status']): void {
776
+ // Only nodes that never got a chance to run are swept to 'skipped' here.
777
+ // While a loop region is active, `this.cursor` stays parked at the
778
+ // region's entry for the whole loop lifetime (body/collect execution is
779
+ // driven by the loop's own bodyCursor, not the main cursor), so this
780
+ // range can include body nodes that already reached a terminal state in
781
+ // an earlier iteration — or, on failure, the very node whose 'failed'
782
+ // state we must not clobber back to 'skipped'.
783
+ for (; this.cursor < this.order.length; this.cursor += 1) {
784
+ const id = this.order[this.cursor];
785
+ if (this.run.nodeStates[id]?.status === 'queued') {
786
+ this.setNodeState(id, { status: 'skipped' });
787
+ }
788
+ }
789
+ this.run.status = status;
790
+ this.run.completedAt = this.now();
791
+ this.events.onRunFinished?.(this.run);
792
+ }
793
+
794
+ private setNodeState(nodeId: string, state: NodeRunState): void {
795
+ this.run.nodeStates[nodeId] = state;
796
+ this.events.onNodeStateChange?.(nodeId, state);
797
+ }
798
+
799
+ private emitLog(level: LogLevel, message: string, node: WorkflowNode): void {
800
+ this.events.onLog?.({
801
+ timestamp: this.now(),
802
+ level,
803
+ runId: this.run.id,
804
+ nodeId: node.id,
805
+ nodeLabel: node.label,
806
+ message,
807
+ });
808
+ }
809
+
810
+ private recordSample(
811
+ node: WorkflowNode,
812
+ input: unknown,
813
+ startedAt: string,
814
+ completedAt: string,
815
+ status: 'succeeded' | 'failed',
816
+ output?: unknown,
817
+ ): void {
818
+ this.trace?.record({
819
+ id: this.newId(),
820
+ workflowId: this.flow.id,
821
+ runId: this.run.id,
822
+ nodeId: node.id,
823
+ nodeType: node.type,
824
+ nodeLabel: node.label,
825
+ input,
826
+ output,
827
+ status,
828
+ startedAt,
829
+ completedAt,
830
+ });
831
+ }
832
+ }
833
+
834
+ export function startRun(opts: StartRunOptions): FlowRun {
835
+ const errors = validateFlow(opts.flow, opts.registry).filter((i) => i.severity === 'error');
836
+ if (errors.length > 0) {
837
+ throw new Error(`Invalid flow: ${errors.map((i) => i.message).join('; ')}`);
838
+ }
839
+ return new FlowRun(opts);
840
+ }