@desplega.ai/agent-swarm 2.5.4 → 2.7.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 (354) hide show
  1. package/README.md +4 -2
  2. package/dist/{acp-adapter-ek38cwsv.js → acp-adapter-ma1js5n7.js} +9 -9
  3. package/dist/{actions-92mepq03.js → actions-1vhmtvg9.js} +21 -21
  4. package/dist/{amp-adapter-9gaxg015.js → amp-adapter-sxqqvmdk.js} +4 -4
  5. package/dist/{app-ctn8gsgx.js → app-mwx1d5a9.js} +3 -3
  6. package/dist/{assistant-zyr5kz47.js → assistant-yx1p6g82.js} +30 -26
  7. package/dist/{boot-reembed-w75maga2.js → boot-reembed-5h37a2c2.js} +1 -1
  8. package/dist/{boot-reembed-dwzfttdr.js → boot-reembed-7h78vmvv.js} +2 -1
  9. package/dist/{boot-scrub-sweep-e9kq43kf.js → boot-scrub-sweep-8mawd4gw.js} +4 -4
  10. package/dist/{boot-seeds-x28v3fhb.js → boot-seeds-ca84dhnn.js} +4 -4
  11. package/dist/{claude-adapter-f0afqnne.js → claude-adapter-wqt4hfkq.js} +9 -9
  12. package/dist/{claude-managed-adapter-x81a7r65.js → claude-managed-adapter-6fjqtr2z.js} +2 -2
  13. package/dist/{claude-managed-setup-bqzwjd9x.js → claude-managed-setup-rqv98sea.js} +10 -3
  14. package/dist/{claude-sdk-session-7b7whkaq.js → claude-sdk-session-f29f3b2x.js} +11 -11
  15. package/dist/{cli-sxgtf5fw.js → cli-07q03xm2.js} +1 -1
  16. package/dist/{cli-kxpfx2xr.js → cli-0z1xjzwa.js} +2 -2
  17. package/dist/{cli-g9vd9ece.js → cli-1hhbzkbb.js} +14 -16
  18. package/dist/{cli-qqzmjxkf.js → cli-1m9x8dr5.js} +11 -6
  19. package/dist/{cli-jbnn4vxb.js → cli-2pvzmas1.js} +3 -3
  20. package/dist/{cli-x77gen2p.js → cli-2y7v133v.js} +7 -7
  21. package/dist/{cli-dew6x255.js → cli-39aet655.js} +2 -2
  22. package/dist/{cli-g1cw7xtt.js → cli-39rrw8yp.js} +1 -1
  23. package/dist/{cli-jb30dzvk.js → cli-48gxjmcz.js} +530 -190
  24. package/dist/{cli-pzatvw94.js → cli-54sj64fc.js} +2 -2
  25. package/dist/{cli-bvpgr9ny.js → cli-60sty27p.js} +5 -5
  26. package/dist/{cli-asq5ydm4.js → cli-6a9qfa8b.js} +411 -69
  27. package/dist/{cli-277c31dw.js → cli-6mgdba42.js} +225 -3
  28. package/dist/{cli-19202kew.js → cli-6srv653c.js} +5 -5
  29. package/dist/{cli-n4c8e3s5.js → cli-708wdy32.js} +1 -1
  30. package/dist/{cli-pasyvjmm.js → cli-737pyqxr.js} +1 -1
  31. package/dist/{cli-6x9ma5w5.js → cli-803188pb.js} +1 -1
  32. package/dist/{cli-hh5we352.js → cli-86ehn4s5.js} +7 -7
  33. package/dist/{cli-9s05422w.js → cli-8mbc9za9.js} +1 -1
  34. package/dist/{cli-ff1tm9n9.js → cli-8wmj18cz.js} +1 -1
  35. package/dist/{cli-1r5ad7j5.js → cli-9gsm2rzp.js} +12 -12
  36. package/dist/{cli-mz9afxxf.js → cli-9pshx6rd.js} +17 -3
  37. package/dist/{cli-s682zgqz.js → cli-a6ds2epe.js} +1 -1
  38. package/dist/{cli-6a1xg1p7.js → cli-a89zdqz9.js} +2 -2
  39. package/dist/{cli-03vpq45j.js → cli-ay40dnj3.js} +2 -2
  40. package/dist/{cli-tsma7p8v.js → cli-aypd09nf.js} +2 -2
  41. package/dist/{cli-ethgy7ha.js → cli-b3yzyc57.js} +2 -2
  42. package/dist/{cli-bn0rewsy.js → cli-c081bj3b.js} +2 -2
  43. package/dist/{cli-mmhj8vd7.js → cli-c668vvcf.js} +11 -2
  44. package/dist/{cli-sbwsxvtd.js → cli-d20qvpr2.js} +2 -2
  45. package/dist/{cli-c952c7ac.js → cli-e1595bgx.js} +3 -3
  46. package/dist/{cli-621ce7kw.js → cli-f12mr20z.js} +3 -3
  47. package/dist/{cli-dqygeeh5.js → cli-f1e0ptjj.js} +10 -1
  48. package/dist/{cli-xzfq4at3.js → cli-f4dbh7a5.js} +16 -12
  49. package/dist/{cli-ha9rmwgq.js → cli-fb8m699c.js} +2 -2
  50. package/dist/{cli-5bnsc0e1.js → cli-gsfk58nw.js} +5 -5
  51. package/dist/cli-gt4h8cxw.js +895 -0
  52. package/dist/{cli-nwyw3zhg.js → cli-gxee0bqa.js} +1 -1
  53. package/dist/{cli-25n3mv5d.js → cli-h5jeg46p.js} +6 -6
  54. package/dist/{cli-gn5af0x5.js → cli-he9gbdx0.js} +62 -12
  55. package/dist/{cli-gbfxp48k.js → cli-j0ppywdr.js} +42 -35
  56. package/dist/{cli-k4tjsdx9.js → cli-j3y0x2n4.js} +277 -199
  57. package/dist/{cli-7p4jxgtt.js → cli-j9bpmykh.js} +75 -27
  58. package/dist/{cli-dabehxhp.js → cli-kt907da6.js} +1 -1
  59. package/dist/{cli-wx2nj19m.js → cli-kv7sbc2c.js} +6 -6
  60. package/dist/{cli-abs8ndtr.js → cli-m30k9777.js} +26 -26
  61. package/dist/{cli-cz5nmdxt.js → cli-m5e802r7.js} +2 -2
  62. package/dist/{cli-0shx9j93.js → cli-pfk92eqz.js} +2 -2
  63. package/dist/{cli-xtrz6bzs.js → cli-ptfq202x.js} +5 -3
  64. package/dist/{cli-6n8axjv1.js → cli-qj442fws.js} +2 -2
  65. package/dist/{cli-c61mbdds.js → cli-rd5m70gj.js} +4 -4
  66. package/dist/{cli-eftzyh9y.js → cli-s09j9vp5.js} +2 -2
  67. package/dist/{cli-m4ecdxvz.js → cli-s0mgx214.js} +7 -7
  68. package/dist/{cli-wghjkr0e.js → cli-sgqm2582.js} +10 -7
  69. package/dist/{cli-2655k16q.js → cli-sjctjr48.js} +2 -2
  70. package/dist/{cli-nzfnt2gs.js → cli-tf1n0r0b.js} +1 -1
  71. package/dist/{cli-x0f2v69c.js → cli-twctnkng.js} +12 -7
  72. package/dist/{cli-j6rktc91.js → cli-v6shg7na.js} +1 -1
  73. package/dist/{cli-gx4rm0qx.js → cli-vz78s5wm.js} +1 -1
  74. package/dist/{cli-gwhjmh8g.js → cli-wqr7cea4.js} +5 -5
  75. package/dist/{cli-e42vss7b.js → cli-x37r7qwf.js} +16 -8
  76. package/dist/{cli-4b2z6n0m.js → cli-yd2nreqw.js} +17 -8
  77. package/dist/{cli-64mh9mf3.js → cli-ymjpkxgn.js} +69 -17
  78. package/dist/{cli-5kqnemen.js → cli-yvy48785.js} +1 -1
  79. package/dist/{cli-9j129brq.js → cli-yxbsmx56.js} +6 -6
  80. package/dist/cli.js +22 -20
  81. package/dist/{codex-adapter-wch12q99.js → codex-adapter-zk59x2zh.js} +7 -7
  82. package/dist/{codex-hook-brpnycn5.js → codex-hook-xyjhvxwd.js} +1 -1
  83. package/dist/{codex-login-rn7c5gxt.js → codex-login-8cg0vhf2.js} +1 -1
  84. package/dist/{codex-session-runner-41pfcr95.js → codex-session-runner-kh6s4j3n.js} +8 -8
  85. package/dist/{commands-9849dkj6.js → commands-68et1qv2.js} +2 -2
  86. package/dist/{controller-ja0sd95p.js → controller-fyrbnzg4.js} +4 -4
  87. package/dist/{controllers-connect-xp05dj6x.js → controllers-connect-nep9h2xv.js} +23 -23
  88. package/dist/{cursor-adapter-623qxqew.js → cursor-adapter-yss3pg7t.js} +2 -2
  89. package/dist/{db-w759qmx0.js → db-28gjyvxj.js} +4 -2
  90. package/dist/{db-47vj40dy.js → db-8ftwvzba.js} +3 -3
  91. package/dist/{devin-adapter-4ycy4vrf.js → devin-adapter-byen2cxt.js} +1 -1
  92. package/dist/{dispatcher-7b8e4z2j.js → dispatcher-hayexmkr.js} +20 -20
  93. package/dist/{docker-s84hjs91.js → docker-fdzyewpz.js} +2 -2
  94. package/dist/{dsh-adapter-2ezw3xea.js → dsh-adapter-46wawff9.js} +2 -2
  95. package/dist/{e2b-9gkdhm3c.js → e2b-222hsajc.js} +2 -2
  96. package/dist/{e2b-cjtf8k1z.js → e2b-fn3zacvt.js} +2 -2
  97. package/dist/{easter-egg-3d-e4qv8dxj.js → easter-egg-3d-8d1e0h4m.js} +1 -1
  98. package/dist/{event-bus-3z78ep5n.js → event-bus-pghg8hqy.js} +1 -1
  99. package/dist/{execute-0xpz8950.js → execute-cjrfxfnm.js} +1 -1
  100. package/dist/{external-channel-e2s028da.js → external-channel-xbmxcg09.js} +2 -2
  101. package/dist/{grok-adapter-ys7gaqee.js → grok-adapter-ag4wks2a.js} +10 -10
  102. package/dist/handlers-tzmevqb5.js +41 -0
  103. package/dist/{heartbeat-w3mntghj.js → heartbeat-psfvcgwp.js} +23 -23
  104. package/dist/{hook-g7kvv6x9.js → hook-8pz04697.js} +6 -6
  105. package/dist/{hook-3jjywnpb.js → hook-cyq4mryn.js} +8 -8
  106. package/dist/{http-tkp7d4kb.js → http-haj1nd6k.js} +921 -169
  107. package/dist/{index-q7m26kvs.js → index-jwt2bj6f.js} +265 -97
  108. package/dist/{index-3n8m9ea2.js → index-q52f47pe.js} +1 -1
  109. package/dist/{index-31ceyf42.js → index-syvhnvtw.js} +1 -1
  110. package/dist/index-y1ajx04a.js +43 -0
  111. package/dist/{index-1ef1myq2.js → index-z5h22pes.js} +20 -20
  112. package/dist/{index-xdaahthb.js → index-zyq8ct52.js} +20 -20
  113. package/dist/{keepalive-v4ta8wgg.js → keepalive-xdvsamvj.js} +4 -4
  114. package/dist/lead-hqg57kdp.js +79 -0
  115. package/dist/{maintenance-84n1ex21.js → maintenance-4y2xzh3a.js} +1 -1
  116. package/dist/{oauth-refresh-sweep-vjnnandw.js → oauth-refresh-sweep-jd4r7zbs.js} +9 -4
  117. package/dist/{onboard-sfmpe4np.js → onboard-18447q9q.js} +6 -6
  118. package/dist/{opencode-adapter-jmpst4q4.js → opencode-adapter-26b2frdf.js} +7 -7
  119. package/dist/{otel-impl-v6cc0zve.js → otel-impl-f9tgr8yw.js} +2 -2
  120. package/dist/{pi-mono-adapter-fk8fkcf6.js → pi-mono-adapter-qn6qbga1.js} +38 -17
  121. package/dist/{pricing-refresh-fsk1e1zc.js → pricing-refresh-dw2tgb53.js} +3 -3
  122. package/dist/{rbac-roles-prmkdnkw.js → rbac-roles-aygm2760.js} +2 -2
  123. package/dist/{rbac-roles-zxmxqa39.js → rbac-roles-s4nwj2m3.js} +1 -1
  124. package/dist/render-v2-yfvdfr0x.js +16 -0
  125. package/dist/{resume-frdtgfa0.js → resume-1bmvjbf3.js} +9 -9
  126. package/dist/{runtime-bt5pkpm1.js → runtime-f7tpzedy.js} +1 -1
  127. package/dist/{session-access-fpez4y5q.js → session-access-r5garej6.js} +4 -4
  128. package/dist/session-source-backfill-424wzv0r.js +110 -0
  129. package/dist/{setup-mq6m3m8q.js → setup-8tygvxkq.js} +1 -1
  130. package/dist/{virtual-modules-mnfbh5mr.js → virtual-modules-xsw323b3.js} +2 -2
  131. package/dist/{work-objects-kfahhjsq.js → work-objects-ggnghpgf.js} +23 -23
  132. package/dist/worker-sqjd1nqf.js +79 -0
  133. package/dist/{x-pbpezr0s.js → x-wxrzvzx8.js} +2 -2
  134. package/openapi.json +1324 -14
  135. package/package.json +5 -3
  136. package/src/assets/source-key.ts +95 -0
  137. package/src/be/automation-preflight.ts +2 -0
  138. package/src/be/db/api-keys.ts +89 -0
  139. package/src/be/db/session-labels.ts +28 -0
  140. package/src/be/db/session-moves.ts +5 -6
  141. package/src/be/db/session-tree.ts +153 -14
  142. package/src/be/db/tasks/read.ts +11 -0
  143. package/src/be/db/tasks/write.ts +6 -1
  144. package/src/be/db/workflow-runs.ts +219 -6
  145. package/src/be/db-retention.ts +61 -6
  146. package/src/be/db.ts +135 -132
  147. package/src/be/memory/providers/openai-embedding.ts +5 -1
  148. package/src/be/memory/task-learning.ts +155 -0
  149. package/src/be/migrations/244_api_key_validation.sql +4 -0
  150. package/src/be/migrations/245_script_executor.sql +3 -0
  151. package/src/be/oauth-refresh-sweep.ts +2 -1
  152. package/src/be/onboarding.ts +4 -0
  153. package/src/be/scripts/boot-reembed.ts +2 -0
  154. package/src/be/scripts/db.ts +14 -4
  155. package/src/be/scripts/embeddings.ts +2 -0
  156. package/src/be/scripts/run-saved.ts +1 -0
  157. package/src/be/scripts/typecheck.ts +10 -4
  158. package/src/be/seed-skills/index.ts +7 -0
  159. package/src/be/session-source-backfill.ts +163 -0
  160. package/src/be/swarm-config-guard.ts +1 -0
  161. package/src/comb/review-batch.ts +28 -1
  162. package/src/comb/templates.ts +7 -2
  163. package/src/commands/claude-managed-setup.ts +7 -1
  164. package/src/commands/credential-refresh.ts +76 -1
  165. package/src/commands/credential-wait.ts +2 -2
  166. package/src/commands/onboard/compose-generator.ts +5 -3
  167. package/src/commands/provider-credentials.ts +212 -171
  168. package/src/commands/runner.ts +25 -4
  169. package/src/extensions/apply-task-create.ts +8 -1
  170. package/src/extensions/contract-types.generated.ts +1 -1
  171. package/src/extensions/contract.ts +5 -0
  172. package/src/heartbeat/heartbeat.ts +14 -1
  173. package/src/heartbeat/script-runner-probe.ts +47 -0
  174. package/src/http/all-routes.ts +2 -0
  175. package/src/http/api-keys.ts +77 -9
  176. package/src/http/assets.ts +15 -0
  177. package/src/http/codex-oauth-accounts.ts +191 -0
  178. package/src/http/comb.ts +15 -2
  179. package/src/http/config.ts +1 -1
  180. package/src/http/core.ts +6 -0
  181. package/src/http/fleet-benchmark.ts +5 -2
  182. package/src/http/idempotency-key.ts +1 -1
  183. package/src/http/index.ts +14 -3
  184. package/src/http/integrations.ts +5 -1
  185. package/src/http/kv.ts +8 -3
  186. package/src/http/onboarding.ts +6 -1
  187. package/src/http/pages-public.ts +38 -5
  188. package/src/http/pool-keys.ts +354 -0
  189. package/src/http/scripts.ts +27 -0
  190. package/src/http/sessions.ts +37 -12
  191. package/src/http/stats.ts +1 -0
  192. package/src/http/status.ts +26 -12
  193. package/src/http/tasks.ts +3 -0
  194. package/src/http/used-by.ts +7 -2
  195. package/src/http/workflows.ts +88 -9
  196. package/src/http/x.ts +5 -0
  197. package/src/integrations/chat-sdk/inbound.ts +27 -0
  198. package/src/kv-reserved-namespaces.ts +19 -0
  199. package/src/prompts/base-prompt.ts +21 -1
  200. package/src/prompts/session-templates.ts +17 -3
  201. package/src/providers/acp-targets.ts +1 -1
  202. package/src/providers/amp-adapter.ts +1 -1
  203. package/src/providers/claude-adapter.ts +5 -1
  204. package/src/providers/claude-managed-adapter.ts +16 -9
  205. package/src/providers/codex-adapter.ts +71 -11
  206. package/src/providers/dsh-adapter.ts +1 -1
  207. package/src/providers/pi-mono-adapter.ts +41 -11
  208. package/src/scheduler/scheduler.ts +5 -3
  209. package/src/scripts-runtime/eval-harness.ts +8 -1
  210. package/src/scripts-runtime/executors/native.ts +22 -16
  211. package/src/scripts-runtime/executors/quickjs-prelude.ts +103 -1
  212. package/src/scripts-runtime/executors/quickjs-runner.ts +2 -1
  213. package/src/scripts-runtime/executors/quickjs.ts +7 -0
  214. package/src/scripts-runtime/executors/registry.ts +32 -0
  215. package/src/scripts-runtime/executors/types.ts +7 -0
  216. package/src/scripts-runtime/host-validated-args.ts +133 -0
  217. package/src/scripts-runtime/loader.ts +30 -3
  218. package/src/scripts-runtime/swarm-sdk.ts +16 -3
  219. package/src/scripts-runtime/types/stdlib.d.ts +10 -2
  220. package/src/scripts-runtime/types/swarm-extension.d.ts +17 -8
  221. package/src/scripts-runtime/types/swarm-sdk.d.ts +10 -2
  222. package/src/slack/assistant.ts +7 -0
  223. package/src/slack/channel-labels.ts +74 -0
  224. package/src/slack/handlers.ts +8 -0
  225. package/src/slack/inbound-files.ts +5 -2
  226. package/src/slack/thread-buffer.ts +4 -0
  227. package/src/tasks/sibling-awareness.ts +12 -3
  228. package/src/telemetry-context.ts +5 -6
  229. package/src/telemetry-identity.ts +3 -3
  230. package/src/tests/api-key-status-by-suffix.test.ts +242 -0
  231. package/src/tests/api-keys-validation.test.ts +170 -0
  232. package/src/tests/base-prompt.test.ts +70 -2
  233. package/src/tests/claude-managed-adapter.test.ts +46 -0
  234. package/src/tests/claude-routes-e2e.test.ts +7 -2
  235. package/src/tests/codex-credential-pools.test.ts +120 -0
  236. package/src/tests/codex-gateway-route.test.ts +165 -0
  237. package/src/tests/codex-oauth-accounts.test.ts +357 -0
  238. package/src/tests/comb-review-batch.test.ts +63 -2
  239. package/src/tests/community-linear-delivery-loop.test.ts +2099 -0
  240. package/src/tests/config-pool-keys.test.ts +533 -0
  241. package/src/tests/credential-check.test.ts +294 -4
  242. package/src/tests/credential-refresh.test.ts +172 -1
  243. package/src/tests/db-retention.test.ts +9 -3
  244. package/src/tests/db-task-read-contract.test.ts +1 -0
  245. package/src/tests/db-task-write-contract.test.ts +1 -3
  246. package/src/tests/docs-index-footprint.test.js +3 -3
  247. package/src/tests/docs-index.test.ts +135 -0
  248. package/src/tests/dsh-adapter.test.ts +1 -1
  249. package/src/tests/extensions-pre-task.test.ts +53 -1
  250. package/src/tests/fetch-resolved-env.test.ts +16 -2
  251. package/src/tests/fixtures/extensions/record-task-create/hooks.ts +9 -0
  252. package/src/tests/fixtures/extensions/record-task-create/manifest.json +7 -0
  253. package/src/tests/fleet-benchmark.test.ts +8 -0
  254. package/src/tests/get-workflow-run-tool.test.ts +98 -0
  255. package/src/tests/heartbeat-checklist.test.ts +89 -0
  256. package/src/tests/heartbeat-script-runner-probe.test.ts +83 -0
  257. package/src/tests/integrations-http.test.ts +32 -2
  258. package/src/tests/internal-ai/complete-structured.test.ts +37 -1
  259. package/src/tests/internal-ai/credentials.test.ts +23 -0
  260. package/src/tests/kv-http.test.ts +32 -0
  261. package/src/tests/kv-tool.test.ts +25 -0
  262. package/src/tests/multi-runtime-registration.test.ts +12 -12
  263. package/src/tests/oauth-refresh-sweep.test.ts +34 -1
  264. package/src/tests/onboard-compose.test.ts +20 -0
  265. package/src/tests/onboarding.test.ts +108 -1
  266. package/src/tests/openai-embedding-credentials.test.ts +35 -0
  267. package/src/tests/opencode-adapter.test.ts +2 -1
  268. package/src/tests/openrouter-attribution-call-sites.test.ts +2 -2
  269. package/src/tests/org-website.test.ts +36 -0
  270. package/src/tests/pages-frame-ancestors.test.ts +162 -0
  271. package/src/tests/pi-codemode.test.ts +2 -1
  272. package/src/tests/pi-mono-adapter.test.ts +54 -0
  273. package/src/tests/pi-native-mcp.test.ts +13 -1
  274. package/src/tests/pi-tool-exposure.test.ts +8 -8
  275. package/src/tests/probe-tool-schema-tokens.test.ts +144 -0
  276. package/src/tests/prompt-template-session.test.ts +4 -3
  277. package/src/tests/provider-key-probes.test.ts +181 -0
  278. package/src/tests/rbac-e2e-helpers.test.ts +88 -1
  279. package/src/tests/rbac-e2e-helpers.ts +36 -11
  280. package/src/tests/request-composition-derive.test.ts +329 -0
  281. package/src/tests/script-connections.test.ts +4 -0
  282. package/src/tests/script-executor-conformance.test.ts +36 -1
  283. package/src/tests/script-executor-quickjs-pool.test.ts +50 -0
  284. package/src/tests/script-executor-quickjs.test.ts +140 -0
  285. package/src/tests/script-host-validated-args-native.test.ts +196 -0
  286. package/src/tests/script-per-script-executor.test.ts +415 -0
  287. package/src/tests/script-runs-retention.test.ts +212 -0
  288. package/src/tests/scripts-external-api.test.ts +45 -0
  289. package/src/tests/sdk-allowlist.test.ts +42 -2
  290. package/src/tests/self-improvement.test.ts +4 -4
  291. package/src/tests/session-source-backfill.test.ts +429 -0
  292. package/src/tests/session-source-keys.test.ts +236 -0
  293. package/src/tests/session-source-labels.test.ts +419 -0
  294. package/src/tests/sessions-detail-batch.test.ts +2 -0
  295. package/src/tests/sessions-move.test.ts +131 -0
  296. package/src/tests/sessions-tree.test.ts +299 -2
  297. package/src/tests/slack-inbound-files.test.ts +103 -23
  298. package/src/tests/status.test.ts +101 -18
  299. package/src/tests/store-progress-learning.test.ts +288 -0
  300. package/src/tests/tasks-workflow-run-filter.test.ts +146 -0
  301. package/src/tests/test-net.ts +31 -0
  302. package/src/tests/transcription.test.ts +8 -0
  303. package/src/tests/ui-logs-parser.test.ts +180 -0
  304. package/src/tests/used-by-route.test.ts +11 -0
  305. package/src/tests/workflow-cascade-cancel.test.ts +172 -0
  306. package/src/tests/workflow-engine-v2.test.ts +116 -1
  307. package/src/tests/workflow-http-v2.test.ts +131 -0
  308. package/src/tests/workflow-mcp-trigger-schema.test.ts +209 -0
  309. package/src/tests/workflow-run-finished-event.test.ts +263 -0
  310. package/src/tests/workflow-step-order.test.ts +92 -0
  311. package/src/tests/workflow-sub-workflow.test.ts +30 -0
  312. package/src/tests/workflow-swarm-script.test.ts +34 -0
  313. package/src/tools/get-tasks.ts +8 -0
  314. package/src/tools/kv/kv-read-auth.ts +4 -2
  315. package/src/tools/script-common.ts +1 -0
  316. package/src/tools/script-run.ts +7 -0
  317. package/src/tools/script-upsert.ts +7 -0
  318. package/src/tools/store-progress.ts +67 -1
  319. package/src/tools/task-action.ts +1 -1
  320. package/src/tools/workflows/get-workflow-run.ts +14 -4
  321. package/src/tools/workflows/trigger-workflow.ts +104 -10
  322. package/src/types.ts +36 -1
  323. package/src/utils/company-profile.ts +51 -1
  324. package/src/utils/credentials.ts +10 -4
  325. package/src/utils/internal-ai/complete-structured.ts +5 -0
  326. package/src/utils/internal-ai/credentials.ts +4 -3
  327. package/src/utils/provider-key-probes.ts +328 -0
  328. package/src/utils/transcription.ts +4 -1
  329. package/src/workflows/engine.ts +18 -16
  330. package/src/workflows/executors/sub-workflow.ts +8 -1
  331. package/src/workflows/executors/swarm-script.ts +5 -0
  332. package/src/workflows/foreach-driver.ts +5 -11
  333. package/src/workflows/node-timeout.ts +30 -0
  334. package/templates/ai-toolbox.manifest.json +2 -2
  335. package/templates/community/linear-delivery-loop/PLAYBOOK.md +323 -0
  336. package/templates/community/linear-delivery-loop/README.md +38 -0
  337. package/templates/community/linear-delivery-loop/config.example.json +31 -0
  338. package/templates/community/linear-delivery-loop/lead-prompt.md +34 -0
  339. package/templates/community/linear-delivery-loop/schedule.json +10 -0
  340. package/templates/community/linear-delivery-loop/scripts/linear-delivery-setup.ts +148 -0
  341. package/templates/community/linear-delivery-loop/scripts/linear-delivery-tick.ts +1781 -0
  342. package/templates/skills/researching/content.md +2 -0
  343. package/templates/skills/swarm-docs/config.json +14 -0
  344. package/templates/skills/swarm-docs/content.md +144 -0
  345. package/templates/skills/swarm-scripts/SKILL.md +1 -0
  346. package/templates/skills/swarm-scripts/content.md +1 -0
  347. package/dist/cli-7421jzsw.js +0 -84
  348. package/dist/cli-e45e4m6b.js +0 -413
  349. package/dist/cli-zw88zjba.js +0 -307
  350. package/dist/handlers-wy1dr6ja.js +0 -41
  351. package/dist/index-nmm79qgr.js +0 -43
  352. package/dist/lead-sawnyqxk.js +0 -79
  353. package/dist/render-v2-pjwxvnkw.js +0 -12
  354. package/dist/worker-v5e5rxj3.js +0 -79
@@ -0,0 +1,135 @@
1
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
+ import { mkdir, mkdtemp, rm } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import {
6
+ collectDocsPages,
7
+ coverageErrors,
8
+ INDEX_END,
9
+ INDEX_START,
10
+ renderDocsIndex,
11
+ replaceIndexBlock,
12
+ SKILL_CONTENT_PATH,
13
+ } from "../../scripts/build-docs-index";
14
+ import { loadSeedSkills } from "../be/seed-skills";
15
+
16
+ const page = (frontmatter: string, body = "Body.\n") => `---\n${frontmatter}\n---\n\n${body}`;
17
+
18
+ describe("docs index (real docs tree)", () => {
19
+ test("every playbook is in the index and the coverage rules pass", async () => {
20
+ const pages = await collectDocsPages();
21
+ expect(coverageErrors(pages)).toEqual([]);
22
+
23
+ const index = renderDocsIndex(pages);
24
+ const playbooks = pages.filter((p) => p.path.startsWith("playbooks/"));
25
+ expect(playbooks.length).toBeGreaterThan(10);
26
+ for (const playbook of playbooks) {
27
+ expect(index).toContain(`(https://docs.agent-swarm.dev/docs/${playbook.slug})`);
28
+ }
29
+ // Positive control: a known playbook carries its trigger.
30
+ expect(index).toContain(
31
+ "[Open Oncall](https://docs.agent-swarm.dev/docs/playbooks/open-oncall). When: ",
32
+ );
33
+ // Negative control: opted-out pages are reference only, release notes are skipped.
34
+ expect(index).toContain("(https://docs.agent-swarm.dev/docs/playbooks). Reference.");
35
+ expect(index).not.toContain("/docs/releases/");
36
+ });
37
+
38
+ test("the committed skill body matches the generator", async () => {
39
+ const content = await Bun.file(SKILL_CONTENT_PATH).text();
40
+ const regenerated = replaceIndexBlock(content, renderDocsIndex(await collectDocsPages()));
41
+ expect(content).toBe(regenerated);
42
+ });
43
+
44
+ test("the generator is deterministic", async () => {
45
+ const first = renderDocsIndex(await collectDocsPages());
46
+ const second = renderDocsIndex(await collectDocsPages());
47
+ expect(second).toBe(first);
48
+ });
49
+
50
+ test("the seeded skill is a system default and carries the index", async () => {
51
+ const skill = (await loadSeedSkills()).find((s) => s.name === "swarm-docs");
52
+ expect(skill?.systemDefault).toBe(true);
53
+ expect(skill?.content).toContain("https://docs.agent-swarm.dev/docs/playbooks/open-oncall");
54
+ });
55
+ });
56
+
57
+ describe("docs index (fixture tree)", () => {
58
+ let root: string;
59
+
60
+ beforeAll(async () => {
61
+ root = await mkdtemp(join(tmpdir(), "docs-index-"));
62
+ await mkdir(join(root, "playbooks"), { recursive: true });
63
+ await mkdir(join(root, "guides"), { recursive: true });
64
+ await mkdir(join(root, "releases"), { recursive: true });
65
+ await Bun.write(join(root, "meta.json"), JSON.stringify({ pages: ["index", "playbooks"] }));
66
+ await Bun.write(join(root, "index.mdx"), page("title: Home"));
67
+ await Bun.write(
68
+ join(root, "playbooks", "meta.json"),
69
+ JSON.stringify({ pages: ["index", "b", "---Separator---", "a"] }),
70
+ );
71
+ await Bun.write(
72
+ join(root, "playbooks", "index.mdx"),
73
+ page("title: Playbooks\nagent:\n offer: false"),
74
+ );
75
+ await Bun.write(
76
+ join(root, "playbooks", "a.mdx"),
77
+ page('title: A\nagent:\n when: "Someone wants A"\n template: "community/a"'),
78
+ );
79
+ await Bun.write(join(root, "playbooks", "b.mdx"), page("title: B"));
80
+ await Bun.write(
81
+ join(root, "guides", "g.mdx"),
82
+ page("title: G\nagent:\n offer: true\n notWhen: never"),
83
+ );
84
+ await Bun.write(join(root, "releases", "r.mdx"), page("title: R"));
85
+ });
86
+
87
+ afterAll(async () => {
88
+ await rm(root, { recursive: true, force: true });
89
+ });
90
+
91
+ test("orders by meta.json, appends unlisted folders, and skips releases", async () => {
92
+ const pages = await collectDocsPages(root);
93
+ expect(pages.map((p) => p.path)).toEqual([
94
+ "index",
95
+ "playbooks/index",
96
+ "playbooks/b",
97
+ "playbooks/a",
98
+ "guides/g",
99
+ ]);
100
+ expect(pages.map((p) => p.slug)).toEqual([
101
+ "",
102
+ "playbooks",
103
+ "playbooks/b",
104
+ "playbooks/a",
105
+ "guides/g",
106
+ ]);
107
+ });
108
+
109
+ test("a playbook without when, and an offered guide without when, fail coverage", async () => {
110
+ const errors = coverageErrors(await collectDocsPages(root));
111
+ expect(errors).toEqual([
112
+ "playbooks/b: playbook needs agent.when (or agent.offer: false)",
113
+ "guides/g: agent.offer is true but agent.when is missing",
114
+ ]);
115
+ });
116
+
117
+ test("renders triggers and templates for offered pages only", async () => {
118
+ const index = renderDocsIndex(await collectDocsPages(root));
119
+ expect(index).toContain(
120
+ "- [A](https://docs.agent-swarm.dev/docs/playbooks/a). When: Someone wants A. Template: `community/a`.",
121
+ );
122
+ expect(index).toContain(
123
+ "- [Playbooks](https://docs.agent-swarm.dev/docs/playbooks). Reference.",
124
+ );
125
+ expect(index).toContain("- [Home](https://docs.agent-swarm.dev/docs)");
126
+ });
127
+
128
+ test("replaceIndexBlock swaps only the marked block and rejects missing markers", () => {
129
+ const content = `before\n${INDEX_START}\nold\n${INDEX_END}\nafter\n`;
130
+ expect(replaceIndexBlock(content, `${INDEX_START}\nnew\n${INDEX_END}`)).toBe(
131
+ `before\n${INDEX_START}\nnew\n${INDEX_END}\nafter\n`,
132
+ );
133
+ expect(() => replaceIndexBlock("no markers", "x")).toThrow();
134
+ });
135
+ });
@@ -376,7 +376,7 @@ describe("dsh harness", () => {
376
376
  await rename(join(config.cwd, "dsh"), join(config.cwd, "npx"));
377
377
  config.env = { ...config.env, DSH_BINARY: "", PATH: config.cwd };
378
378
  await expect(new DshAdapter().createSession(config)).rejects.toThrow(
379
- "dsh CLI not found. Install @deepseek-ai/dsh@0.2.1-rc.3",
379
+ "dsh CLI not found. Install @deepseek-ai/dsh@0.2.1-rc.4",
380
380
  );
381
381
  expect(await Bun.file(join(config.cwd, "invocation.json")).exists()).toBe(false);
382
382
  });
@@ -14,6 +14,7 @@ import {
14
14
  createWorkflowRunStep,
15
15
  getDbClient,
16
16
  getKv,
17
+ getMostRecentTaskInThread,
17
18
  getTaskByWorkflowRunStepId,
18
19
  initDb,
19
20
  startTask,
@@ -22,7 +23,7 @@ import * as dbClient from "../be/db-client";
22
23
  import type { InstallExtensionArgs as ExtensionInstallBody } from "../be/extensions/db";
23
24
  import { installExtension, listExtensionRuns } from "../be/extensions/db";
24
25
  import { getCatalogEntry } from "../extensions/catalog";
25
- import type { TaskBlockedEvent } from "../extensions/contract";
26
+ import type { TaskBlockedEvent, TaskCreateEvent } from "../extensions/contract";
26
27
  import { disableExtension, enableExtension, stopExtensionRuntime } from "../extensions/lifecycle";
27
28
  import { initExtensionPostBridge, teardownExtensionPostBridge } from "../extensions/post-bridge";
28
29
  import { handleTasks } from "../http/tasks";
@@ -551,6 +552,57 @@ export default extension;
551
552
  );
552
553
  });
553
554
 
555
+ test("Slack event metadata retains mixed-sender batches and rejects metadata rewrites", async () => {
556
+ await createAgent({ name: "slack-metadata-lead", isLead: true, status: "idle" });
557
+ await installSource(
558
+ "a-rewrite-slack-metadata",
559
+ `import type { SwarmExtension } from "swarm-extension";
560
+ const extension: SwarmExtension = (api) => {
561
+ api.on("pre.task.create", () => ({ action: "modify", data: {
562
+ priority: 7, slack: { trigger: "mention", messages: [] }
563
+ } as never }));
564
+ };
565
+ export default extension;
566
+ `,
567
+ );
568
+ const recorder = await installAndEnable("record-task-create");
569
+ const channelId = "C_EVENT_METADATA";
570
+ const threadTs = "3000.0001";
571
+ bufferThreadMessage(channelId, threadTs, "first request", "U_FIRST", "3000.0002");
572
+ bufferThreadMessage(channelId, threadTs, "second request", "U_SECOND", "3000.0003");
573
+
574
+ await instantFlush(`${channelId}:${threadTs}`);
575
+
576
+ const event = (await getKv(`task:agent:${recorder.agentId}`, "ext:record-task-create:last"))
577
+ ?.value as TaskCreateEvent & { slack?: unknown };
578
+ expect(event.slack).toEqual({
579
+ trigger: "thread-buffer",
580
+ messages: [
581
+ { ts: "3000.0002", userId: "U_FIRST", text: "first request" },
582
+ { ts: "3000.0003", userId: "U_SECOND", text: "second request" },
583
+ ],
584
+ });
585
+ expect(event.origin).toBe("slack");
586
+ expect(event.options).toMatchObject({
587
+ slackTriggerMessageTs: "3000.0003",
588
+ slackUserId: "U_FIRST",
589
+ priority: 7,
590
+ });
591
+ expect(event.options).not.toHaveProperty("slack");
592
+ const task = await getMostRecentTaskInThread(channelId, threadTs);
593
+ expect(task).toMatchObject({
594
+ priority: 7,
595
+ slackTriggerMessageTs: "3000.0003",
596
+ slackUserId: "U_FIRST",
597
+ });
598
+ expect(task).not.toHaveProperty("slack");
599
+
600
+ await createTaskWithSiblingAwareness("ordinary API task", { source: "api" });
601
+ const apiEvent = (await getKv(`task:agent:${recorder.agentId}`, "ext:record-task-create:last"))
602
+ ?.value as TaskCreateEvent & { slack?: unknown };
603
+ expect(apiEvent).not.toHaveProperty("slack");
604
+ });
605
+
554
606
  test("scheduler and workflow executor provide their task creation origins", async () => {
555
607
  const extension = await installSource(
556
608
  "record-task-origin",
@@ -41,8 +41,12 @@ const defaultMockResponse: MockResponse = {
41
41
  const mockResponsesByAgentId = new Map<string, MockResponse>();
42
42
  const requestedRepoIds = new Map<string, string | null>();
43
43
  const requestedTaskIds = new Map<string, string | null>();
44
- /** Every `GET /api/keys/available` query string observed by the mock server, most recent last. */
44
+ /**
45
+ * Every `GET /api/keys/available` query string observed by the mock server,
46
+ * most recent last, without `keySuffixes` (recorded in `keysAvailableSuffixes`).
47
+ */
45
48
  const keysAvailableRequests: string[] = [];
49
+ const keysAvailableSuffixes: Array<string | null> = [];
46
50
 
47
51
  beforeAll(() => {
48
52
  server = Bun.serve({
@@ -63,7 +67,10 @@ beforeAll(() => {
63
67
  }
64
68
 
65
69
  if (url.pathname === "/api/keys/available") {
66
- keysAvailableRequests.push(url.search);
70
+ const params = new URLSearchParams(url.search);
71
+ keysAvailableSuffixes.push(params.get("keySuffixes"));
72
+ params.delete("keySuffixes");
73
+ keysAvailableRequests.push(`?${params}`);
67
74
  const totalKeys = Number(url.searchParams.get("totalKeys") ?? "1");
68
75
  const availableIndices = Array.from({ length: totalKeys }, (_, i) => i);
69
76
  return Response.json({ success: true, availableIndices, totalKeys });
@@ -455,6 +462,13 @@ describe("fetchResolvedEnv", () => {
455
462
  expect(keysAvailableRequests).toEqual([
456
463
  "?keyType=CLAUDE_CODE_OAUTH_TOKEN&totalKeys=32&model=fable",
457
464
  ]);
465
+ // The pool's own suffixes, in order, so status is matched by key (issue #2425).
466
+ expect(keysAvailableSuffixes.at(-1)).toBe(
467
+ Object.values(envWithOauthPool(32))[0]!
468
+ .split(",")
469
+ .map((key) => key.slice(-5))
470
+ .join(","),
471
+ );
458
472
  });
459
473
 
460
474
  test("model: 'claude-fable-5-1' issues the same model=fable", async () => {
@@ -0,0 +1,9 @@
1
+ import type { SwarmExtension } from "swarm-extension";
2
+
3
+ const extension: SwarmExtension = (api) => {
4
+ api.on("pre.task.create", async (event, ctx) => {
5
+ await ctx.state.set("last", event);
6
+ });
7
+ };
8
+
9
+ export default extension;
@@ -0,0 +1,7 @@
1
+ {
2
+ "name": "record-task-create",
3
+ "description": "Records task creation events",
4
+ "version": "1.0.0",
5
+ "runtime": "api",
6
+ "assets": { "hooks": "hooks.ts" }
7
+ }
@@ -326,6 +326,14 @@ describe("GET /api/fleet-benchmark", () => {
326
326
  expect(fetched).toHaveLength(1);
327
327
  });
328
328
 
329
+ test("a use case the catalog does not list yet compares against the whole fleet", async () => {
330
+ await addTasks(10, "completed", 1);
331
+ await setProfile("2-10", "content");
332
+ const response = await buildFleetBenchmarkResponse(NOW);
333
+ expect(response).toMatchObject({ state: "ready", level: "full", peerGroup: null });
334
+ expect(fetched).toEqual(["http://proxy.test/v1/fleet/benchmarks?window=30d"]);
335
+ });
336
+
329
337
  test("full profile tolerates absent or null peer_group from the proxy", async () => {
330
338
  await addTasks(10, "completed", 1);
331
339
  await setProfile();
@@ -0,0 +1,98 @@
1
+ import { afterAll, beforeAll, expect, test } from "bun:test";
2
+ import { unlink } from "node:fs/promises";
3
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
+ import {
5
+ closeDb,
6
+ createWorkflow,
7
+ createWorkflowRun,
8
+ createWorkflowRunStep,
9
+ initDb,
10
+ updateWorkflowRun,
11
+ updateWorkflowRunStep,
12
+ } from "../be/db";
13
+ import { registerGetWorkflowRunTool } from "../tools/workflows/get-workflow-run";
14
+
15
+ const dbPath = `./test-get-workflow-run-tool-${process.pid}.sqlite`;
16
+
17
+ type ToolResult = {
18
+ isError?: boolean;
19
+ structuredContent: {
20
+ message: string;
21
+ run?: Record<string, unknown>;
22
+ steps?: Array<Record<string, unknown>>;
23
+ };
24
+ };
25
+ let handler: (args: unknown, extra: unknown) => Promise<ToolResult>;
26
+ let runId: string;
27
+
28
+ async function removeDb() {
29
+ for (const suffix of ["", "-wal", "-shm"]) await unlink(dbPath + suffix).catch(() => undefined);
30
+ }
31
+
32
+ beforeAll(async () => {
33
+ await removeDb();
34
+ initDb(dbPath);
35
+ const workflow = await createWorkflow({
36
+ name: "get-workflow-run-tool",
37
+ definition: { nodes: [{ id: "poll", type: "notify", config: { template: "x" } }] },
38
+ });
39
+ runId = crypto.randomUUID();
40
+ await createWorkflowRun({ id: runId, workflowId: workflow.id });
41
+ const context = { poll: { attempts: 2 }, blob: "x".repeat(2_000) };
42
+ for (let i = 0; i < 2; i++) {
43
+ const step = await createWorkflowRunStep({
44
+ id: crypto.randomUUID(),
45
+ runId,
46
+ nodeId: "poll",
47
+ nodeType: "notify",
48
+ input: { ...context, iteration: i },
49
+ });
50
+ await updateWorkflowRunStep(step.id, {
51
+ status: "completed",
52
+ output: { iteration: i },
53
+ retryCount: i,
54
+ finishedAt: new Date().toISOString(),
55
+ });
56
+ }
57
+ await updateWorkflowRun(runId, { status: "completed", context });
58
+
59
+ const server = new McpServer({ name: "get-workflow-run-tool", version: "1.0.0" });
60
+ registerGetWorkflowRunTool(server);
61
+ handler = (server as unknown as { _registeredTools: Record<string, { handler: typeof handler }> })
62
+ ._registeredTools["get-workflow-run"]!.handler;
63
+ });
64
+
65
+ afterAll(async () => {
66
+ closeDb();
67
+ await removeDb();
68
+ });
69
+
70
+ const extra = { sessionId: "get-workflow-run-tool", requestInfo: { headers: {} } };
71
+
72
+ test("defaults to the full run with context and step inputs", async () => {
73
+ const result = await handler({ id: runId }, extra);
74
+ expect(result.isError).not.toBe(true);
75
+ const { run, steps } = result.structuredContent;
76
+ expect(run?.context).toBeDefined();
77
+ expect(steps).toHaveLength(2);
78
+ for (const step of steps!) expect(step.input).toBeDefined();
79
+ });
80
+
81
+ test("fields=slim drops context and step inputs, keeps outputs and retry fields", async () => {
82
+ const result = await handler({ id: runId, fields: "slim" }, extra);
83
+ expect(result.isError).not.toBe(true);
84
+ const { run, steps } = result.structuredContent;
85
+ expect(run?.status).toBe("completed");
86
+ expect(run?.context).toBeUndefined();
87
+ expect(steps).toHaveLength(2);
88
+ for (const step of steps!) {
89
+ expect(step.input).toBeUndefined();
90
+ expect(step.output).toBeDefined();
91
+ expect(typeof step.retryCount).toBe("number");
92
+ }
93
+ });
94
+
95
+ test("fields=slim reports a missing run as an error", async () => {
96
+ const result = await handler({ id: crypto.randomUUID(), fields: "slim" }, extra);
97
+ expect(result.isError).toBe(true);
98
+ });
@@ -24,6 +24,7 @@ import {
24
24
  upsertSwarmConfig,
25
25
  } from "../be/db";
26
26
  import { getScript, upsertScriptByName } from "../be/scripts/db";
27
+ import * as savedScriptRunner from "../be/scripts/run-saved";
27
28
  import { runSavedScriptAsAgent } from "../be/scripts/run-saved";
28
29
  import {
29
30
  checkHeartbeatChecklist,
@@ -624,6 +625,66 @@ describe("Heartbeat Checklist", () => {
624
625
  });
625
626
  });
626
627
 
628
+ for (const quiet of [true, false]) {
629
+ test(`healthy probe preserves the saved gate quiet=${quiet} verdict`, async () => {
630
+ process.env.HEARTBEAT_CHECKLIST_GATE_MODE = "enforce";
631
+ await saveGlobalGateScript("quiet-gate", "export default () => ({ quiet: true });");
632
+ const saved = spyOn(savedScriptRunner, "runSavedScriptAsAgent").mockResolvedValue({
633
+ exitCode: 0,
634
+ result: { quiet, reason: "existing verdict" },
635
+ stdout: "",
636
+ stderr: "",
637
+ durationMs: 1,
638
+ truncated: { stdout: false, stderr: false },
639
+ });
640
+ try {
641
+ setChecklistGateRunnerForTests((name, agentId, signal) =>
642
+ defaultChecklistGateRunner(name, agentId, signal, { probe: async () => null }),
643
+ );
644
+ await seedLead();
645
+ await checkHeartbeatChecklist();
646
+ expect(saved).toHaveBeenCalledTimes(1);
647
+ const tasks = await checklistTasks();
648
+ expect(tasks).toHaveLength(quiet ? 0 : 1);
649
+ if (!quiet) expect(tasks[0]!.tags).toContain("heartbeat-gate:wake");
650
+ } finally {
651
+ saved.mockRestore();
652
+ }
653
+ });
654
+ }
655
+
656
+ for (const mode of ["shadow", "enforce"] as const) {
657
+ test(`${mode}: an unhealthy probe produces a wake checklist`, async () => {
658
+ process.env.HEARTBEAT_CHECKLIST_GATE_MODE = mode;
659
+ setChecklistGateRunnerForTests((name, agentId, signal) =>
660
+ defaultChecklistGateRunner(name, agentId, signal, {
661
+ probe: async () => ({ quiet: false, reason: "script-runner-unhealthy: timeout" }),
662
+ }),
663
+ );
664
+ await seedLead();
665
+ await checkHeartbeatChecklist();
666
+ const tasks = await checklistTasks();
667
+ expect(tasks).toHaveLength(1);
668
+ expect(tasks[0]!.tags).toContain("heartbeat-gate:wake");
669
+ });
670
+ }
671
+
672
+ test("a bug in the probe produces wake rather than error", async () => {
673
+ process.env.HEARTBEAT_CHECKLIST_GATE_MODE = "enforce";
674
+ setChecklistGateRunnerForTests((name, agentId, signal) =>
675
+ defaultChecklistGateRunner(name, agentId, signal, {
676
+ probe: async () => {
677
+ throw new Error("probe bug");
678
+ },
679
+ }),
680
+ );
681
+ await seedLead();
682
+ await checkHeartbeatChecklist();
683
+ const tasks = await checklistTasks();
684
+ expect(tasks).toHaveLength(1);
685
+ expect(tasks[0]!.tags).toContain("heartbeat-gate:wake");
686
+ });
687
+
627
688
  test("shadow mode (default): a quiet verdict still creates the task, tagged quiet", async () => {
628
689
  useGate({ quiet: true, reason: "no change", summary: "nothing moved" });
629
690
  const lead = await seedLead();
@@ -703,6 +764,9 @@ describe("Heartbeat Checklist", () => {
703
764
  test("enforce mode: a missing global script fails open through the real runner", async () => {
704
765
  process.env.HEARTBEAT_CHECKLIST_GATE_MODE = "enforce";
705
766
  process.env.HEARTBEAT_CHECKLIST_GATE_SCRIPT = "no-such-gate-script";
767
+ setChecklistGateRunnerForTests((name, agentId, signal) =>
768
+ defaultChecklistGateRunner(name, agentId, signal, { probe: async () => null }),
769
+ );
706
770
  await seedLead();
707
771
 
708
772
  await checkHeartbeatChecklist();
@@ -739,6 +803,7 @@ describe("Heartbeat Checklist", () => {
739
803
  let run: Promise<unknown> | undefined;
740
804
  setChecklistGateRunnerForTests((scriptName, leadAgentId, signal) => {
741
805
  run = defaultChecklistGateRunner(scriptName, leadAgentId, signal, {
806
+ probe: async () => null,
742
807
  buildCredentials: () => {
743
808
  setupStarted.resolve();
744
809
  return new Promise((resolve) => {
@@ -798,6 +863,9 @@ describe("Heartbeat Checklist", () => {
798
863
  "export default async function run() { await new Promise((r) => setTimeout(r, 30_000)); return { quiet: true }; }",
799
864
  );
800
865
  setChecklistGateTimeoutMsForTests(1_500);
866
+ setChecklistGateRunnerForTests((name, agentId, signal) =>
867
+ defaultChecklistGateRunner(name, agentId, signal, { probe: async () => null }),
868
+ );
801
869
  await seedLead();
802
870
 
803
871
  const started = Date.now();
@@ -808,6 +876,27 @@ describe("Heartbeat Checklist", () => {
808
876
  expect(tasks).toHaveLength(1);
809
877
  expect(tasks[0]!.tags).toContain("heartbeat-gate:error");
810
878
  });
879
+
880
+ test("enforce mode: the real probe passes with no swarm API key, using the Lead's run token", async () => {
881
+ process.env.HEARTBEAT_CHECKLIST_GATE_MODE = "enforce";
882
+ await saveGlobalGateScript("quiet-gate", "export default () => ({ quiet: true });");
883
+ const savedKeys = { swarm: process.env.AGENT_SWARM_API_KEY, legacy: process.env.API_KEY };
884
+ process.env.AGENT_SWARM_API_KEY = "";
885
+ process.env.API_KEY = "";
886
+ try {
887
+ await seedLead();
888
+ await checkHeartbeatChecklist();
889
+ expect(await checklistTasks()).toHaveLength(0);
890
+ } finally {
891
+ for (const [name, value] of [
892
+ ["AGENT_SWARM_API_KEY", savedKeys.swarm],
893
+ ["API_KEY", savedKeys.legacy],
894
+ ] as const) {
895
+ if (value === undefined) delete process.env[name];
896
+ else process.env[name] = value;
897
+ }
898
+ }
899
+ });
811
900
  });
812
901
 
813
902
  // ==========================================================================
@@ -0,0 +1,83 @@
1
+ import { afterEach, describe, expect, mock, spyOn, test } from "bun:test";
2
+ import { probeScriptRunner } from "../heartbeat/script-runner-probe";
3
+ import type { RunScriptOutput } from "../scripts-runtime/loader";
4
+
5
+ const ok: RunScriptOutput = {
6
+ result: true,
7
+ stdout: "",
8
+ stderr: "",
9
+ truncated: { stdout: false, stderr: false },
10
+ durationMs: 1,
11
+ exitCode: 0,
12
+ };
13
+
14
+ afterEach(() => mock.restore());
15
+
16
+ describe("heartbeat script runner probe", () => {
17
+ test("success leaves the gate verdict unchanged and clears its deadline", async () => {
18
+ const clear = spyOn(globalThis, "clearTimeout");
19
+ const runner = mock(async () => ok);
20
+ expect(await probeScriptRunner("lead", runner)).toBeNull();
21
+ expect(runner).toHaveBeenCalledWith({
22
+ source: "export default async function () { return true; }",
23
+ agentId: "lead",
24
+ fsMode: "none",
25
+ timeoutMs: 10_000,
26
+ signal: expect.any(AbortSignal),
27
+ });
28
+ expect(clear).toHaveBeenCalledTimes(1);
29
+ });
30
+
31
+ test("timeout wakes without waiting for a stalled runner and aborts it", async () => {
32
+ let expire: (() => void) | undefined;
33
+ spyOn(globalThis, "setTimeout").mockImplementation(((callback: () => void, ms: number) => {
34
+ expect(ms).toBe(10_000);
35
+ expire = callback;
36
+ return 1;
37
+ }) as typeof setTimeout);
38
+ const clear = spyOn(globalThis, "clearTimeout").mockImplementation(() => {});
39
+ let signal: AbortSignal | undefined;
40
+ let rejectRun: ((error: Error) => void) | undefined;
41
+ const result = probeScriptRunner("lead", async (input) => {
42
+ signal = input.signal;
43
+ return new Promise<RunScriptOutput>((_, reject) => {
44
+ rejectRun = reject;
45
+ });
46
+ });
47
+ expire!();
48
+ expect(await result).toEqual({
49
+ quiet: false,
50
+ reason: "script-runner-unhealthy: timeout after 10000ms",
51
+ });
52
+ expect(signal?.aborted).toBe(true);
53
+ expect(clear).toHaveBeenCalledTimes(1);
54
+ // A runner that rejects after the deadline remains handled by Promise.race.
55
+ rejectRun!(new Error("late rejection"));
56
+ await Promise.resolve();
57
+ });
58
+
59
+ test("a thrown runner error wakes", async () => {
60
+ expect(
61
+ await probeScriptRunner("lead", async () => {
62
+ throw new Error("ECONNRESET");
63
+ }),
64
+ ).toEqual({
65
+ quiet: false,
66
+ reason: "script-runner-unhealthy: ECONNRESET",
67
+ });
68
+ });
69
+
70
+ for (const [label, failure, reason] of [
71
+ ["nonzero exit", { exitCode: 1 }, "script exited with code 1"],
72
+ ["runner error", { error: "killed" }, "killed"],
73
+ ["runtime error", { runtimeError: { message: "runtime failed" } }, "runtime failed"],
74
+ ] as const) {
75
+ test(`${label} wakes`, async () => {
76
+ const result = await probeScriptRunner(
77
+ "lead",
78
+ async () => ({ ...ok, ...failure }) as RunScriptOutput,
79
+ );
80
+ expect(result).toEqual({ quiet: false, reason: `script-runner-unhealthy: ${reason}` });
81
+ });
82
+ }
83
+ });
@@ -21,6 +21,7 @@ const TEST_DB_PATH = "./test-integrations-http.sqlite";
21
21
 
22
22
  interface FakeClientLog {
23
23
  retrieveCalls: string[];
24
+ apiKeys: string[];
24
25
  retrieveResult?: { name?: string | null; model?: string | null };
25
26
  retrieveError?: Error;
26
27
  }
@@ -60,7 +61,7 @@ async function clearManagedConfigRows() {
60
61
  describe("POST /api/integrations/claude-managed/test", () => {
61
62
  let server: Server;
62
63
  let baseUrl = "";
63
- const log: FakeClientLog = { retrieveCalls: [] };
64
+ const log: FakeClientLog = { retrieveCalls: [], apiKeys: [] };
64
65
  const savedEnv = {
65
66
  ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY,
66
67
  MANAGED_AGENT_ID: process.env.MANAGED_AGENT_ID,
@@ -71,7 +72,10 @@ describe("POST /api/integrations/claude-managed/test", () => {
71
72
  initDb(TEST_DB_PATH);
72
73
 
73
74
  const handler = createIntegrationsHandler({
74
- buildClient: () => buildFakeClient(log),
75
+ buildClient: (apiKey) => {
76
+ log.apiKeys.push(apiKey);
77
+ return buildFakeClient(log);
78
+ },
75
79
  });
76
80
 
77
81
  server = createHttpServer(async (req, res) => {
@@ -101,6 +105,7 @@ describe("POST /api/integrations/claude-managed/test", () => {
101
105
 
102
106
  beforeEach(async () => {
103
107
  log.retrieveCalls = [];
108
+ log.apiKeys = [];
104
109
  log.retrieveResult = undefined;
105
110
  log.retrieveError = undefined;
106
111
  await clearManagedConfigRows();
@@ -138,6 +143,31 @@ describe("POST /api/integrations/claude-managed/test", () => {
138
143
  expect(log.retrieveCalls).toEqual(["agent_abc123"]);
139
144
  });
140
145
 
146
+ test("a comma pool sends one key to Anthropic", async () => {
147
+ await upsertSwarmConfig({
148
+ scope: "global",
149
+ key: "ANTHROPIC_API_KEY",
150
+ value: "sk-ant-first,sk-ant-second",
151
+ isSecret: true,
152
+ });
153
+ await upsertSwarmConfig({
154
+ scope: "global",
155
+ key: "MANAGED_AGENT_ID",
156
+ value: "agent_abc123",
157
+ isSecret: false,
158
+ });
159
+
160
+ const res = await fetch(`${baseUrl}/api/integrations/claude-managed/test`, {
161
+ method: "POST",
162
+ headers: { "Content-Type": "application/json" },
163
+ body: "{}",
164
+ });
165
+
166
+ expect((await res.json()) as { ok: boolean }).toMatchObject({ ok: true });
167
+ expect(log.apiKeys).toHaveLength(1);
168
+ expect(["sk-ant-first", "sk-ant-second"]).toContain(log.apiKeys[0]);
169
+ });
170
+
141
171
  test("missing-config path — returns ok:false with helpful error", async () => {
142
172
  // No swarm_config rows, no env. resolveConfigValue should return null
143
173
  // for both keys and short-circuit before calling Anthropic.