@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
@@ -100,7 +100,7 @@ describe("findTaskByVcs", () => {
100
100
  agentId: "vcs-lead-001",
101
101
  });
102
102
 
103
- const found = await findTaskByVcs("findme/gh-repo", 100);
103
+ const found = await findTaskByVcs("findme/gh-repo", 100, "github");
104
104
  expect(found).not.toBeNull();
105
105
  expect(found?.vcsProvider).toBe("github");
106
106
  });
@@ -114,7 +114,7 @@ describe("findTaskByVcs", () => {
114
114
  agentId: "vcs-lead-001",
115
115
  });
116
116
 
117
- const found = await findTaskByVcs("findme/gl-project", 200);
117
+ const found = await findTaskByVcs("findme/gl-project", 200, "gitlab");
118
118
  expect(found).not.toBeNull();
119
119
  expect(found?.vcsProvider).toBe("gitlab");
120
120
  });
@@ -133,12 +133,12 @@ describe("findTaskByVcs", () => {
133
133
  await db.startTask(task.id);
134
134
  await db.completeTask(task.id, "Done");
135
135
 
136
- const found = await findTaskByVcs("findme/completed", 300);
136
+ const found = await findTaskByVcs("findme/completed", 300, "gitlab");
137
137
  expect(found).toBeNull();
138
138
  });
139
139
 
140
140
  test("returns null for non-existent repo/number", async () => {
141
- const found = await findTaskByVcs("nonexistent/repo", 9999);
141
+ const found = await findTaskByVcs("nonexistent/repo", 9999, "github");
142
142
  expect(found).toBeNull();
143
143
  });
144
144
  });
@@ -12,6 +12,8 @@
12
12
  */
13
13
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
14
14
  import { initAgentMail, isAgentMailEnabled, resetAgentMail } from "../agentmail/app";
15
+ import { validateConfigValue } from "../be/swarm-config-guard";
16
+ import { isBitbucketEnabled, resetBitbucket } from "../bitbucket/auth";
15
17
  import { initGitHub, isGitHubEnabled, resetGitHub } from "../github/app";
16
18
  import { isJiraEnabled, resetJira } from "../jira/app";
17
19
  import { isLinearEnabled, resetLinear } from "../linear/app";
@@ -28,6 +30,8 @@ const ENV_KEYS = [
28
30
  "LINEAR_ENABLED",
29
31
  "AGENTMAIL_DISABLE",
30
32
  "AGENTMAIL_WEBHOOK_SECRET",
33
+ "BITBUCKET_DISABLE",
34
+ "BITBUCKET_WEBHOOK_SECRET",
31
35
  ] as const;
32
36
  const originalEnv = new Map(ENV_KEYS.map((key) => [key, process.env[key]]));
33
37
 
@@ -45,6 +49,7 @@ afterEach(() => {
45
49
  resetAgentMail();
46
50
  resetJira();
47
51
  resetLinear();
52
+ resetBitbucket();
48
53
  for (const key of ENV_KEYS) {
49
54
  const value = originalEnv.get(key);
50
55
  if (value === undefined) delete process.env[key];
@@ -185,6 +190,37 @@ describe("AgentMail AGENTMAIL_DISABLE", () => {
185
190
  });
186
191
  });
187
192
 
193
+ describe("Bitbucket BITBUCKET_DISABLE", () => {
194
+ test("every truthy spelling disables the integration", () => {
195
+ process.env.BITBUCKET_WEBHOOK_SECRET = "secret";
196
+ for (const spelling of TRUTHY_SPELLINGS) {
197
+ process.env.BITBUCKET_DISABLE = spelling;
198
+ expect(isBitbucketEnabled()).toBe(false);
199
+ }
200
+ });
201
+
202
+ test("non-disabling values keep the integration enabled", () => {
203
+ process.env.BITBUCKET_WEBHOOK_SECRET = "secret";
204
+ for (const value of NON_DISABLING) {
205
+ if (value === undefined) delete process.env.BITBUCKET_DISABLE;
206
+ else process.env.BITBUCKET_DISABLE = value;
207
+ expect(isBitbucketEnabled()).toBe(true);
208
+ }
209
+ });
210
+
211
+ test("stays disabled without a webhook secret", () => {
212
+ expect(isBitbucketEnabled()).toBe(false);
213
+ });
214
+
215
+ test("the config guard validates BITBUCKET_DISABLE as a boolean like GITLAB_DISABLE", () => {
216
+ for (const key of ["BITBUCKET_DISABLE", "GITLAB_DISABLE"]) {
217
+ expect(validateConfigValue(key, "true")).toBeNull();
218
+ expect(validateConfigValue(key, "false")).toBeNull();
219
+ expect(validateConfigValue(key, "maybe")).not.toBeNull();
220
+ }
221
+ });
222
+ });
223
+
188
224
  describe("Slack SLACK_DISABLE", () => {
189
225
  test("every truthy spelling reports disabled", () => {
190
226
  for (const spelling of TRUTHY_SPELLINGS) {
@@ -52,7 +52,7 @@ describe("script MCP tools", () => {
52
52
  "script-upsert":
53
53
  'Typecheck and persist a TypeScript script to the swarm catalog under your agent scope (or global if you\'re a lead). Import `ScriptContext` from "swarm-sdk" for a real context type. Other agents and workflow nodes will be able to find and run it. For local-only scripts, use code-mode `save`.',
54
54
  "script-delete":
55
- "Remove a swarm-shared script from the catalog. Versions table preserves history.",
55
+ "Permanently remove a swarm-shared script and all its versions from the catalog. This cannot be undone.",
56
56
  "script-query-types":
57
57
  // Updated for the frozen SwarmToolResult contract refactor: `name` is
58
58
  // now optional — omit it to get the swarm-wide type surface instead
@@ -34,6 +34,7 @@ const ENV_KEYS = [
34
34
  "EMBEDDING_MODEL",
35
35
  "GITHUB_TOKEN",
36
36
  "GITLAB_TOKEN",
37
+ "BITBUCKET_TOKEN",
37
38
  "SLACK_BOT_TOKEN",
38
39
  "SLACK_APP_TOKEN",
39
40
  "SLACK_MODE",
@@ -380,6 +381,17 @@ describe("onboarding state", () => {
380
381
  expect(body.state.steps.integrations).toMatchObject({ status: "done", method: "github" });
381
382
  });
382
383
 
384
+ test("GET derives the Bitbucket integration from BITBUCKET_TOKEN", async () => {
385
+ process.env.BITBUCKET_TOKEN = "example-bitbucket-token";
386
+ const body = (await (await request("/api/onboarding")).json()) as {
387
+ state: OnboardingState;
388
+ signals: { integrations: Record<string, boolean> };
389
+ };
390
+
391
+ expect(body.signals.integrations.bitbucket).toBe(true);
392
+ expect(body.state.steps.integrations).toMatchObject({ status: "done", method: "bitbucket" });
393
+ });
394
+
383
395
  test("the selected first task completes onboarding after that task completes", async () => {
384
396
  // Onboarding starts before the first task exists (the task alone would mark an existing install).
385
397
  await request("/api/onboarding");
@@ -340,8 +340,14 @@ describe("bounded codemode", () => {
340
340
  undefined,
341
341
  fakeCtx().ctx,
342
342
  );
343
- expect(Date.now() - started).toBeLessThan(5_000);
344
- expect(textOf(result)).toContain("Codemode worker limit reached");
343
+ const elapsed = Date.now() - started;
344
+ expect(elapsed).toBeGreaterThanOrEqual(250);
345
+ expect(elapsed).toBeLessThan(5_000);
346
+ // pi 1.4 queues the script for a worker; our deadline ends the wait.
347
+ expect(textOf(result)).toContain(
348
+ "Script aborted: Codemode script exceeded its 300 ms deadline",
349
+ );
350
+ expect(textOf(result)).toContain("The script did not start.");
345
351
  } finally {
346
352
  holders.abort(new Error("test done"));
347
353
  await Promise.all(holding);
@@ -0,0 +1,57 @@
1
+ import { expect, test } from "bun:test";
2
+ import { mkdtemp, rm } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { CHILD_PROCESS_TEST_BUDGET_MS, expectChildOk, runChild } from "./test-proc";
6
+
7
+ test(
8
+ "preload clears inherited worker configuration before test files load",
9
+ async () => {
10
+ const directory = await mkdtemp(join(tmpdir(), "swarm-preload-env-"));
11
+ const fixture = join(directory, "worker-env.test.ts");
12
+ const keys = [
13
+ "SLACK_RENDER_V2",
14
+ "SLACK_STATUS_ICON_COMPLETED",
15
+ "SLACK_FUTURE_SETTING",
16
+ "COMB_ENABLED",
17
+ "APP_URL",
18
+ "AGENT_FS_API_URL",
19
+ "AGENT_FS_LIVE_URL",
20
+ "AGENT_FS_DEFAULT_ORG_ID",
21
+ "AGENT_FS_DEFAULT_DRIVE_ID",
22
+ "SWARM_ORG_NAME",
23
+ "SWARM_ORG_WEBSITE",
24
+ ];
25
+ try {
26
+ await Bun.write(
27
+ fixture,
28
+ `import { expect, test } from "bun:test";
29
+ const inherited = ${JSON.stringify(keys)}.filter(key => process.env[key] !== undefined);
30
+ test("worker defaults are cleared before module evaluation", () => {
31
+ expect(inherited).toEqual([]);
32
+ expect(process.env.PRELOAD_ENV_CONTROL).toBe("preserved");
33
+ process.env.SLACK_STATUS_ICON_COMPLETED = "test-icon";
34
+ });
35
+ test("suite configuration survives between tests", () => {
36
+ expect(process.env.SLACK_STATUS_ICON_COMPLETED).toBe("test-icon");
37
+ });
38
+ `,
39
+ );
40
+ const result = expectChildOk(
41
+ await runChild([process.execPath, "test", fixture], {
42
+ cwd: join(import.meta.dir, "../.."),
43
+ env: {
44
+ ...process.env,
45
+ ...Object.fromEntries(keys.map((key) => [key, "inherited-worker-setting"])),
46
+ PRELOAD_ENV_CONTROL: "preserved",
47
+ },
48
+ }),
49
+ "test runner with inherited worker env",
50
+ );
51
+ expect(result.stderr).toContain("2 pass");
52
+ } finally {
53
+ await rm(directory, { recursive: true, force: true });
54
+ }
55
+ },
56
+ CHILD_PROCESS_TEST_BUDGET_MS,
57
+ );
@@ -22,6 +22,27 @@ import { startHangWatchdog } from "./hang-watchdog";
22
22
 
23
23
  startHangWatchdog();
24
24
 
25
+ // Worker deployments configure Slack rendering, links, and organization defaults.
26
+ // Start tests from the same unset defaults as CI; suites can still opt in by
27
+ // setting their own values after the preload runs.
28
+ for (const key of Object.keys(process.env)) {
29
+ if (
30
+ key.startsWith("SLACK_") ||
31
+ [
32
+ "COMB_ENABLED",
33
+ "APP_URL",
34
+ "AGENT_FS_API_URL",
35
+ "AGENT_FS_LIVE_URL",
36
+ "AGENT_FS_DEFAULT_ORG_ID",
37
+ "AGENT_FS_DEFAULT_DRIVE_ID",
38
+ "SWARM_ORG_NAME",
39
+ "SWARM_ORG_WEBSITE",
40
+ ].includes(key)
41
+ ) {
42
+ delete process.env[key];
43
+ }
44
+ }
45
+
25
46
  // The API drain (src/be/api-drain.ts) is off unless API_DRAIN_MAX_MS is set,
26
47
  // but a shell can inherit it (a swarm worker container gets the deploy's value).
27
48
  // Test servers are stopped with SIGTERM and their fixtures leave in_progress rows
@@ -6,6 +6,7 @@ import { resolveTemplate } from "../prompts/resolver";
6
6
 
7
7
  // Side-effect imports: register all templates from each source
8
8
  import "../gitlab/templates";
9
+ import "../bitbucket/templates";
9
10
  import "../agentmail/templates";
10
11
  import "../linear/templates";
11
12
  import "../heartbeat/templates";
@@ -28,6 +29,7 @@ async function ensureTemplatesRegistered(): Promise<void> {
28
29
  // module state across parallel test files, a single-template check is racy.
29
30
  const ts = Date.now();
30
31
  await import(`../gitlab/templates?t=${ts}`);
32
+ await import(`../bitbucket/templates?t=${ts}`);
31
33
  await import(`../agentmail/templates?t=${ts}`);
32
34
  await import(`../linear/templates?t=${ts}`);
33
35
  await import(`../heartbeat/templates?t=${ts}`);
@@ -94,6 +96,42 @@ describe("template registration — all sources", () => {
94
96
  expect(eventTypes).toContain("gitlab.pipeline.failed");
95
97
  });
96
98
 
99
+ test("Bitbucket templates are registered (2 common + 3 event)", () => {
100
+ const eventTypes = getAllTemplateDefinitions().map((d) => d.eventType);
101
+ expect(eventTypes).toContain("common.delegation_instruction.bitbucket");
102
+ expect(eventTypes).toContain("common.command_suggestions.bitbucket_pr");
103
+ expect(eventTypes).toContain("bitbucket.pull_request.opened");
104
+ expect(eventTypes).toContain("bitbucket.pull_request.review_requested");
105
+ expect(eventTypes).toContain("bitbucket.comment.mentioned");
106
+ });
107
+
108
+ test("Bitbucket templates resolve with no unresolved variables", () => {
109
+ const shared = {
110
+ pr_id: 42,
111
+ pr_title: "Fix retry",
112
+ repo: "acme-ws/widgets",
113
+ username: "Dana",
114
+ pr_url: "https://bitbucket.org/acme-ws/widgets/pull-requests/42",
115
+ };
116
+ const pr = { ...shared, source_branch: "feat", target_branch: "main", context_section: "" };
117
+ for (const id of ["bitbucket.pull_request.opened", "bitbucket.pull_request.review_requested"]) {
118
+ const result = resolveTemplate(id, pr);
119
+ expect(result.skipped).toBe(false);
120
+ expect(result.unresolved).toEqual([]);
121
+ expect(result.text).toContain("[Bitbucket PR #42]");
122
+ expect(result.text).toContain("api.bitbucket.org/2.0/repositories/acme-ws/widgets");
123
+ }
124
+ const comment = resolveTemplate("bitbucket.comment.mentioned", {
125
+ ...shared,
126
+ location_section: "File: src/a.ts:3\n",
127
+ context: "please fix",
128
+ existing_task_note: "",
129
+ });
130
+ expect(comment.unresolved).toEqual([]);
131
+ expect(comment.text).toContain("File: src/a.ts:3");
132
+ expect(comment.text).toContain("please fix");
133
+ });
134
+
97
135
  test("AgentMail templates are registered (5 event)", () => {
98
136
  const all = getAllTemplateDefinitions();
99
137
  const eventTypes = all.map((d) => d.eventType);
@@ -0,0 +1,39 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { mergeLcov } from "../../scripts/run-root-coverage";
3
+
4
+ describe("mergeLcov", () => {
5
+ test("disjoint function hits remain a conservative lower bound without FN/FNDA identities", () => {
6
+ // a.test.ts calls a(); b.test.ts calls b() in the same two-function module.
7
+ const report = "SF:src/example.ts\nFNF:2\nFNH:1\nend_of_record\n";
8
+ const merged = mergeLcov([report, report]);
9
+
10
+ expect(merged).toContain("FNF:2\nFNH:1\n");
11
+ });
12
+
13
+ test("DA hits sum across reports and LH counts lines hit in any report", () => {
14
+ const merged = mergeLcov([
15
+ "SF:src/example.ts\nDA:1,2\nDA:2,0\nDA:3,0\nend_of_record\n",
16
+ "SF:src/example.ts\nDA:1,3\nDA:2,1\nDA:3,0\nend_of_record\n",
17
+ ]);
18
+
19
+ expect(merged).toBe(
20
+ "TN:\nSF:src/example.ts\nFNF:0\nFNH:0\nDA:1,5\nDA:2,1\nDA:3,0\nLF:3\nLH:2\nend_of_record\n",
21
+ );
22
+ });
23
+
24
+ test("FNH never exceeds FNF when merging function totals", () => {
25
+ const reports = [
26
+ "SF:src/example.ts\nFNF:2\nFNH:2\nend_of_record\n",
27
+ "SF:src/example.ts\nFNF:3\nFNH:1\nend_of_record\n",
28
+ ];
29
+ for (const inputs of [reports, reports.toReversed(), [...reports, ...reports]]) {
30
+ const merged = mergeLcov(inputs);
31
+ const found = Number(merged.match(/^FNF:(\d+)$/m)![1]);
32
+ const hit = Number(merged.match(/^FNH:(\d+)$/m)![1]);
33
+
34
+ expect(found).toBe(3);
35
+ expect(hit).toBe(2);
36
+ expect(hit).toBeLessThanOrEqual(found);
37
+ }
38
+ });
39
+ });
@@ -0,0 +1,191 @@
1
+ /**
2
+ * Worker-side Bitbucket PR linking. There is no Bitbucket CLI in the worker
3
+ * image, so `detectVcsForTask` asks the Bitbucket REST API (via `fetch`) for an
4
+ * open PR on the current branch and PATCHes `/api/tasks/:id/vcs` when it finds
5
+ * one. Both calls go through `globalThis.fetch`, which these tests stub, so no
6
+ * server or port is involved. The git side runs against a real temp repo.
7
+ */
8
+ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from "bun:test";
9
+ import { mkdtemp, rm } from "node:fs/promises";
10
+ import { tmpdir } from "node:os";
11
+ import { join } from "node:path";
12
+ import { detectVcsForTask, findBitbucketOpenPullRequest } from "../commands/runner";
13
+ import { expectChildOk, runChild } from "./test-proc";
14
+
15
+ const API_URL = "http://swarm.invalid";
16
+ const BRANCH = "feature/bb-link";
17
+ const PR_HTML = "https://bitbucket.org/acme-ws/web.app/pull-requests/17";
18
+
19
+ type Call = { url: string; method: string; headers: Headers; body: string };
20
+ type BitbucketReply = { status: number; body: unknown } | "throw";
21
+
22
+ const originalFetch = globalThis.fetch;
23
+ const originalToken = process.env.BITBUCKET_TOKEN;
24
+ let calls: Call[] = [];
25
+ let bitbucketReply: BitbucketReply = { status: 200, body: { values: [] } };
26
+ let repoDir = "";
27
+ let taskSeq = 0;
28
+
29
+ function nextTaskId(): string {
30
+ taskSeq += 1;
31
+ return `00000000-0000-4000-8000-${String(taskSeq).padStart(12, "0")}`;
32
+ }
33
+
34
+ async function git(args: string[]): Promise<void> {
35
+ expectChildOk(await runChild(["git", "-C", repoDir, ...args]), `git ${args.join(" ")}`);
36
+ }
37
+
38
+ function stubFetch(): void {
39
+ globalThis.fetch = (async (input: string | URL | Request, init?: RequestInit) => {
40
+ const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
41
+ calls.push({
42
+ url,
43
+ method: init?.method ?? "GET",
44
+ headers: new Headers(init?.headers),
45
+ body: typeof init?.body === "string" ? init.body : "",
46
+ });
47
+ if (url.startsWith("https://api.bitbucket.org/")) {
48
+ if (bitbucketReply === "throw") throw new TypeError("network down");
49
+ return new Response(JSON.stringify(bitbucketReply.body), {
50
+ status: bitbucketReply.status,
51
+ headers: { "Content-Type": "application/json" },
52
+ });
53
+ }
54
+ return new Response("{}", { status: 200, headers: { "Content-Type": "application/json" } });
55
+ }) as typeof fetch;
56
+ }
57
+
58
+ const bitbucketCalls = () => calls.filter((c) => c.url.startsWith("https://api.bitbucket.org/"));
59
+ const patchCalls = () => calls.filter((c) => c.method === "PATCH");
60
+
61
+ beforeAll(async () => {
62
+ repoDir = await mkdtemp(join(tmpdir(), "runner-bb-vcs-"));
63
+ await git(["init", "--quiet"]);
64
+ await git(["checkout", "--quiet", "-b", BRANCH]);
65
+ await git(["remote", "add", "origin", "https://bitbucket.org/acme-ws/web.app.git"]);
66
+ });
67
+
68
+ afterAll(async () => {
69
+ await rm(repoDir, { recursive: true, force: true });
70
+ });
71
+
72
+ beforeEach(() => {
73
+ calls = [];
74
+ bitbucketReply = { status: 200, body: { values: [] } };
75
+ process.env.BITBUCKET_TOKEN = "example-bb-token";
76
+ stubFetch();
77
+ });
78
+
79
+ afterEach(() => {
80
+ globalThis.fetch = originalFetch;
81
+ if (originalToken === undefined) delete process.env.BITBUCKET_TOKEN;
82
+ else process.env.BITBUCKET_TOKEN = originalToken;
83
+ });
84
+
85
+ describe("detectVcsForTask on a Bitbucket remote", () => {
86
+ test("an open PR for the branch links the task", async () => {
87
+ bitbucketReply = {
88
+ status: 200,
89
+ body: { values: [{ id: 17, links: { html: { href: PR_HTML } } }] },
90
+ };
91
+ const taskId = nextTaskId();
92
+ await detectVcsForTask(API_URL, "swarm-key", taskId, repoDir);
93
+
94
+ const [lookup] = bitbucketCalls();
95
+ expect(lookup).toBeDefined();
96
+ const lookupUrl = new URL(lookup!.url);
97
+ expect(lookupUrl.pathname).toBe("/2.0/repositories/acme-ws/web.app/pullrequests");
98
+ expect(lookupUrl.searchParams.get("q")).toBe(`source.branch.name="${BRANCH}" AND state="OPEN"`);
99
+ expect(lookupUrl.searchParams.get("pagelen")).toBe("1");
100
+ expect(lookup!.headers.get("authorization")).toBe("Bearer example-bb-token");
101
+
102
+ const patches = patchCalls();
103
+ expect(patches).toHaveLength(1);
104
+ expect(patches[0]!.url).toBe(`${API_URL}/api/tasks/${taskId}/vcs`);
105
+ expect(patches[0]!.headers.get("authorization")).toBe("Bearer swarm-key");
106
+ expect(JSON.parse(patches[0]!.body)).toEqual({
107
+ vcsProvider: "bitbucket",
108
+ vcsRepo: "acme-ws/web.app",
109
+ vcsNumber: 17,
110
+ vcsUrl: PR_HTML,
111
+ });
112
+ });
113
+
114
+ test("no open PR sends no PATCH", async () => {
115
+ await detectVcsForTask(API_URL, "swarm-key", nextTaskId(), repoDir);
116
+ expect(bitbucketCalls()).toHaveLength(1);
117
+ expect(patchCalls()).toHaveLength(0);
118
+ });
119
+
120
+ test("a non-200 answer is swallowed and sends no PATCH", async () => {
121
+ bitbucketReply = { status: 401, body: { type: "error", error: { message: "Unauthorized" } } };
122
+ await expect(
123
+ detectVcsForTask(API_URL, "swarm-key", nextTaskId(), repoDir),
124
+ ).resolves.toBeUndefined();
125
+ expect(bitbucketCalls()).toHaveLength(1);
126
+ expect(patchCalls()).toHaveLength(0);
127
+ });
128
+
129
+ test("a network failure is swallowed and sends no PATCH", async () => {
130
+ bitbucketReply = "throw";
131
+ await expect(
132
+ detectVcsForTask(API_URL, "swarm-key", nextTaskId(), repoDir),
133
+ ).resolves.toBeUndefined();
134
+ expect(patchCalls()).toHaveLength(0);
135
+ });
136
+
137
+ test("without BITBUCKET_TOKEN it calls nothing", async () => {
138
+ delete process.env.BITBUCKET_TOKEN;
139
+ await detectVcsForTask(API_URL, "swarm-key", nextTaskId(), repoDir);
140
+ expect(calls).toHaveLength(0);
141
+ });
142
+ });
143
+
144
+ describe("findBitbucketOpenPullRequest", () => {
145
+ test("escapes quotes in the branch name inside the BBQL query", async () => {
146
+ let seen = "";
147
+ const fetchImpl = (async (input: string | URL | Request) => {
148
+ seen = String(input);
149
+ return new Response(JSON.stringify({ values: [] }), { status: 200 });
150
+ }) as typeof fetch;
151
+ await findBitbucketOpenPullRequest("git@bitbucket.org:ws/repo.git", 'we"ird', {
152
+ token: "t",
153
+ fetchImpl,
154
+ });
155
+ expect(new URL(seen).searchParams.get("q")).toBe(
156
+ 'source.branch.name="we\\"ird" AND state="OPEN"',
157
+ );
158
+ });
159
+
160
+ test("rejects a reply without a numeric id or an html link", async () => {
161
+ for (const values of [
162
+ [{ id: "17", links: { html: { href: PR_HTML } } }],
163
+ [{ id: 17, links: {} }],
164
+ [{}],
165
+ ]) {
166
+ const fetchImpl = (async () =>
167
+ new Response(JSON.stringify({ values }), { status: 200 })) as unknown as typeof fetch;
168
+ expect(
169
+ await findBitbucketOpenPullRequest("https://bitbucket.org/ws/repo", "b", {
170
+ token: "t",
171
+ fetchImpl,
172
+ }),
173
+ ).toBeNull();
174
+ }
175
+ });
176
+
177
+ test("returns null for a non-Bitbucket remote without calling fetch", async () => {
178
+ let called = false;
179
+ const fetchImpl = (async () => {
180
+ called = true;
181
+ return new Response("{}");
182
+ }) as unknown as typeof fetch;
183
+ expect(
184
+ await findBitbucketOpenPullRequest("https://github.com/ws/repo", "b", {
185
+ token: "t",
186
+ fetchImpl,
187
+ }),
188
+ ).toBeNull();
189
+ expect(called).toBe(false);
190
+ });
191
+ });
@@ -34,6 +34,9 @@ type StubState = {
34
34
  };
35
35
  calls: { signature: number; list: number; sync: number };
36
36
  skillFilesStatus: number;
37
+ /** 404 by default so the legacy list path stays covered; bundle tests opt in. */
38
+ bundleStatus: number;
39
+ bundleCalls: number;
37
40
  };
38
41
 
39
42
  const FAKE_HOME = join(tmpdir(), `runner-refresh-test-${process.pid}`);
@@ -67,6 +70,8 @@ const state: StubState = {
67
70
  signature: "hash-v1",
68
71
  },
69
72
  calls: { signature: 0, list: 0, sync: 0 },
73
+ bundleStatus: 404,
74
+ bundleCalls: 0,
70
75
  };
71
76
 
72
77
  let server: ReturnType<typeof Bun.serve> | null = null;
@@ -91,6 +96,22 @@ describe("refreshSkillsIfChanged", () => {
91
96
  generatedAt: new Date().toISOString(),
92
97
  });
93
98
  }
99
+ if (url.pathname.endsWith("/skills/bundle")) {
100
+ state.bundleCalls++;
101
+ if (state.bundleStatus !== 200) {
102
+ return new Response("not found", { status: state.bundleStatus });
103
+ }
104
+ return Response.json({
105
+ skills: state.skillsBody.skills.map((skill) => ({
106
+ ...skill,
107
+ files: skill.isComplex
108
+ ? [{ path: "refs/notes.md", content: "bundled note", isBinary: false }]
109
+ : [],
110
+ })),
111
+ total: state.skillsBody.skills.length,
112
+ signature: state.skillsBody.signature,
113
+ });
114
+ }
94
115
  if (url.pathname.match(/\/api\/agents\/[^/]+\/skills$/)) {
95
116
  state.calls.list++;
96
117
  if (state.listStatus !== 200) {
@@ -337,6 +358,43 @@ describe("refreshSkillsIfChanged", () => {
337
358
  rmSync(cleanHome, { recursive: true, force: true });
338
359
  });
339
360
 
361
+ test("bundle endpoint replaces the list call and the per-file fan-out", async () => {
362
+ state.calls = { signature: 0, list: 0, sync: 0 };
363
+ state.bundleCalls = 0;
364
+ state.bundleStatus = 200;
365
+ state.signatureHash = "hash-bundle";
366
+ state.skillsBody.signature = "hash-bundle";
367
+ state.skillsBody.skills = [
368
+ {
369
+ id: "skill-complex",
370
+ name: "complex-skill",
371
+ description: "has bundled files",
372
+ content: "---\nname: complex-skill\n---\n\nComplex body.",
373
+ isComplex: true,
374
+ isActive: true,
375
+ isEnabled: true,
376
+ },
377
+ ];
378
+
379
+ const home = join(FAKE_HOME, "bundle-home");
380
+ mkdirSync(home, { recursive: true });
381
+ const lastHash = { current: null as string | null };
382
+ const result = await refreshWithHome(makeCtx(), lastHash, home);
383
+
384
+ expect(result.changed).toBe(true);
385
+ expect(lastHash.current).toBe("hash-bundle");
386
+ // One signature probe + one bundle call. No list call, no manifest and no
387
+ // per-file fetches.
388
+ expect(state.bundleCalls).toBe(1);
389
+ expect(state.calls).toEqual({ signature: 1, list: 0, sync: 0 });
390
+ expect(
391
+ readFileSync(join(home, ".claude", "skills", "complex-skill", "refs", "notes.md"), "utf-8"),
392
+ ).toBe("bundled note");
393
+
394
+ state.bundleStatus = 404;
395
+ rmSync(home, { recursive: true, force: true });
396
+ });
397
+
340
398
  test("list fetch failure after signature drift leaves pre-existing skills on disk and does not advance hash", async () => {
341
399
  // Arrange: fresh isolated home with a pre-existing swarm-managed skill
342
400
  const isolatedHome = join(FAKE_HOME, "list-fail-home");
@@ -74,6 +74,21 @@ describe("scrubSecrets — env-based replacement", () => {
74
74
  expect(out).toBe("Authorization: Bearer [REDACTED:GITHUB_TOKEN] end");
75
75
  });
76
76
 
77
+ test("redacts the Bitbucket token and webhook secret as exact keys", () => {
78
+ const values = {
79
+ BITBUCKET_TOKEN: `example-bitbucket-${randomToken(24)}`,
80
+ BITBUCKET_WEBHOOK_SECRET: `example-bbhook-${randomToken(24)}`,
81
+ };
82
+ for (const [name, value] of Object.entries(values)) process.env[name] = value;
83
+ refreshSecretScrubberCache();
84
+ for (const [name, value] of Object.entries(values)) {
85
+ expect(isSensitiveKey(name)).toBe(true);
86
+ expect(scrubSecrets(`Authorization: Bearer ${value} end`)).toBe(
87
+ `Authorization: Bearer [REDACTED:${name}] end`,
88
+ );
89
+ }
90
+ });
91
+
77
92
  test("redacts any key with _API_KEY suffix", () => {
78
93
  process.env.FOO_SERVICE_API_KEY = "example-supersecretFooApiKey_longerthan12chars";
79
94
  refreshSecretScrubberCache();