@desplega.ai/agent-swarm 1.172.0 → 2.0.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 (525) hide show
  1. package/README.md +1 -1
  2. package/dist/{acp-adapter-fpecx1g1.js → acp-adapter-sj2g0m84.js} +18 -17
  3. package/dist/{actions-pw87a1a8.js → actions-fegk44gr.js} +65 -35
  4. package/dist/{amp-adapter-8jem2bc0.js → amp-adapter-ad8ft89x.js} +8 -7
  5. package/dist/{anthropic-messages-8ee2f22q.js → anthropic-messages-r6czkjv1.js} +7 -7
  6. package/dist/{app-wmf0m843.js → app-kfvgbvzy.js} +19 -13
  7. package/dist/{assistant-emp1e108.js → assistant-z7zdy9ja.js} +65 -38
  8. package/dist/{boot-reembed-1de4f4aj.js → boot-reembed-5c2z0mbe.js} +11 -9
  9. package/dist/{boot-reembed-4ymn9atn.js → boot-reembed-j8xa181e.js} +11 -9
  10. package/dist/{boot-scrub-sweep-adhk1s8c.js → boot-scrub-sweep-hnkn3rgt.js} +12 -10
  11. package/dist/{boot-seeds-8q637aew.js → boot-seeds-kwjq4d2z.js} +15 -13
  12. package/dist/{claude-adapter-t26cv8a4.js → claude-adapter-9vmvpvgd.js} +20 -19
  13. package/dist/{claude-managed-adapter-xgxhqrt2.js → claude-managed-adapter-k9qae05j.js} +7 -5
  14. package/dist/{claude-managed-setup-bnmjarjc.js → claude-managed-setup-q54zx5q2.js} +6 -4
  15. package/dist/{claude-sdk-session-gsfsh662.js → claude-sdk-session-9jye6cxr.js} +12698 -11995
  16. package/dist/{cli-k46r9hmk.js → cli-03gedhng.js} +16 -14
  17. package/dist/{cli-kb4ehzx5.js → cli-1e4b6bzh.js} +7 -7
  18. package/dist/{cli-d603qf86.js → cli-29jc5q7w.js} +10 -8
  19. package/dist/cli-2m1ktfzz.js +17795 -0
  20. package/dist/{cli-8vs9trd4.js → cli-3dj8k4vy.js} +1 -1
  21. package/dist/{cli-zxm9p3x4.js → cli-3fbmmawm.js} +5 -5
  22. package/dist/{cli-gmdajfw5.js → cli-4jxm2qtz.js} +4987 -1708
  23. package/dist/{cli-tadfgd0t.js → cli-5hkm7whv.js} +33 -4
  24. package/dist/{cli-6jb2rf77.js → cli-5t4f29ez.js} +3 -3
  25. package/dist/{cli-7a0sde2x.js → cli-6cagzc96.js} +9 -0
  26. package/dist/cli-6cvfxs8r.js +47 -0
  27. package/dist/{cli-pj6x9qce.js → cli-6jyj59nb.js} +2 -2
  28. package/dist/{cli-388egyf6.js → cli-6razj6t4.js} +2 -2
  29. package/dist/{cli-qsztpfry.js → cli-74ahz61x.js} +2 -2
  30. package/dist/{cli-4nz8p1p5.js → cli-7x0g4sn1.js} +15474 -16082
  31. package/dist/{cli-yatxqynm.js → cli-9ha6hs2d.js} +1 -290
  32. package/dist/{cli-81r1wjyv.js → cli-9nfx44eh.js} +1 -1
  33. package/dist/cli-9traj3dn.js +638 -0
  34. package/dist/{cli-smcrrmhy.js → cli-agzebbd5.js} +9 -9
  35. package/dist/{cli-bf7485pm.js → cli-apx87jne.js} +2 -2
  36. package/dist/{cli-ztrv9y21.js → cli-bzszjf2p.js} +3689 -1081
  37. package/dist/{cli-c5km5znr.js → cli-c8r3kzzb.js} +1 -1
  38. package/dist/{cli-hsem16hb.js → cli-cgtksawx.js} +7 -4
  39. package/dist/{cli-db3m6zdt.js → cli-d2qr7aj4.js} +1 -1
  40. package/dist/{cli-1hgat6pa.js → cli-d5f9ed5c.js} +5 -5
  41. package/dist/cli-e3g5mbkz.js +776 -0
  42. package/dist/{cli-7jykv7ef.js → cli-e6mpjhed.js} +1 -1
  43. package/dist/{cli-j6kpj9az.js → cli-ee9gtz70.js} +1553 -68
  44. package/dist/{cli-e0a9sk00.js → cli-eejsgvm4.js} +4 -4
  45. package/dist/{cli-tgpfnm6r.js → cli-ef1hg1wv.js} +518 -59
  46. package/dist/cli-f527ce74.js +781 -0
  47. package/dist/{cli-ppy4nnnh.js → cli-f7kr7svz.js} +2 -2
  48. package/dist/cli-hjwmhcg1.js +10622 -0
  49. package/dist/{cli-2bw3w47j.js → cli-hnhtk6cj.js} +1 -1
  50. package/dist/{cli-ywm1azg9.js → cli-hnmhtzvh.js} +6 -4
  51. package/dist/{cli-4fnb6884.js → cli-hvqbtna4.js} +7 -1259
  52. package/dist/{cli-pstq9888.js → cli-j0pfptdp.js} +220 -103
  53. package/dist/cli-jta2eyzc.js +619 -0
  54. package/dist/{cli-esnpehf5.js → cli-k382tshb.js} +12 -13
  55. package/dist/cli-k3dp5tpf.js +263 -0
  56. package/dist/{cli-wd0hz6ah.js → cli-kbrt8gqa.js} +9 -91
  57. package/dist/cli-kceah1eh.js +92 -0
  58. package/dist/{cli-prrt9fa0.js → cli-ke896fcf.js} +10 -10
  59. package/dist/{cli-nv4zd0b9.js → cli-kt6zqg0e.js} +6 -6
  60. package/dist/{cli-g6t9texm.js → cli-m50t4309.js} +5 -18
  61. package/dist/cli-n2wq4j7m.js +6985 -0
  62. package/dist/{cli-c2pq2rjb.js → cli-n3hzg89b.js} +46 -15
  63. package/dist/{cli-tmhsxhb8.js → cli-n5vwg842.js} +23 -6
  64. package/dist/{cli-5rv1j9ga.js → cli-ne236afy.js} +53 -7
  65. package/dist/cli-p0sg24zp.js +16 -0
  66. package/dist/{cli-d161g69q.js → cli-p1x85d2t.js} +50 -2
  67. package/dist/{cli-7pz3b7x5.js → cli-przpmdgr.js} +25 -0
  68. package/dist/{cli-g4f5pj26.js → cli-q0ke1yww.js} +111 -25
  69. package/dist/{cli-e3pmyt39.js → cli-q50942rs.js} +1 -1
  70. package/dist/{cli-r44t9wqc.js → cli-r106725k.js} +2 -2
  71. package/dist/{cli-gj81e47t.js → cli-r2pnvj8g.js} +2 -2
  72. package/dist/{cli-r8smj8b8.js → cli-r62a35ea.js} +19 -3
  73. package/dist/{cli-cd0hv9nh.js → cli-ra0xzggj.js} +2 -2
  74. package/dist/cli-rhs9pz33.js +294 -0
  75. package/dist/{cli-nyhb1r6x.js → cli-s2sp957d.js} +52 -29
  76. package/dist/{cli-1x7da0de.js → cli-s4setq91.js} +8 -8
  77. package/dist/{cli-xczpdp9e.js → cli-se8w6wny.js} +6 -6
  78. package/dist/cli-skxvvpap.js +10140 -0
  79. package/dist/{cli-014vns2y.js → cli-stfs0k5s.js} +208 -525
  80. package/dist/{cli-0p3enk35.js → cli-tqaazemc.js} +9 -1
  81. package/dist/{cli-cf69s3ew.js → cli-tr4sxp68.js} +3 -3
  82. package/dist/{cli-8hzfb6xz.js → cli-tsde1hdr.js} +15 -15
  83. package/dist/{cli-cceskcvt.js → cli-txqb9s38.js} +8 -8
  84. package/dist/{cli-aspcaef3.js → cli-v1yb0rqb.js} +6 -6
  85. package/dist/{cli-j1pmc07z.js → cli-vatyhshy.js} +268 -40
  86. package/dist/{cli-04emfe2q.js → cli-vdach5a9.js} +5 -5
  87. package/dist/{cli-7avy4v8w.js → cli-w1q7h2r2.js} +5 -3
  88. package/dist/{cli-rcp5aqdw.js → cli-w7m81tb7.js} +1 -1
  89. package/dist/{cli-hwwvy36w.js → cli-wayw93nt.js} +3 -3
  90. package/dist/cli-wmgrkscj.js +23254 -0
  91. package/dist/{cli-gp1rtyvk.js → cli-z5jjpaxw.js} +29 -6
  92. package/dist/{cli-451ks6aw.js → cli-zkpks3r3.js} +1 -1
  93. package/dist/cli.js +32 -48
  94. package/dist/{codex-adapter-m4tgdysy.js → codex-adapter-y81sssk0.js} +16 -15
  95. package/dist/{codex-login-841kv5fk.js → codex-login-f2rcfpp9.js} +3 -2
  96. package/dist/{codex-session-runner-ymwzm6ea.js → codex-session-runner-8vx4j6a9.js} +16 -15
  97. package/dist/{commands-n0hshcvs.js → commands-kcpvpcrr.js} +21 -10
  98. package/dist/{controller-hq5ggpcp.js → controller-ywrmt6yd.js} +5 -4
  99. package/dist/{controllers-connect-y07cqcvy.js → controllers-connect-kwpvj3rf.js} +45 -38
  100. package/dist/{cursor-adapter-q8pgv5qa.js → cursor-adapter-cehv1qns.js} +7 -6
  101. package/dist/{db-hkqvrdcb.js → db-k8ankj20.js} +13 -11
  102. package/dist/{db-4w7tmt1g.js → db-ww5d7jq3.js} +29 -9
  103. package/dist/{devin-adapter-xgtj680j.js → devin-adapter-x1022qb1.js} +3 -2
  104. package/dist/{dispatcher-5sd69y14.js → dispatcher-e8gq5m68.js} +41 -34
  105. package/dist/{docker-f503k9p0.js → docker-ff6y1190.js} +3 -2
  106. package/dist/{dsh-adapter-2h8m4cab.js → dsh-adapter-3epjm3af.js} +7 -6
  107. package/dist/{e2b-66xra4az.js → e2b-8ygwhg0f.js} +4 -3
  108. package/dist/{e2b-xwhyj8cw.js → e2b-swhnpw3n.js} +4 -3
  109. package/dist/{event-bus-vjtv82gm.js → event-bus-6axnngdj.js} +3 -2
  110. package/dist/external-channel-h3359cwn.js +52 -0
  111. package/dist/{google-generative-ai-7pavrbv7.js → google-generative-ai-g8db7x0f.js} +5 -3
  112. package/dist/{google-vertex-3f1jk41f.js → google-vertex-r2692q0r.js} +5 -3
  113. package/dist/{grok-adapter-8m9yqzbq.js → grok-adapter-0d4x7gkx.js} +19 -18
  114. package/dist/{handlers-s20xr10r.js → handlers-an1gzv21.js} +44 -37
  115. package/dist/{heartbeat-3k31q4g2.js → heartbeat-6dakeb61.js} +46 -39
  116. package/dist/{hook-gdn6a2fn.js → hook-6v6cv6d5.js} +12 -24
  117. package/dist/{hook-ygwerrdh.js → hook-sa5xye4y.js} +13 -12
  118. package/dist/{http-jdteeg81.js → http-r7f3eky0.js} +1598 -279
  119. package/dist/index-52j1aja4.js +3465 -0
  120. package/dist/{index-e0snym4t.js → index-7ep4gzdk.js} +41 -34
  121. package/dist/index-9gw58n4k.js +193 -0
  122. package/dist/index-9j87mcm8.js +56305 -0
  123. package/dist/index-bsmcresk.js +79424 -0
  124. package/dist/index-mh85qhec.js +475 -0
  125. package/dist/{index-vm3bm9t7.js → index-rmpw9a8q.js} +41 -34
  126. package/dist/{index-6ksewd5y.js → index-s2wxgzr0.js} +1015 -732
  127. package/dist/{index-tsszt7g9.js → index-v1v6n7hg.js} +46 -39
  128. package/dist/index-we27kd31.js +12810 -0
  129. package/dist/{index-ffqzqgwb.js → index-zk96aq1k.js} +11 -9
  130. package/dist/{keepalive-jekc896t.js → keepalive-be9vwy5h.js} +13 -11
  131. package/dist/{lead-4k9s9szx.js → lead-0861cwrf.js} +64 -57
  132. package/dist/{maintenance-shazch4n.js → maintenance-mzzgc8bn.js} +11 -9
  133. package/dist/node-c9we95xf.js +1 -1
  134. package/dist/{node-fkajaq96.js → node-mn1zs7an.js} +2 -1
  135. package/dist/{oauth-refresh-sweep-5gb36kn8.js → oauth-refresh-sweep-cpdnfwdj.js} +12 -10
  136. package/dist/{onboard-ydrbnvww.js → onboard-fh8qs4r3.js} +11 -3
  137. package/dist/{opencode-adapter-5zh0he20.js → opencode-adapter-nm42wwj8.js} +19 -14
  138. package/dist/{otel-impl-6pf5cv86.js → otel-impl-x6c1c9g4.js} +248 -869
  139. package/dist/{pi-mono-adapter-ef9wwx9m.js → pi-mono-adapter-9n2ya240.js} +245 -107
  140. package/dist/{pricing-refresh-s52nsa5q.js → pricing-refresh-qb0rv0ap.js} +13 -11
  141. package/dist/{rbac-roles-aq3msh4e.js → rbac-roles-1gzdwbh1.js} +12 -10
  142. package/dist/{rbac-roles-q5gb5e8v.js → rbac-roles-3mraey52.js} +13 -11
  143. package/dist/{render-v2-pd1rg54q.js → render-v2-agpa45zz.js} +19 -13
  144. package/dist/{resume-gw4a2c6v.js → resume-x0ydy9s9.js} +27 -21
  145. package/dist/{runtime-qfry88v2.js → runtime-sa2vr16g.js} +2 -2
  146. package/dist/{setup-jda1ksdh.js → setup-as9ga0d6.js} +2 -2
  147. package/dist/{virtual-modules-b3ga2m65.js → virtual-modules-9gkxqrap.js} +7 -6
  148. package/dist/{work-objects-tb7ycye8.js → work-objects-y5g1y7ev.js} +49 -37
  149. package/dist/{worker-njjccz5d.js → worker-860c3e38.js} +64 -57
  150. package/dist/{x-qfmygqqv.js → x-m477fr0h.js} +3 -2
  151. package/openapi.json +951 -356
  152. package/package.json +12 -13
  153. package/src/apps/catalog.generated.json +1309 -53
  154. package/src/apps/page-validator.ts +93 -31
  155. package/src/azure-devops/api.ts +94 -18
  156. package/src/azure-devops/auth.ts +32 -0
  157. package/src/azure-devops/boards-sync.ts +708 -0
  158. package/src/azure-devops/handlers.ts +52 -8
  159. package/src/azure-devops/html.ts +26 -0
  160. package/src/azure-devops/index.ts +15 -0
  161. package/src/azure-devops/templates.ts +86 -0
  162. package/src/azure-devops/types.ts +65 -1
  163. package/src/be/config-refs.ts +60 -0
  164. package/src/be/db/page-versions.ts +15 -0
  165. package/src/be/db/runtime.ts +35 -8
  166. package/src/be/db/skill-name-resolution.ts +36 -0
  167. package/src/be/db/tasks/read.ts +230 -0
  168. package/src/be/db/tasks/write.ts +7 -0
  169. package/src/be/db.ts +74 -13
  170. package/src/be/migrations/213_script_connections_sql.sql +79 -0
  171. package/src/be/migrations/215_swarm_config_export_to_env.sql +34 -0
  172. package/src/be/migrations/216_task_resolved_effort.sql +5 -0
  173. package/src/be/migrations/217_chat_sdk_state.sql +104 -0
  174. package/src/be/model-catalog-store.ts +5 -1
  175. package/src/be/model-tier-resolution.ts +12 -1
  176. package/src/be/model-validation.ts +5 -2
  177. package/src/be/models-catalog.ts +26 -1
  178. package/src/be/modelsdev-cache.json +198 -63
  179. package/src/be/modelsdev-cache.ts +9 -1
  180. package/src/be/script-connections.ts +47 -8
  181. package/src/be/script-credential-broker.ts +39 -2
  182. package/src/be/scripts/run-saved.ts +1 -0
  183. package/src/be/scripts/typecheck.ts +10 -8
  184. package/src/be/seed/runner.ts +13 -1
  185. package/src/be/seed/state-db.ts +5 -0
  186. package/src/be/seed/types.ts +6 -0
  187. package/src/be/seed-pricing.ts +19 -2
  188. package/src/be/seed-scripts/catalog/ops-catalog-audit.inline.ts +1 -1
  189. package/src/be/seed-scripts/catalog/ops-catalog-audit.ts +1 -1
  190. package/src/be/seed-skills/bundled-files.generated.json +10 -18
  191. package/src/be/seed-skills/index.ts +107 -3
  192. package/src/be/sql-connections.ts +118 -0
  193. package/src/be/swarm-config-guard.ts +53 -0
  194. package/src/cli.tsx +0 -16
  195. package/src/commands/onboard/non-interactive.ts +4 -2
  196. package/src/commands/profile-sync.ts +5 -3
  197. package/src/commands/provider-credentials.ts +52 -6
  198. package/src/commands/runner.ts +196 -56
  199. package/src/e2b/env.ts +5 -0
  200. package/src/extensions/contract-types.generated.ts +1 -1
  201. package/src/extensions/contract.ts +45 -0
  202. package/src/extensions/dispatcher.ts +7 -0
  203. package/src/extensions/post-bridge.ts +40 -0
  204. package/src/github/handlers.ts +11 -4
  205. package/src/heartbeat/templates.ts +8 -5
  206. package/src/hooks/hook.ts +0 -26
  207. package/src/http/all-routes.ts +0 -1
  208. package/src/http/config.ts +36 -17
  209. package/src/http/core.ts +4 -0
  210. package/src/http/index.ts +14 -2
  211. package/src/http/integrations.ts +88 -1
  212. package/src/http/mcp-oauth.ts +6 -13
  213. package/src/http/mcp.ts +15 -5
  214. package/src/http/page-proxy.ts +2 -4
  215. package/src/http/pages-public.ts +6 -5
  216. package/src/http/pages.ts +5 -14
  217. package/src/http/poll.ts +28 -68
  218. package/src/http/requested-by.ts +54 -0
  219. package/src/http/route-def.ts +7 -6
  220. package/src/http/script-connection-proxy.ts +51 -4
  221. package/src/http/script-connections.ts +5 -2
  222. package/src/http/scripts.ts +1 -0
  223. package/src/http/session-cost-recompute.ts +12 -2
  224. package/src/http/session-data.ts +4 -0
  225. package/src/http/stats.ts +1 -30
  226. package/src/http/tasks.ts +61 -5
  227. package/src/http/utils.ts +14 -0
  228. package/src/http/webhooks.ts +183 -2
  229. package/src/http/x.ts +1 -0
  230. package/src/integrations/chat-sdk/adapters.ts +129 -0
  231. package/src/integrations/chat-sdk/attachments.ts +232 -0
  232. package/src/integrations/chat-sdk/background.ts +36 -0
  233. package/src/integrations/chat-sdk/commands.ts +194 -0
  234. package/src/integrations/chat-sdk/config.ts +198 -0
  235. package/src/integrations/chat-sdk/discord-api.ts +63 -0
  236. package/src/integrations/chat-sdk/discord-gateway.ts +185 -0
  237. package/src/integrations/chat-sdk/handlers.ts +28 -0
  238. package/src/integrations/chat-sdk/inbound.ts +426 -0
  239. package/src/integrations/chat-sdk/limits.ts +48 -0
  240. package/src/integrations/chat-sdk/logger.ts +81 -0
  241. package/src/integrations/chat-sdk/outbound.ts +123 -0
  242. package/src/integrations/chat-sdk/relay.ts +172 -0
  243. package/src/integrations/chat-sdk/router.ts +169 -0
  244. package/src/integrations/chat-sdk/runtime.ts +351 -0
  245. package/src/integrations/chat-sdk/schema.ts +245 -0
  246. package/src/integrations/chat-sdk/state.ts +243 -0
  247. package/src/integrations/chat-sdk/status.ts +244 -0
  248. package/src/integrations/chat-sdk/steering.ts +58 -0
  249. package/src/integrations/chat-sdk/teams-origin.ts +77 -0
  250. package/src/integrations/chat-sdk/telegram-api.ts +56 -0
  251. package/src/integrations/chat-sdk/templates.ts +16 -0
  252. package/src/integrations/chat-sdk/test-hooks.ts +37 -0
  253. package/src/integrations/chat-sdk/thread-buffer.ts +100 -0
  254. package/src/integrations/chat-sdk/typing.ts +65 -0
  255. package/src/jira/adf.ts +3 -1
  256. package/src/kv-reserved-namespaces.ts +2 -0
  257. package/src/oauth/mcp-wrapper.ts +10 -6
  258. package/src/otel-impl.ts +2 -1
  259. package/src/pages/version.ts +4 -5
  260. package/src/prompts/base-prompt.ts +111 -45
  261. package/src/providers/acp-target-catalog.ts +6 -0
  262. package/src/providers/acp-targets.ts +56 -3
  263. package/src/providers/amp-adapter.ts +1 -1
  264. package/src/providers/claude-adapter.ts +27 -2
  265. package/src/providers/claude-flag-settings.ts +133 -0
  266. package/src/providers/claude-managed-adapter.ts +1 -1
  267. package/src/providers/claude-sdk-session.ts +7 -0
  268. package/src/providers/claude-session-events.ts +6 -1
  269. package/src/providers/dsh-adapter.ts +1 -1
  270. package/src/providers/opencode-adapter.ts +268 -37
  271. package/src/providers/opencode-billing.ts +86 -0
  272. package/src/providers/opencode-swarm-plugin.ts +51 -0
  273. package/src/providers/pi-mono-adapter.ts +398 -153
  274. package/src/providers/pricing-sources.md +37 -3
  275. package/src/providers/reasoning-effort.ts +27 -0
  276. package/src/providers/types.ts +11 -2
  277. package/src/rbac/legacy-policy.ts +8 -2
  278. package/src/rbac/permissions.ts +21 -0
  279. package/src/scheduler/scheduler.ts +4 -2
  280. package/src/scripts-runtime/api-client.ts +15 -1
  281. package/src/scripts-runtime/api-types.ts +9 -1
  282. package/src/scripts-runtime/ctx.ts +1 -1
  283. package/src/scripts-runtime/loader.ts +19 -7
  284. package/src/scripts-runtime/mcp-client.ts +30 -19
  285. package/src/scripts-runtime/sdk-allowlist.ts +8 -6
  286. package/src/scripts-runtime/swarm-sdk.ts +0 -11
  287. package/src/scripts-runtime/types/stdlib.d.ts +27 -15
  288. package/src/scripts-runtime/types/swarm-extension.d.ts +63 -1
  289. package/src/scripts-runtime/types/swarm-sdk.d.ts +27 -15
  290. package/src/server.ts +52 -18
  291. package/src/slack/actions.ts +29 -1
  292. package/src/slack/app.ts +14 -2
  293. package/src/slack/assistant.ts +36 -2
  294. package/src/slack/channel-join.ts +3 -16
  295. package/src/slack/commands.ts +9 -1
  296. package/src/slack/event-dedup.ts +1 -1
  297. package/src/slack/external-channel-egress.ts +113 -0
  298. package/src/slack/external-channel.ts +309 -0
  299. package/src/slack/handlers.ts +61 -0
  300. package/src/slack/render-v2.ts +40 -47
  301. package/src/slack/work-objects.ts +5 -0
  302. package/src/tasks/additive-buffer.ts +4 -2
  303. package/src/tasks/context-key.ts +94 -2
  304. package/src/tasks/model-pin.ts +67 -0
  305. package/src/tasks/worker-follow-up.ts +4 -31
  306. package/src/telemetry.ts +2 -1
  307. package/src/tests/acp-adapter.test.ts +42 -0
  308. package/src/tests/api-drain-worker-e2e.test.ts +1 -6
  309. package/src/tests/apps-catalog-feedback.test.ts +310 -0
  310. package/src/tests/apps-catalog-inputs.test.ts +70 -0
  311. package/src/tests/apps-catalog-table.test.ts +113 -0
  312. package/src/tests/azure-devops-boards-sync.test.ts +584 -0
  313. package/src/tests/azure-devops-html.test.ts +13 -0
  314. package/src/tests/azure-devops-webhook-malformed.test.ts +4 -0
  315. package/src/tests/base-prompt.test.ts +58 -54
  316. package/src/tests/chat-sdk-commands.test.ts +191 -0
  317. package/src/tests/chat-sdk-config.test.ts +334 -0
  318. package/src/tests/chat-sdk-discord-api.test.ts +68 -0
  319. package/src/tests/chat-sdk-discord-gateway.test.ts +252 -0
  320. package/src/tests/chat-sdk-discord-webhook.test.ts +126 -0
  321. package/src/tests/chat-sdk-fields-sync.test.ts +50 -0
  322. package/src/tests/chat-sdk-harness.ts +146 -0
  323. package/src/tests/chat-sdk-inbound.test.ts +363 -0
  324. package/src/tests/chat-sdk-relay.test.ts +470 -0
  325. package/src/tests/chat-sdk-reload.test.ts +128 -0
  326. package/src/tests/chat-sdk-shutdown.test.ts +220 -0
  327. package/src/tests/chat-sdk-slack.test.ts +268 -0
  328. package/src/tests/chat-sdk-state.test.ts +163 -0
  329. package/src/tests/chat-sdk-status-route.test.ts +382 -0
  330. package/src/tests/chat-sdk-teams.test.ts +277 -0
  331. package/src/tests/chat-sdk-tools.test.ts +520 -0
  332. package/src/tests/chat-sdk-typing.test.ts +120 -0
  333. package/src/tests/chat-sdk-webhook-telegram.test.ts +200 -0
  334. package/src/tests/claude-adapter-binary.test.ts +144 -0
  335. package/src/tests/claude-flag-settings.test.ts +114 -0
  336. package/src/tests/claude-managed-adapter.test.ts +1 -1
  337. package/src/tests/claude-sdk-lifecycle.test.ts +148 -1
  338. package/src/tests/claude-sdk-process.fixture.ts +7 -0
  339. package/src/tests/claude-thinking-tokens.test.ts +57 -0
  340. package/src/tests/claude-worker-parity.test.ts +2 -7
  341. package/src/tests/config-env-export-chat-sdk.test.ts +11 -0
  342. package/src/tests/config-refs.test.ts +66 -0
  343. package/src/tests/context-key.test.ts +51 -0
  344. package/src/tests/cors-allowlist.test.ts +25 -0
  345. package/src/tests/create-page-tool.test.ts +18 -0
  346. package/src/tests/credential-check.test.ts +139 -0
  347. package/src/tests/credentials.test.ts +21 -0
  348. package/src/tests/db-runtime.test.ts +61 -0
  349. package/src/tests/db-task-read-contract.test.ts +20 -0
  350. package/src/tests/db-task-write-contract.test.ts +10 -0
  351. package/src/tests/deferred-task-wake.test.ts +52 -4
  352. package/src/tests/docs-index-footprint.test.js +3 -3
  353. package/src/tests/dsh-adapter.test.ts +1 -1
  354. package/src/tests/enabled-capabilities.test.ts +20 -0
  355. package/src/tests/entrypoint-api-readiness.test.ts +14 -8
  356. package/src/tests/entrypoint-config-env-export.test.ts +5 -2
  357. package/src/tests/entrypoint-export-to-env.test.ts +78 -0
  358. package/src/tests/explicit-model-validation.test.ts +105 -1
  359. package/src/tests/extensions-example-route-chat-channel.test.ts +154 -0
  360. package/src/tests/extensions-post-bridge.test.ts +51 -0
  361. package/src/tests/fetch-resolved-env.test.ts +128 -0
  362. package/src/tests/fixtures/azure-boards/workitem.commented.json +89 -0
  363. package/src/tests/fixtures/azure-boards/workitem.created.json +81 -0
  364. package/src/tests/fixtures/azure-boards/workitem.deleted.json +70 -0
  365. package/src/tests/fixtures/azure-boards/workitem.restored.json +82 -0
  366. package/src/tests/fixtures/azure-boards/workitem.updated.json +130 -0
  367. package/src/tests/fixtures/catalog-feedback-definition.json.txt +860 -0
  368. package/src/tests/fixtures/catalog-inputs-definition.json.txt +910 -0
  369. package/src/tests/fixtures/catalog-table-definition.json.txt +468 -0
  370. package/src/tests/fixtures/chat-sdk/telegram-private-text.json +10 -0
  371. package/src/tests/fixtures/extensions/route-chat-channel-to-agent/hooks.ts +27 -0
  372. package/src/tests/fixtures/extensions/route-chat-channel-to-agent/manifest.json +9 -0
  373. package/src/tests/fixtures/openai-compatible-registry-extension.child.ts +146 -0
  374. package/src/tests/fixtures/runner-identity-refresh.ts +3 -0
  375. package/src/tests/fixtures/runner-legacy-resume-prompt.ts +122 -0
  376. package/src/tests/github-handlers.test.ts +35 -0
  377. package/src/tests/harness-provider-resolution.test.ts +12 -0
  378. package/src/tests/heartbeat-checklist.test.ts +3 -2
  379. package/src/tests/hook-runtime-close.test.ts +0 -2
  380. package/src/tests/http-api-integration.test.ts +0 -25
  381. package/src/tests/migration-046-budgets.test.ts +17 -16
  382. package/src/tests/migration-217-chat-sdk-state.test.ts +113 -0
  383. package/src/tests/model-tier-resolution.test.ts +93 -0
  384. package/src/tests/models-catalog.test.ts +42 -1
  385. package/src/tests/node-env-runtime.test.ts +98 -0
  386. package/src/tests/openai-compatible-providers.test.ts +134 -0
  387. package/src/tests/openai-compatible-registry-extension.test.ts +42 -0
  388. package/src/tests/opencode-adapter.test.ts +348 -3
  389. package/src/tests/opencode-billing.test.ts +28 -0
  390. package/src/tests/opencode-helmcode-sdk.test.ts +201 -0
  391. package/src/tests/opencode-shell-env.test.ts +145 -0
  392. package/src/tests/openrouter-base-url.test.ts +101 -151
  393. package/src/tests/page-proxy.test.ts +1 -1
  394. package/src/tests/pages-browser-sdk.test.ts +76 -0
  395. package/src/tests/pages-feedback.test.ts +1 -1
  396. package/src/tests/paused-tasks-requested-by.test.ts +102 -0
  397. package/src/tests/pi-custom-provider-fake-server.test.ts +442 -0
  398. package/src/tests/pi-mono-adapter.test.ts +385 -5
  399. package/src/tests/pi-shell-env.test.ts +170 -0
  400. package/src/tests/prompt-template-remaining.test.ts +35 -0
  401. package/src/tests/rbac-engine.test.ts +11 -3
  402. package/src/tests/runner-exit-after-tasks.test.ts +0 -4
  403. package/src/tests/runner-identity-refresh.test.ts +3 -0
  404. package/src/tests/runner-legacy-resume-prompt.test.ts +64 -0
  405. package/src/tests/script-connections-sql.test.ts +675 -0
  406. package/src/tests/script-connections.test.ts +7 -1
  407. package/src/tests/scripts-http.test.ts +201 -1
  408. package/src/tests/scripts-only-gating.test.ts +2 -3
  409. package/src/tests/scripts-runtime.test.ts +58 -40
  410. package/src/tests/secret-scrubber.test.ts +86 -0
  411. package/src/tests/seed-skills-bundled-files.test.ts +45 -53
  412. package/src/tests/seed-skills-retire.test.ts +182 -0
  413. package/src/tests/send-task-requester-gate.test.ts +58 -0
  414. package/src/tests/session-cost-recompute-unit.test.ts +94 -0
  415. package/src/tests/skill-get-name-resolution.test.ts +137 -0
  416. package/src/tests/slack-channel-join.test.ts +4 -1
  417. package/src/tests/slack-external-channel-config.test.ts +65 -0
  418. package/src/tests/slack-external-channel-egress.test.ts +237 -0
  419. package/src/tests/slack-external-channel-guard.test.ts +280 -0
  420. package/src/tests/slack-external-channel-tools.test.ts +173 -0
  421. package/src/tests/slack-external-channel.test.ts +370 -0
  422. package/src/tests/slack-render-v2.test.ts +86 -10
  423. package/src/tests/sonnet-5-5-registry.test.ts +9 -1
  424. package/src/tests/spec-base-url.test.ts +16 -1
  425. package/src/tests/swarm-config-export-to-env.test.ts +267 -0
  426. package/src/tests/swarm-config-reserved-keys.test.ts +16 -0
  427. package/src/tests/swarm-diff.test.ts +1 -1
  428. package/src/tests/task-model-effort-rerun.test.ts +164 -0
  429. package/src/tests/task-session-prompts.test.ts +132 -0
  430. package/src/tests/task-tool-manifest.test.ts +14 -0
  431. package/src/tests/task-tools-ctx.test.ts +2 -0
  432. package/src/tests/tool-annotations.test.ts +6 -6
  433. package/src/tests/ui-logs-parser.test.ts +43 -0
  434. package/src/tests/used-by-route.test.ts +6 -0
  435. package/src/tests/vendored-openapi.test.ts +16 -1
  436. package/src/tests/vercel-ai-gateway.test.ts +149 -0
  437. package/src/tests/workflow-swarm-script.test.ts +38 -0
  438. package/src/tests/workflow-wait-filter.test.ts +5 -3
  439. package/src/tools/chat-access.ts +41 -0
  440. package/src/tools/chat-open-dm.ts +59 -0
  441. package/src/tools/chat-post.ts +74 -0
  442. package/src/tools/chat-read-thread.ts +100 -0
  443. package/src/tools/chat-reply.ts +75 -0
  444. package/src/tools/create-page.ts +3 -13
  445. package/src/tools/defer-task.ts +17 -0
  446. package/src/tools/model-catalog/index.ts +2 -1
  447. package/src/tools/register-chat-channel.ts +139 -0
  448. package/src/tools/send-task.ts +24 -0
  449. package/src/tools/skills/skill-get.ts +17 -9
  450. package/src/tools/slack-post.ts +6 -0
  451. package/src/tools/slack-read.ts +8 -0
  452. package/src/tools/slack-reply.ts +6 -0
  453. package/src/tools/slack-start-thread.ts +6 -0
  454. package/src/tools/slack-update.ts +6 -0
  455. package/src/tools/slack-upload-file.ts +6 -0
  456. package/src/tools/swarm-config/get-config.ts +2 -1
  457. package/src/tools/swarm-config/list-config.ts +2 -1
  458. package/src/tools/swarm-config/set-config.ts +16 -3
  459. package/src/tools/tool-config.ts +8 -6
  460. package/src/types.ts +12 -2
  461. package/src/utils/config-env-export.ts +74 -0
  462. package/src/utils/credentials.test.ts +92 -0
  463. package/src/utils/credentials.ts +52 -4
  464. package/src/utils/harness-provider.ts +15 -6
  465. package/src/utils/node-env.ts +24 -0
  466. package/src/utils/runtime-model-catalog.ts +11 -1
  467. package/src/utils/secret-scrubber.ts +71 -2
  468. package/src/utils/task-tool-manifest.ts +19 -2
  469. package/src/workflows/executors/swarm-script.ts +1 -0
  470. package/templates/skills/agent-browser/SKILL.md +1 -1
  471. package/templates/skills/agent-browser/content.md +1 -1
  472. package/templates/skills/apps/SKILL.md +749 -0
  473. package/templates/skills/apps/content.md +60 -22
  474. package/templates/skills/chat-sdk/config.json +14 -0
  475. package/templates/skills/chat-sdk/content.md +84 -0
  476. package/templates/skills/composio-gmail/content.md +7 -0
  477. package/templates/skills/composio-google-docs/content.md +1 -1
  478. package/templates/skills/email-domain-health/config.json +14 -0
  479. package/templates/skills/email-domain-health/content.md +131 -0
  480. package/templates/skills/kv-storage/content.md +2 -2
  481. package/templates/skills/pages/content.md +8 -10
  482. package/templates/skills/qa-loom/config.json +14 -0
  483. package/templates/skills/qa-loom/content.md +187 -0
  484. package/templates/skills/qa-loom/files/build-loom.mjs +221 -0
  485. package/templates/skills/qa-loom/files/run-qa.sh +57 -0
  486. package/templates/skills/slack-interaction/content.md +4 -0
  487. package/templates/skills/swarm-extensions/content.md +2 -0
  488. package/templates/skills/swarm-scripts/SKILL.md +4 -2
  489. package/templates/skills/swarm-scripts/config.json +1 -1
  490. package/templates/skills/swarm-scripts/content.md +4 -2
  491. package/templates/skills/workflow-iterate/content.md +38 -0
  492. package/dist/artifact-eczsknc3.js +0 -3843
  493. package/dist/cli-9xsmdpdt.js +0 -56377
  494. package/dist/cli-bd957dgb.js +0 -153
  495. package/dist/cli-q51yprfq.js +0 -1104
  496. package/dist/cli-t1ze64h3.js +0 -811
  497. package/dist/index-9ch1n761.js +0 -1102
  498. package/src/artifact-sdk/index.ts +0 -2
  499. package/src/artifact-sdk/localtunnel.d.ts +0 -20
  500. package/src/artifact-sdk/port.ts +0 -12
  501. package/src/artifact-sdk/server.ts +0 -183
  502. package/src/artifact-sdk/tunnel.ts +0 -19
  503. package/src/commands/artifact.ts +0 -249
  504. package/src/http/ecosystem.ts +0 -77
  505. package/src/tests/artifact-commands.test.ts +0 -92
  506. package/src/tests/artifact-sdk.test.ts +0 -793
  507. package/src/tests/register-service-security.test.ts +0 -194
  508. package/src/tools/list-services.ts +0 -107
  509. package/src/tools/register-service.ts +0 -177
  510. package/src/tools/unregister-service.ts +0 -73
  511. package/src/tools/update-service-status.ts +0 -96
  512. package/templates/skills/artifacts/config.json +0 -14
  513. package/templates/skills/artifacts/content.md +0 -344
  514. package/templates/skills/artifacts/files/examples/approval-flow.ts +0 -34
  515. package/templates/skills/artifacts/files/examples/hono-dashboard.ts +0 -31
  516. package/templates/skills/artifacts/files/examples/multi-artifact.ts +0 -20
  517. package/templates/skills/artifacts/files/examples/static-report.sh +0 -17
  518. package/dist/{azure-openai-responses-ap1gwxgr.js → azure-openai-responses-ygfreq4t.js} +5 -5
  519. package/dist/{cli-5rqrj7q0.js → cli-pw7ebjdt.js} +0 -0
  520. package/dist/{openai-codex-responses-33qhkkdy.js → openai-codex-responses-3bk0grnr.js} +4 -4
  521. package/dist/{openai-completions-s88xm3ns.js → openai-completions-5d4jpakc.js} +9 -9
  522. package/dist/{openai-responses-7xd2f2e2.js → openai-responses-krb51f59.js} +5 -5
  523. package/dist/{pi-messages-xszk613n.js → pi-messages-x9gzynm1.js} +3 -3
  524. /package/src/{artifact-sdk → pages}/browser-sdk.ts +0 -0
  525. /package/src/{artifact-sdk → pages}/feedback-overlay.ts +0 -0
@@ -0,0 +1,584 @@
1
+ import {
2
+ afterAll,
3
+ afterEach,
4
+ beforeAll,
5
+ beforeEach,
6
+ describe,
7
+ expect,
8
+ mock,
9
+ spyOn,
10
+ test,
11
+ } from "bun:test";
12
+ import { unlink } from "node:fs/promises";
13
+ import type { IncomingMessage, ServerResponse } from "node:http";
14
+ import { resetAzureDevOpsBotIdCache } from "../azure-devops/api";
15
+ import { initAzureDevOps, resetAzureDevOps } from "../azure-devops/auth";
16
+ import { closeDb, createAgent, getTaskById, initDb } from "../be/db";
17
+ import {
18
+ createTrackerSyncIfAbsent,
19
+ deleteTrackerSync,
20
+ getAllTrackerSyncs,
21
+ getTrackerSyncByExternalId,
22
+ hasTrackerDelivery,
23
+ } from "../be/db-queries/tracker";
24
+ import { handleWebhooks } from "../http/webhooks";
25
+ import { parseContextKey } from "../tasks/context-key";
26
+ import commentedFixture from "./fixtures/azure-boards/workitem.commented.json";
27
+ import createdFixture from "./fixtures/azure-boards/workitem.created.json";
28
+ import deletedFixture from "./fixtures/azure-boards/workitem.deleted.json";
29
+ import restoredFixture from "./fixtures/azure-boards/workitem.restored.json";
30
+ import updatedFixture from "./fixtures/azure-boards/workitem.updated.json";
31
+
32
+ // Fixtures are real service-hook deliveries (Task #9 in acme-org/Contoso),
33
+ // copied verbatim. Every delivery in production has its own notification id and
34
+ // comment id, so `fixture()` rewrites both to keep tests from deduplicating each other.
35
+ const TEST_DB_PATH = "./test-azure-devops-boards-sync.sqlite";
36
+ const SECRET = "boards-secret";
37
+ const BOT_ID = "7d40ac7f-6bdb-6baf-a2e2-dc9594e967d4";
38
+ const BOT_EMAIL = "swarm-bot@example.com";
39
+ const ORG_URL = "https://dev.azure.com/acme-org";
40
+ const LINK_9 = "dev.azure.com/acme-org#9";
41
+ const ACTOR = "alex.example@example.com";
42
+ const ENV_KEYS = [
43
+ "AZURE_DEVOPS_WEBHOOK_SECRET",
44
+ "AZURE_DEVOPS_TOKEN",
45
+ "AZURE_DEVOPS_ORG_URL",
46
+ "AZURE_DEVOPS_BOT_ID",
47
+ "AZURE_DEVOPS_DISABLE",
48
+ "AZURE_DEVOPS_ALLOWED_ACTORS",
49
+ ] as const;
50
+
51
+ type Json = Record<string, any>;
52
+ const originalFetch = globalThis.fetch;
53
+ let nextId = 0;
54
+
55
+ function clearEnv() {
56
+ for (const key of ENV_KEYS) delete process.env[key];
57
+ resetAzureDevOps();
58
+ resetAzureDevOpsBotIdCache();
59
+ }
60
+
61
+ function fixture(sample: unknown, overrides: Json = {}): Json {
62
+ const event = structuredClone(sample) as Json;
63
+ event.id = `00000000-0000-4000-8000-${String(++nextId).padStart(12, "0")}`;
64
+ if (event.resource?.commentVersionRef)
65
+ event.resource.commentVersionRef.commentId = 1_000_000 + nextId;
66
+ return Object.assign(event, overrides);
67
+ }
68
+
69
+ function basic(password: string): string {
70
+ return `Basic ${Buffer.from(`hook:${password}`).toString("base64")}`;
71
+ }
72
+
73
+ async function post(body: Json, password = SECRET): Promise<{ status: number; json: Json }> {
74
+ const req = {
75
+ method: "POST",
76
+ headers: { authorization: basic(password) },
77
+ async *[Symbol.asyncIterator]() {
78
+ yield Buffer.from(JSON.stringify(body));
79
+ },
80
+ } as unknown as IncomingMessage;
81
+ const captured = { status: 0, body: "" };
82
+ const res = {
83
+ writeHead(status: number) {
84
+ captured.status = status;
85
+ return this;
86
+ },
87
+ end(chunk?: string) {
88
+ if (chunk) captured.body = chunk;
89
+ return this;
90
+ },
91
+ } as unknown as ServerResponse;
92
+ await handleWebhooks(req, res, ["api", "azure-devops", "webhook"]);
93
+ return { status: captured.status, json: captured.body ? JSON.parse(captured.body) : {} };
94
+ }
95
+
96
+ function assignedToBot(sample: Json): Json {
97
+ const event = fixture(sample);
98
+ event.resource.fields["System.AssignedTo"] = `Swarm Bot <${BOT_EMAIL}>`;
99
+ return event;
100
+ }
101
+
102
+ beforeAll(async () => {
103
+ clearEnv();
104
+ try {
105
+ await unlink(TEST_DB_PATH);
106
+ } catch {}
107
+ initDb(TEST_DB_PATH);
108
+ await createAgent({ id: "lead-boards-001", name: "BoardsLead", status: "idle", isLead: true });
109
+ });
110
+
111
+ afterAll(async () => {
112
+ clearEnv();
113
+ closeDb();
114
+ for (const suffix of ["", "-wal", "-shm"]) {
115
+ try {
116
+ await unlink(`${TEST_DB_PATH}${suffix}`);
117
+ } catch {}
118
+ }
119
+ });
120
+
121
+ beforeEach(async () => {
122
+ process.env.AZURE_DEVOPS_WEBHOOK_SECRET = SECRET;
123
+ process.env.AZURE_DEVOPS_TOKEN = "pat";
124
+ process.env.AZURE_DEVOPS_ORG_URL = ORG_URL;
125
+ process.env.AZURE_DEVOPS_BOT_ID = BOT_ID;
126
+ process.env.AZURE_DEVOPS_ALLOWED_ACTORS = ACTOR;
127
+ initAzureDevOps();
128
+ // The bot's sign-in name comes from connectionData; work-item identities carry no GUID.
129
+ globalThis.fetch = (async (input: RequestInfo | URL) => {
130
+ if (String(input).endsWith("/_apis/connectionData")) {
131
+ return Response.json({
132
+ authenticatedUser: { id: BOT_ID, properties: { Account: { $value: BOT_EMAIL } } },
133
+ });
134
+ }
135
+ throw new Error(`unexpected fetch ${String(input)}`);
136
+ }) as typeof fetch;
137
+ // Work item #9 is reused across tests; start each from no link.
138
+ for (const sync of await getAllTrackerSyncs("azure-devops")) {
139
+ await deleteTrackerSync(sync.id);
140
+ }
141
+ spyOn(console, "log").mockImplementation(() => {});
142
+ });
143
+
144
+ afterEach(() => {
145
+ mock.restore();
146
+ globalThis.fetch = originalFetch;
147
+ clearEnv();
148
+ });
149
+
150
+ describe("workitem.created", () => {
151
+ test("assigned to the bot creates one task and links it", async () => {
152
+ const { status, json } = await post(assignedToBot(createdFixture));
153
+ expect(status).toBe(200);
154
+ expect(json.created).toBe(true);
155
+
156
+ const task = await getTaskById(json.taskId);
157
+ expect(task?.source).toBe("azure-devops");
158
+ expect(task?.taskType).toBe("azure-devops-work-item");
159
+ expect(task?.task).toContain("[Azure DevOps Task #9]");
160
+ expect(task?.task).toContain("Test task for Azure Boards integration");
161
+ expect(task?.task).toContain("https://dev.azure.com/acme-org/Contoso/_workitems/edit/9");
162
+ expect(parseContextKey(task!.contextKey!)).toEqual({
163
+ family: "trackers",
164
+ subFamily: "azure-devops",
165
+ parts: { organization: "acme-org", kind: "workitem", workItemId: 9 },
166
+ });
167
+
168
+ const sync = await getTrackerSyncByExternalId("azure-devops", "task", LINK_9);
169
+ expect(sync?.swarmId).toBe(json.taskId);
170
+ expect(sync?.providerEntityType).toBe("WorkItem");
171
+ expect(sync?.externalIdentifier).toBe("Contoso#9");
172
+ });
173
+
174
+ test("the real payload, with no assignee, creates nothing", async () => {
175
+ const { json } = await post(fixture(createdFixture));
176
+ expect(json).toEqual({ created: false });
177
+ expect(await getTrackerSyncByExternalId("azure-devops", "task", LINK_9)).toBeNull();
178
+ });
179
+
180
+ test("a bot mention in the description creates a task", async () => {
181
+ const event = fixture(createdFixture);
182
+ event.resource.fields["System.Description"] =
183
+ `<div><a href="#" data-vss-mention="version:2.0,${BOT_ID}">@Swarm Bot</a> please look</div>`;
184
+ const { json } = await post(event);
185
+ expect(json.created).toBe(true);
186
+ expect((await getTaskById(json.taskId))?.task).toContain("please look");
187
+ });
188
+
189
+ test("duplicate delivery creates no second task", async () => {
190
+ const event = assignedToBot(createdFixture);
191
+ const first = await post(event);
192
+ const second = await post(event);
193
+ expect(first.json.created).toBe(true);
194
+ expect(second.json).toEqual({ created: false });
195
+ });
196
+
197
+ test("the notification id is persisted on the link, so a retry after a restart is recognized", async () => {
198
+ const event = assignedToBot(createdFixture);
199
+ expect((await post(event)).json.created).toBe(true);
200
+ expect(await hasTrackerDelivery("azure-devops", event.id)).toBe(true);
201
+ });
202
+ });
203
+
204
+ describe("workitem.updated", () => {
205
+ test("assignment to the bot creates exactly one task", async () => {
206
+ const { json } = await post(fixture(updatedFixture));
207
+ expect(json.created).toBe(true);
208
+ const task = await getTaskById(json.taskId);
209
+ expect(task?.task).toContain("[Azure DevOps Task #9]");
210
+ const sync = await getTrackerSyncByExternalId("azure-devops", "task", LINK_9);
211
+ expect(sync?.swarmId).toBe(json.taskId);
212
+ });
213
+
214
+ test("an update without an AssignedTo change is ignored", async () => {
215
+ const event = fixture(updatedFixture);
216
+ delete event.resource.fields["System.AssignedTo"];
217
+ const { json } = await post(event);
218
+ expect(json).toEqual({ created: false });
219
+ expect(await getTrackerSyncByExternalId("azure-devops", "task", LINK_9)).toBeNull();
220
+ });
221
+
222
+ test("a second assignment while the task is active creates no new task", async () => {
223
+ const first = await post(fixture(updatedFixture));
224
+ const second = await post(fixture(updatedFixture));
225
+ expect(first.json.created).toBe(true);
226
+ expect(second.json).toEqual({ created: false });
227
+ });
228
+
229
+ test("assignment away from the bot is ignored", async () => {
230
+ const event = fixture(updatedFixture);
231
+ event.resource.fields["System.AssignedTo"] = {
232
+ oldValue: `Swarm Bot <${BOT_EMAIL}>`,
233
+ newValue: "Someone Else <someone@example.com>",
234
+ };
235
+ expect((await post(event)).json).toEqual({ created: false });
236
+ });
237
+ });
238
+
239
+ describe("workitem.commented", () => {
240
+ test("a picker mention creates a task with the comment as context", async () => {
241
+ const { json } = await post(fixture(commentedFixture));
242
+ expect(json.created).toBe(true);
243
+ const task = await getTaskById(json.taskId);
244
+ expect(task?.task).toContain("please do this task.");
245
+ expect(task?.task).not.toContain("data-vss-mention");
246
+ expect(task?.task).toContain("mentioned bot");
247
+ const sync = await getTrackerSyncByExternalId("azure-devops", "task", LINK_9);
248
+ expect(sync?.swarmId).toBe(json.taskId);
249
+ });
250
+
251
+ test("a plain-text @swarm-bot mention triggers", async () => {
252
+ const event = fixture(commentedFixture);
253
+ event.resource.fields["System.History"] = "<div>@swarm-bot can you check this?</div>";
254
+ const { json } = await post(event);
255
+ expect(json.created).toBe(true);
256
+ });
257
+
258
+ test("a comment without a mention is ignored", async () => {
259
+ const event = fixture(commentedFixture);
260
+ event.resource.fields["System.History"] = "<div>just a note</div>";
261
+ expect((await post(event)).json).toEqual({ created: false });
262
+ });
263
+
264
+ test("a mention while the task is active creates a child task and keeps the link", async () => {
265
+ const first = await post(fixture(updatedFixture));
266
+ const second = await post(fixture(commentedFixture));
267
+ expect(second.json.created).toBe(true);
268
+ expect(second.json.taskId).not.toBe(first.json.taskId);
269
+ const sync = await getTrackerSyncByExternalId("azure-devops", "task", LINK_9);
270
+ expect(sync?.swarmId).toBe(first.json.taskId);
271
+ expect((await getTaskById(second.json.taskId))?.parentTaskId).toBe(first.json.taskId);
272
+ });
273
+
274
+ test("a comment written by the bot is ignored", async () => {
275
+ const event = fixture(commentedFixture);
276
+ event.resource.fields["System.ChangedBy"] = `Swarm Bot <${BOT_EMAIL}>`;
277
+ expect((await post(event)).json).toEqual({ created: false });
278
+ });
279
+ });
280
+
281
+ describe("workitem.deleted and workitem.restored", () => {
282
+ test("deleting a linked work item cancels its task; restoring it re-engages the swarm", async () => {
283
+ const first = await post(fixture(updatedFixture));
284
+ const taskId = first.json.taskId as string;
285
+
286
+ const deleted = await post(fixture(deletedFixture));
287
+ expect(deleted.json).toEqual({ created: false });
288
+ expect((await getTaskById(taskId))?.status).toBe("cancelled");
289
+
290
+ const restored = await post(fixture(restoredFixture));
291
+ expect(restored.json.created).toBe(true);
292
+ expect(restored.json.taskId).not.toBe(taskId);
293
+ const sync = await getTrackerSyncByExternalId("azure-devops", "task", LINK_9);
294
+ expect(sync?.swarmId).toBe(restored.json.taskId);
295
+ expect((await getTaskById(restored.json.taskId))?.task).toContain("Follow-up");
296
+ });
297
+
298
+ test("deleting an unlinked work item does nothing", async () => {
299
+ expect((await post(fixture(deletedFixture))).json).toEqual({ created: false });
300
+ });
301
+
302
+ test("restoring a linked work item whose task is still active does nothing", async () => {
303
+ await post(fixture(updatedFixture));
304
+ expect((await post(fixture(restoredFixture))).json).toEqual({ created: false });
305
+ });
306
+
307
+ test("restoring a never-linked work item behaves like created", async () => {
308
+ const { json } = await post(fixture(restoredFixture));
309
+ expect(json.created).toBe(true);
310
+ const sync = await getTrackerSyncByExternalId("azure-devops", "task", LINK_9);
311
+ expect(sync?.swarmId).toBe(json.taskId);
312
+ });
313
+
314
+ test("restoring a never-linked, unassigned work item creates nothing", async () => {
315
+ const event = fixture(restoredFixture);
316
+ delete event.resource.fields["System.AssignedTo"];
317
+ expect((await post(event)).json).toEqual({ created: false });
318
+ });
319
+ });
320
+
321
+ describe("webhook auth", () => {
322
+ test("a bad Basic password returns 401 and creates nothing", async () => {
323
+ const { status } = await post(fixture(updatedFixture), "wrong");
324
+ expect(status).toBe(401);
325
+ expect(await getTrackerSyncByExternalId("azure-devops", "task", LINK_9)).toBeNull();
326
+ });
327
+ });
328
+
329
+ // ── Review regressions (PR #2119) ──
330
+
331
+ function countingFetch(calls: string[], failures = { remaining: 0 }) {
332
+ globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
333
+ calls.push(`${String(input)} ${init?.redirect ?? ""}`);
334
+ if (failures.remaining > 0) {
335
+ failures.remaining--;
336
+ return new Response("unavailable", { status: 503 });
337
+ }
338
+ return Response.json({
339
+ authenticatedUser: { id: BOT_ID, properties: { Account: { $value: BOT_EMAIL } } },
340
+ });
341
+ }) as typeof fetch;
342
+ }
343
+
344
+ function inOrg(event: Json, baseUrl: string): Json {
345
+ event.resourceContainers = {
346
+ account: { baseUrl },
347
+ collection: { baseUrl },
348
+ };
349
+ event.resource.url = `${baseUrl}/proj/_apis/wit/workItems/9`;
350
+ return event;
351
+ }
352
+
353
+ describe("organization URL validation", () => {
354
+ test.each([
355
+ ["a foreign host", "https://attacker.example.com/acme-org/"],
356
+ ["plain http", "http://dev.azure.com/acme-org/"],
357
+ ["another organization", "https://dev.azure.com/other-org/"],
358
+ ["embedded credentials", "https://user:pw@dev.azure.com/acme-org/"],
359
+ ])("a hostile payload (%s) makes zero credentialed requests", async (_name, baseUrl) => {
360
+ delete process.env.AZURE_DEVOPS_BOT_ID; // forces connectionData lookups
361
+ const calls: string[] = [];
362
+ countingFetch(calls);
363
+ resetAzureDevOpsBotIdCache();
364
+ const event = inOrg(assignedToBot(createdFixture), baseUrl);
365
+ expect((await post(event)).json).toEqual({ created: false });
366
+ expect(calls).toEqual([]);
367
+ expect(await getAllTrackerSyncs("azure-devops")).toHaveLength(0);
368
+ });
369
+
370
+ test("a hostile resource.url alone is rejected as well", async () => {
371
+ delete process.env.AZURE_DEVOPS_BOT_ID;
372
+ const calls: string[] = [];
373
+ countingFetch(calls);
374
+ const event = assignedToBot(createdFixture);
375
+ delete event.resourceContainers;
376
+ event.resource.url = "https://attacker.example.com/x/_apis/wit/workItems/9";
377
+ expect((await post(event)).json).toEqual({ created: false });
378
+ expect(calls).toEqual([]);
379
+ });
380
+
381
+ test("credentialed lookups refuse redirects", async () => {
382
+ delete process.env.AZURE_DEVOPS_BOT_ID;
383
+ const calls: string[] = [];
384
+ countingFetch(calls);
385
+ await post(assignedToBot(createdFixture));
386
+ expect(calls.length).toBeGreaterThan(0);
387
+ expect(calls.every((c) => c.endsWith("error"))).toBe(true);
388
+ });
389
+ });
390
+
391
+ describe("organization-scoped tracker identity", () => {
392
+ test("deleting work item 9 in organization B does not cancel organization A's task", async () => {
393
+ const first = await post(assignedToBot(createdFixture));
394
+ const taskId = first.json.taskId as string;
395
+ // A second organization is only reachable when it is not pinned by config.
396
+ delete process.env.AZURE_DEVOPS_ORG_URL;
397
+ initAzureDevOps();
398
+ resetAzureDevOps();
399
+ process.env.AZURE_DEVOPS_WEBHOOK_SECRET = SECRET;
400
+ process.env.AZURE_DEVOPS_TOKEN = "pat";
401
+ initAzureDevOps();
402
+ const deleted = inOrg(fixture(deletedFixture), "https://dev.azure.com/other-org/");
403
+ await post(deleted);
404
+ expect((await getTaskById(taskId))?.status).not.toBe("cancelled");
405
+
406
+ const sync = await getTrackerSyncByExternalId("azure-devops", "task", LINK_9);
407
+ expect(sync?.swarmId).toBe(taskId);
408
+ expect(await getTrackerSyncByExternalId("azure-devops", "task", "9")).toBeNull();
409
+ });
410
+
411
+ test("the same work item id in two organizations gets two links and two tasks", async () => {
412
+ delete process.env.AZURE_DEVOPS_ORG_URL;
413
+ resetAzureDevOps();
414
+ process.env.AZURE_DEVOPS_WEBHOOK_SECRET = SECRET;
415
+ process.env.AZURE_DEVOPS_TOKEN = "pat";
416
+ initAzureDevOps();
417
+ const a = await post(inOrg(assignedToBot(createdFixture), "https://dev.azure.com/acme-org/"));
418
+ const b = await post(inOrg(assignedToBot(createdFixture), "https://dev.azure.com/other-org/"));
419
+ expect(a.json.created).toBe(true);
420
+ expect(b.json.created).toBe(true);
421
+ expect(b.json.taskId).not.toBe(a.json.taskId);
422
+ expect(await getAllTrackerSyncs("azure-devops")).toHaveLength(2);
423
+ });
424
+ });
425
+
426
+ describe("concurrent deliveries", () => {
427
+ test("two overlapping assignments create exactly one task and one link", async () => {
428
+ const [a, b] = await Promise.all([
429
+ post(fixture(updatedFixture)),
430
+ post(fixture(updatedFixture)),
431
+ ]);
432
+ expect([a.json.created, b.json.created].filter(Boolean)).toHaveLength(1);
433
+ const syncs = await getAllTrackerSyncs("azure-devops");
434
+ expect(syncs).toHaveLength(1);
435
+ expect(syncs[0]?.swarmId).toBe(a.json.created ? a.json.taskId : b.json.taskId);
436
+ });
437
+
438
+ test("the same notification delivered twice at once is handled once", async () => {
439
+ const event = fixture(updatedFixture);
440
+ const [a, b] = await Promise.all([post(event), post(event)]);
441
+ expect([a.json.created, b.json.created].filter(Boolean)).toHaveLength(1);
442
+ });
443
+
444
+ test("a stale pending claim does not hide the link: the next delivery recovers it", async () => {
445
+ await createTrackerSyncIfAbsent({
446
+ provider: "azure-devops",
447
+ entityType: "task",
448
+ swarmId: `pending:azure-devops:${LINK_9}`,
449
+ externalId: LINK_9,
450
+ });
451
+ const { json } = await post(fixture(updatedFixture));
452
+ expect(json.created).toBe(true);
453
+ expect((await getTrackerSyncByExternalId("azure-devops", "task", LINK_9))?.swarmId).toBe(
454
+ json.taskId,
455
+ );
456
+ });
457
+ });
458
+
459
+ async function holdFirstCreate() {
460
+ let release!: (err: Error) => void;
461
+ const gate = new Promise<never>((_, reject) => {
462
+ release = reject;
463
+ });
464
+ const spy = spyOn(await import("../tasks/sibling-awareness"), "createTaskWithSiblingAwareness");
465
+ spy.mockImplementationOnce(() => gate);
466
+ spyOn(console, "error").mockImplementation(() => {});
467
+ return { release, spy };
468
+ }
469
+
470
+ async function spyCalled(spy: { mock: { calls: unknown[] } }) {
471
+ for (let i = 0; i < 200 && spy.mock.calls.length === 0; i++) await Bun.sleep(5);
472
+ expect(spy.mock.calls.length).toBeGreaterThan(0);
473
+ }
474
+
475
+ describe("failed deliveries stay retryable", () => {
476
+ test("a transient connectionData failure returns 500 and the retry succeeds", async () => {
477
+ delete process.env.AZURE_DEVOPS_BOT_ID;
478
+ const calls: string[] = [];
479
+ countingFetch(calls, { remaining: 1 });
480
+ spyOn(console, "error").mockImplementation(() => {});
481
+ const event = assignedToBot(createdFixture);
482
+ expect((await post(event)).status).toBe(500);
483
+ expect(await hasTrackerDelivery("azure-devops", event.id)).toBe(false);
484
+ const retry = await post(event);
485
+ expect(retry.status).toBe(200);
486
+ expect(retry.json.created).toBe(true);
487
+ });
488
+
489
+ test("a failed task creation leaves no claim or receipt, and the retry creates the task", async () => {
490
+ const event = assignedToBot(createdFixture);
491
+ const spy = spyOn(await import("../tasks/sibling-awareness"), "createTaskWithSiblingAwareness");
492
+ spy.mockRejectedValueOnce(new Error("boom"));
493
+ spyOn(console, "error").mockImplementation(() => {});
494
+ expect((await post(event)).status).toBe(500);
495
+ expect(await getAllTrackerSyncs("azure-devops")).toHaveLength(0);
496
+ expect(await hasTrackerDelivery("azure-devops", event.id)).toBe(false);
497
+ spy.mockRestore();
498
+ expect((await post(event)).json.created).toBe(true);
499
+ });
500
+
501
+ test("a replay of the same notification waits for an in-flight create that then fails", async () => {
502
+ const event = fixture(commentedFixture);
503
+ const { release, spy } = await holdFirstCreate();
504
+ const first = post(event);
505
+ await spyCalled(spy);
506
+ const second = post(event);
507
+ await Bun.sleep(20);
508
+ release(new Error("boom"));
509
+ expect((await first).status).toBe(500);
510
+ expect((await second).status).toBe(500);
511
+ expect(await getAllTrackerSyncs("azure-devops")).toHaveLength(0);
512
+ spy.mockRestore();
513
+ const retry = await post(event);
514
+ expect(retry.status).toBe(200);
515
+ expect(retry.json.created).toBe(true);
516
+ });
517
+
518
+ test("another notification for the same comment is not acknowledged while the first create fails", async () => {
519
+ const first = fixture(commentedFixture);
520
+ const other = fixture(commentedFixture);
521
+ other.resource.commentVersionRef.commentId = first.resource.commentVersionRef.commentId;
522
+ const { release, spy } = await holdFirstCreate();
523
+ const firstPost = post(first);
524
+ await spyCalled(spy);
525
+ const otherPost = post(other);
526
+ await Bun.sleep(20);
527
+ release(new Error("boom"));
528
+ expect((await firstPost).status).toBe(500);
529
+ expect((await otherPost).status).toBe(500);
530
+ spy.mockRestore();
531
+ const replay = await post(other);
532
+ expect(replay.status).toBe(200);
533
+ expect(replay.json.created).toBe(true);
534
+ });
535
+
536
+ test("lastDeliveryId is written only after the task exists", async () => {
537
+ const event = assignedToBot(createdFixture);
538
+ const { json } = await post(event);
539
+ const sync = await getTrackerSyncByExternalId("azure-devops", "task", LINK_9);
540
+ expect(sync?.swarmId).toBe(json.taskId);
541
+ expect(sync?.lastDeliveryId).toBe(event.id);
542
+ });
543
+ });
544
+
545
+ function byStranger(event: Json, email: string): Json {
546
+ const who = `Stranger <${email}>`;
547
+ if (event.resource.fields) event.resource.fields["System.ChangedBy"] = who;
548
+ if (event.resource.revisedBy) event.resource.revisedBy.uniqueName = email;
549
+ return event;
550
+ }
551
+
552
+ describe("actor authorization", () => {
553
+ test("an unmapped actor creates no task", async () => {
554
+ delete process.env.AZURE_DEVOPS_ALLOWED_ACTORS;
555
+ spyOn(console, "warn").mockImplementation(() => {});
556
+ const email = "stranger@example.net";
557
+ expect((await post(byStranger(assignedToBot(createdFixture), email))).json).toEqual({
558
+ created: false,
559
+ });
560
+ expect((await post(byStranger(fixture(commentedFixture), email))).json).toEqual({
561
+ created: false,
562
+ });
563
+ expect((await post(byStranger(fixture(updatedFixture), email))).json).toEqual({
564
+ created: false,
565
+ });
566
+ expect(await getAllTrackerSyncs("azure-devops")).toHaveLength(0);
567
+ });
568
+
569
+ test("an allowlisted actor creates a task", async () => {
570
+ expect((await post(assignedToBot(createdFixture))).json.created).toBe(true);
571
+ });
572
+
573
+ test("an actor linked to a swarm user creates a task without the allowlist", async () => {
574
+ delete process.env.AZURE_DEVOPS_ALLOWED_ACTORS;
575
+ const email = "linked@example.org";
576
+ const actor = { kind: "system", id: "test" } as const;
577
+ const { findOrCreateUserByEmail, linkIdentity } = await import("../be/users");
578
+ const { user } = await findOrCreateUserByEmail(email, { name: "Linked" }, actor);
579
+ await linkIdentity(user.id, "azure-devops", email, actor);
580
+ const { json } = await post(byStranger(assignedToBot(createdFixture), email));
581
+ expect(json.created).toBe(true);
582
+ expect((await getTaskById(json.taskId))?.requestedByUserId).toBe(user.id);
583
+ });
584
+ });
@@ -0,0 +1,13 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { htmlToText } from "../azure-devops/html";
3
+
4
+ describe("htmlToText", () => {
5
+ test("strips tags, decodes entities, keeps block breaks", () => {
6
+ expect(htmlToText("<div>a &amp; b</div><div>&lt;c&gt;&nbsp;d</div>")).toBe("a & b\n<c> d");
7
+ });
8
+
9
+ test("handles empty input", () => {
10
+ expect(htmlToText(null)).toBe("");
11
+ expect(htmlToText("")).toBe("");
12
+ });
13
+ });
@@ -62,6 +62,10 @@ const cases = [
62
62
  },
63
63
  },
64
64
  },
65
+ ...["created", "updated", "commented", "deleted", "restored"].map((kind) => ({
66
+ name: `workitem.${kind} with an empty resource`,
67
+ body: { eventType: `workitem.${kind}`, id: "evt-1", resource: {} },
68
+ })),
65
69
  ];
66
70
 
67
71
  let emitted: string[] = [];