@desplega.ai/agent-swarm 2.4.4 → 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 (382) hide show
  1. package/README.md +4 -2
  2. package/dist/{acp-adapter-thq04adh.js → acp-adapter-11gp67nr.js} +8 -6
  3. package/dist/{actions-j6ywh6x5.js → actions-8p2kze8j.js} +24 -22
  4. package/dist/{amp-adapter-k8b5xmj8.js → amp-adapter-z4qvhacj.js} +2 -2
  5. package/dist/{anthropic-messages-g6pqmqk4.js → anthropic-messages-txs190dd.js} +17 -20
  6. package/dist/{app-rb7vc02s.js → app-0qp0a2gk.js} +3 -3
  7. package/dist/{assistant-ftta1zjf.js → assistant-jedd3ktm.js} +27 -25
  8. package/dist/{azure-openai-responses-9dbmj346.js → azure-openai-responses-9kfwhmn7.js} +18 -13
  9. package/dist/{boot-reembed-xns7h5h9.js → boot-reembed-4xwq5tg0.js} +1 -1
  10. package/dist/{boot-reembed-srp6v6af.js → boot-reembed-n4wq5dd2.js} +1 -1
  11. package/dist/{boot-scrub-sweep-3qqrh2mz.js → boot-scrub-sweep-69h90xcq.js} +4 -4
  12. package/dist/{boot-seeds-9ks8hce7.js → boot-seeds-3vn0m9sg.js} +4 -4
  13. package/dist/{claude-adapter-k5ksyres.js → claude-adapter-221mvad5.js} +8 -6
  14. package/dist/{claude-managed-adapter-h2y889ge.js → claude-managed-adapter-gt6pa35d.js} +2 -2
  15. package/dist/{claude-managed-setup-n5a5eqks.js → claude-managed-setup-c3hp36ph.js} +4 -4
  16. package/dist/{claude-sdk-session-2j3j4kwn.js → claude-sdk-session-8thjsk5e.js} +9729 -9628
  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-amyhm3gj.js → cli-0y6f0ckq.js} +1 -1
  20. package/dist/{cli-4dg55wsk.js → cli-0z3vjrpm.js} +1 -1
  21. package/dist/{cli-pccfmtb5.js → cli-179xcyky.js} +11 -11
  22. package/dist/{cli-3r8samv3.js → cli-1f0gp9vz.js} +3 -2
  23. package/dist/{cli-hj3debgw.js → cli-1pr672hz.js} +3 -3
  24. package/dist/{cli-0bqteszy.js → cli-2nf3b08t.js} +328 -25
  25. package/dist/{cli-t7cyef5g.js → cli-36j4jnk3.js} +4 -4
  26. package/dist/{cli-er83cjbn.js → cli-3qv8naq8.js} +527 -231
  27. package/dist/cli-3zwye9sn.js +1193 -0
  28. package/dist/{cli-60cgh1rv.js → cli-459fk97x.js} +7 -7
  29. package/dist/{cli-xj0re5tm.js → cli-4w24f7z3.js} +1 -1
  30. package/dist/{cli-v3yr2gbg.js → cli-5kzk3472.js} +2198 -968
  31. package/dist/{cli-h7gc0b4r.js → cli-5m84zd2k.js} +55 -18
  32. package/dist/{cli-w6nadkh4.js → cli-5rvg1ddk.js} +5 -5
  33. package/dist/{cli-dvkmp9bt.js → cli-6ctwrf1z.js} +20 -10
  34. package/dist/{cli-9qtjfd60.js → cli-6rg2jn2y.js} +3 -2
  35. package/dist/{cli-2v045xyf.js → cli-7bwtc6yx.js} +3 -3
  36. package/dist/{cli-3cgngyd8.js → cli-7g40g8hq.js} +35 -17
  37. package/dist/{cli-q6r7t299.js → cli-92mqenkc.js} +2379 -154
  38. package/dist/{cli-cynvc9fj.js → cli-9ce2xw98.js} +8 -8
  39. package/dist/{cli-gqvh8d8a.js → cli-bgdtv62r.js} +36 -22
  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-p20r8w42.js → cli-fcc5qe3f.js} +763 -268
  44. package/dist/{cli-1ephgwnq.js → cli-fes51ya2.js} +3 -3
  45. package/dist/{cli-1d70crsy.js → cli-ftmq4nvk.js} +2 -2
  46. package/dist/{cli-qkhc5aqy.js → cli-fy1jyyym.js} +13 -13
  47. package/dist/{cli-s95dknxh.js → cli-g1cw7xtt.js} +1 -1
  48. package/dist/{cli-qje9k2nh.js → cli-g9ypd5en.js} +12 -2
  49. package/dist/{cli-74c1wpm0.js → cli-gpv60278.js} +1 -1
  50. package/dist/{cli-pemywhfq.js → cli-ha9rmwgq.js} +2 -1
  51. package/dist/{cli-bnz0r3kc.js → cli-j1eghqfm.js} +5 -5
  52. package/dist/{cli-2zqpkvtt.js → cli-jpxsrxcb.js} +1 -1
  53. package/dist/{cli-ejs3m1sx.js → cli-kxpfx2xr.js} +1 -1
  54. package/dist/{cli-37yb7307.js → cli-mbfj11fq.js} +1 -1
  55. package/dist/{cli-pnp2j5zg.js → cli-mmhj8vd7.js} +2 -2
  56. package/dist/{cli-xf234ws8.js → cli-n4c8e3s5.js} +2 -0
  57. package/dist/{cli-a0kcty51.js → cli-n6kmqngw.js} +2 -2
  58. package/dist/{cli-te6b5wvg.js → cli-nnvjb17s.js} +3 -3
  59. package/dist/{cli-1dmcq2eh.js → cli-nzfnt2gs.js} +1 -1
  60. package/dist/{cli-625tb7aq.js → cli-p859hcc8.js} +2 -2
  61. package/dist/{cli-smvcw9z5.js → cli-pasyvjmm.js} +1 -1
  62. package/dist/cli-pkaqk57w.js +128 -0
  63. package/dist/{cli-8nyhf7za.js → cli-qqzmjxkf.js} +129 -105
  64. package/dist/{cli-zrwpctp4.js → cli-r25qpaxt.js} +161 -63
  65. package/dist/{cli-nc35pxb7.js → cli-rh5smt8t.js} +19 -4
  66. package/dist/{cli-qwkcwjdn.js → cli-rycdaj0a.js} +1 -1
  67. package/dist/{cli-esxerzg7.js → cli-s4sntwt4.js} +2 -2
  68. package/dist/{cli-b8k7epj1.js → cli-segs7hx9.js} +8 -8
  69. package/dist/{cli-36ckw0wx.js → cli-stxdrqs2.js} +2 -2
  70. package/dist/{cli-vdm20wej.js → cli-sy8f9705.js} +4 -4
  71. package/dist/{cli-1m17dc9d.js → cli-ta8bd7th.js} +2 -2
  72. package/dist/{cli-2y91wnbh.js → cli-we0ra057.js} +222 -10
  73. package/dist/{cli-55qmbp6z.js → cli-x21dqn18.js} +5 -5
  74. package/dist/{cli-dqb4n629.js → cli-x6vf4sad.js} +11 -11
  75. package/dist/{cli-d2mb5s8a.js → cli-x77gen2p.js} +680 -440
  76. package/dist/{cli-vq97g1jq.js → cli-xawhpgc9.js} +95 -195
  77. package/dist/{cli-82d3nkc3.js → cli-xd4eqtr2.js} +2 -2
  78. package/dist/{cli-9fhkkpj5.js → cli-xt768r5m.js} +2 -2
  79. package/dist/{cli-zrk6jxv8.js → cli-y4x8ww6s.js} +2 -2
  80. package/dist/{cli-xc27fwzg.js → cli-y9jvxfhk.js} +3 -3
  81. package/dist/{cli-44159k28.js → cli-z63a269p.js} +2 -2
  82. package/dist/{cli-sm8rses9.js → cli-zfdp01hn.js} +8 -5
  83. package/dist/cli.js +31 -29
  84. package/dist/{cloudflare-workers-ai-system-one-ch2wxfcp.js → cloudflare-workers-ai-system-one-43v9qkjk.js} +7 -2
  85. package/dist/{codex-adapter-968xwfy7.js → codex-adapter-tcg1q3jk.js} +6 -4
  86. package/dist/{codex-login-m819c41r.js → codex-login-rn7c5gxt.js} +1 -1
  87. package/dist/{codex-session-runner-ht4z99fa.js → codex-session-runner-xfx4wd9x.js} +7 -5
  88. package/dist/{commands-pcymaeg9.js → commands-ew83nj70.js} +2 -2
  89. package/dist/{controller-fpjyk46r.js → controller-gnq1k1mf.js} +4 -4
  90. package/dist/{controllers-connect-mwzx1fh6.js → controllers-connect-tw1dan9r.js} +26 -24
  91. package/dist/{cursor-adapter-cp4s2e2f.js → cursor-adapter-yq5xgfcg.js} +1 -1
  92. package/dist/{db-xsjg9jqq.js → db-7gbc53fd.js} +14 -2
  93. package/dist/{db-sgd12bt9.js → db-t86xrnes.js} +3 -3
  94. package/dist/{devin-adapter-zgqadvnm.js → devin-adapter-4ycy4vrf.js} +1 -1
  95. package/dist/{dispatcher-x0jfw2c9.js → dispatcher-64bdyhps.js} +23 -21
  96. package/dist/{docker-fkgtr75t.js → docker-nfy2gc8t.js} +2 -2
  97. package/dist/{dsh-adapter-jrq7ma7h.js → dsh-adapter-tvhc4k2d.js} +1 -1
  98. package/dist/{e2b-16d6ch39.js → e2b-9gkdhm3c.js} +2 -2
  99. package/dist/{e2b-zn0b4w5w.js → e2b-cjtf8k1z.js} +2 -2
  100. package/dist/{easter-egg-3d-5cgynaja.js → easter-egg-3d-e4qv8dxj.js} +1 -1
  101. package/dist/{event-bus-9vpk8gmq.js → event-bus-3z78ep5n.js} +1 -1
  102. package/dist/{execute-t4e6m3rw.js → execute-0xpz8950.js} +85 -23
  103. package/dist/{external-channel-8njj4h3b.js → external-channel-e9nw24zv.js} +2 -2
  104. package/dist/{google-generative-ai-bmq8gkf3.js → google-generative-ai-mk9vcpe1.js} +15 -15
  105. package/dist/{google-vertex-b1rvp52s.js → google-vertex-q580kz8k.js} +20 -20
  106. package/dist/{grok-adapter-kkr97v2f.js → grok-adapter-v82srhrx.js} +9 -7
  107. package/dist/handlers-c9ajzhbn.js +41 -0
  108. package/dist/{heartbeat-wd03zhn0.js → heartbeat-2vqs1319.js} +26 -24
  109. package/dist/{hook-xzj2ts2p.js → hook-01nk13wx.js} +7 -5
  110. package/dist/{hook-f0vghj42.js → hook-jzpheegg.js} +9 -7
  111. package/dist/{http-4hwxv62p.js → http-fc1k6ec8.js} +859 -252
  112. package/dist/index-07ef7nkm.js +42 -0
  113. package/dist/{index-g6tbb6ve.js → index-31ceyf42.js} +1 -1
  114. package/dist/index-7yts8n9s.js +43 -0
  115. package/dist/{index-c4ykd0f1.js → index-dr94zbnh.js} +13348 -11611
  116. package/dist/index-q3t40ryv.js +46 -0
  117. package/dist/{index-ww2vznkp.js → index-sepja052.js} +1 -1
  118. package/dist/{index-97q4xn03.js → index-zdycv3ja.js} +135 -79
  119. package/dist/{index-g216q6jr.js → index-zsyx51ap.js} +1 -4
  120. package/dist/{keepalive-hrh7xnr9.js → keepalive-5nh2rhmr.js} +4 -4
  121. package/dist/lead-aayq64jn.js +79 -0
  122. package/dist/{llama-cpp-classify-vv43gswy.js → llama-cpp-classify-ee7vmd7q.js} +4 -1
  123. package/dist/{maintenance-e8wnwnqx.js → maintenance-hjhzdt3k.js} +1 -1
  124. package/dist/{mistral-conversations-1medfwv7.js → mistral-conversations-sd0xf1tn.js} +13 -13
  125. package/dist/{node-3v3f08f7.js → node-jzjj7r9x.js} +10 -9
  126. package/dist/{oauth-refresh-sweep-zm0yj4sj.js → oauth-refresh-sweep-y3n1knmd.js} +3 -3
  127. package/dist/{onboard-dtwcq603.js → onboard-1zq3m7db.js} +2 -2
  128. package/dist/{openai-codex-responses-de9pvs4d.js → openai-codex-responses-79s8xnbh.js} +29 -23
  129. package/dist/{openai-completions-mtqge3nf.js → openai-completions-tdbyts0x.js} +21 -20
  130. package/dist/{openai-decisions-ygk4kkxd.js → openai-decisions-bqhnjyfz.js} +2 -1
  131. package/dist/{openai-responses-dkyng71g.js → openai-responses-dm3t5vqp.js} +26 -17
  132. package/dist/{opencode-adapter-x4tqzhkh.js → opencode-adapter-e56086m1.js} +7 -5
  133. package/dist/{openrouter-images-wvv7camj.js → openrouter-images-ssttr1y4.js} +6 -3
  134. package/dist/{otel-impl-w3fbhpn0.js → otel-impl-h9yyza47.js} +2 -2
  135. package/dist/{pi-messages-nhh1pkz2.js → pi-messages-3nf8bq1a.js} +6 -4
  136. package/dist/{pi-mono-adapter-mcfkn9vf.js → pi-mono-adapter-tzps8f38.js} +9 -37
  137. package/dist/{pricing-refresh-j38dehfs.js → pricing-refresh-nevhat8h.js} +3 -3
  138. package/dist/{rbac-roles-ae8yttq4.js → rbac-roles-4y6ngvzq.js} +1 -1
  139. package/dist/{rbac-roles-ejxx9b35.js → rbac-roles-exbg8xb6.js} +2 -2
  140. package/dist/{registry-rxw10xzg.js → registry-ajhexe44.js} +1 -1
  141. package/dist/{render-v2-n16n0phe.js → render-v2-cfsh13z3.js} +3 -3
  142. package/dist/{resume-z0fgjc38.js → resume-5s2t0dbs.js} +10 -8
  143. package/dist/{runtime-xe3aqy4x.js → runtime-bt5pkpm1.js} +1 -1
  144. package/dist/{session-access-cv3mq6mc.js → session-access-dnbyp3cn.js} +6 -4
  145. package/dist/{setup-mafhj3yt.js → setup-1wj1y77j.js} +1 -1
  146. package/dist/{typesafe-system-one-2hgpayzy.js → typesafe-system-one-bre38hvs.js} +4 -2
  147. package/dist/{virtual-modules-4r3zkc20.js → virtual-modules-mnfbh5mr.js} +8 -2
  148. package/dist/{work-objects-8b86mgjp.js → work-objects-7mg6216g.js} +26 -24
  149. package/dist/worker-nwb789mg.js +79 -0
  150. package/dist/{x-y45mthwn.js → x-pbpezr0s.js} +2 -2
  151. package/openapi.json +793 -6
  152. package/package.json +11 -11
  153. package/src/apps/catalog.generated.json +206 -4
  154. package/src/apps/page-validator.ts +13 -1
  155. package/src/assets/key.ts +17 -0
  156. package/src/azure-devops/handlers.ts +1 -1
  157. package/src/be/chunking.ts +42 -9
  158. package/src/be/db/approvals.ts +18 -0
  159. package/src/be/db/session-moves.ts +164 -0
  160. package/src/be/db/session-tree.ts +384 -0
  161. package/src/be/db/tasks/draft-text.ts +15 -0
  162. package/src/be/db/tasks/read.ts +54 -5
  163. package/src/be/db/workflow-runs.ts +96 -0
  164. package/src/be/db.ts +85 -237
  165. package/src/be/memory/index-content.ts +36 -17
  166. package/src/be/memory/providers/openai-embedding.ts +115 -61
  167. package/src/be/memory/providers/sqlite-store.ts +69 -23
  168. package/src/be/memory/reranker.ts +31 -1
  169. package/src/be/memory/search-candidates.ts +47 -0
  170. package/src/be/memory/types.ts +14 -0
  171. package/src/be/onboarding.ts +3 -0
  172. package/src/be/scripts/typecheck.ts +2 -0
  173. package/src/be/seed-scripts/catalog/heartbeat-jev-gate.ts +106 -54
  174. package/src/be/seed-scripts/index.ts +2 -2
  175. package/src/be/skill-sync.ts +43 -2
  176. package/src/be/swarm-config-guard.ts +25 -1
  177. package/src/be/task-attachment-store.ts +30 -0
  178. package/src/bitbucket/api.ts +81 -0
  179. package/src/bitbucket/auth.ts +64 -0
  180. package/src/bitbucket/handlers.ts +434 -0
  181. package/src/bitbucket/index.ts +28 -0
  182. package/src/bitbucket/templates.ts +134 -0
  183. package/src/bitbucket/types.ts +65 -0
  184. package/src/commands/runner.ts +55 -3
  185. package/src/e2b/env.ts +1 -0
  186. package/src/extensions/catalog.generated.json +4 -4
  187. package/src/extensions/contract-types.generated.ts +1 -1
  188. package/src/extensions/contract.ts +9 -2
  189. package/src/extensions/post-bridge.ts +1 -0
  190. package/src/fs/provider.ts +13 -1
  191. package/src/github/handlers.ts +6 -6
  192. package/src/gitlab/handlers.ts +5 -5
  193. package/src/heartbeat/heartbeat.ts +31 -11
  194. package/src/http/assets.ts +14 -2
  195. package/src/http/core.ts +12 -0
  196. package/src/http/fs.ts +54 -34
  197. package/src/http/index.ts +4 -0
  198. package/src/http/mcp-body.ts +37 -0
  199. package/src/http/mcp-user.ts +3 -5
  200. package/src/http/mcp.ts +6 -7
  201. package/src/http/memory.ts +26 -15
  202. package/src/http/onboarding.ts +1 -0
  203. package/src/http/page-proxy.ts +61 -5
  204. package/src/http/pages-public.ts +27 -44
  205. package/src/http/pages.ts +4 -4
  206. package/src/http/script-runs.ts +2 -0
  207. package/src/http/sessions.ts +366 -12
  208. package/src/http/skills.ts +48 -1
  209. package/src/http/tasks.ts +1 -1
  210. package/src/http/users.ts +13 -1
  211. package/src/http/webhooks.ts +141 -0
  212. package/src/integrations/chat-sdk/adapters.ts +24 -4
  213. package/src/integrations/chat-sdk/attachments.ts +90 -12
  214. package/src/integrations/chat-sdk/facts.ts +138 -0
  215. package/src/integrations/chat-sdk/inbound.ts +156 -8
  216. package/src/integrations/chat-sdk/limits.ts +24 -0
  217. package/src/integrations/chat-sdk/outbound-files.ts +186 -0
  218. package/src/integrations/chat-sdk/outbound.ts +272 -5
  219. package/src/integrations/chat-sdk/relay.ts +4 -1
  220. package/src/integrations/chat-sdk/router.ts +28 -2
  221. package/src/integrations/chat-sdk/steering.ts +9 -0
  222. package/src/integrations/chat-sdk/templates.ts +14 -0
  223. package/src/integrations/chat-sdk/thread-buffer.ts +4 -0
  224. package/src/mcp-limits.ts +20 -0
  225. package/src/pages/browser-sdk.ts +8 -3
  226. package/src/pages/feedback-overlay.ts +1 -1
  227. package/src/providers/acp-targets.ts +1 -1
  228. package/src/providers/amp-adapter.ts +1 -1
  229. package/src/providers/codex-adapter.ts +106 -119
  230. package/src/providers/codex-agents-md.ts +35 -229
  231. package/src/providers/cursor-adapter.ts +1 -1
  232. package/src/providers/pi-mono-adapter.ts +5 -53
  233. package/src/realtime/transport.ts +1 -2
  234. package/src/script-workflows/workflow-ctx.ts +1 -0
  235. package/src/scripts-runtime/types/stdlib.d.ts +2 -0
  236. package/src/scripts-runtime/types/swarm-extension.d.ts +14 -4
  237. package/src/scripts-runtime/types/swarm-sdk.d.ts +2 -0
  238. package/src/server-user.ts +2 -0
  239. package/src/server.ts +2 -0
  240. package/src/slack/assistant.ts +2 -2
  241. package/src/slack/files.ts +2 -0
  242. package/src/slack/handlers.test.ts +158 -0
  243. package/src/slack/handlers.ts +23 -32
  244. package/src/slack/inbound-files.ts +39 -6
  245. package/src/slack/thread-buffer.ts +2 -1
  246. package/src/tasks/context-key.ts +61 -1
  247. package/src/tasks/task-terminal-effects.ts +21 -17
  248. package/src/tests/apps-card-grid.test.ts +197 -0
  249. package/src/tests/apps-catalog-additions.test.ts +120 -0
  250. package/src/tests/apps-tree.test.ts +129 -0
  251. package/src/tests/bitbucket-auth.test.ts +156 -0
  252. package/src/tests/bitbucket-handlers.test.ts +425 -0
  253. package/src/tests/bitbucket-webhook.test.ts +220 -0
  254. package/src/tests/chat-sdk-attachments-reply.test.ts +52 -0
  255. package/src/tests/chat-sdk-config.test.ts +11 -0
  256. package/src/tests/chat-sdk-facts.test.ts +23 -0
  257. package/src/tests/chat-sdk-inbound.test.ts +597 -4
  258. package/src/tests/chat-sdk-outbound-files.test.ts +652 -0
  259. package/src/tests/chat-sdk-telegram-markdown.test.ts +92 -0
  260. package/src/tests/codex-adapter.test.ts +167 -242
  261. package/src/tests/config-api-only-keys.test.ts +9 -0
  262. package/src/tests/context-key.test.ts +50 -0
  263. package/src/tests/core-auth.test.ts +5 -0
  264. package/src/tests/cors-allowlist.test.ts +1 -1
  265. package/src/tests/db-task-read-contract.test.ts +7 -0
  266. package/src/tests/db-task-write-contract.test.ts +6 -0
  267. package/src/tests/docs-index-footprint.test.js +3 -3
  268. package/src/tests/extensions-example-slack-reply-gate.test.ts +262 -91
  269. package/src/tests/fixtures/bitbucket/pullrequest-comment-created-inline.json +174 -0
  270. package/src/tests/fixtures/bitbucket/pullrequest-comment-created.json +170 -0
  271. package/src/tests/fixtures/bitbucket/pullrequest-created-mention.json +132 -0
  272. package/src/tests/fixtures/bitbucket/pullrequest-created-reviewer.json +140 -0
  273. package/src/tests/fixtures/bitbucket/pullrequest-fulfilled.json +134 -0
  274. package/src/tests/fixtures/bitbucket/pullrequest-rejected.json +133 -0
  275. package/src/tests/generate-mcp-docs.test.ts +101 -0
  276. package/src/tests/gitlab-handlers.test.ts +3 -3
  277. package/src/tests/gitlab-vcs-db.test.ts +4 -4
  278. package/src/tests/heartbeat-checklist.test.ts +33 -0
  279. package/src/tests/heartbeat-jev-gate-credentials.test.ts +44 -0
  280. package/src/tests/heartbeat-jev-gate.test.ts +242 -1
  281. package/src/tests/integration-disable-flags.test.ts +36 -0
  282. package/src/tests/kv-page-proxy.test.ts +91 -5
  283. package/src/tests/launch-password-rejection.test.ts +2 -2
  284. package/src/tests/mcp-input-caps.test.ts +294 -0
  285. package/src/tests/mcp-tools.test.ts +1 -1
  286. package/src/tests/memory-chunked-search-starvation.test.ts +200 -0
  287. package/src/tests/memory-embedding-input-cap.test.ts +114 -0
  288. package/src/tests/memory.test.ts +14 -0
  289. package/src/tests/onboarding.test.ts +12 -0
  290. package/src/tests/page-guest-session.test.ts +19 -8
  291. package/src/tests/page-proxy-authed.test.ts +1 -1
  292. package/src/tests/page-proxy.test.ts +17 -10
  293. package/src/tests/page-session.test.ts +102 -1
  294. package/src/tests/pages-authed-mode.test.ts +82 -12
  295. package/src/tests/pages-password-mode.test.ts +5 -5
  296. package/src/tests/pi-codemode.test.ts +8 -2
  297. package/src/tests/preload-env.test.ts +57 -0
  298. package/src/tests/preload.ts +21 -0
  299. package/src/tests/prompt-template-remaining.test.ts +38 -0
  300. package/src/tests/realtime-transport.test.ts +13 -7
  301. package/src/tests/run-root-coverage.test.ts +39 -0
  302. package/src/tests/runner-bitbucket-vcs.test.ts +191 -0
  303. package/src/tests/runner-skills-refresh.test.ts +58 -0
  304. package/src/tests/script-runs-http.test.ts +84 -0
  305. package/src/tests/scripts-http.test.ts +10 -1
  306. package/src/tests/secret-scrubber.test.ts +15 -0
  307. package/src/tests/sessions-move.test.ts +576 -0
  308. package/src/tests/sessions-tree.test.ts +493 -0
  309. package/src/tests/skill-files-http.test.ts +50 -0
  310. package/src/tests/skills-bundle.test.ts +99 -0
  311. package/src/tests/swarm-config-export-to-env.test.ts +23 -3
  312. package/src/tests/task-audio-transcription.test.ts +299 -0
  313. package/src/tests/task-completion-memory-chunking.test.ts +434 -0
  314. package/src/tests/task-terminal-secret-scrub.test.ts +6 -1
  315. package/src/tests/transcription.test.ts +299 -0
  316. package/src/tests/vcs-bitbucket.test.ts +91 -0
  317. package/src/tests/vcs-provider-isolation.test.ts +123 -0
  318. package/src/tests/vcs-provider.test.ts +16 -1
  319. package/src/tests/vcs-tracking.test.ts +3 -3
  320. package/src/tests/workflow-foreach-bodies.test.ts +637 -0
  321. package/src/tests/workflow-foreach-concurrency.test.ts +342 -0
  322. package/src/tests/workflow-foreach-retry.test.ts +537 -0
  323. package/src/tests/workflow-foreach.test.ts +44 -23
  324. package/src/tests/workflow-step-claims.test.ts +6 -6
  325. package/src/tests/workflow-system-one-decision-providers.test.ts +98 -1
  326. package/src/tests/workflow-system-one-decision.test.ts +2 -1
  327. package/src/tests/workflow-tla-races.test.ts +2 -2
  328. package/src/tools/chat-access.ts +10 -0
  329. package/src/tools/chat-post.ts +31 -11
  330. package/src/tools/chat-reply.ts +32 -10
  331. package/src/tools/memory-search.ts +3 -1
  332. package/src/tools/script-delete.ts +1 -1
  333. package/src/tools/system-one-decide.ts +17 -4
  334. package/src/tools/workflows/create-workflow.ts +5 -3
  335. package/src/types.ts +9 -2
  336. package/src/utils/config-env-export.ts +4 -1
  337. package/src/utils/page-session.ts +133 -20
  338. package/src/utils/secret-scrubber.ts +4 -0
  339. package/src/utils/skills-refresh.ts +83 -43
  340. package/src/utils/transcription.ts +246 -0
  341. package/src/vcs/bitbucket.ts +58 -0
  342. package/src/vcs/index.ts +2 -0
  343. package/src/vcs/types.ts +2 -2
  344. package/src/workflows/approval-resolution.ts +4 -1
  345. package/src/workflows/checkpoint.ts +9 -1
  346. package/src/workflows/constants.ts +3 -0
  347. package/src/workflows/definition.ts +48 -17
  348. package/src/workflows/engine.ts +25 -15
  349. package/src/workflows/executors/foreach.ts +45 -99
  350. package/src/workflows/executors/system-one-decision.ts +28 -7
  351. package/src/workflows/executors/system-one-providers.ts +39 -4
  352. package/src/workflows/foreach-body.ts +146 -0
  353. package/src/workflows/foreach-driver.ts +552 -0
  354. package/src/workflows/foreach-join.ts +206 -7
  355. package/src/workflows/node-timeout.ts +21 -0
  356. package/src/workflows/recovery.ts +68 -3
  357. package/src/workflows/resume.ts +221 -43
  358. package/src/workflows/retry-poller.ts +39 -24
  359. package/src/workflows/step-input.ts +59 -0
  360. package/src/workflows/task-step-routing.ts +114 -56
  361. package/templates/extensions/slack-reply-gate/README.md +24 -22
  362. package/templates/extensions/slack-reply-gate/hooks.ts +146 -95
  363. package/templates/extensions/slack-reply-gate/manifest.json +2 -2
  364. package/templates/skills/apps/SKILL.md +28 -6
  365. package/templates/skills/apps/content.md +28 -6
  366. package/templates/skills/chat-sdk/content.md +6 -2
  367. package/templates/skills/code-quality/content.md +2 -1
  368. package/templates/skills/heartbeat-runbook/content.md +5 -5
  369. package/templates/skills/pages/content.md +6 -4
  370. package/templates/skills/slack-interaction/content.md +5 -0
  371. package/templates/skills/swarm-scripts/SKILL.md +2 -2
  372. package/templates/skills/swarm-scripts/content.md +2 -2
  373. package/templates/skills/work-on-task/SKILL.md +1 -1
  374. package/templates/skills/work-on-task/content.md +1 -1
  375. package/dist/cli-51y09c6p.js +0 -2657
  376. package/dist/cli-yw91rnmw.js +0 -13
  377. package/dist/handlers-0pvee9nw.js +0 -39
  378. package/dist/index-gdh8jrdm.js +0 -43
  379. package/dist/index-hg0x2cdv.js +0 -40
  380. package/dist/index-thm0y6a8.js +0 -41
  381. package/dist/lead-ee8vazz2.js +0 -77
  382. package/dist/worker-16s75hm5.js +0 -77
@@ -159,8 +159,7 @@ async function authenticate(req: IncomingMessage): Promise<AuthenticatedConnecti
159
159
  throw new Error("Invalid page origin");
160
160
  const page = await getPage(pageId);
161
161
  if (!page) throw new Error("Page not found");
162
- const cookie = await extractAndVerifyCookie(req);
163
- const session = cookie?.pageId === pageId ? cookie : null;
162
+ const session = await extractAndVerifyCookie(req, pageId);
164
163
  if (!session && page.authMode !== "public") throw new Error("Page session required");
165
164
  if (session?.uid && (await findUserById(session.uid))?.status !== "active")
166
165
  throw new Error("Viewer is inactive");
@@ -36,6 +36,7 @@ type AgentTaskConfig = {
36
36
  routingNote?: string;
37
37
  tags?: string[];
38
38
  priority?: number;
39
+ dependsOn?: string[];
39
40
  offerMode?: boolean;
40
41
  dir?: string;
41
42
  vcsRepo?: string;
@@ -53,6 +53,8 @@ declare module "swarm-sdk" {
53
53
  routingNote?: string;
54
54
  tags?: string[];
55
55
  priority?: number;
56
+ /** Task IDs that must complete before this task starts. */
57
+ dependsOn?: string[];
56
58
  offerMode?: boolean;
57
59
  dir?: string;
58
60
  vcsRepo?: string;
@@ -15,6 +15,7 @@ declare module "swarm-extension" {
15
15
  | "github"
16
16
  | "gitlab"
17
17
  | "azure-devops"
18
+ | "bitbucket"
18
19
  | "system"
19
20
  | "linear"
20
21
  | "jira"
@@ -41,7 +42,7 @@ declare module "swarm-extension" {
41
42
  slackTriggerMessageTs?: string | undefined;
42
43
  slackUserId?: string | undefined;
43
44
  overrideSlackContext?: boolean | undefined;
44
- vcsProvider?: "github" | "gitlab" | "azure-devops" | undefined;
45
+ vcsProvider?: "github" | "gitlab" | "azure-devops" | "bitbucket" | undefined;
45
46
  vcsRepo?: string | undefined;
46
47
  vcsEventType?: string | undefined;
47
48
  vcsNumber?: number | undefined;
@@ -67,6 +68,7 @@ declare module "swarm-extension" {
67
68
  sourceTaskId?: string | undefined;
68
69
  outputSchema?: Record<string, unknown> | undefined;
69
70
  inheritParentOutputSchema?: boolean | undefined;
71
+ inheritParentRequester?: boolean | undefined;
70
72
  inheritParentRoutingAffinity?: boolean | undefined;
71
73
  inheritParentFollowUpConfig?: boolean | undefined;
72
74
  followUpConfig?:
@@ -131,6 +133,7 @@ declare module "swarm-extension" {
131
133
  | "github"
132
134
  | "gitlab"
133
135
  | "azure-devops"
136
+ | "bitbucket"
134
137
  | "system"
135
138
  | "linear"
136
139
  | "jira"
@@ -171,7 +174,7 @@ declare module "swarm-extension" {
171
174
  slackUserId?: string | undefined;
172
175
  slackProgressMessageTs?: string | undefined;
173
176
  slackTreeRootMessageTs?: string | undefined;
174
- vcsProvider?: "github" | "gitlab" | "azure-devops" | undefined;
177
+ vcsProvider?: "github" | "gitlab" | "azure-devops" | "bitbucket" | undefined;
175
178
  vcsRepo?: string | undefined;
176
179
  vcsEventType?: string | undefined;
177
180
  vcsNumber?: number | undefined;
@@ -485,8 +488,15 @@ declare module "swarm-extension" {
485
488
  userName?: string;
486
489
  /** The buffered messages, joined. */
487
490
  text: string;
491
+ /** True for an @mention and for a reply to one of the bot's messages. */
488
492
  botMentioned: boolean;
489
493
  isDirectMessage: boolean;
494
+ /** Platform chat type, such as Telegram `private`, `group`, `supergroup` or `channel`. */
495
+ chatType?: string;
496
+ /** Set for a Telegram supergroup with topics. */
497
+ isForum?: boolean;
498
+ /** A buffered message replies to one of the bot's own messages. */
499
+ replyToBot: boolean;
490
500
  /** Set when earlier swarm tasks exist in this thread. */
491
501
  threadContext?: {
492
502
  adapter: string;
@@ -617,7 +627,7 @@ declare module "swarm-extension" {
617
627
  /** The text, or a placeholder naming the type for a non-text message. */
618
628
  text: string;
619
629
  }
620
- export type VcsProvider = "github" | "gitlab" | "azure-devops";
630
+ export type VcsProvider = "github" | "gitlab" | "azure-devops" | "bitbucket";
621
631
  /**
622
632
  * The provider object the event is about, as the provider names it. GitLab
623
633
  * calls a pull request a merge request, and a comment a `note`.
@@ -640,7 +650,7 @@ declare module "swarm-extension" {
640
650
  provider: VcsProvider;
641
651
  kind: VcsEventKind;
642
652
  action: string;
643
- /** `owner/name` on GitHub and GitLab, the repository URL on Azure DevOps. */
653
+ /** `owner/name` on GitHub and GitLab, `workspace/repo_slug` on Bitbucket, the repository URL on Azure DevOps. */
644
654
  repo: string;
645
655
  /** The PR, merge request, or issue number. Absent for a pipeline outside a merge request. */
646
656
  number?: number;
@@ -35,6 +35,8 @@ declare module "swarm-sdk" {
35
35
  routingNote?: string;
36
36
  tags?: string[];
37
37
  priority?: number;
38
+ /** Task IDs that must complete before this task starts. */
39
+ dependsOn?: string[];
38
40
  offerMode?: boolean;
39
41
  dir?: string;
40
42
  vcsRepo?: string;
@@ -4,6 +4,7 @@ import * as z from "zod";
4
4
  import pkg from "../package.json";
5
5
  import { enqueueAdmissionRow } from "./be/rbac-audit";
6
6
  import { getUserGrant } from "./be/rbac-roles";
7
+ import { MCP_MAX_TOOL_INPUT_ELEMENTS } from "./mcp-limits";
7
8
  import {
8
9
  type AdmissionRbac,
9
10
  decideToolAdmission,
@@ -107,6 +108,7 @@ export function createUserServer(user: User): McpServer {
107
108
  capabilities: {
108
109
  logging: {},
109
110
  },
111
+ maxToolInputElements: MCP_MAX_TOOL_INPUT_ELEMENTS,
110
112
  },
111
113
  );
112
114
 
package/src/server.ts CHANGED
@@ -7,6 +7,7 @@ import { isSteeringEnabled } from "./be/steering";
7
7
  import { registerGithubTaskReactions } from "./github/task-reactions";
8
8
  import { loadGlobalConfigsIntoEnv } from "./http/core";
9
9
  import { isChatSdkEnabled } from "./integrations/chat-sdk/config";
10
+ import { MCP_MAX_TOOL_INPUT_ELEMENTS } from "./mcp-limits";
10
11
  import { resolveTemplate } from "./prompts/resolver";
11
12
  import { isRbacEnabled } from "./rbac";
12
13
  import { getSlackConfiguration } from "./slack/config";
@@ -450,6 +451,7 @@ export async function createServer(
450
451
  capabilities: {
451
452
  logging: {},
452
453
  },
454
+ maxToolInputElements: MCP_MAX_TOOL_INPUT_ELEMENTS,
453
455
  },
454
456
  );
455
457
 
@@ -204,13 +204,13 @@ export function createAssistant(): Assistant {
204
204
  // Fetch shared files before the task text is final: a file that can't be
205
205
  // downloaded is flagged in it.
206
206
  if (files?.length) await safeSetStatus("Downloading attachments...");
207
- await using inbound = await fetchSlackFiles(client, files);
207
+ await using inbound = await fetchSlackFiles(client, files, { transcribe: true });
208
208
  // Any in-body `<@U…>` mention the requester typed is rewritten via
209
209
  // the identity primitive before it reaches agent-visible task text —
210
210
  // never a raw Slack ID. Bot-mention routing checks above use the
211
211
  // raw `messageText`, not this rendered copy.
212
212
  const renderedMessageText = await rewriteSlackMentions(
213
- buildEffectiveText(messageText, inbound.files, inbound.failed),
213
+ buildEffectiveText(messageText, inbound.files, inbound.failed, inbound.notes),
214
214
  cachedBotUserId ?? undefined,
215
215
  );
216
216
 
@@ -22,6 +22,8 @@ export interface SlackFile {
22
22
  title?: string;
23
23
  mimetype: string;
24
24
  filetype: string;
25
+ /** `slack_audio` on a voice clip recorded in Slack. */
26
+ subtype?: string;
25
27
  size: number;
26
28
  url_private: string;
27
29
  url_private_download: string;
@@ -20,6 +20,7 @@ import {
20
20
  checkUserAccess,
21
21
  isBotMessage,
22
22
  isSwarmThreadRoot,
23
+ type isUserAllowed,
23
24
  registerMessageHandler,
24
25
  type UserFilterConfig,
25
26
  } from "./handlers";
@@ -189,6 +190,163 @@ describe("checkUserAccess", () => {
189
190
  });
190
191
  });
191
192
 
193
+ describe("isUserAllowed", () => {
194
+ const usersInfo = mock(async () => ({ ok: true, user: {} }));
195
+ const client = { users: { info: usersInfo } } as never;
196
+ let enrichSpy: ReturnType<typeof spyOn<typeof slackEnrichModule, "enrichSlackUserEmail">>;
197
+ let logSpy: ReturnType<typeof spyOn<typeof console, "log">>;
198
+
199
+ // handlers.ts reads SLACK_ALLOWED_* once at module load, so each case evaluates a fresh
200
+ // copy under its own env and calls the production two-argument isUserAllowed. The copy
201
+ // shares ./enrich with this file, so the enrichSlackUserEmail spy still applies.
202
+ const ENV_KEYS = ["SLACK_ALLOWED_EMAIL_DOMAINS", "SLACK_ALLOWED_USER_IDS"] as const;
203
+ let loadCount = 0;
204
+ async function loadIsUserAllowed(env: {
205
+ SLACK_ALLOWED_EMAIL_DOMAINS?: string;
206
+ SLACK_ALLOWED_USER_IDS?: string;
207
+ }): Promise<typeof isUserAllowed> {
208
+ const saved = ENV_KEYS.map((key) => [key, process.env[key]] as const);
209
+ try {
210
+ for (const key of ENV_KEYS) {
211
+ if (env[key] === undefined) delete process.env[key];
212
+ else process.env[key] = env[key];
213
+ }
214
+ loadCount += 1;
215
+ const fresh = (await import(`./handlers?env=${loadCount}`)) as typeof import("./handlers");
216
+ return fresh.isUserAllowed;
217
+ } finally {
218
+ for (const [key, value] of saved) {
219
+ if (value === undefined) delete process.env[key];
220
+ else process.env[key] = value;
221
+ }
222
+ }
223
+ }
224
+
225
+ beforeEach(() => {
226
+ usersInfo.mockClear();
227
+ enrichSpy = spyOn(slackEnrichModule, "enrichSlackUserEmail");
228
+ logSpy = spyOn(console, "log").mockImplementation(() => {});
229
+ });
230
+
231
+ afterEach(() => {
232
+ enrichSpy.mockRestore();
233
+ logSpy.mockRestore();
234
+ });
235
+
236
+ const loggedLines = () => logSpy.mock.calls.map((call) => String(call[0]));
237
+
238
+ describe("filtering off (neither env var set)", () => {
239
+ test("allows everyone without an email lookup", async () => {
240
+ const allowed = await loadIsUserAllowed({});
241
+ expect(await allowed(client, "U_ANY")).toBe(true);
242
+ expect(enrichSpy).not.toHaveBeenCalled();
243
+ expect(usersInfo).not.toHaveBeenCalled();
244
+ });
245
+
246
+ test("treats blank and comma-only values as unset", async () => {
247
+ const allowed = await loadIsUserAllowed({
248
+ SLACK_ALLOWED_EMAIL_DOMAINS: " , ",
249
+ SLACK_ALLOWED_USER_IDS: ",",
250
+ });
251
+ expect(await allowed(client, "U_ANY")).toBe(true);
252
+ expect(enrichSpy).not.toHaveBeenCalled();
253
+ });
254
+ });
255
+
256
+ describe("user IDs only", () => {
257
+ const env = { SLACK_ALLOWED_USER_IDS: " U_LISTED , U_SECOND ," };
258
+
259
+ test("allows trimmed allowlisted IDs without an email lookup", async () => {
260
+ const allowed = await loadIsUserAllowed(env);
261
+ expect(await allowed(client, "U_LISTED")).toBe(true);
262
+ expect(await allowed(client, "U_SECOND")).toBe(true);
263
+ expect(enrichSpy).not.toHaveBeenCalled();
264
+ expect(usersInfo).not.toHaveBeenCalled();
265
+ });
266
+
267
+ test("denies an unlisted ID without an email lookup", async () => {
268
+ enrichSpy.mockResolvedValue("person@company.com");
269
+ const allowed = await loadIsUserAllowed(env);
270
+ expect(await allowed(client, "U_OTHER")).toBe(false);
271
+ expect(enrichSpy).not.toHaveBeenCalled();
272
+ expect(usersInfo).not.toHaveBeenCalled();
273
+ });
274
+ });
275
+
276
+ describe("email domains only", () => {
277
+ const env = { SLACK_ALLOWED_EMAIL_DOMAINS: " Company.COM , other.org " };
278
+
279
+ test("allows trimmed, case-insensitive domains after one lookup", async () => {
280
+ const allowed = await loadIsUserAllowed(env);
281
+ enrichSpy.mockResolvedValue("person@company.com");
282
+ expect(await allowed(client, "U_A")).toBe(true);
283
+ enrichSpy.mockResolvedValue("person@Other.Org");
284
+ expect(await allowed(client, "U_B")).toBe(true);
285
+ expect(enrichSpy).toHaveBeenCalledTimes(2);
286
+ expect(enrichSpy).toHaveBeenNthCalledWith(1, client, "U_A");
287
+ expect(enrichSpy).toHaveBeenNthCalledWith(2, client, "U_B");
288
+ expect(loggedLines()).toEqual([]);
289
+ });
290
+
291
+ test("denies and logs a domain that is not allowed", async () => {
292
+ enrichSpy.mockResolvedValue("person@evil.com");
293
+ const allowed = await loadIsUserAllowed(env);
294
+ expect(await allowed(client, "U_OTHER")).toBe(false);
295
+ expect(loggedLines()).toEqual([
296
+ '[Slack] User U_OTHER email domain "evil.com" not in allowed list',
297
+ ]);
298
+ });
299
+ });
300
+
301
+ describe("user IDs and email domains", () => {
302
+ const env = {
303
+ SLACK_ALLOWED_EMAIL_DOMAINS: "company.com",
304
+ SLACK_ALLOWED_USER_IDS: "U_LISTED",
305
+ };
306
+
307
+ test("allows an allowlisted ID without an email lookup", async () => {
308
+ const allowed = await loadIsUserAllowed(env);
309
+ expect(await allowed(client, "U_LISTED")).toBe(true);
310
+ expect(enrichSpy).not.toHaveBeenCalled();
311
+ expect(usersInfo).not.toHaveBeenCalled();
312
+ });
313
+
314
+ test("allows an unlisted ID whose email domain is allowed", async () => {
315
+ enrichSpy.mockResolvedValue("person@Company.com");
316
+ const allowed = await loadIsUserAllowed(env);
317
+ expect(await allowed(client, "U_OTHER")).toBe(true);
318
+ expect(enrichSpy).toHaveBeenCalledTimes(1);
319
+ expect(enrichSpy).toHaveBeenCalledWith(client, "U_OTHER");
320
+ expect(loggedLines()).toEqual([]);
321
+ });
322
+
323
+ test("denies and logs a domain that is not allowed", async () => {
324
+ enrichSpy.mockResolvedValue("person@other.com");
325
+ const allowed = await loadIsUserAllowed(env);
326
+ expect(await allowed(client, "U_OTHER")).toBe(false);
327
+ expect(loggedLines()).toEqual([
328
+ '[Slack] User U_OTHER email domain "other.com" not in allowed list',
329
+ ]);
330
+ });
331
+
332
+ test("denies and logs a user with no email", async () => {
333
+ enrichSpy.mockResolvedValue(null);
334
+ const allowed = await loadIsUserAllowed(env);
335
+ expect(await allowed(client, "U_OTHER")).toBe(false);
336
+ expect(loggedLines()).toEqual(["[Slack] User U_OTHER has no email, denying access"]);
337
+ });
338
+
339
+ test("denies and logs a user with a malformed email", async () => {
340
+ enrichSpy.mockResolvedValue("no-at-sign");
341
+ const allowed = await loadIsUserAllowed(env);
342
+ expect(await allowed(client, "U_OTHER")).toBe(false);
343
+ expect(loggedLines()).toEqual([
344
+ "[Slack] User U_OTHER has invalid email format, denying access",
345
+ ]);
346
+ });
347
+ });
348
+ });
349
+
192
350
  // Helper to create a minimal SlackFile for testing
193
351
  function makeFile(overrides: Partial<SlackFile> = {}): SlackFile {
194
352
  return {
@@ -79,8 +79,6 @@ const allowedUserIds = (process.env.SLACK_ALLOWED_USER_IDS || "")
79
79
  .map((id) => id.trim())
80
80
  .filter(Boolean);
81
81
 
82
- const filteringEnabled = allowedEmailDomains.length > 0 || allowedUserIds.length > 0;
83
-
84
82
  /**
85
83
  * Configuration for user filtering.
86
84
  */
@@ -89,6 +87,8 @@ export interface UserFilterConfig {
89
87
  allowedUserIds: string[];
90
88
  }
91
89
 
90
+ const envUserFilterConfig: UserFilterConfig = { allowedEmailDomains, allowedUserIds };
91
+
92
92
  /**
93
93
  * Core logic for checking if a user is allowed based on email and/or user ID.
94
94
  * Exported for testing.
@@ -137,40 +137,31 @@ export function checkUserAccess(
137
137
  /**
138
138
  * Check if a user is allowed to interact with the swarm.
139
139
  * Returns true if filtering is disabled, user is in whitelist, or user's email domain is allowed.
140
+ * The allow/deny decision is `checkUserAccess`; this wrapper only adds the lazy email lookup
141
+ * and the deny logs. The config comes from the SLACK_ALLOWED_* env vars, read at module load.
140
142
  */
141
143
  export async function isUserAllowed(client: WebClient, userId: string): Promise<boolean> {
142
- // If no filtering configured, allow all users (backwards compatible)
143
- if (!filteringEnabled) {
144
- return true;
145
- }
146
-
147
- // Check user ID whitelist first (fast path)
148
- if (allowedUserIds.includes(userId)) {
149
- return true;
150
- }
151
-
152
- // No email domains configured and not in user whitelist
153
- if (allowedEmailDomains.length === 0) {
154
- return false;
144
+ const config = envUserFilterConfig;
145
+ // Decide without an email first: filtering off and allowlisted IDs pass, and with no
146
+ // email domains configured an unlisted ID is denied. Only domain checks need a lookup.
147
+ const allowedWithoutEmail = checkUserAccess(userId, null, config);
148
+ if (allowedWithoutEmail || config.allowedEmailDomains.length === 0) {
149
+ return allowedWithoutEmail;
155
150
  }
156
151
 
157
152
  // Check email domain — uses kv-backed enrichment (24h TTL, only success cached).
158
153
  const email = await enrichSlackUserEmail(client, userId);
154
+ const allowed = checkUserAccess(userId, email, config);
159
155
 
160
- if (!email) {
161
- console.log(`[Slack] User ${userId} has no email, denying access`);
162
- return false;
163
- }
164
-
165
- const domain = email.split("@")[1]?.toLowerCase();
166
- if (!domain) {
167
- console.log(`[Slack] User ${userId} has invalid email format, denying access`);
168
- return false;
169
- }
170
-
171
- const allowed = allowedEmailDomains.includes(domain);
172
156
  if (!allowed) {
173
- console.log(`[Slack] User ${userId} email domain "${domain}" not in allowed list`);
157
+ const domain = email?.split("@")[1]?.toLowerCase();
158
+ if (!email) {
159
+ console.log(`[Slack] User ${userId} has no email, denying access`);
160
+ } else if (!domain) {
161
+ console.log(`[Slack] User ${userId} has invalid email format, denying access`);
162
+ } else {
163
+ console.log(`[Slack] User ${userId} email domain "${domain}" not in allowed list`);
164
+ }
174
165
  }
175
166
  return allowed;
176
167
  }
@@ -702,8 +693,8 @@ export function registerMessageHandler(app: App): void {
702
693
  return ignoreSlackInbound("rate_limited");
703
694
  }
704
695
 
705
- await using inbound = await fetchSlackFiles(client, msg.files);
706
- const taskText = buildEffectiveText(msg.text, inbound.files, inbound.failed);
696
+ await using inbound = await fetchSlackFiles(client, msg.files, { transcribe: true });
697
+ const taskText = buildEffectiveText(msg.text, inbound.files, inbound.failed, inbound.notes);
707
698
  const taskDescription = isImplicitMention
708
699
  ? taskText
709
700
  : extractTaskFromMessage(taskText, botUserId);
@@ -778,9 +769,9 @@ export function registerMessageHandler(app: App): void {
778
769
  }
779
770
 
780
771
  // Extract task description (the text plus a `[File: …]` line per attachment)
781
- await using inbound = await fetchSlackFiles(client, msg.files);
772
+ await using inbound = await fetchSlackFiles(client, msg.files, { transcribe: true });
782
773
  const taskDescription = extractTaskFromMessage(
783
- buildEffectiveText(msg.text, inbound.files, inbound.failed),
774
+ buildEffectiveText(msg.text, inbound.files, inbound.failed, inbound.notes),
784
775
  botUserId,
785
776
  );
786
777
  if (!taskDescription) {
@@ -13,6 +13,8 @@
13
13
  * could not be attached — never dropped silently. A download failure is
14
14
  * also flagged on that line; a storage failure happens after the text is
15
15
  * written, so it shows up only in the reply and in the missing attachment.
16
+ * Audio files (voice clips) a user shares get their transcript on the next
17
+ * line when transcription is configured, and keep the audio attached.
16
18
  */
17
19
  import { mkdtemp, rm } from "node:fs/promises";
18
20
  import { tmpdir } from "node:os";
@@ -27,6 +29,7 @@ import { createTaskWithSiblingAwareness } from "../tasks/sibling-awareness";
27
29
  import type { AgentTask, CreateTaskOptions, TaskAttachment } from "../types";
28
30
  import { scrubSecrets } from "../utils/secret-scrubber";
29
31
  import { taskAttachmentFetchCommand } from "../utils/task-attachment-links";
32
+ import { transcribeAudioNotes } from "../utils/transcription";
30
33
  import { getFileInfo, type SlackFile } from "./files";
31
34
  import { noteSlackInboundSideEffect } from "./inbound-dispatch";
32
35
  // Side-effect import: registers all Slack event templates in the in-memory registry
@@ -73,6 +76,8 @@ export type InboundSlackFiles = {
73
76
  files: SlackFile[];
74
77
  fetched: FetchedSlackFile[];
75
78
  failed: SlackFileFailure[];
79
+ /** Task-text note per audio file id (a transcript), filled when fetched with `transcribe`. */
80
+ notes?: Map<string, string>;
76
81
  /** Deletes the downloaded copies; hold the batch with `await using`. */
77
82
  [Symbol.asyncDispose](): Promise<void>;
78
83
  };
@@ -96,9 +101,14 @@ export function formatFileSize(bytes: number): string {
96
101
  * Build a text representation of file attachments for inclusion in messages.
97
102
  * Each file is formatted as: [File: filename.ext (mimetype, size) id=FILE_ID],
98
103
  * followed by `(not attached: <reason>)` when the file never made it into the
99
- * task's attachments.
104
+ * task's attachments, and by the file's note (a voice-clip transcript) on the
105
+ * next line.
100
106
  */
101
- export function buildAttachmentText(files: SlackFile[], failed: SlackFileFailure[] = []): string {
107
+ export function buildAttachmentText(
108
+ files: SlackFile[],
109
+ failed: SlackFileFailure[] = [],
110
+ notes: Map<string, string> = new Map(),
111
+ ): string {
102
112
  const reasons = new Map(failed.map((f) => [f.file.id, f.reason]));
103
113
  return files
104
114
  .map((f) => {
@@ -106,7 +116,8 @@ export function buildAttachmentText(files: SlackFile[], failed: SlackFileFailure
106
116
  const size = typeof f.size === "number" ? formatFileSize(f.size) : "unknown size";
107
117
  const line = `[File: ${f.name ?? f.id} (${f.mimetype ?? "unknown type"}, ${size}) id=${f.id}]`;
108
118
  const reason = reasons.get(f.id);
109
- return reason ? `${line} (not attached: ${reason})` : line;
119
+ const note = notes.get(f.id);
120
+ return [reason ? `${line} (not attached: ${reason})` : line, note].filter(Boolean).join("\n");
110
121
  })
111
122
  .join("\n");
112
123
  }
@@ -121,15 +132,16 @@ export function buildEffectiveText(
121
132
  text: string | undefined,
122
133
  files?: SlackFile[],
123
134
  failed?: SlackFileFailure[],
135
+ notes?: Map<string, string>,
124
136
  ): string {
125
137
  const hasText = !!text?.trim();
126
138
  const hasFiles = files && files.length > 0;
127
139
 
128
140
  if (hasText && hasFiles) {
129
- return `${text}\n\n${buildAttachmentText(files, failed)}`;
141
+ return `${text}\n\n${buildAttachmentText(files, failed, notes)}`;
130
142
  }
131
143
  if (hasFiles) {
132
- return buildAttachmentText(files, failed);
144
+ return buildAttachmentText(files, failed, notes);
133
145
  }
134
146
  return text || "";
135
147
  }
@@ -147,11 +159,14 @@ export function bufferedFileFailures(files: SlackFile[] | undefined): SlackFileF
147
159
  * Download every file on a Slack message with the bot token, each to its own
148
160
  * temp file. Never throws: a file that can't be fetched lands in `failed` with
149
161
  * a reason the user and the agent can read. Hold the result with `await using`
150
- * so the temp files go away with it.
162
+ * so the temp files go away with it. With `transcribe`, every downloaded audio
163
+ * file also gets a `notes` entry: its transcript, the failure, or a line saying
164
+ * no provider is configured.
151
165
  */
152
166
  export async function fetchSlackFiles(
153
167
  client: WebClient,
154
168
  files: SlackFile[] | undefined,
169
+ options: { transcribe?: boolean } = {},
155
170
  ): Promise<InboundSlackFiles> {
156
171
  let dir: string | undefined;
157
172
  const result: InboundSlackFiles = {
@@ -182,9 +197,27 @@ export async function fetchSlackFiles(
182
197
  result.fetched.push({ file, ...outcome });
183
198
  }
184
199
  }
200
+ if (options.transcribe) {
201
+ result.notes = await transcribeAudioNotes(
202
+ result.fetched
203
+ .filter(({ file }) => isSlackAudio(file))
204
+ .map(({ file, path }) => ({
205
+ key: file.id,
206
+ name: file.name ?? file.id,
207
+ data: Bun.file(path),
208
+ mimeType: file.mimetype,
209
+ })),
210
+ "[Slack]",
211
+ );
212
+ }
185
213
  return result;
186
214
  }
187
215
 
216
+ /** An audio file, or a voice clip recorded in Slack (its MIME type can read `video/…`). */
217
+ function isSlackAudio(file: SlackFile): boolean {
218
+ return file.subtype === "slack_audio" || Boolean(file.mimetype?.startsWith("audio/"));
219
+ }
220
+
188
221
  /** Does the start of an HTML response read like Slack's sign-in page? */
189
222
  function looksLikeSignIn(head: Uint8Array[]): boolean {
190
223
  const joined = new Uint8Array(head.reduce((total, part) => total + part.byteLength, 0));
@@ -177,7 +177,7 @@ async function slackFlush(
177
177
  const app = getSlackApp();
178
178
  const files = items.flatMap((item) => item.files ?? []);
179
179
  await using inbound = app
180
- ? await fetchSlackFiles(app.client, files)
180
+ ? await fetchSlackFiles(app.client, files, { transcribe: true })
181
181
  : {
182
182
  files,
183
183
  fetched: [],
@@ -196,6 +196,7 @@ async function slackFlush(
196
196
  item.text,
197
197
  item.files?.map((file) => resolvedFiles.get(file.id) ?? file),
198
198
  inbound.failed,
199
+ inbound.notes,
199
200
  ),
200
201
  )
201
202
  .join("\n---\n"),
@@ -21,6 +21,10 @@
21
21
  * task:trackers:gitlab:{projectId}:{mr|issue}:{iid}
22
22
  * task:trackers:azure-devops:{repositoryId}:pr:{pullRequestId}
23
23
  * task:trackers:azure-devops:{organization}:workitem:{workItemId}
24
+ * task:trackers:bitbucket:{repoUuid}:pr:{pullRequestId}
25
+ * (repo UUID with Bitbucket's `{}` braces
26
+ * stripped and lowercased, so renames keep
27
+ * task threads)
24
28
  * task:trackers:linear:{issueIdentifier} (e.g. DES-42 — case preserved)
25
29
  * task:trackers:jira:{issueIdentifier} (e.g. PROJ-123 — case preserved)
26
30
  * task:schedule:{scheduleId}
@@ -50,7 +54,13 @@ export type ContextKeyFamily =
50
54
  | "schedule"
51
55
  | "workflow";
52
56
 
53
- export type TrackerProvider = "github" | "gitlab" | "azure-devops" | "linear" | "jira";
57
+ export type TrackerProvider =
58
+ | "github"
59
+ | "gitlab"
60
+ | "azure-devops"
61
+ | "bitbucket"
62
+ | "linear"
63
+ | "jira";
54
64
 
55
65
  export type ParsedContextKey =
56
66
  | { family: "slack"; parts: { channelId: string; threadTs: string } }
@@ -76,6 +86,11 @@ export type ParsedContextKey =
76
86
  subFamily: "azure-devops";
77
87
  parts: { organization: string; kind: "workitem"; workItemId: number };
78
88
  }
89
+ | {
90
+ family: "trackers";
91
+ subFamily: "bitbucket";
92
+ parts: { repoUuid: string; kind: "pr"; pullRequestId: number };
93
+ }
79
94
  | {
80
95
  family: "trackers";
81
96
  subFamily: "linear";
@@ -224,6 +239,34 @@ export function azureDevOpsWorkItemContextKey(input: {
224
239
  return ["task", "trackers", "azure-devops", organization, "workitem", workItemId].join(SEPARATOR);
225
240
  }
226
241
 
242
+ /**
243
+ * Bitbucket Cloud wraps repository UUIDs in braces (`{1a2b...}`). Strip them
244
+ * and lowercase so the key is stable regardless of which payload field the
245
+ * UUID came from.
246
+ */
247
+ export function bitbucketContextKey({
248
+ repoUuid,
249
+ pullRequestId,
250
+ }: {
251
+ repoUuid: string;
252
+ pullRequestId: number | string;
253
+ }): string {
254
+ const rawUuid = typeof repoUuid === "string" ? repoUuid.trim().replace(/^\{|\}$/g, "") : repoUuid;
255
+ const uuid = assertSafePart(rawUuid, "repoUuid").toLowerCase();
256
+ if (/[{}]/.test(uuid)) {
257
+ throw new Error(
258
+ `context-key: bitbucket "repoUuid" has unbalanced braces (got ${JSON.stringify(repoUuid)})`,
259
+ );
260
+ }
261
+ const id = assertSafePart(pullRequestId, "pullRequestId");
262
+ if (!/^\d+$/.test(id)) {
263
+ throw new Error(
264
+ `context-key: bitbucket "pullRequestId" must be a positive integer (got ${JSON.stringify(id)})`,
265
+ );
266
+ }
267
+ return ["task", "trackers", "bitbucket", uuid, "pr", id].join(SEPARATOR);
268
+ }
269
+
227
270
  export function linearContextKey(input: { issueIdentifier: string }): string {
228
271
  const issueIdentifier = assertSafePart(input.issueIdentifier, "issueIdentifier");
229
272
  return ["task", "trackers", "linear", issueIdentifier].join(SEPARATOR);
@@ -401,6 +444,23 @@ export function parseContextKey(key: string): ParsedContextKey {
401
444
  parts: { repositoryId: parts[3] as string, kind: "pr", pullRequestId },
402
445
  };
403
446
  }
447
+ if (subFamily === "bitbucket") {
448
+ if (parts.length !== 6 || parts[4] !== "pr" || !parts[3]) {
449
+ throw new Error(`context-key: malformed bitbucket key: ${JSON.stringify(key)}`);
450
+ }
451
+ const idStr = parts[5] as string;
452
+ const pullRequestId = Number.parseInt(idStr, 10);
453
+ if (!/^\d+$/.test(idStr) || !Number.isFinite(pullRequestId)) {
454
+ throw new Error(
455
+ `context-key: malformed bitbucket pullRequestId "${idStr}": ${JSON.stringify(key)}`,
456
+ );
457
+ }
458
+ return {
459
+ family: "trackers",
460
+ subFamily: "bitbucket",
461
+ parts: { repoUuid: parts[3] as string, kind: "pr", pullRequestId },
462
+ };
463
+ }
404
464
  if (subFamily === "linear") {
405
465
  if (parts.length !== 4) {
406
466
  throw new Error(`context-key: malformed linear key: ${JSON.stringify(key)}`);