@desplega.ai/agent-swarm 1.150.0 → 1.152.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 (311) hide show
  1. package/README.md +11 -0
  2. package/dist/{acp-adapter-d5mzggyk.js → acp-adapter-d05sg0q4.js} +5 -5
  3. package/dist/{actions-wn6ew0p7.js → actions-4sc0v342.js} +38 -11
  4. package/dist/{anthropic-messages-ec35ktvx.js → anthropic-messages-2y8nktar.js} +16 -16
  5. package/dist/{app-dty7p9hg.js → app-2d0v6kt6.js} +7 -7
  6. package/dist/{assistant-q4vhgjtx.js → assistant-zs6wdnrf.js} +41 -16
  7. package/dist/{azure-openai-responses-g85jv7m8.js → azure-openai-responses-f38x2avq.js} +13 -13
  8. package/dist/{boot-reembed-n15q6s9x.js → boot-reembed-3dwdnnsr.js} +13 -15
  9. package/dist/{boot-reembed-92292pgf.js → boot-reembed-vhh33520.js} +8 -8
  10. package/dist/{boot-scrub-logs-44pc955m.js → boot-scrub-logs-e1fnabcg.js} +5 -5
  11. package/dist/{claude-adapter-cjegjbje.js → claude-adapter-h58xqzzd.js} +12 -13
  12. package/dist/{claude-managed-adapter-p3ts5m9m.js → claude-managed-adapter-dsmkcrjy.js} +3 -3
  13. package/dist/{claude-sdk-session-7ywbn12v.js → claude-sdk-session-pbaczbgt.js} +12 -13
  14. package/dist/{cli-9gqddmxs.js → cli-4fqn2vpg.js} +8 -8
  15. package/dist/{cli-mzb0n642.js → cli-4wppfkcm.js} +350 -20
  16. package/dist/{cli-h505yhex.js → cli-5bqt4gwa.js} +2 -2
  17. package/dist/{cli-hbagregr.js → cli-5p007m2q.js} +88 -13
  18. package/dist/{cli-3ckzxdt1.js → cli-6k2athpf.js} +94 -81
  19. package/dist/{cli-3mh119qv.js → cli-72vksk1n.js} +9855 -693
  20. package/dist/{cli-taym3dmg.js → cli-87hdfyde.js} +13 -13
  21. package/dist/{cli-zthatyrx.js → cli-8fy9dmpv.js} +5 -5
  22. package/dist/{cli-asvrh7kj.js → cli-9xvdm3bx.js} +1 -1
  23. package/dist/{cli-72xdszyx.js → cli-awb59f08.js} +2 -2
  24. package/dist/{cli-vr6qj104.js → cli-cs9z1hw3.js} +1 -1
  25. package/dist/{cli-kt7wevqh.js → cli-cwvey7n1.js} +164 -56
  26. package/dist/{cli-5my3bjsd.js → cli-d3p82erb.js} +7 -7
  27. package/dist/{cli-7kwmt3r5.js → cli-evgy5w3y.js} +9 -3
  28. package/dist/{cli-111ey3ra.js → cli-f1f9n77e.js} +5 -5
  29. package/dist/{cli-ttje5a4z.js → cli-fhdssm2v.js} +105 -56
  30. package/dist/{cli-a45d5v9b.js → cli-fm4ytsdk.js} +2 -2
  31. package/dist/{cli-qp69dd5d.js → cli-gacq5hg8.js} +116 -14
  32. package/dist/{cli-hmyynq9b.js → cli-ht5rs63n.js} +1 -1
  33. package/dist/{cli-dyx0237s.js → cli-j4nv2wqy.js} +2 -4
  34. package/dist/{cli-gm0fhz13.js → cli-jh9txj4n.js} +2 -1
  35. package/dist/{cli-8chp8xab.js → cli-nd5s3m4y.js} +2 -2
  36. package/dist/{cli-zd21jrrc.js → cli-p2hx1pxm.js} +9872 -424
  37. package/dist/cli-pf9q3696.js +172 -0
  38. package/dist/{cli-7fesa1ky.js → cli-pgj4a5fb.js} +1 -1
  39. package/dist/{cli-07jd68a1.js → cli-re8z5p8p.js} +1 -1
  40. package/dist/{cli-ryz8q8p1.js → cli-t1mykkng.js} +40 -40
  41. package/dist/{cli-ck1vqzdj.js → cli-t6axtdcg.js} +9 -4
  42. package/dist/{cli-1mx65de9.js → cli-wbyk465z.js} +1 -1
  43. package/dist/{cli-kyysm7m3.js → cli-wp7cejrg.js} +1 -1
  44. package/dist/{cli-n75jt793.js → cli-z61ykvam.js} +6 -5
  45. package/dist/{cli-ceb5y160.js → cli-zdcdh02m.js} +1 -1
  46. package/dist/{cli-cmp8g3sw.js → cli-zfzznn7c.js} +45 -9
  47. package/dist/cli.js +22 -21
  48. package/dist/{codex-adapter-rn3aq7jm.js → codex-adapter-v3cz462q.js} +8 -9
  49. package/dist/{codex-hook-gthcmm0g.js → codex-hook-8jexc34x.js} +2 -2
  50. package/dist/{codex-session-runner-bge0cbsw.js → codex-session-runner-de1a5g5z.js} +8 -9
  51. package/dist/{commands-26phw0pc.js → commands-wfqqpn95.js} +5 -5
  52. package/dist/{db-jxjx6s1b.js → db-9zzv5y05.js} +5 -5
  53. package/dist/dispatcher-hfm1jbgx.js +74 -0
  54. package/dist/{google-generative-ai-tsskx7te.js → google-generative-ai-ndjmj5fa.js} +7 -7
  55. package/dist/{google-vertex-pkhpkbq9.js → google-vertex-fr665tqs.js} +10 -10
  56. package/dist/handlers-wrrhsack.js +65 -0
  57. package/dist/{hook-jw0ztn96.js → hook-9skye8zd.js} +9 -10
  58. package/dist/{hook-t6fqsggw.js → hook-fgjv62k5.js} +9 -10
  59. package/dist/{http-hyc7s57s.js → http-7grttjfq.js} +1434 -579
  60. package/dist/{index-bcyx5djw.js → index-29gxx9ar.js} +25 -17
  61. package/dist/{index-wtwpthe1.js → index-30pz2h9w.js} +25 -19
  62. package/dist/{index-1d9r6a74.js → index-4ek1df87.js} +26 -20
  63. package/dist/{index-sfhaq59d.js → index-myfwqqyq.js} +120 -102
  64. package/dist/index-pfhzrh1d.js +25 -0
  65. package/dist/{keepalive-fbsak4ya.js → keepalive-j75m2m7k.js} +7 -7
  66. package/dist/{lead-rdzdm627.js → lead-a1hpjt0a.js} +41 -49
  67. package/dist/{maintenance-25z1c1vf.js → maintenance-tngjzqdq.js} +8 -8
  68. package/dist/{node-209sxmg3.js → node-x1g4v0pq.js} +9 -9
  69. package/dist/{oauth-refresh-sweep-j3d7gtse.js → oauth-refresh-sweep-em1fn044.js} +6 -6
  70. package/dist/{onboard-dkcwrc75.js → onboard-fv7ezvwx.js} +13 -4
  71. package/dist/{openai-codex-responses-pm5gnyfa.js → openai-codex-responses-wy6a43wd.js} +11 -11
  72. package/dist/{openai-responses-vwvk1taj.js → openai-responses-6xwf7bzg.js} +13 -13
  73. package/dist/{opencode-adapter-dce4wskd.js → opencode-adapter-28axqa09.js} +4 -4
  74. package/dist/{otel-impl-8rgyy3j8.js → otel-impl-6vbb568y.js} +1 -1
  75. package/dist/{pi-mono-adapter-4s2bbkgq.js → pi-mono-adapter-jcqt71cg.js} +14 -16
  76. package/dist/{pricing-refresh-9cek71nq.js → pricing-refresh-nmm949an.js} +7 -7
  77. package/dist/{rbac-roles-vk8mqp9v.js → rbac-roles-0fz5qryx.js} +7 -7
  78. package/dist/{rbac-roles-z98s2ypy.js → rbac-roles-xtp05syf.js} +13 -9
  79. package/dist/{render-v2-3j26g0qh.js → render-v2-fpb8whme.js} +7 -7
  80. package/dist/{seed-pricing-7rxhcnzm.js → seed-pricing-zwa8bs3t.js} +6 -6
  81. package/dist/{setup-e2t7vz0c.js → setup-3ybwbdpd.js} +2 -2
  82. package/dist/{work-objects-8d3nd3x2.js → work-objects-rmayxcx7.js} +41 -16
  83. package/dist/{worker-0x9fn7nq.js → worker-rng4jhnh.js} +41 -49
  84. package/openapi.json +1351 -265
  85. package/package.json +2 -1
  86. package/src/agentmail/handlers.ts +95 -71
  87. package/src/be/db-client.ts +5 -0
  88. package/src/be/db.ts +12 -4
  89. package/src/be/extensions/db.ts +461 -0
  90. package/src/be/extensions/validate.ts +107 -0
  91. package/src/be/memory/boot-reembed.ts +17 -12
  92. package/src/be/memory/index.ts +12 -0
  93. package/src/be/memory/providers/openai-embedding.ts +4 -0
  94. package/src/be/memory/types.ts +2 -0
  95. package/src/be/migrations/155_extensions.sql +66 -0
  96. package/src/be/migrations/156_inbox_item_state_read_at.sql +7 -0
  97. package/src/be/rbac-roles.ts +3 -2
  98. package/src/be/scripts/typecheck.ts +56 -38
  99. package/src/be/seed-skills/index.ts +7 -0
  100. package/src/be/swarm-config-guard.ts +10 -2
  101. package/src/commands/onboard/compose-generator.ts +14 -2
  102. package/src/commands/onboard/env-generator.ts +7 -0
  103. package/src/commands/runner.ts +1 -1
  104. package/src/extensions/apply-task-create.ts +135 -0
  105. package/src/extensions/bundle-path.ts +10 -0
  106. package/src/extensions/contract-runtime.ts +7 -0
  107. package/src/extensions/contract-types.generated.ts +4 -0
  108. package/src/extensions/contract.ts +270 -0
  109. package/src/extensions/ctx.ts +186 -0
  110. package/src/extensions/dispatcher.ts +479 -0
  111. package/src/extensions/identity.ts +44 -0
  112. package/src/extensions/lifecycle.ts +303 -0
  113. package/src/extensions/loader.ts +207 -0
  114. package/src/extensions/post-bridge.ts +161 -0
  115. package/src/github/handlers.ts +185 -149
  116. package/src/gitlab/handlers.ts +97 -81
  117. package/src/heartbeat/heartbeat.ts +152 -83
  118. package/src/http/agents.ts +1 -1
  119. package/src/http/all-routes.ts +1 -0
  120. package/src/http/apps.ts +14 -10
  121. package/src/http/core.ts +34 -0
  122. package/src/http/extensions.ts +552 -0
  123. package/src/http/inbox-state.ts +2 -0
  124. package/src/http/index.ts +25 -0
  125. package/src/http/mcp-bridge.ts +96 -62
  126. package/src/http/mcp.ts +1 -1
  127. package/src/http/status.ts +6 -4
  128. package/src/http/tasks.ts +51 -35
  129. package/src/integrations/kapso/inbound.ts +15 -11
  130. package/src/jira/sync.ts +26 -18
  131. package/src/linear/sync.ts +26 -18
  132. package/src/prompts/session-templates.ts +5 -4
  133. package/src/rbac/admission.ts +1 -1
  134. package/src/rbac/elevated-agents.ts +24 -0
  135. package/src/rbac/legacy-policy.ts +47 -6
  136. package/src/rbac/permissions.ts +8 -0
  137. package/src/scheduler/deferred-task-waits.ts +133 -51
  138. package/src/scheduler/schedule-task.ts +50 -24
  139. package/src/scheduler/scheduler.ts +4 -2
  140. package/src/scripts-runtime/executors/native.ts +5 -1
  141. package/src/scripts-runtime/executors/types.ts +5 -0
  142. package/src/scripts-runtime/import-allowlist.ts +79 -19
  143. package/src/scripts-runtime/sdk-allowlist.ts +8 -0
  144. package/src/scripts-runtime/swarm-config.ts +8 -0
  145. package/src/scripts-runtime/swarm-sdk.ts +3 -0
  146. package/src/scripts-runtime/types/stdlib.d.ts +13 -0
  147. package/src/scripts-runtime/types/swarm-extension.d.ts +562 -0
  148. package/src/scripts-runtime/types/swarm-sdk.d.ts +13 -0
  149. package/src/server.ts +18 -0
  150. package/src/slack/HEURISTICS.md +1 -1
  151. package/src/slack/actions.ts +21 -17
  152. package/src/slack/blocks.ts +1 -1
  153. package/src/slack/handlers.ts +98 -5
  154. package/src/slack/inbound-files.ts +6 -2
  155. package/src/slack/render-v2.ts +181 -39
  156. package/src/slack/responses.ts +7 -4
  157. package/src/slack/router.ts +13 -3
  158. package/src/slack/steering.ts +3 -3
  159. package/src/slack/task-output.ts +21 -0
  160. package/src/slack/watcher.ts +3 -2
  161. package/src/slack/work-objects.ts +2 -1
  162. package/src/tasks/errors.ts +9 -0
  163. package/src/tasks/sibling-awareness.ts +35 -1
  164. package/src/tasks/worker-follow-up.ts +64 -12
  165. package/src/tests/base-prompt.test.ts +1 -0
  166. package/src/tests/deferred-task-wake.test.ts +161 -9
  167. package/src/tests/docs-index-footprint.test.js +3 -3
  168. package/src/tests/env-flag.test.ts +1 -0
  169. package/src/tests/extensions-db.test.ts +216 -0
  170. package/src/tests/extensions-dispatcher.test.ts +275 -0
  171. package/src/tests/extensions-example-heartbeat-remediate.test.ts +69 -0
  172. package/src/tests/extensions-example-mcp-install.test.ts +63 -0
  173. package/src/tests/extensions-example-no-exclamation-marks.test.ts +72 -0
  174. package/src/tests/extensions-example-rewrite-and-suppress.test.ts +70 -0
  175. package/src/tests/extensions-example-route-slack-channel.test.ts +217 -0
  176. package/src/tests/extensions-heartbeat.test.ts +291 -0
  177. package/src/tests/extensions-http.test.ts +512 -0
  178. package/src/tests/extensions-lead-equivalence.test.ts +70 -0
  179. package/src/tests/extensions-lifecycle.test.ts +209 -0
  180. package/src/tests/extensions-loader.test.ts +95 -0
  181. package/src/tests/extensions-mcp-tools.test.ts +353 -0
  182. package/src/tests/extensions-pre-task.test.ts +487 -0
  183. package/src/tests/extensions-run-context.test.ts +41 -0
  184. package/src/tests/extensions-tool-call.test.ts +288 -0
  185. package/src/tests/extensions-validate.test.ts +196 -0
  186. package/src/tests/fixtures/extensions/bad-import/hooks.ts +8 -0
  187. package/src/tests/fixtures/extensions/bad-import/manifest.json +9 -0
  188. package/src/tests/fixtures/extensions/bad-return-shape/hooks.ts +10 -0
  189. package/src/tests/fixtures/extensions/bad-return-shape/manifest.json +9 -0
  190. package/src/tests/fixtures/extensions/block-tasks-from-source/hooks.ts +22 -0
  191. package/src/tests/fixtures/extensions/block-tasks-from-source/manifest.json +9 -0
  192. package/src/tests/fixtures/extensions/ignore-channel/hooks.ts +22 -0
  193. package/src/tests/fixtures/extensions/ignore-channel/manifest.json +9 -0
  194. package/src/tests/fixtures/extensions/load.ts +10 -0
  195. package/src/tests/fixtures/extensions/minimal/hooks.ts +7 -0
  196. package/src/tests/fixtures/extensions/minimal/manifest.json +9 -0
  197. package/src/tests/fixtures/extensions/never-fail-long-tasks/hooks.ts +11 -0
  198. package/src/tests/fixtures/extensions/never-fail-long-tasks/manifest.json +9 -0
  199. package/src/tests/fixtures/extensions/no-exclamation-marks/hooks.ts +23 -0
  200. package/src/tests/fixtures/extensions/no-exclamation-marks/manifest.json +9 -0
  201. package/src/tests/fixtures/extensions/post-logger/hooks.ts +9 -0
  202. package/src/tests/fixtures/extensions/post-logger/manifest.json +9 -0
  203. package/src/tests/fixtures/extensions/priority-a/hooks.ts +7 -0
  204. package/src/tests/fixtures/extensions/priority-a/manifest.json +9 -0
  205. package/src/tests/fixtures/extensions/priority-b/hooks.ts +7 -0
  206. package/src/tests/fixtures/extensions/priority-b/manifest.json +9 -0
  207. package/src/tests/fixtures/extensions/record-only-on-tag/hooks.ts +16 -0
  208. package/src/tests/fixtures/extensions/record-only-on-tag/manifest.json +9 -0
  209. package/src/tests/fixtures/extensions/reserved-assets/hooks.ts +5 -0
  210. package/src/tests/fixtures/extensions/reserved-assets/manifest.json +10 -0
  211. package/src/tests/fixtures/extensions/rewrite-progress-prefix/hooks.ts +22 -0
  212. package/src/tests/fixtures/extensions/rewrite-progress-prefix/manifest.json +9 -0
  213. package/src/tests/fixtures/extensions/rewrite-task-priority/hooks.ts +7 -0
  214. package/src/tests/fixtures/extensions/rewrite-task-priority/manifest.json +9 -0
  215. package/src/tests/fixtures/extensions/route-channel-to-agent/hooks.ts +26 -0
  216. package/src/tests/fixtures/extensions/route-channel-to-agent/manifest.json +9 -0
  217. package/src/tests/fixtures/extensions/slow/hooks.ts +10 -0
  218. package/src/tests/fixtures/extensions/slow/manifest.json +9 -0
  219. package/src/tests/fixtures/extensions/suppress-lead-follow-up/hooks.ts +12 -0
  220. package/src/tests/fixtures/extensions/suppress-lead-follow-up/manifest.json +9 -0
  221. package/src/tests/fixtures/extensions/throws/hooks.ts +12 -0
  222. package/src/tests/fixtures/extensions/throws/manifest.json +9 -0
  223. package/src/tests/fixtures/extensions/worker-runtime/hooks.ts +5 -0
  224. package/src/tests/fixtures/extensions/worker-runtime/manifest.json +9 -0
  225. package/src/tests/fixtures/store-progress-malformed-output.json +4 -0
  226. package/src/tests/memory-edit-authorization.test.ts +32 -0
  227. package/src/tests/multi-runtime-enabled-stats.test.ts +2 -2
  228. package/src/tests/multi-runtime-registration.test.ts +14 -14
  229. package/src/tests/onboard-compose.test.ts +44 -0
  230. package/src/tests/preload.ts +5 -0
  231. package/src/tests/rbac-admission-e2e.test.ts +35 -4
  232. package/src/tests/rbac-admission.test.ts +18 -3
  233. package/src/tests/rbac-e2e-helpers.ts +18 -0
  234. package/src/tests/rbac-engine.test.ts +39 -0
  235. package/src/tests/rbac-mcp-admission-e2e.test.ts +22 -4
  236. package/src/tests/realtime-room-auth.test.ts +1 -1
  237. package/src/tests/reload-config.test.ts +61 -0
  238. package/src/tests/routing-affinity-inventory.test.ts +1 -1
  239. package/src/tests/script-connections.test.ts +2 -0
  240. package/src/tests/scripts-boot-reembed.test.ts +4 -0
  241. package/src/tests/scripts-embeddings.test.ts +4 -0
  242. package/src/tests/scripts-import-allowlist.test.ts +44 -1
  243. package/src/tests/scripts-mcp-e2e.test.ts +80 -0
  244. package/src/tests/scripts-only-gating.test.ts +6 -0
  245. package/src/tests/scripts-runtime-identity.test.ts +1 -1
  246. package/src/tests/slack-blocks.test.ts +12 -1
  247. package/src/tests/slack-render-v2.test.ts +100 -5
  248. package/src/tests/slack-router.test.ts +17 -2
  249. package/src/tests/slack-steering.test.ts +19 -0
  250. package/src/tests/slack-task-output.test.ts +36 -0
  251. package/src/tests/status.test.ts +12 -0
  252. package/src/tests/steering-enabled.test.ts +6 -8
  253. package/src/tests/store-progress-attachments-handler.test.ts +100 -1
  254. package/src/tests/swarm-config-reserved-keys.test.ts +6 -1
  255. package/src/tests/task-tool-preload-mcp.test.ts +4 -1
  256. package/src/tests/tool-annotations.test.ts +1 -1
  257. package/src/tests/tool-registrar-no-input.test.ts +42 -1
  258. package/src/tools/defer-task.ts +1 -1
  259. package/src/tools/extension-activate-version.ts +38 -0
  260. package/src/tools/extension-common.ts +85 -0
  261. package/src/tools/extension-delete.ts +29 -0
  262. package/src/tools/extension-disable.ts +36 -0
  263. package/src/tools/extension-enable.ts +36 -0
  264. package/src/tools/extension-install.ts +65 -0
  265. package/src/tools/extension-list.ts +66 -0
  266. package/src/tools/script-common.ts +43 -18
  267. package/src/tools/send-task.ts +69 -107
  268. package/src/tools/store-progress.ts +30 -0
  269. package/src/tools/task-action.ts +43 -22
  270. package/src/tools/task-tool-ctx.ts +2 -0
  271. package/src/tools/tool-config.ts +7 -1
  272. package/src/tools/utils.ts +140 -16
  273. package/src/types.ts +133 -0
  274. package/src/utils/multi-runtime.ts +2 -2
  275. package/src/utils/steering-enabled.ts +4 -4
  276. package/src/workflows/executors/agent-task.ts +13 -3
  277. package/templates/extensions/README.md +17 -0
  278. package/templates/extensions/notify-on-complete/README.md +11 -0
  279. package/templates/extensions/notify-on-complete/hooks.ts +61 -0
  280. package/templates/extensions/notify-on-complete/manifest.json +7 -0
  281. package/templates/extensions/require-ticket-ref/README.md +11 -0
  282. package/templates/extensions/require-ticket-ref/hooks.ts +30 -0
  283. package/templates/extensions/require-ticket-ref/manifest.json +7 -0
  284. package/templates/extensions/require-verification-note/README.md +11 -0
  285. package/templates/extensions/require-verification-note/hooks.ts +32 -0
  286. package/templates/extensions/require-verification-note/manifest.json +7 -0
  287. package/templates/skills/agent-browser/SKILL.md +84 -0
  288. package/templates/skills/agent-browser/content.md +21 -0
  289. package/templates/skills/scheduling/content.md +2 -0
  290. package/templates/skills/swarm-extensions/config.json +14 -0
  291. package/templates/skills/swarm-extensions/content.md +120 -0
  292. package/dist/cli-b8x19vbh.js +0 -823
  293. package/dist/cli-n1yj5fd5.js +0 -164
  294. package/dist/cli-q04q68be.js +0 -3066
  295. package/dist/cli-qgm2pfpq.js +0 -6377
  296. package/dist/cli-rnsr8asm.js +0 -4323
  297. package/dist/cli-rwp3ksm7.js +0 -545
  298. package/dist/cli-xjb9wghj.js +0 -668
  299. package/dist/cli-xvkmedgn.js +0 -332
  300. package/dist/cli-ydk0wjj8.js +0 -338
  301. package/dist/handlers-vr1djg4n.js +0 -41
  302. package/dist/{claude-managed-setup-f5pmsyrp.js → claude-managed-setup-48dwg9bv.js} +3 -3
  303. package/dist/{cli-81r683bv.js → cli-ch38q5z4.js} +0 -0
  304. package/dist/{cli-wk0gjrn3.js → cli-kj76p1ez.js} +6 -6
  305. package/dist/{cli-c71mg51n.js → cli-r2fpe7cx.js} +5 -5
  306. package/dist/{cli-8qx7jnrk.js → cli-y6x0gqtg.js} +16 -16
  307. package/dist/{index-aa3sa6s8.js → index-2wsdsnmm.js} +1 -1
  308. package/dist/{mistral-conversations-4ryk7h83.js → mistral-conversations-qhr84ph3.js} +9 -9
  309. package/dist/{openai-completions-qdcmkfnr.js → openai-completions-32rdwmze.js} +17 -17
  310. package/dist/{openrouter-images-qx18fyp8.js → openrouter-images-9f9ek4rw.js} +3 -3
  311. package/dist/{pi-messages-79jvzz1d.js → pi-messages-b1mbr6gp.js} +4 -4
@@ -314,3 +314,21 @@ export async function waitForAuditCount(
314
314
  }
315
315
  return n;
316
316
  }
317
+
318
+ /** Poll until audit rows satisfy `predicate` (writer flushes every 2s). */
319
+ export async function waitForAuditRows(
320
+ dbPath: string,
321
+ predicate: (rows: AuditRow[]) => boolean,
322
+ deadlineMs = 8_000,
323
+ ): Promise<AuditRow[]> {
324
+ const start = Date.now();
325
+ let rows = readAuditRows(dbPath);
326
+ while (!predicate(rows)) {
327
+ if (Date.now() - start >= deadlineMs) {
328
+ throw new Error(`audit rows did not satisfy predicate within ${deadlineMs}ms`);
329
+ }
330
+ await Bun.sleep(250);
331
+ rows = readAuditRows(dbPath);
332
+ }
333
+ return rows;
334
+ }
@@ -81,6 +81,9 @@ const LEAD_ONLY_VERBS: PermissionVerb[] = [
81
81
  "script.api.delete",
82
82
  ];
83
83
 
84
+ const OPERATOR_ONLY_VERBS: PermissionVerb[] = [];
85
+ const LEAD_OR_OPERATOR_VERBS: PermissionVerb[] = ["extension.write", "extension.activate"];
86
+
84
87
  const LEAD_OR_RESOURCE_OWNER_VERBS: PermissionVerb[] = [
85
88
  "memory.edit.any",
86
89
  "skill.update.any",
@@ -171,6 +174,8 @@ describe("verb-group partition", () => {
171
174
  ...ANY_AUTHENTICATED_VERBS,
172
175
  ...REQUESTER_OWNS_TASK_VERBS,
173
176
  ...COMPOSITE_VERBS,
177
+ ...OPERATOR_ONLY_VERBS,
178
+ ...LEAD_OR_OPERATOR_VERBS,
174
179
  ];
175
180
  expect(new Set(grouped).size).toBe(grouped.length);
176
181
  expect(grouped.sort()).toEqual([...PERMISSION_VERBS].sort());
@@ -216,6 +221,40 @@ describe("lead-only verbs", () => {
216
221
  });
217
222
  });
218
223
 
224
+ describe("operator-or-user verbs", () => {
225
+ const expected: Expected = {
226
+ lead: false,
227
+ worker: false,
228
+ ownerWorker: false,
229
+ creatorWorker: false,
230
+ userRequester: true,
231
+ foreignUser: true,
232
+ operator: true,
233
+ };
234
+ for (const verb of OPERATOR_ONLY_VERBS) {
235
+ test(`${verb}: operator or user allowed`, () => {
236
+ expectDecisions(verb, { kind: "none" }, expected);
237
+ });
238
+ }
239
+ });
240
+
241
+ describe("lead-or-operator-or-user verbs", () => {
242
+ const expected: Expected = {
243
+ lead: true,
244
+ worker: false,
245
+ ownerWorker: false,
246
+ creatorWorker: false,
247
+ userRequester: true,
248
+ foreignUser: true,
249
+ operator: true,
250
+ };
251
+ for (const verb of LEAD_OR_OPERATOR_VERBS) {
252
+ test(`${verb}: lead, operator, or user allowed`, () => {
253
+ expectDecisions(verb, { kind: "none" }, expected);
254
+ });
255
+ }
256
+ });
257
+
219
258
  describe("lead-or-resource-owner verbs", () => {
220
259
  const expected: Expected = {
221
260
  lead: true,
@@ -12,12 +12,11 @@ import {
12
12
  countAuditRows,
13
13
  LEAD,
14
14
  makeScratchDir,
15
- readAuditRows,
16
15
  registerAgent,
17
16
  removeScratchDir,
18
17
  type SwarmServer,
19
18
  spawnSwarmServer,
20
- waitForAuditCount,
19
+ waitForAuditRows,
21
20
  } from "./rbac-e2e-helpers";
22
21
 
23
22
  setDefaultTimeout(120_000);
@@ -312,8 +311,27 @@ describe("RBAC admission over /mcp-user", () => {
312
311
  }),
313
312
  );
314
313
 
315
- expect(await waitForAuditCount(enabledDbPath, 10)).toBeGreaterThanOrEqual(10);
316
- const mcpRows = readAuditRows(enabledDbPath).filter((row) => row.source === "mcp");
314
+ const mcpRows = (
315
+ await waitForAuditRows(enabledDbPath, (rows) => {
316
+ const auditRows = rows.filter((row) => row.source === "mcp");
317
+ return (
318
+ auditRows.some(
319
+ (row) =>
320
+ row.principalId === requester.userId &&
321
+ row.resourceType === "mcp-tool" &&
322
+ row.resourceId === "send-task" &&
323
+ row.verb === "task.create.own" &&
324
+ row.decision === "allow",
325
+ ) &&
326
+ ["task.create.own", "task.cancel.own", "task.action.own"].every((verb) =>
327
+ auditRows.some(
328
+ (row) =>
329
+ row.principalId === empty.userId && row.decision === "deny" && row.verb === verb,
330
+ ),
331
+ )
332
+ );
333
+ })
334
+ ).filter((row) => row.source === "mcp");
317
335
  expect(
318
336
  mcpRows.some(
319
337
  (row) =>
@@ -44,7 +44,7 @@ describe("realtime room namespace guards", () => {
44
44
 
45
45
  test("allows page writes for user and operator contexts when RBAC is disabled", async () => {
46
46
  const previous = process.env.RBAC_ENABLED;
47
- delete process.env.RBAC_ENABLED;
47
+ process.env.RBAC_ENABLED = "false";
48
48
  try {
49
49
  await expect(
50
50
  authorizeRoomNamespace(
@@ -3,6 +3,7 @@ import { unlink } from "node:fs/promises";
3
3
  import { createServer as createHttpServer, type Server } from "node:http";
4
4
  import { initAgentMail, resetAgentMail } from "../agentmail";
5
5
  import { closeDb, deleteSwarmConfig, getDbClient, initDb, upsertSwarmConfig } from "../be/db";
6
+ import { getEmbeddingProvider, resetEmbeddingProvider } from "../be/memory";
6
7
  import { initGitHub, resetGitHub } from "../github";
7
8
  import {
8
9
  __resetInjectedEnvTracking,
@@ -10,6 +11,7 @@ import {
10
11
  _resetAutoReloadForTests,
11
12
  flushPendingIntegrationsReload,
12
13
  loadGlobalConfigsIntoEnv,
14
+ reloadGlobalConfigsAndIntegrations,
13
15
  scheduleIntegrationsReload,
14
16
  } from "../http/core";
15
17
  import { isOriginAllowedForCredentials } from "../http/utils";
@@ -225,6 +227,23 @@ describe("reload-config", () => {
225
227
  }
226
228
  });
227
229
 
230
+ test("stored lead activation gate cannot override the deployment at boot or reload", async () => {
231
+ const key = "EXTENSION_ALLOW_LEAD_ACTIVATION";
232
+ const original = process.env[key];
233
+ const rowId = await insertLegacyReservedRow(key, "true");
234
+ try {
235
+ process.env[key] = "false";
236
+ for (const override of [false, true]) {
237
+ expect(await loadGlobalConfigsIntoEnv(override)).not.toContain(key);
238
+ expect(process.env[key]).toBe("false");
239
+ }
240
+ } finally {
241
+ await deleteSwarmConfig(rowId);
242
+ if (original === undefined) delete process.env[key];
243
+ else process.env[key] = original;
244
+ }
245
+ });
246
+
228
247
  test("loadGlobalConfigsIntoEnv skips unreadable reserved secret rows before decrypting them", async () => {
229
248
  const id = await insertUnreadableReservedSecretRow("SECRETS_ENCRYPTION_KEY");
230
249
 
@@ -262,6 +281,48 @@ describe("reload-config", () => {
262
281
  expect(res.status).toBe(404);
263
282
  });
264
283
 
284
+ // Regression: EMBEDDING_API_KEY / OPENAI_API_KEY used to be read once at
285
+ // OpenAIEmbeddingProvider construction and never revisited, so a key set
286
+ // (or rotated) via the dashboard/API stayed inert until the API process
287
+ // restarted (customer-reported, Jan Carbonell 2026-09-16: valid key,
288
+ // reload reported it loaded, embeddings still stayed off). Assert the
289
+ // full path: no key -> provider unconfigured -> config write + reload ->
290
+ // provider live, without a restart.
291
+ test("reload turns embeddings on without a restart once a key lands via config", async () => {
292
+ const originalEmbeddingKey = process.env.EMBEDDING_API_KEY;
293
+ const originalOpenAiKey = process.env.OPENAI_API_KEY;
294
+ delete process.env.EMBEDDING_API_KEY;
295
+ delete process.env.OPENAI_API_KEY;
296
+ resetEmbeddingProvider();
297
+ expect(getEmbeddingProvider().isConfigured()).toBe(false);
298
+
299
+ const config = await upsertSwarmConfig({
300
+ scope: "global",
301
+ key: "EMBEDDING_API_KEY",
302
+ value: "sk-test-not-a-real-key",
303
+ isSecret: true,
304
+ });
305
+
306
+ try {
307
+ const result = await reloadGlobalConfigsAndIntegrations();
308
+ expect(result.keysUpdated).toContain("EMBEDDING_API_KEY");
309
+ expect(result.integrationsReinitialized).toContain("embeddings");
310
+ expect(getEmbeddingProvider().isConfigured()).toBe(true);
311
+ } finally {
312
+ await deleteSwarmConfig(config.id);
313
+ delete process.env.EMBEDDING_API_KEY;
314
+ if (originalOpenAiKey === undefined) {
315
+ delete process.env.OPENAI_API_KEY;
316
+ } else {
317
+ process.env.OPENAI_API_KEY = originalOpenAiKey;
318
+ }
319
+ resetEmbeddingProvider();
320
+ if (originalEmbeddingKey !== undefined) {
321
+ process.env.EMBEDDING_API_KEY = originalEmbeddingKey;
322
+ }
323
+ }
324
+ });
325
+
265
326
  // Regression: injection used to be one-way. Deleting a global row left the
266
327
  // previously-injected value live in process.env until the process restarted,
267
328
  // so "reset to default" in the dashboard silently did nothing.
@@ -101,7 +101,7 @@ const EXPECTED_INVENTORY: InventoryEntry[] = [
101
101
  file: "src/tools/send-task.ts",
102
102
  owner: "sendTaskHandler",
103
103
  syntax: "PropertyAssignment",
104
- count: 3,
104
+ count: 1,
105
105
  kind: "producer",
106
106
  dispatchProof: {
107
107
  file: "src/tests/task-tools-ctx.test.ts",
@@ -570,6 +570,8 @@ describe("script connections", () => {
570
570
  markMigrationApplied(database, "151_repair_scheduled_task_required_params.sql");
571
571
  // 152 indexes task routing context absent from this migration-112-only fixture.
572
572
  markMigrationApplied(database, "152_routing_decisions.sql");
573
+ // 156 alters inbox_item_state, which this migration-112-only fixture does not create.
574
+ markMigrationApplied(database, "156_inbox_item_state_read_at.sql");
573
575
 
574
576
  const id = crypto.randomUUID();
575
577
  const now = new Date().toISOString();
@@ -41,6 +41,10 @@ class FakeEmbeddingProvider implements EmbeddingProvider {
41
41
  return Promise.all(texts.map((text) => this.embed(text)));
42
42
  }
43
43
 
44
+ isConfigured(): boolean {
45
+ return true;
46
+ }
47
+
44
48
  reset(): void {
45
49
  this.calls.length = 0;
46
50
  }
@@ -58,6 +58,10 @@ class FakeEmbeddingProvider implements EmbeddingProvider {
58
58
  return Promise.all(texts.map((text) => this.embed(text)));
59
59
  }
60
60
 
61
+ isConfigured(): boolean {
62
+ return true;
63
+ }
64
+
61
65
  reset(): void {
62
66
  this.calls.length = 0;
63
67
  }
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, test } from "bun:test";
2
- import { validateScriptImports } from "../scripts-runtime/import-allowlist";
2
+ import { checkImportAllowlist, validateScriptImports } from "../scripts-runtime/import-allowlist";
3
3
 
4
4
  describe("script import allowlist", () => {
5
5
  test("allows relative imports and runtime barrels", () => {
@@ -61,6 +61,49 @@ describe("script import allowlist", () => {
61
61
  if (!result.ok) expect(result.diagnostic).toContain("fs");
62
62
  });
63
63
 
64
+ test("allows literal dynamic imports from the existing script allowlist", () => {
65
+ expect(validateScriptImports("export default async () => import('zod')")).toEqual({
66
+ ok: true,
67
+ });
68
+ });
69
+
70
+ test("allows computed dynamic imports by default", () => {
71
+ expect(validateScriptImports("const x = 'zod'; export default async () => import(x)")).toEqual({
72
+ ok: true,
73
+ });
74
+ });
75
+
76
+ test("strictDynamic rejects computed dynamic imports and requires", () => {
77
+ for (const source of [
78
+ "const name = 'zod'; export default async () => import(name)",
79
+ "const name = 'zod'; export default () => require(name)",
80
+ ]) {
81
+ const result = checkImportAllowlist(source, {
82
+ allowedBare: ["swarm-extension", "stdlib", "zod"],
83
+ allowRelative: false,
84
+ strictDynamic: true,
85
+ });
86
+ expect(result.ok).toBe(false);
87
+ if (!result.ok) expect(result.diagnostic).toContain("Computed");
88
+ }
89
+ });
90
+
91
+ test("strictDynamic checks require and import-equals specifiers", () => {
92
+ for (const source of [
93
+ "export default () => require('node:fs')",
94
+ "import fs = require('node:fs'); export default () => fs",
95
+ ]) {
96
+ expect(validateScriptImports(source)).toEqual({ ok: true });
97
+ const result = checkImportAllowlist(source, {
98
+ allowedBare: ["swarm-extension", "stdlib", "zod"],
99
+ allowRelative: false,
100
+ strictDynamic: true,
101
+ });
102
+ expect(result.ok).toBe(false);
103
+ if (!result.ok) expect(result.diagnostic).toContain("node:fs");
104
+ }
105
+ });
106
+
64
107
  test("rejects Function constructor dynamic import bypasses", () => {
65
108
  const result = validateScriptImports(
66
109
  `export default async () => new Function("return import('node:fs')")()`,
@@ -10,6 +10,7 @@ import { handleCore } from "../http/core";
10
10
  import { handleScriptRuns } from "../http/script-runs";
11
11
  import { handleScripts } from "../http/scripts";
12
12
  import { getPathSegments, parseQueryParams } from "../http/utils";
13
+ import { proxyScriptsApi } from "../tools/script-common";
13
14
  import { registerScriptDeleteTool } from "../tools/script-delete";
14
15
  import { registerScriptRunTool } from "../tools/script-run";
15
16
  import { registerScriptRunsTools } from "../tools/script-runs";
@@ -221,6 +222,85 @@ beforeEach(async () => {
221
222
  });
222
223
 
223
224
  describe("script_ MCP HTTP proxy tools", () => {
225
+ test.each([200, 502])("rejects HTML responses with HTTP %i", async (status) => {
226
+ const tools = buildToolServer();
227
+ const html = "<!doctype html><html><body>Static site fallback</body></html>";
228
+ const previousFetch = globalThis.fetch;
229
+ globalThis.fetch = (async () =>
230
+ new Response(html, {
231
+ status,
232
+ headers: { "Content-Type": "text/html; charset=utf-8" },
233
+ })) as typeof globalThis.fetch;
234
+ try {
235
+ const result = (await tools.listScriptRuns.handler(
236
+ { limit: 10, offset: 0 },
237
+ meta(workerId),
238
+ )) as StructuredResult<unknown>;
239
+
240
+ expect(result.isError).toBe(true);
241
+ expect(result.structuredContent.success).toBe(false);
242
+ expect(result.structuredContent.status).toBe(status);
243
+ expect(result.structuredContent.message).toContain("non-JSON");
244
+ expect(result.structuredContent.message).toContain(`HTTP ${status}`);
245
+ expect(result.structuredContent.message).toContain("text/html");
246
+ expect(result.content[0]?.text).toContain(html);
247
+ expect(result.content[0]?.text).not.toContain("Found 0 script run(s)");
248
+ } finally {
249
+ globalThis.fetch = previousFetch;
250
+ }
251
+ });
252
+
253
+ test("caps invalid JSON diagnostics without retaining the full body", async () => {
254
+ const body = `<html>${"x".repeat(20_000)}BODY_TAIL</html>`;
255
+ const previousFetch = globalThis.fetch;
256
+ globalThis.fetch = (async () =>
257
+ new Response(body, {
258
+ status: 200,
259
+ headers: { "Content-Type": "application/json" },
260
+ })) as typeof globalThis.fetch;
261
+ try {
262
+ const result = await proxyScriptsApi({
263
+ method: "GET",
264
+ path: "/api/script-runs",
265
+ requestInfo: {
266
+ sessionId: "s",
267
+ agentId: workerId,
268
+ runtimeInstanceId: undefined,
269
+ sourceTaskId: undefined,
270
+ contextKey: undefined,
271
+ callOrigin: "mcp",
272
+ },
273
+ successMessage: () => {
274
+ throw new Error("Invalid JSON must not reach the success callback");
275
+ },
276
+ uncappedDetails: true,
277
+ });
278
+
279
+ expect(result.ok).toBe(false);
280
+ expect(result.message).toContain("application/json");
281
+ expect(result.details).toContain("<html>");
282
+ expect(result.details).toContain("[truncated");
283
+ expect(result.details?.length).toBeLessThan(16_100);
284
+ expect(JSON.stringify(result)).not.toContain("BODY_TAIL");
285
+ } finally {
286
+ globalThis.fetch = previousFetch;
287
+ }
288
+ });
289
+
290
+ test("reports a legitimate JSON empty run list as success", async () => {
291
+ const tools = buildToolServer();
292
+ const result = (await tools.listScriptRuns.handler(
293
+ { limit: 10, offset: 0 },
294
+ meta(workerId),
295
+ )) as StructuredResult<{ total: number; runs: unknown[] }>;
296
+
297
+ expect(result.isError).toBeFalsy();
298
+ expect(result.structuredContent.success).toBe(true);
299
+ expect(result.structuredContent.status).toBe(200);
300
+ expect(result.structuredContent.data?.runs).toEqual([]);
301
+ expect(result.structuredContent.message).toBe("Found 0 script run(s).");
302
+ });
303
+
224
304
  test("upserts the canonical script authoring contract example verbatim", async () => {
225
305
  const tools = buildToolServer();
226
306
  // The authoring contract lives in the seeded `swarm-scripts` skill since
@@ -17,6 +17,12 @@ import { listenOnFreePort } from "./test-net";
17
17
 
18
18
  const TEST_DB_PATH = "./test-scripts-only-gating.sqlite";
19
19
  const SCRIPT_TOOL_NAMES = [
20
+ "extension-delete",
21
+ "extension-enable",
22
+ "extension-disable",
23
+ "extension-activate-version",
24
+ "extension-install",
25
+ "extension-list",
20
26
  "get-script-run",
21
27
  "launch-script-run",
22
28
  "list-script-runs",
@@ -374,7 +374,7 @@ describe("script SDK dispatch hits the real runtime gates", () => {
374
374
  });
375
375
 
376
376
  test("with the flag off, the SDK dispatches without runtime identity as before", async () => {
377
- delete process.env.MULTI_RUNTIME_ENABLED;
377
+ process.env.MULTI_RUNTIME_ENABLED = "false";
378
378
  const agentId = await makeAgent(1);
379
379
  const task = await createTaskExtended("legacy-claim-work");
380
380
 
@@ -298,8 +298,10 @@ describe("buildAssignmentSummaryBlocks", () => {
298
298
  failed: [{ agentName: "Delta", reason: "error" }],
299
299
  });
300
300
 
301
+ expect(blocks).toHaveLength(1);
302
+ expect(blocks[0].text.text).not.toBe("");
301
303
  expect(blocks[0].text.text).toContain("⚠️");
302
- expect(blocks[0].text.text).toContain("Could not assign");
304
+ expect(blocks[0].text.text).toContain("Could not assign to:");
303
305
  });
304
306
  });
305
307
 
@@ -463,6 +465,15 @@ function makeTaskId(prefix: string): string {
463
465
  }
464
466
 
465
467
  describe("buildTreeBlocks", () => {
468
+ test("empty tree omits the section with empty text", () => {
469
+ const blocks = buildTreeBlocks([]);
470
+
471
+ expect(blocks).toHaveLength(0);
472
+ expect(blocks.every((block) => block.type !== "section" || block.text.text.length > 0)).toBe(
473
+ true,
474
+ );
475
+ });
476
+
466
477
  test("single root, no children", () => {
467
478
  const root: TreeNode = {
468
479
  taskId: makeTaskId("aaaa0001"),
@@ -6,6 +6,7 @@ import {
6
6
  completeTask,
7
7
  createAgent,
8
8
  createLogEntry,
9
+ createScheduledTask,
9
10
  createTaskExtended,
10
11
  ensureSlackDelegationActivation,
11
12
  ensureSlackRenderV2Activation,
@@ -26,6 +27,7 @@ import {
26
27
  supersedeTask,
27
28
  upsertSwarmConfig,
28
29
  } from "../be/db";
30
+ import { createStandaloneScheduleTask } from "../scheduler/schedule-task";
29
31
  import { getTaskLink, MAX_SECTION_LENGTH } from "../slack/blocks";
30
32
  import {
31
33
  _resetSlackRenderV2ForTests,
@@ -300,12 +302,83 @@ describe("Slack renderer v2", () => {
300
302
  });
301
303
  });
302
304
 
303
- test("defaults off and accepts an explicit opt-in", () => {
305
+ for (const delegationEnabled of [false, true]) {
306
+ test(`relays a deferred schedule continuation with delegation=${delegationEnabled}`, async () => {
307
+ process.env.SLACK_RENDER_V2_DELEGATION = String(delegationEnabled);
308
+ if (delegationEnabled) await ensureSlackDelegationActivation();
309
+ const lead = await createAgent({ name: "Continuation Lead", isLead: true, status: "idle" });
310
+ const { channelId, threadTs } = uniqueSlackAddress("C_CONTINUATION");
311
+ const ask = await createTaskExtended("answer when ready", {
312
+ agentId: lead.id,
313
+ source: "slack",
314
+ slackChannelId: channelId,
315
+ slackThreadTs: threadTs,
316
+ contextKey: slackContextKey({ channelId, threadTs }),
317
+ });
318
+ await startTask(ask.id);
319
+ await completeTask(ask.id, "Waiting for the result.", { addTags: ["deferred"] });
320
+ await backdateLastUpdated([ask.id], 20);
321
+ await processSlackRenderV2();
322
+ expect((await getSlackOutcomeMessage(ask.id))?.finalizedAt).toBeTruthy();
323
+
324
+ const schedule = await createScheduledTask({
325
+ name: "deferred-answer",
326
+ scheduleType: "one_time",
327
+ taskTemplate: "Deliver the result",
328
+ taskType: "deferred",
329
+ tags: ["deferred"],
330
+ parentTaskId: ask.id,
331
+ targetAgentId: lead.id,
332
+ createdByAgentId: lead.id,
333
+ nextRunAt: new Date().toISOString(),
334
+ });
335
+ const continuation = await createStandaloneScheduleTask(schedule);
336
+ expect(continuation.source).toBe("schedule");
337
+ expect(continuation.scheduleId).toBe(schedule.id);
338
+ expect(continuation.slackChannelId).toBe(channelId);
339
+ expect(continuation.slackThreadTs).toBe(threadTs);
340
+ await startTask(continuation.id);
341
+ await processSlackRenderV2();
342
+ expect(await getSlackOutcomeMessage(continuation.id)).toBeNull();
343
+ await completeTask(continuation.id, "Here is the complete answer.");
344
+ calls.length = 0;
345
+
346
+ await processSlackRenderV2();
347
+ await processSlackRenderV2();
348
+
349
+ expect((await getSlackOutcomeMessage(continuation.id))?.finalizedAt).toBeTruthy();
350
+ const streams = calls.filter((call) => call.method === "chat.startStream");
351
+ expect(streams).toHaveLength(1);
352
+ expect(streams[0]?.payload).toMatchObject({
353
+ channel: channelId,
354
+ thread_ts: threadTs,
355
+ markdown_text: "✅\n\nHere is the complete answer.",
356
+ });
357
+ });
358
+ }
359
+
360
+ test("does not relay an ordinary scheduled task without Slack context", async () => {
361
+ const schedule = await createScheduledTask({
362
+ name: "ordinary-maintenance",
363
+ scheduleType: "one_time",
364
+ taskTemplate: "Run maintenance",
365
+ taskType: "maintenance",
366
+ nextRunAt: new Date().toISOString(),
367
+ });
368
+ const task = await createStandaloneScheduleTask(schedule);
369
+ await startTask(task.id);
370
+ await completeTask(task.id, "Maintenance completed.");
371
+ await processSlackRenderV2();
372
+ expect(await getSlackOutcomeMessage(task.id)).toBeNull();
373
+ expect(calls).toHaveLength(0);
374
+ });
375
+
376
+ test("defaults on and accepts an explicit opt-out", () => {
304
377
  const previous = process.env.SLACK_RENDER_V2;
305
378
  delete process.env.SLACK_RENDER_V2;
306
- expect(isSlackRenderV2Enabled()).toBe(false);
307
- process.env.SLACK_RENDER_V2 = "true";
308
379
  expect(isSlackRenderV2Enabled()).toBe(true);
380
+ process.env.SLACK_RENDER_V2 = "false";
381
+ expect(isSlackRenderV2Enabled()).toBe(false);
309
382
  if (previous === undefined) delete process.env.SLACK_RENDER_V2;
310
383
  else process.env.SLACK_RENDER_V2 = previous;
311
384
  });
@@ -1716,7 +1789,7 @@ describe("Slack renderer v2", () => {
1716
1789
  expect(outcome?.finalizedAt).toBeDefined();
1717
1790
  });
1718
1791
 
1719
- test("keeps a deferral's human-facing line even when the agent already sent a slack-reply", async () => {
1792
+ test("omits the deferral ETA even when the agent already sent a slack-reply", async () => {
1720
1793
  const lead = await createAgent({ name: "Deferring Lead", isLead: true, status: "idle" });
1721
1794
  const { channelId, threadTs } = uniqueSlackAddress("C_RENDER_DEFER_AFTER_REPLY");
1722
1795
  const ask = await createTaskExtended("defer after replying by hand", {
@@ -1741,10 +1814,32 @@ describe("Slack renderer v2", () => {
1741
1814
  await processSlackRenderV2();
1742
1815
 
1743
1816
  const started = calls.find((call) => call.method === "chat.startStream");
1744
- expect(started?.payload.markdown_text).toBe(`✅\n\n${deferralLine}`);
1817
+ expect(started?.payload.markdown_text).toBe("✅\n\nPending: checking the new defer card");
1818
+ expect(JSON.stringify(calls)).not.toContain("17:30:19");
1819
+ expect(JSON.stringify(calls)).not.toContain("Deferred until");
1820
+ expect((await getTaskById(ask.id))?.output).toBe(deferralLine);
1745
1821
  expect(started?.payload.markdown_text).not.toBe(`✅ ${lead.name} completed`);
1746
1822
  });
1747
1823
 
1824
+ test("does not post an ETA-only deferral notice", async () => {
1825
+ const { channelId, threadTs } = uniqueSlackAddress("C_ETA_ONLY");
1826
+ const ask = await createTaskExtended("wait", {
1827
+ source: "slack",
1828
+ slackChannelId: channelId,
1829
+ slackThreadTs: threadTs,
1830
+ });
1831
+ await startTask(ask.id);
1832
+ await completeTask(
1833
+ ask.id,
1834
+ "Deferred until today 17:30 UTC ([12345678](https://example.com/schedules/12345678)) -> ",
1835
+ { addTags: ["deferred"] },
1836
+ );
1837
+ calls.length = 0;
1838
+ await processSlackRenderV2();
1839
+ expect(calls.filter((call) => call.method === "chat.startStream")).toHaveLength(0);
1840
+ expect(JSON.stringify(calls)).not.toContain("17:30");
1841
+ });
1842
+
1748
1843
  test("refreshes a stream started with stale content before finalizing it", async () => {
1749
1844
  const lead = await createAgent({ name: "Refresh Lead", isLead: true, status: "idle" });
1750
1845
  const { channelId, threadTs } = uniqueSlackAddress("C_RENDER_REFRESH_STALE");
@@ -1,7 +1,7 @@
1
1
  import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
2
  import { unlinkSync } from "node:fs";
3
- import { closeDb, createAgent, createTaskExtended, initDb } from "../be/db";
4
- import { routeMessage } from "../slack/router";
3
+ import { closeDb, createAgent, createTaskExtended, getAllAgents, initDb } from "../be/db";
4
+ import { broadcastMatches, routeMessage } from "../slack/router";
5
5
  import type { Agent } from "../types";
6
6
 
7
7
  const TEST_DB_PATH = "./test-slack-router.sqlite";
@@ -169,6 +169,21 @@ describe("Slack router — thread follow-up routing", () => {
169
169
  // Lead should not be in the matches
170
170
  expect(matches.every((m) => !m.agent.isLead)).toBe(true);
171
171
  });
172
+
173
+ test("swarm#all includes offline non-lead agents", async () => {
174
+ const offlineWorker = await createAgent({
175
+ name: "offline-broadcast-worker",
176
+ isLead: false,
177
+ status: "offline",
178
+ capabilities: [],
179
+ });
180
+
181
+ const matches = broadcastMatches(await getAllAgents());
182
+
183
+ expect(matches.map((match) => match.agent.id)).toContain(offlineWorker.id);
184
+ expect(matches.every((match) => !match.agent.isLead)).toBe(true);
185
+ expect(matches.every((match) => match.matchedText === "swarm#all")).toBe(true);
186
+ });
172
187
  });
173
188
 
174
189
  describe("no thread context behaves normally", () => {
@@ -105,6 +105,25 @@ describe("Slack thread steering", () => {
105
105
  expect(await getChildTasks(leadTask.id)).toHaveLength(1);
106
106
  });
107
107
 
108
+ test("unset configuration steers the lead with queued delivery", async () => {
109
+ delete process.env.SLACK_THREAD_STEERING;
110
+ delete process.env.SLACK_THREAD_STEERING_MODE;
111
+ const channelId = "C_DEFAULT_STEER";
112
+ const threadTs = "1500.0001";
113
+ const task = await createRunningSlackTask(leadId, channelId, threadTs);
114
+ const result = await requestSlackThreadSteering({
115
+ channelId,
116
+ threadTs,
117
+ message: "new context",
118
+ });
119
+ expect(result).toMatchObject({
120
+ task: { id: task.id },
121
+ result: { outcome: "queued", effectiveMode: "queue" },
122
+ });
123
+ expect(await getSteeringMessagesForTask(task.id)).toHaveLength(1);
124
+ expect(await getChildTasks(task.id)).toEqual([]);
125
+ });
126
+
108
127
  test("lead mode sends one steering message to an in-progress lead and creates no task", async () => {
109
128
  process.env.SLACK_THREAD_STEERING = "lead";
110
129
  process.env.SLACK_THREAD_STEERING_MODE = "steer";