@desplega.ai/agent-swarm 2.4.5 → 2.5.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 (272) hide show
  1. package/README.md +4 -2
  2. package/dist/{acp-adapter-2wdkktv6.js → acp-adapter-11gp67nr.js} +8 -6
  3. package/dist/{actions-c40ebcm4.js → actions-8p2kze8j.js} +23 -21
  4. package/dist/{amp-adapter-wb67m95z.js → amp-adapter-z4qvhacj.js} +2 -2
  5. package/dist/{anthropic-messages-g6pqmqk4.js → anthropic-messages-txs190dd.js} +17 -20
  6. package/dist/{app-353vre8g.js → app-0qp0a2gk.js} +3 -3
  7. package/dist/{assistant-sj1njh23.js → assistant-jedd3ktm.js} +24 -22
  8. package/dist/{azure-openai-responses-9dbmj346.js → azure-openai-responses-9kfwhmn7.js} +18 -13
  9. package/dist/{boot-reembed-qdkvxeem.js → boot-reembed-4xwq5tg0.js} +1 -1
  10. package/dist/{boot-reembed-b9c3az3c.js → boot-reembed-n4wq5dd2.js} +1 -1
  11. package/dist/{boot-scrub-sweep-m3wh8eks.js → boot-scrub-sweep-69h90xcq.js} +4 -4
  12. package/dist/{boot-seeds-j2273s54.js → boot-seeds-3vn0m9sg.js} +4 -4
  13. package/dist/{claude-adapter-m7q0a085.js → claude-adapter-221mvad5.js} +8 -6
  14. package/dist/{claude-managed-adapter-5ptb6tzs.js → claude-managed-adapter-gt6pa35d.js} +2 -2
  15. package/dist/{claude-managed-setup-smrqk5j5.js → claude-managed-setup-c3hp36ph.js} +4 -4
  16. package/dist/{claude-sdk-session-38xnz21e.js → claude-sdk-session-8thjsk5e.js} +9012 -8928
  17. package/dist/{cli-b9fcxxev.js → cli-007cnhm1.js} +50 -4
  18. package/dist/{cli-j4rqdf2n.js → cli-03vpq45j.js} +2 -2
  19. package/dist/{cli-dk44dxec.js → cli-0y6f0ckq.js} +1 -1
  20. package/dist/{cli-kpw04va0.js → cli-0z3vjrpm.js} +1 -1
  21. package/dist/{cli-94gwyg7m.js → cli-179xcyky.js} +9 -9
  22. package/dist/{cli-3r8samv3.js → cli-1f0gp9vz.js} +3 -2
  23. package/dist/{cli-4zfqgjc5.js → cli-1pr672hz.js} +2 -2
  24. package/dist/{cli-0bqteszy.js → cli-2nf3b08t.js} +328 -25
  25. package/dist/{cli-43x64hv3.js → cli-36j4jnk3.js} +4 -4
  26. package/dist/{cli-k8g16gz3.js → cli-3qv8naq8.js} +115 -119
  27. package/dist/cli-3zwye9sn.js +1193 -0
  28. package/dist/{cli-60cgh1rv.js → cli-459fk97x.js} +7 -7
  29. package/dist/{cli-v8fjakwf.js → cli-4w24f7z3.js} +1 -1
  30. package/dist/{cli-n72rg53p.js → cli-5kzk3472.js} +812 -418
  31. package/dist/{cli-h7gc0b4r.js → cli-5m84zd2k.js} +55 -18
  32. package/dist/{cli-j99azg3g.js → cli-5rvg1ddk.js} +5 -5
  33. package/dist/{cli-dvkmp9bt.js → cli-6ctwrf1z.js} +20 -10
  34. package/dist/{cli-848hxdwj.js → cli-6rg2jn2y.js} +2 -1
  35. package/dist/{cli-fv5hepyy.js → cli-7bwtc6yx.js} +3 -3
  36. package/dist/{cli-ms85309d.js → cli-7g40g8hq.js} +11 -11
  37. package/dist/{cli-xpa64rek.js → cli-92mqenkc.js} +2362 -159
  38. package/dist/{cli-018r4jq6.js → cli-9ce2xw98.js} +8 -8
  39. package/dist/{cli-1syn2xy1.js → cli-bgdtv62r.js} +32 -18
  40. package/dist/{cli-wxbn3yfx.js → cli-cz5nmdxt.js} +2 -2
  41. package/dist/{cli-wt645v7q.js → cli-dabehxhp.js} +1 -1
  42. package/dist/{cli-vfha21c9.js → cli-e5bxnjps.js} +2 -2
  43. package/dist/{cli-wt0jf9hm.js → cli-fcc5qe3f.js} +553 -193
  44. package/dist/{cli-ftc1g5tj.js → cli-fes51ya2.js} +3 -3
  45. package/dist/{cli-3a45zcgm.js → cli-ftmq4nvk.js} +2 -2
  46. package/dist/{cli-37j04k05.js → cli-fy1jyyym.js} +13 -13
  47. package/dist/{cli-s95dknxh.js → cli-g1cw7xtt.js} +1 -1
  48. package/dist/{cli-74c1wpm0.js → cli-gpv60278.js} +1 -1
  49. package/dist/{cli-pemywhfq.js → cli-ha9rmwgq.js} +2 -1
  50. package/dist/{cli-g35m2xwm.js → cli-j1eghqfm.js} +2 -2
  51. package/dist/{cli-qfxc84gh.js → cli-jpxsrxcb.js} +1 -1
  52. package/dist/{cli-ejs3m1sx.js → cli-kxpfx2xr.js} +1 -1
  53. package/dist/{cli-p14m2cnp.js → cli-mbfj11fq.js} +1 -1
  54. package/dist/{cli-pnp2j5zg.js → cli-mmhj8vd7.js} +2 -2
  55. package/dist/{cli-xf234ws8.js → cli-n4c8e3s5.js} +2 -0
  56. package/dist/{cli-f01h25yt.js → cli-n6kmqngw.js} +2 -2
  57. package/dist/{cli-v0yrfmdh.js → cli-nnvjb17s.js} +3 -3
  58. package/dist/{cli-1dmcq2eh.js → cli-nzfnt2gs.js} +1 -1
  59. package/dist/{cli-625tb7aq.js → cli-p859hcc8.js} +2 -2
  60. package/dist/{cli-smvcw9z5.js → cli-pasyvjmm.js} +1 -1
  61. package/dist/cli-pkaqk57w.js +128 -0
  62. package/dist/{cli-8nyhf7za.js → cli-qqzmjxkf.js} +129 -105
  63. package/dist/{cli-9yqgm9nd.js → cli-r25qpaxt.js} +161 -63
  64. package/dist/{cli-eww120g8.js → cli-rh5smt8t.js} +18 -4
  65. package/dist/{cli-qwkcwjdn.js → cli-rycdaj0a.js} +1 -1
  66. package/dist/{cli-a92r1127.js → cli-s4sntwt4.js} +2 -2
  67. package/dist/{cli-be2161h1.js → cli-segs7hx9.js} +6 -6
  68. package/dist/{cli-36ckw0wx.js → cli-stxdrqs2.js} +2 -2
  69. package/dist/{cli-z7d29rw9.js → cli-sy8f9705.js} +4 -4
  70. package/dist/{cli-1m17dc9d.js → cli-ta8bd7th.js} +2 -2
  71. package/dist/{cli-st8aw1m6.js → cli-we0ra057.js} +4 -4
  72. package/dist/{cli-7c9qs530.js → cli-x21dqn18.js} +5 -5
  73. package/dist/{cli-1sp861s4.js → cli-x6vf4sad.js} +11 -11
  74. package/dist/{cli-d2mb5s8a.js → cli-x77gen2p.js} +680 -440
  75. package/dist/{cli-tw4p90jb.js → cli-xawhpgc9.js} +5 -5
  76. package/dist/{cli-f0g3pgvf.js → cli-xd4eqtr2.js} +2 -2
  77. package/dist/{cli-1f3qzw6y.js → cli-xt768r5m.js} +2 -2
  78. package/dist/{cli-cxxe61f6.js → cli-y4x8ww6s.js} +2 -2
  79. package/dist/{cli-bnes4z6b.js → cli-y9jvxfhk.js} +3 -3
  80. package/dist/{cli-qwhy2qma.js → cli-z63a269p.js} +2 -2
  81. package/dist/{cli-sm8rses9.js → cli-zfdp01hn.js} +8 -5
  82. package/dist/cli.js +29 -27
  83. package/dist/{cloudflare-workers-ai-system-one-ch2wxfcp.js → cloudflare-workers-ai-system-one-43v9qkjk.js} +7 -2
  84. package/dist/{codex-adapter-tn6k5mkg.js → codex-adapter-tcg1q3jk.js} +6 -4
  85. package/dist/{codex-login-m819c41r.js → codex-login-rn7c5gxt.js} +1 -1
  86. package/dist/{codex-session-runner-7vjt7jbw.js → codex-session-runner-xfx4wd9x.js} +7 -5
  87. package/dist/{commands-t7rz9146.js → commands-ew83nj70.js} +2 -2
  88. package/dist/{controller-m2jasma5.js → controller-gnq1k1mf.js} +4 -4
  89. package/dist/{controllers-connect-9ftpex99.js → controllers-connect-tw1dan9r.js} +25 -23
  90. package/dist/{cursor-adapter-mjv79e4z.js → cursor-adapter-yq5xgfcg.js} +1 -1
  91. package/dist/{db-6sfrn9g3.js → db-7gbc53fd.js} +14 -2
  92. package/dist/{db-rn673r06.js → db-t86xrnes.js} +3 -3
  93. package/dist/{devin-adapter-zgqadvnm.js → devin-adapter-4ycy4vrf.js} +1 -1
  94. package/dist/{dispatcher-c155xz0p.js → dispatcher-64bdyhps.js} +22 -20
  95. package/dist/{docker-fkgtr75t.js → docker-nfy2gc8t.js} +2 -2
  96. package/dist/{dsh-adapter-3mmszc1x.js → dsh-adapter-tvhc4k2d.js} +1 -1
  97. package/dist/{e2b-16d6ch39.js → e2b-9gkdhm3c.js} +2 -2
  98. package/dist/{e2b-zn0b4w5w.js → e2b-cjtf8k1z.js} +2 -2
  99. package/dist/{easter-egg-3d-5cgynaja.js → easter-egg-3d-e4qv8dxj.js} +1 -1
  100. package/dist/{event-bus-9vpk8gmq.js → event-bus-3z78ep5n.js} +1 -1
  101. package/dist/{execute-t4e6m3rw.js → execute-0xpz8950.js} +85 -23
  102. package/dist/{external-channel-35k6tkv8.js → external-channel-e9nw24zv.js} +2 -2
  103. package/dist/{google-generative-ai-bmq8gkf3.js → google-generative-ai-mk9vcpe1.js} +15 -15
  104. package/dist/{google-vertex-b1rvp52s.js → google-vertex-q580kz8k.js} +20 -20
  105. package/dist/{grok-adapter-tfrq7tg1.js → grok-adapter-v82srhrx.js} +9 -7
  106. package/dist/handlers-c9ajzhbn.js +41 -0
  107. package/dist/{heartbeat-dyg4npkp.js → heartbeat-2vqs1319.js} +25 -23
  108. package/dist/{hook-x26b31bg.js → hook-01nk13wx.js} +7 -5
  109. package/dist/{hook-drspbwpe.js → hook-jzpheegg.js} +9 -7
  110. package/dist/{http-pk972xyw.js → http-fc1k6ec8.js} +544 -76
  111. package/dist/index-07ef7nkm.js +42 -0
  112. package/dist/{index-g6tbb6ve.js → index-31ceyf42.js} +1 -1
  113. package/dist/index-7yts8n9s.js +43 -0
  114. package/dist/index-q3t40ryv.js +46 -0
  115. package/dist/{index-arytgj5q.js → index-sepja052.js} +1 -1
  116. package/dist/{index-jwxkkex2.js → index-zdycv3ja.js} +13 -11
  117. package/dist/{index-g216q6jr.js → index-zsyx51ap.js} +1 -4
  118. package/dist/{keepalive-7yrng7px.js → keepalive-5nh2rhmr.js} +4 -4
  119. package/dist/lead-aayq64jn.js +79 -0
  120. package/dist/{llama-cpp-classify-vv43gswy.js → llama-cpp-classify-ee7vmd7q.js} +4 -1
  121. package/dist/{maintenance-e6cemdqt.js → maintenance-hjhzdt3k.js} +1 -1
  122. package/dist/{mistral-conversations-1medfwv7.js → mistral-conversations-sd0xf1tn.js} +13 -13
  123. package/dist/{node-de6akczr.js → node-jzjj7r9x.js} +1 -1
  124. package/dist/{oauth-refresh-sweep-yw3xdbcj.js → oauth-refresh-sweep-y3n1knmd.js} +3 -3
  125. package/dist/{onboard-0a9y5yrc.js → onboard-1zq3m7db.js} +2 -2
  126. package/dist/{openai-codex-responses-de9pvs4d.js → openai-codex-responses-79s8xnbh.js} +29 -23
  127. package/dist/{openai-completions-mtqge3nf.js → openai-completions-tdbyts0x.js} +21 -20
  128. package/dist/{openai-decisions-ygk4kkxd.js → openai-decisions-bqhnjyfz.js} +2 -1
  129. package/dist/{openai-responses-dkyng71g.js → openai-responses-dm3t5vqp.js} +26 -17
  130. package/dist/{opencode-adapter-x4tqzhkh.js → opencode-adapter-e56086m1.js} +7 -5
  131. package/dist/{openrouter-images-wvv7camj.js → openrouter-images-ssttr1y4.js} +6 -3
  132. package/dist/{otel-impl-trjpkfb7.js → otel-impl-h9yyza47.js} +2 -2
  133. package/dist/{pi-messages-nhh1pkz2.js → pi-messages-3nf8bq1a.js} +6 -4
  134. package/dist/{pi-mono-adapter-mcfkn9vf.js → pi-mono-adapter-tzps8f38.js} +9 -37
  135. package/dist/{pricing-refresh-d2b5my0y.js → pricing-refresh-nevhat8h.js} +3 -3
  136. package/dist/{rbac-roles-nxh5ahwk.js → rbac-roles-4y6ngvzq.js} +1 -1
  137. package/dist/{rbac-roles-hey3yv9d.js → rbac-roles-exbg8xb6.js} +2 -2
  138. package/dist/{render-v2-rr3p2atg.js → render-v2-cfsh13z3.js} +3 -3
  139. package/dist/{resume-2efkf1s1.js → resume-5s2t0dbs.js} +10 -8
  140. package/dist/{runtime-xe3aqy4x.js → runtime-bt5pkpm1.js} +1 -1
  141. package/dist/{session-access-s2mttxpw.js → session-access-dnbyp3cn.js} +6 -4
  142. package/dist/{setup-vypma5yt.js → setup-1wj1y77j.js} +1 -1
  143. package/dist/{typesafe-system-one-2hgpayzy.js → typesafe-system-one-bre38hvs.js} +4 -2
  144. package/dist/{virtual-modules-4r3zkc20.js → virtual-modules-mnfbh5mr.js} +8 -2
  145. package/dist/{work-objects-rvhckdkp.js → work-objects-7mg6216g.js} +25 -23
  146. package/dist/worker-nwb789mg.js +79 -0
  147. package/dist/{x-y45mthwn.js → x-pbpezr0s.js} +2 -2
  148. package/openapi.json +785 -5
  149. package/package.json +9 -9
  150. package/src/assets/key.ts +17 -0
  151. package/src/azure-devops/handlers.ts +1 -1
  152. package/src/be/db/approvals.ts +18 -0
  153. package/src/be/db/session-moves.ts +164 -0
  154. package/src/be/db/session-tree.ts +384 -0
  155. package/src/be/db/tasks/read.ts +40 -4
  156. package/src/be/db/workflow-runs.ts +96 -0
  157. package/src/be/db.ts +80 -236
  158. package/src/be/onboarding.ts +3 -0
  159. package/src/be/skill-sync.ts +43 -2
  160. package/src/be/swarm-config-guard.ts +1 -0
  161. package/src/bitbucket/api.ts +81 -0
  162. package/src/bitbucket/auth.ts +64 -0
  163. package/src/bitbucket/handlers.ts +434 -0
  164. package/src/bitbucket/index.ts +28 -0
  165. package/src/bitbucket/templates.ts +134 -0
  166. package/src/bitbucket/types.ts +65 -0
  167. package/src/commands/runner.ts +55 -3
  168. package/src/e2b/env.ts +1 -0
  169. package/src/extensions/contract-types.generated.ts +1 -1
  170. package/src/extensions/contract.ts +2 -2
  171. package/src/extensions/post-bridge.ts +1 -0
  172. package/src/github/handlers.ts +6 -6
  173. package/src/gitlab/handlers.ts +5 -5
  174. package/src/http/assets.ts +14 -2
  175. package/src/http/core.ts +12 -0
  176. package/src/http/index.ts +4 -0
  177. package/src/http/onboarding.ts +1 -0
  178. package/src/http/sessions.ts +366 -12
  179. package/src/http/skills.ts +48 -1
  180. package/src/http/tasks.ts +1 -1
  181. package/src/http/users.ts +1 -0
  182. package/src/http/webhooks.ts +141 -0
  183. package/src/integrations/chat-sdk/adapters.ts +19 -3
  184. package/src/integrations/chat-sdk/facts.ts +30 -12
  185. package/src/integrations/chat-sdk/outbound.ts +38 -1
  186. package/src/providers/acp-targets.ts +1 -1
  187. package/src/providers/amp-adapter.ts +1 -1
  188. package/src/providers/pi-mono-adapter.ts +5 -53
  189. package/src/scripts-runtime/types/swarm-extension.d.ts +6 -4
  190. package/src/slack/handlers.test.ts +158 -0
  191. package/src/slack/handlers.ts +19 -28
  192. package/src/tasks/context-key.ts +61 -1
  193. package/src/tests/bitbucket-auth.test.ts +156 -0
  194. package/src/tests/bitbucket-handlers.test.ts +425 -0
  195. package/src/tests/bitbucket-webhook.test.ts +220 -0
  196. package/src/tests/chat-sdk-inbound.test.ts +39 -0
  197. package/src/tests/chat-sdk-telegram-markdown.test.ts +92 -0
  198. package/src/tests/context-key.test.ts +50 -0
  199. package/src/tests/core-auth.test.ts +5 -0
  200. package/src/tests/db-task-read-contract.test.ts +7 -0
  201. package/src/tests/db-task-write-contract.test.ts +6 -0
  202. package/src/tests/docs-index-footprint.test.js +3 -3
  203. package/src/tests/fixtures/bitbucket/pullrequest-comment-created-inline.json +174 -0
  204. package/src/tests/fixtures/bitbucket/pullrequest-comment-created.json +170 -0
  205. package/src/tests/fixtures/bitbucket/pullrequest-created-mention.json +132 -0
  206. package/src/tests/fixtures/bitbucket/pullrequest-created-reviewer.json +140 -0
  207. package/src/tests/fixtures/bitbucket/pullrequest-fulfilled.json +134 -0
  208. package/src/tests/fixtures/bitbucket/pullrequest-rejected.json +133 -0
  209. package/src/tests/generate-mcp-docs.test.ts +101 -0
  210. package/src/tests/gitlab-handlers.test.ts +3 -3
  211. package/src/tests/gitlab-vcs-db.test.ts +4 -4
  212. package/src/tests/integration-disable-flags.test.ts +36 -0
  213. package/src/tests/mcp-tools.test.ts +1 -1
  214. package/src/tests/onboarding.test.ts +12 -0
  215. package/src/tests/pi-codemode.test.ts +8 -2
  216. package/src/tests/preload-env.test.ts +57 -0
  217. package/src/tests/preload.ts +21 -0
  218. package/src/tests/prompt-template-remaining.test.ts +38 -0
  219. package/src/tests/run-root-coverage.test.ts +39 -0
  220. package/src/tests/runner-bitbucket-vcs.test.ts +191 -0
  221. package/src/tests/runner-skills-refresh.test.ts +58 -0
  222. package/src/tests/secret-scrubber.test.ts +15 -0
  223. package/src/tests/sessions-move.test.ts +576 -0
  224. package/src/tests/sessions-tree.test.ts +493 -0
  225. package/src/tests/skill-files-http.test.ts +50 -0
  226. package/src/tests/skills-bundle.test.ts +99 -0
  227. package/src/tests/swarm-config-export-to-env.test.ts +2 -1
  228. package/src/tests/vcs-bitbucket.test.ts +91 -0
  229. package/src/tests/vcs-provider-isolation.test.ts +123 -0
  230. package/src/tests/vcs-provider.test.ts +16 -1
  231. package/src/tests/vcs-tracking.test.ts +3 -3
  232. package/src/tests/workflow-foreach-bodies.test.ts +637 -0
  233. package/src/tests/workflow-foreach-concurrency.test.ts +342 -0
  234. package/src/tests/workflow-foreach-retry.test.ts +537 -0
  235. package/src/tests/workflow-foreach.test.ts +44 -23
  236. package/src/tests/workflow-step-claims.test.ts +6 -6
  237. package/src/tests/workflow-tla-races.test.ts +2 -2
  238. package/src/tools/script-delete.ts +1 -1
  239. package/src/tools/workflows/create-workflow.ts +5 -3
  240. package/src/types.ts +3 -2
  241. package/src/utils/config-env-export.ts +1 -0
  242. package/src/utils/secret-scrubber.ts +4 -0
  243. package/src/utils/skills-refresh.ts +83 -43
  244. package/src/vcs/bitbucket.ts +58 -0
  245. package/src/vcs/index.ts +2 -0
  246. package/src/vcs/types.ts +2 -2
  247. package/src/workflows/approval-resolution.ts +4 -1
  248. package/src/workflows/checkpoint.ts +9 -1
  249. package/src/workflows/constants.ts +3 -0
  250. package/src/workflows/definition.ts +48 -17
  251. package/src/workflows/engine.ts +25 -15
  252. package/src/workflows/executors/foreach.ts +45 -99
  253. package/src/workflows/foreach-body.ts +146 -0
  254. package/src/workflows/foreach-driver.ts +552 -0
  255. package/src/workflows/foreach-join.ts +206 -7
  256. package/src/workflows/node-timeout.ts +21 -0
  257. package/src/workflows/recovery.ts +68 -3
  258. package/src/workflows/resume.ts +221 -43
  259. package/src/workflows/retry-poller.ts +39 -24
  260. package/src/workflows/step-input.ts +59 -0
  261. package/src/workflows/task-step-routing.ts +114 -56
  262. package/templates/skills/slack-interaction/content.md +5 -0
  263. package/templates/skills/work-on-task/SKILL.md +1 -1
  264. package/templates/skills/work-on-task/content.md +1 -1
  265. package/dist/cli-9hstj09h.js +0 -2657
  266. package/dist/cli-yw91rnmw.js +0 -13
  267. package/dist/handlers-7gw64dxw.js +0 -39
  268. package/dist/index-bgdyvcnv.js +0 -40
  269. package/dist/index-gabbcdn2.js +0 -43
  270. package/dist/index-tfb26zhy.js +0 -41
  271. package/dist/lead-7e02z839.js +0 -77
  272. package/dist/worker-5snytpbd.js +0 -77
@@ -213,7 +213,7 @@ describe("TLA+ workflow counterexamples", () => {
213
213
  const stepB = await untilStatus(runId, "B", "waiting");
214
214
 
215
215
  // EF: B's task fails -> run failed.
216
- await failStepAndRunIfWaiting(stepB!.id, runId, "task failed");
216
+ await failStepAndRunIfWaiting(def, stepB!.id, runId, "task failed");
217
217
  expect((await getWorkflowRun(runId))?.status).toBe("failed");
218
218
 
219
219
  // U1/U2: the user retries; findReadyNodes returns A because it is not completed.
@@ -240,7 +240,7 @@ describe("TLA+ workflow counterexamples", () => {
240
240
 
241
241
  // EF: B's task fails -> run failed. XCkWait(A): the checkpoint CAS sees a
242
242
  // failed run, so A's row stays `running` with its task out.
243
- await failStepAndRunIfWaiting(stepB!.id, runId, "task failed");
243
+ await failStepAndRunIfWaiting(def, stepB!.id, runId, "task failed");
244
244
  holdA.release();
245
245
  await initial;
246
246
  expect((await stepsOf(runId, "A"))[0]?.status).toBe("running");
@@ -9,7 +9,7 @@ import {
9
9
  } from "./script-common";
10
10
 
11
11
  export const SCRIPT_DELETE_DESCRIPTION =
12
- "Remove a swarm-shared script from the catalog. Versions table preserves history.";
12
+ "Permanently remove a swarm-shared script and all its versions from the catalog. This cannot be undone.";
13
13
 
14
14
  export const registerScriptDeleteTool = (server: McpServer) => {
15
15
  createToolRegistrar(server)(
@@ -40,11 +40,13 @@ export const registerCreateWorkflowTool = (server: McpServer) => {
40
40
  "- STRUCTURED OUTPUT: For agent-task nodes, put outputSchema inside 'config' to validate the agent's raw JSON output. " +
41
41
  "Node-level outputSchema validates the executor's return ({taskId, taskOutput}), which is different.\n" +
42
42
  "- Agent-task config: { template, outputSchema?, agentId?, tags?, priority?, dir?, vcsRepo?, model? }.\n" +
43
- "- FOREACH NODE: type 'foreach' fans out one agent-task per item. Config: " +
44
- "{ over: <array or exact {{input}} token>, itemKey: <property name>, body: { type: 'agent-task', config: {...} } }. " +
43
+ "- FOREACH NODE: type 'foreach' runs its body once per item. Config: " +
44
+ "{ over: <array or exact {{input}} token>, itemKey: <property name>, body: { type: <any executor except foreach>, config: {...} }, " +
45
+ "concurrency?: <integer >= 1, items in flight; unbounded when absent>, failurePolicy?: 'fail-fast' | 'collect-outcomes' }. " +
45
46
  "The body config is interpolated once per item with {{item.*}} and {{index}}. Child steps use synthetic IDs " +
46
47
  "'<foreachNodeId>#<itemKey>'; the parent waits for every child and exposes one aggregate result to successors. " +
47
- "concurrency is not supported in v1; use definition-level onNodeFailure: 'continue' to aggregate failed children.\n" +
48
+ "failurePolicy defaults from onNodeFailure ('continue' = collect-outcomes, else fail-fast); fail-fast cancels queued items. " +
49
+ "retry-workflow-run on a failed fan-out reruns only failed and cancelled items.\n" +
48
50
  "- TRIGGER SCHEMA: Optional 'triggerSchema' is a JSON-Schema object that validates incoming trigger payloads. " +
49
51
  "Supported keywords: type, required, properties, enum, const, items (recursive into arrays). " +
50
52
  "Other JSON-Schema keywords (oneOf/anyOf/$ref/pattern/format/additionalProperties) are silently ignored.\n" +
package/src/types.ts CHANGED
@@ -384,6 +384,7 @@ export const AgentTaskSourceSchema = z.enum([
384
384
  "github",
385
385
  "gitlab",
386
386
  "azure-devops",
387
+ "bitbucket",
387
388
  "agentmail",
388
389
  "system",
389
390
  "schedule",
@@ -697,7 +698,7 @@ export const AgentTaskSchema = z
697
698
  slackTreeRootMessageTs: z.string().optional(),
698
699
 
699
700
  // VCS metadata (GitHub / GitLab / Azure DevOps — provider-agnostic)
700
- vcsProvider: z.enum(["github", "gitlab", "azure-devops"]).optional(),
701
+ vcsProvider: z.enum(["github", "gitlab", "azure-devops", "bitbucket"]).optional(),
701
702
  vcsRepo: z.string().optional(),
702
703
  vcsEventType: z.string().optional(),
703
704
  vcsNumber: z.number().int().optional(),
@@ -863,7 +864,7 @@ export const CreateTaskOptionsSchema = z.object({
863
864
  * this boundary must not let a caller silently persist a mismatch.
864
865
  */
865
866
  overrideSlackContext: z.boolean().optional(),
866
- vcsProvider: z.enum(["github", "gitlab", "azure-devops"]).optional(),
867
+ vcsProvider: z.enum(["github", "gitlab", "azure-devops", "bitbucket"]).optional(),
867
868
  vcsRepo: z.string().optional(),
868
869
  vcsEventType: z.string().optional(),
869
870
  vcsNumber: z.number().int().optional(),
@@ -24,6 +24,7 @@ export const ENV_EXPORT_RESTRICTED_KEYS: ReadonlySet<string> = new Set([
24
24
  "JIRA_CLIENT_SECRET",
25
25
  "JIRA_WEBHOOK_TOKEN",
26
26
  "AZURE_DEVOPS_WEBHOOK_SECRET",
27
+ "BITBUCKET_WEBHOOK_SECRET",
27
28
  "AGENTMAIL_WEBHOOK_SECRET",
28
29
  "API_AGENT_FS_API_KEY",
29
30
  "TRANSCRIPTION_API_KEY",
@@ -53,6 +53,10 @@ const SENSITIVE_KEY_EXACT = new Set<string>([
53
53
  "AZURE_DEVOPS_TOKEN",
54
54
  // Worker-side copy of AZURE_DEVOPS_TOKEN read by the az azure-devops extension
55
55
  "AZURE_DEVOPS_EXT_PAT",
56
+ // The `_TOKEN` / `_SECRET` suffixes already match these, so listing them
57
+ // does not change redaction and does not bump SCRUBBER_RULES_VERSION.
58
+ "BITBUCKET_TOKEN",
59
+ "BITBUCKET_WEBHOOK_SECRET",
56
60
  "CLAUDE_CODE_OAUTH_TOKEN",
57
61
  "ANTHROPIC_API_KEY",
58
62
  "OPENAI_API_KEY",
@@ -77,26 +77,56 @@ export async function refreshSkillsIfChanged(
77
77
  isEnabled: boolean;
78
78
  isActive: boolean;
79
79
  };
80
+ type BundledFile = { path: string; content: string; isBinary: boolean };
80
81
  let skillRows: SkillRow[] = [];
82
+ // Present when the bundle call succeeded: every complex skill's files arrive
83
+ // in the same response, so Step 3 never fans out per file.
84
+ let bundledFiles: Map<string, BundledFile[]> | null = null;
81
85
  let newHash: string | null = null;
82
86
  let listFetchOk = false;
87
+
88
+ // Preferred path: one request for the skills and all of their bundled files.
89
+ // A 4xx from an older server (no bundle endpoint yet) falls through to the
90
+ // legacy list + per-file path below.
83
91
  try {
84
- const skillsResp = await fetch(`${apiUrl}/api/agents/${agentId}/skills`, {
92
+ const bundleResp = await fetch(`${apiUrl}/api/agents/${agentId}/skills/bundle`, {
85
93
  headers: authHeaders,
86
94
  });
87
- if (skillsResp.ok) {
88
- const skillsData = (await skillsResp.json()) as {
89
- skills: SkillRow[];
95
+ if (bundleResp.ok) {
96
+ const bundle = (await bundleResp.json()) as {
97
+ skills: (SkillRow & { files?: BundledFile[] })[];
90
98
  signature?: string;
91
99
  };
92
- skillRows = skillsData.skills;
93
- if (typeof skillsData.signature === "string") {
94
- newHash = skillsData.signature;
100
+ skillRows = bundle.skills;
101
+ bundledFiles = new Map(bundle.skills.map((skill) => [skill.id, skill.files ?? []]));
102
+ if (typeof bundle.signature === "string") {
103
+ newHash = bundle.signature;
95
104
  }
96
105
  listFetchOk = true;
97
106
  }
98
107
  } catch {
99
- // Transient network / parse error — bail out without touching the local FS
108
+ // Transient network / parse error — the legacy path below gets a chance.
109
+ }
110
+
111
+ if (!listFetchOk) {
112
+ try {
113
+ const skillsResp = await fetch(`${apiUrl}/api/agents/${agentId}/skills`, {
114
+ headers: authHeaders,
115
+ });
116
+ if (skillsResp.ok) {
117
+ const skillsData = (await skillsResp.json()) as {
118
+ skills: SkillRow[];
119
+ signature?: string;
120
+ };
121
+ skillRows = skillsData.skills;
122
+ if (typeof skillsData.signature === "string") {
123
+ newHash = skillsData.signature;
124
+ }
125
+ listFetchOk = true;
126
+ }
127
+ } catch {
128
+ // Transient network / parse error — bail out without touching the local FS
129
+ }
100
130
  }
101
131
 
102
132
  // Guard: a failed list fetch must not proceed to writeSkillsToFilesystem.
@@ -125,45 +155,55 @@ export async function refreshSkillsIfChanged(
125
155
  const files: { path: string; content: string; isBinary: boolean }[] = [];
126
156
 
127
157
  if (skill.isComplex) {
128
- // Fetch manifest to know which files exist + which are binary
129
- try {
130
- const manifestResp = await fetch(`${apiUrl}/api/skills/${skill.id}/files`, {
131
- headers: authHeaders,
132
- });
133
- if (manifestResp.ok) {
134
- const manifestData = (await manifestResp.json()) as {
135
- files: { path: string; isBinary: boolean }[];
136
- };
137
-
138
- // Fetch content for each non-binary file (N+1 — acceptable for v1)
139
- for (const manifestEntry of manifestData.files) {
140
- if (manifestEntry.isBinary) {
141
- files.push({ path: manifestEntry.path, content: "", isBinary: true });
142
- continue;
143
- }
144
- try {
145
- const encodedPath = manifestEntry.path.split("/").map(encodeURIComponent).join("/");
146
- const fileResp = await fetch(
147
- `${apiUrl}/api/skills/${skill.id}/files/${encodedPath}`,
148
- { headers: authHeaders },
149
- );
150
- if (fileResp.ok) {
151
- const fileData = (await fileResp.json()) as {
152
- file: { path: string; content: string; isBinary: boolean };
153
- };
154
- files.push({
155
- path: fileData.file.path,
156
- content: fileData.file.content,
157
- isBinary: fileData.file.isBinary,
158
- });
158
+ const bundled = bundledFiles?.get(skill.id);
159
+ if (bundled) {
160
+ // Bundle path: every file (including binaries) arrived with the
161
+ // skill list, so there is nothing left to fetch.
162
+ files.push(...bundled);
163
+ } else {
164
+ // Legacy server: fetch manifest to know which files exist + which are binary
165
+ try {
166
+ const manifestResp = await fetch(`${apiUrl}/api/skills/${skill.id}/files`, {
167
+ headers: authHeaders,
168
+ });
169
+ if (manifestResp.ok) {
170
+ const manifestData = (await manifestResp.json()) as {
171
+ files: { path: string; isBinary: boolean }[];
172
+ };
173
+
174
+ // Fetch content for each non-binary file (N+1 — acceptable for v1)
175
+ for (const manifestEntry of manifestData.files) {
176
+ if (manifestEntry.isBinary) {
177
+ files.push({ path: manifestEntry.path, content: "", isBinary: true });
178
+ continue;
179
+ }
180
+ try {
181
+ const encodedPath = manifestEntry.path
182
+ .split("/")
183
+ .map(encodeURIComponent)
184
+ .join("/");
185
+ const fileResp = await fetch(
186
+ `${apiUrl}/api/skills/${skill.id}/files/${encodedPath}`,
187
+ { headers: authHeaders },
188
+ );
189
+ if (fileResp.ok) {
190
+ const fileData = (await fileResp.json()) as {
191
+ file: { path: string; content: string; isBinary: boolean };
192
+ };
193
+ files.push({
194
+ path: fileData.file.path,
195
+ content: fileData.file.content,
196
+ isBinary: fileData.file.isBinary,
197
+ });
198
+ }
199
+ } catch {
200
+ // Non-fatal — skip this file
159
201
  }
160
- } catch {
161
- // Non-fatal — skip this file
162
202
  }
163
203
  }
204
+ } catch {
205
+ // Non-fatal — treat as no files (will skip complex skill per writer logic)
164
206
  }
165
- } catch {
166
- // Non-fatal — treat as no files (will skip complex skill per writer logic)
167
207
  }
168
208
  }
169
209
 
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Bitbucket Cloud URL helpers. Pure functions, safe on both the API server and
3
+ * the worker (no DB imports). Bitbucket Data Center is out of scope.
4
+ */
5
+
6
+ /**
7
+ * True when the URL's host is bitbucket.org. Only the host counts, so a
8
+ * `bitbucket.org` path segment on another host (`gitlab.com/bitbucket.org/x`)
9
+ * is not Bitbucket.
10
+ */
11
+ export function isBitbucketUrl(url: string): boolean {
12
+ const trimmed = url.trim();
13
+ // SCP-style SSH: user@host:path
14
+ const scp = trimmed.match(/^[^@/\s:]+@([^:/\s]+):/);
15
+ if (scp) return (scp[1] as string).toLowerCase() === "bitbucket.org";
16
+ try {
17
+ return new URL(trimmed).hostname.toLowerCase() === "bitbucket.org";
18
+ } catch {
19
+ // Scheme-less `bitbucket.org/ws/repo`
20
+ return /^bitbucket\.org(\/|$)/i.test(trimmed);
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Split a Bitbucket Cloud remote into its workspace and repository slug.
26
+ * Accepts the HTTPS forms (with or without a `user@` prefix, trailing `/` or
27
+ * `.git`) and the SSH forms (`git@bitbucket.org:ws/repo.git`,
28
+ * `ssh://git@bitbucket.org/ws/repo.git`). Returns null otherwise.
29
+ */
30
+ export function parseBitbucketRepoUrl(url: string): { workspace: string; repoSlug: string } | null {
31
+ const trimmed = url.trim();
32
+ if (!isBitbucketUrl(trimmed)) return null;
33
+
34
+ // SCP-style SSH: git@bitbucket.org:{workspace}/{repo}.git
35
+ const scp = trimmed.match(/^[^@/\s]+@bitbucket\.org:([^/]+)\/([^/]+?)(?:\.git)?\/?$/i);
36
+ if (scp) return { workspace: scp[1] as string, repoSlug: scp[2] as string };
37
+
38
+ let parsed: URL;
39
+ try {
40
+ parsed = new URL(trimmed);
41
+ } catch {
42
+ return null;
43
+ }
44
+ if (parsed.hostname.toLowerCase() !== "bitbucket.org") return null;
45
+ const parts = parsed.pathname
46
+ .replace(/\.git\/?$/, "")
47
+ .split("/")
48
+ .filter(Boolean);
49
+ const [workspace, repoSlug] = parts;
50
+ if (!workspace || !repoSlug) return null;
51
+ return { workspace: decodeURIComponent(workspace), repoSlug: decodeURIComponent(repoSlug) };
52
+ }
53
+
54
+ /** `workspace/repo_slug` for a Bitbucket Cloud remote, or null. */
55
+ export function bitbucketFullName(url: string): string | null {
56
+ const parsed = parseBitbucketRepoUrl(url);
57
+ return parsed ? `${parsed.workspace}/${parsed.repoSlug}` : null;
58
+ }
package/src/vcs/index.ts CHANGED
@@ -3,6 +3,7 @@
3
3
  */
4
4
 
5
5
  import { isAzureDevOpsUrl } from "./azure-devops";
6
+ import { isBitbucketUrl } from "./bitbucket";
6
7
  import type { VcsProvider } from "./types";
7
8
 
8
9
  export type { VcsProvider } from "./types";
@@ -13,6 +14,7 @@ export type { VcsProvider } from "./types";
13
14
  */
14
15
  export function detectVcsProvider(url: string): VcsProvider | null {
15
16
  if (isAzureDevOpsUrl(url)) return "azure-devops";
17
+ if (isBitbucketUrl(url)) return "bitbucket";
16
18
  if (url.includes("gitlab.com") || url.includes("gitlab.")) return "gitlab";
17
19
  if (url.includes("github.com") || /^[\w.-]+\/[\w.-]+$/.test(url)) return "github";
18
20
  return null;
package/src/vcs/types.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * VCS Provider types — shared across GitHub/GitLab/Azure DevOps integration.
2
+ * VCS Provider types — shared across GitHub/GitLab/Azure DevOps/Bitbucket integration.
3
3
  */
4
4
 
5
- export type VcsProvider = "github" | "gitlab" | "azure-devops";
5
+ export type VcsProvider = "github" | "gitlab" | "azure-devops" | "bitbucket";
@@ -1,6 +1,7 @@
1
1
  import type { WorkflowDefinition } from "../types";
2
2
  import type { ApprovalOutcome, ResolvedApproval } from "./executors/base";
3
3
  import type { ExecutorRegistry } from "./executors/registry";
4
+ import { resolveStepNode } from "./foreach-body";
4
5
 
5
6
  /**
6
7
  * Step output and output port for a step whose approval request resolved.
@@ -18,7 +19,9 @@ export function shapeApprovalResolution(
18
19
  parkedOutput: unknown,
19
20
  approval: ResolvedApproval,
20
21
  ): ApprovalOutcome {
21
- const node = definition.nodes.find((candidate) => candidate.id === nodeId);
22
+ // A foreach child resolves to its body node, so a system-one-decision body
23
+ // keeps its own shaping.
24
+ const node = resolveStepNode(definition, nodeId);
22
25
  if (node && registry.has(node.type)) {
23
26
  const shaped = registry.get(node.type).resolveApproval(parkedOutput, approval);
24
27
  if (shaped) return shaped;
@@ -83,11 +83,16 @@ export async function checkpointStepFailure(
83
83
 
84
84
  /**
85
85
  * Checkpoint a step entering waiting state (async executor).
86
+ *
87
+ * `keepRunStatus` parks the step and persists the context but leaves the run
88
+ * `running`: a foreach parent parks before its driver starts the children, and
89
+ * the run must not read as `waiting` until they are dispatched.
86
90
  */
87
91
  export async function checkpointStepWaiting(
88
92
  runId: string,
89
93
  stepId: string,
90
94
  ctx: Record<string, unknown>,
95
+ options: { keepRunStatus?: boolean } = {},
91
96
  ): Promise<boolean> {
92
97
  return getDbClient().transaction(async () => {
93
98
  const step = await getDbClient().get<{ id: string }>(
@@ -103,7 +108,10 @@ export async function checkpointStepWaiting(
103
108
  );
104
109
  if (!step) return false;
105
110
 
106
- await updateWorkflowRun(runId, { status: "waiting", context: ctx });
111
+ await updateWorkflowRun(
112
+ runId,
113
+ options.keepRunStatus ? { context: ctx } : { status: "waiting", context: ctx },
114
+ );
107
115
  return true;
108
116
  });
109
117
  }
@@ -1 +1,4 @@
1
1
  export const FAILED_TASK_OUTPUT_PREFIX = "[FAILED:";
2
+
3
+ /** `waitFor` of a foreach parent that materialized its children; the driver starts them. */
4
+ export const FOREACH_ITEMS_WAIT_FOR = "foreach.items";
@@ -11,6 +11,12 @@ import {
11
11
  systemOneStaticShapeViolations,
12
12
  } from "./executors/system-one-decision";
13
13
  import { systemOneExtractionStaticShapeViolations } from "./executors/system-one-extraction";
14
+ import {
15
+ FOREACH_FAILURE_POLICIES,
16
+ type ForeachFailurePolicy,
17
+ foreachBodyTypeError,
18
+ isForeachBodyType,
19
+ } from "./foreach-body";
14
20
 
15
21
  /** Extract all target node IDs from a node's `next` field */
16
22
  export function getNextTargets(next: string | string[] | Record<string, string>): string[] {
@@ -220,7 +226,7 @@ export function validateDefinition(
220
226
  ...systemOneRetryViolations(node),
221
227
  );
222
228
  if (node.type === "foreach") {
223
- validateForeachNode(node, errors);
229
+ validateForeachNode(node, errors, registry);
224
230
  // A legacy `#` id may stay editable as a NORMAL node, but never as a foreach
225
231
  // parent: its children would be `a#b#item`, and parseSyntheticNodeId splits on
226
232
  // the FIRST `#`, so the join would resolve them to parent "a" and never close.
@@ -312,20 +318,25 @@ export function validateDefinition(
312
318
 
313
319
  reportStaticConfigIssues(node, registry.get(node.type).configSchema, node.config, errors);
314
320
 
315
- // A foreach body is executed by the agent-task executor per item — hold its
316
- // static config to the same schema a top-level agent-task node gets, so an
317
- // invalid field (priority: 101, tags: "x") fails at authoring instead of
321
+ // A foreach body is executed by its own executor per item — hold its
322
+ // static config to the same schema a top-level node of that type gets, so
323
+ // an invalid field (priority: 101, tags: "x") fails at authoring instead of
318
324
  // after the fan-out materialized. Interpolated fields defer as usual.
319
- if (node.type === "foreach" && registry.has("agent-task")) {
320
- const body = node.config.body;
321
- const bodyConfig =
322
- typeof body === "object" && body !== null && !Array.isArray(body)
323
- ? (body as Record<string, unknown>).config
324
- : undefined;
325
+ const body = node.type === "foreach" ? node.config.body : undefined;
326
+ const bodyRecord =
327
+ typeof body === "object" && body !== null && !Array.isArray(body)
328
+ ? (body as Record<string, unknown>)
329
+ : undefined;
330
+ if (
331
+ bodyRecord &&
332
+ typeof bodyRecord.type === "string" &&
333
+ isForeachBodyType(bodyRecord.type, registry)
334
+ ) {
335
+ const bodyConfig = bodyRecord.config;
325
336
  if (typeof bodyConfig === "object" && bodyConfig !== null && !Array.isArray(bodyConfig)) {
326
337
  reportStaticConfigIssues(
327
338
  node,
328
- registry.get("agent-task").configSchema,
339
+ registry.get(bodyRecord.type).configSchema,
329
340
  bodyConfig,
330
341
  errors,
331
342
  "config.body.config",
@@ -401,10 +412,27 @@ function reportStaticConfigIssues(
401
412
  }
402
413
  }
403
414
 
404
- function validateForeachNode(node: WorkflowNode, errors: string[]): void {
415
+ function validateForeachNode(
416
+ node: WorkflowNode,
417
+ errors: string[],
418
+ registry?: ExecutorRegistry,
419
+ ): void {
405
420
  const config = node.config;
406
- if (Object.hasOwn(config, "concurrency")) {
407
- errors.push(`Node "${node.id}" foreach concurrency is not supported in v1`);
421
+ if (
422
+ config.concurrency !== undefined &&
423
+ !(Number.isInteger(config.concurrency) && (config.concurrency as number) >= 1)
424
+ ) {
425
+ errors.push(
426
+ `Node "${node.id}" foreach config.concurrency must be an integer of 1 or more, got ${JSON.stringify(config.concurrency)}`,
427
+ );
428
+ }
429
+ if (
430
+ config.failurePolicy !== undefined &&
431
+ !FOREACH_FAILURE_POLICIES.includes(config.failurePolicy as ForeachFailurePolicy)
432
+ ) {
433
+ errors.push(
434
+ `Node "${node.id}" foreach config.failurePolicy must be one of ${FOREACH_FAILURE_POLICIES.map((p) => `"${p}"`).join(", ")}, got ${JSON.stringify(config.failurePolicy)}`,
435
+ );
408
436
  }
409
437
 
410
438
  // The asynchronous join (joinForeach) checkpoints the aggregate and routes
@@ -436,8 +464,11 @@ function validateForeachNode(node: WorkflowNode, errors: string[]): void {
436
464
  return;
437
465
  }
438
466
  const bodyRecord = body as Record<string, unknown>;
439
- if (bodyRecord.type !== "agent-task") {
440
- errors.push(`Node "${node.id}" foreach config.body.type must be "agent-task" in v1`);
467
+ if (typeof bodyRecord.type !== "string" || bodyRecord.type.length === 0) {
468
+ errors.push(`Node "${node.id}" foreach config.body.type must be a non-empty string`);
469
+ } else {
470
+ const typeError = foreachBodyTypeError(node.id, bodyRecord.type, registry);
471
+ if (typeError) errors.push(typeError);
441
472
  }
442
473
  if (
443
474
  typeof bodyRecord.config !== "object" ||
@@ -448,7 +479,7 @@ function validateForeachNode(node: WorkflowNode, errors: string[]): void {
448
479
  return;
449
480
  }
450
481
  const bodyConfig = bodyRecord.config as Record<string, unknown>;
451
- if (typeof bodyConfig.template !== "string") {
482
+ if (bodyRecord.type === "agent-task" && typeof bodyConfig.template !== "string") {
452
483
  errors.push(`Node "${node.id}" foreach agent-task body.config.template must be a string`);
453
484
  }
454
485
  }
@@ -25,6 +25,7 @@ import { telemetry } from "../telemetry";
25
25
  import type { Workflow, WorkflowDefinition, WorkflowNode, WorkflowRunStep } from "../types";
26
26
  import { checkpointStep, checkpointStepFailure, checkpointStepWaiting } from "./checkpoint";
27
27
  import { loadCompletedStepRouting } from "./completed-step-routing";
28
+ import { FOREACH_ITEMS_WAIT_FOR } from "./constants";
28
29
  import { shouldSkipCooldown } from "./cooldown";
29
30
  import { findEntryNodes, getNextTargets, getSuccessors, resolveValidationPort } from "./definition";
30
31
  import type { AsyncExecutorResult } from "./executors/base";
@@ -34,15 +35,16 @@ import {
34
35
  systemOneRetryViolations,
35
36
  systemOneUnresolvedError,
36
37
  } from "./executors/system-one-decision";
38
+ import { parkAndDriveForeach } from "./foreach-driver";
37
39
  import { FOREACH_TERMINAL_STEP_STATUSES, resolveForeachParent } from "./foreach-join";
38
40
  import { getSecretInputKeys, redactSecretsForStorage, resolveInputs } from "./input";
39
41
  import { validateJsonSchema } from "./json-schema-validator";
40
42
  import { getMaxWorkflowStepsPerRun } from "./limits";
43
+ import { resolveNodeTimeoutMs } from "./node-timeout";
41
44
  import { findWorkflowReadinessProblems, formatReadinessRunError } from "./readiness";
42
45
  import { deepInterpolate } from "./template";
43
46
  import { runStepValidation, type ValidationRunResult } from "./validation";
44
47
 
45
- const DEFAULT_TIMEOUT_MS = 30_000;
46
48
  // Read dynamically, NOT captured at module load: `src/http/index.ts` imports
47
49
  // this module before `loadGlobalConfigsIntoEnv()` hydrates `swarm_config` into
48
50
  // `process.env`, so a module-level const would permanently ignore DB-saved
@@ -510,14 +512,19 @@ async function walkGraphOwned(
510
512
  const hasPendingRetries = finalSteps.some(
511
513
  (s) => s.status === "failed" && s.nextRetryAt != null,
512
514
  );
513
- const failedSteps = finalSteps.filter((s) => s.status === "failed" && s.nextRetryAt == null);
515
+ // A foreach child's outcome is in its parent's aggregate; the parent step
516
+ // is authoritative for the run outcome.
517
+ const isForeachChild = (s: WorkflowRunStep) => resolveForeachParent(def, s.nodeId) !== null;
518
+ const failedSteps = finalSteps.filter(
519
+ (s) => s.status === "failed" && s.nextRetryAt == null && !isForeachChild(s),
520
+ );
514
521
  // Exclude entry/trigger nodes when checking for completed steps — a trigger
515
522
  // completing doesn't mean a meaningful branch succeeded. Without this filter,
516
523
  // a linear workflow (trigger → mustPass validator → action) would be marked
517
524
  // as partial-failure instead of failed when the validator fails.
518
525
  const entryNodeIds = new Set(findEntryNodes(def).map((n) => n.id));
519
526
  const hasCompletedSteps = finalSteps.some(
520
- (s) => s.status === "completed" && !entryNodeIds.has(s.nodeId),
527
+ (s) => s.status === "completed" && !entryNodeIds.has(s.nodeId) && !isForeachChild(s),
521
528
  );
522
529
 
523
530
  if (hasWaitingSteps) {
@@ -880,18 +887,7 @@ async function runClaimedStep(
880
887
  inputCtx: interpolationCtx,
881
888
  };
882
889
 
883
- // Inline script nodes historically expose their wall-clock budget as
884
- // `config.timeout`, while the workflow watchdog reads `config.timeoutMs` for
885
- // every other executor. Keep the outer watchdog aligned with the script
886
- // executor so a script configured for (for example) 90s is not killed by the
887
- // engine's 30s default first.
888
- const configuredTimeoutMs =
889
- typeof node.config?.timeoutMs === "number"
890
- ? node.config.timeoutMs
891
- : node.type === "script" && typeof node.config?.timeout === "number"
892
- ? node.config.timeout
893
- : undefined;
894
- const timeoutMs = configuredTimeoutMs ?? DEFAULT_TIMEOUT_MS;
890
+ const timeoutMs = resolveNodeTimeoutMs(node);
895
891
 
896
892
  let result: Awaited<ReturnType<typeof executor.run>>;
897
893
  try {
@@ -949,6 +945,20 @@ async function runClaimedStep(
949
945
 
950
946
  // Check for async result
951
947
  if ("async" in result && (result as AsyncExecutorResult).async) {
948
+ if ((result as AsyncExecutorResult).waitFor === FOREACH_ITEMS_WAIT_FOR) {
949
+ // The foreach executor materialized its children; park the parent and
950
+ // start them. Instant bodies run here, each against its own budget, and
951
+ // can close the join before the drive returns.
952
+ const drive = await parkAndDriveForeach({
953
+ runId,
954
+ stepId,
955
+ parentNodeId: node.id,
956
+ ctx,
957
+ registry,
958
+ });
959
+ if (drive.joined) return { outcome: "completed", successors: drive.successors };
960
+ return { outcome: drive.parked ? "waiting" : "completed", successors: [] };
961
+ }
952
962
  const waiting = await checkpointStepWaiting(runId, stepId, ctx);
953
963
  // A child run that finished before the step parked had its event dropped.
954
964
  if (waiting && (result as AsyncExecutorResult).waitFor === "workflow.child.finished") {