@desplega.ai/agent-swarm 2.7.5 → 2.7.7

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 (319) hide show
  1. package/dist/{acp-adapter-g3ads55s.js → acp-adapter-7b4vk3h5.js} +5 -4
  2. package/dist/{actions-zwfykqqk.js → actions-3tt153af.js} +18 -17
  3. package/dist/{amp-adapter-z3s0ssdt.js → amp-adapter-njpts0yr.js} +1 -1
  4. package/dist/{anthropic-messages-x4q6rfq5.js → anthropic-messages-xey2skwx.js} +7 -7
  5. package/dist/{app-vmp4fgep.js → app-nky4378f.js} +3 -3
  6. package/dist/{assistant-vwde653f.js → assistant-jnb669pa.js} +24 -22
  7. package/dist/{azure-openai-responses-s1w5n70z.js → azure-openai-responses-26t7j9ra.js} +5 -5
  8. package/dist/{boot-reembed-375t7tbk.js → boot-reembed-9mgvaztt.js} +1 -1
  9. package/dist/{boot-reembed-gq9rcpnj.js → boot-reembed-qs2djdej.js} +1 -1
  10. package/dist/{boot-scrub-sweep-nd938kyf.js → boot-scrub-sweep-yjpdj7s1.js} +3 -3
  11. package/dist/{boot-seeds-aykx51v3.js → boot-seeds-5cm6ambd.js} +4 -4
  12. package/dist/{boxd-k1j26g3h.js → boxd-nv507va8.js} +5 -5
  13. package/dist/{claude-adapter-8j4hm099.js → claude-adapter-fn287zmm.js} +5 -4
  14. package/dist/{claude-managed-setup-0wyynpn1.js → claude-managed-setup-wtdz2mj2.js} +1 -1
  15. package/dist/{claude-sdk-session-q6wj5hmy.js → claude-sdk-session-x91kqyhm.js} +9677 -9514
  16. package/dist/{cli-j3jvnpe8.js → cli-08bmz9z9.js} +1 -1
  17. package/dist/{cli-gjr2y2xk.js → cli-0agcv1v5.js} +4 -1
  18. package/dist/{cli-y24sc29k.js → cli-1c6tzt8g.js} +30 -8
  19. package/dist/{cli-mjzynrse.js → cli-35rsp8ye.js} +1 -1
  20. package/dist/{cli-rpm2sgv0.js → cli-3fg0bdcv.js} +19 -2
  21. package/dist/{cli-nn4d8dne.js → cli-5b456bpf.js} +2 -2
  22. package/dist/{cli-bhjt02p8.js → cli-5jnhv72d.js} +1 -1
  23. package/dist/{cli-fqzx7gwa.js → cli-5zx699j9.js} +2 -2
  24. package/dist/{cli-hf6zt5m6.js → cli-69szn7w7.js} +17 -1
  25. package/dist/{cli-vqypxzh4.js → cli-6g0hb4cp.js} +8 -8
  26. package/dist/{cli-tqgg40ek.js → cli-6pfd54vx.js} +9 -8
  27. package/dist/{cli-bsn8nhrq.js → cli-6vcp76qk.js} +3 -3
  28. package/dist/{cli-zppwg9p9.js → cli-72v799xn.js} +1 -1
  29. package/dist/cli-82j36f2m.js +45 -0
  30. package/dist/{cli-hn9hxmz9.js → cli-8305gbq0.js} +29 -16
  31. package/dist/{cli-z8eh3ex8.js → cli-8sckt74b.js} +8 -8
  32. package/dist/{cli-bnz7a7qf.js → cli-95jzxcp3.js} +1 -1
  33. package/dist/{cli-0bxqnvq9.js → cli-adeegh7d.js} +29 -7
  34. package/dist/{cli-g61qxmge.js → cli-afkqkrtw.js} +3 -3
  35. package/dist/{cli-7a42nfy0.js → cli-ayayvfg4.js} +24 -4
  36. package/dist/{cli-bqt1mptt.js → cli-az48n0nr.js} +4 -4
  37. package/dist/{cli-z6jkx23q.js → cli-bfv7vdg6.js} +9 -9
  38. package/dist/{cli-pywf3eh5.js → cli-caj33vcq.js} +5 -5
  39. package/dist/{cli-5pag6rmn.js → cli-cb2a8pna.js} +1 -1
  40. package/dist/{cli-wvaawacd.js → cli-cppkf404.js} +5236 -72
  41. package/dist/{cli-mry0qzym.js → cli-ddaanqkd.js} +123 -10
  42. package/dist/{cli-0gey5fzy.js → cli-e0kmphd6.js} +2 -2
  43. package/dist/{cli-zh3fp8v2.js → cli-e46hg0xw.js} +1 -1
  44. package/dist/{cli-jm2vfpfd.js → cli-e5bzrn01.js} +1 -1
  45. package/dist/{cli-qxnf8z2z.js → cli-excnnjg2.js} +165 -153
  46. package/dist/{cli-71ptav7x.js → cli-ferkkrg9.js} +1 -1
  47. package/dist/{cli-vpf89f34.js → cli-fmjd0t1y.js} +3 -3
  48. package/dist/{cli-rm6hqj3z.js → cli-fyt2g4wd.js} +33 -139
  49. package/dist/cli-g08zdaec.js +55 -0
  50. package/dist/{cli-ehq736cn.js → cli-g7s47cc5.js} +4 -4
  51. package/dist/{cli-82ae98pz.js → cli-gz6q4eve.js} +3 -3
  52. package/dist/{cli-82jg7fp9.js → cli-he5nxaa8.js} +1 -1
  53. package/dist/{cli-rqkxrmgw.js → cli-je518txm.js} +39 -14
  54. package/dist/{cli-8hs41zy2.js → cli-jtjtabxe.js} +1 -1
  55. package/dist/{cli-611a1294.js → cli-k0wwfqbg.js} +18 -1
  56. package/dist/{cli-nzxr556s.js → cli-k1nje3v3.js} +38 -43
  57. package/dist/{cli-3h4nhjn8.js → cli-k8196sh2.js} +9 -7
  58. package/dist/{cli-afrrppsw.js → cli-k899ehvh.js} +2 -2
  59. package/dist/{cli-0731hvbw.js → cli-m0da8j35.js} +23 -9
  60. package/dist/cli-m2z8jagn.js +12078 -0
  61. package/dist/{cli-8veytvbh.js → cli-mkk9rnzz.js} +5 -5
  62. package/dist/{cli-7wspm5gm.js → cli-n6bv12jn.js} +528 -6
  63. package/dist/{cli-spknvxbq.js → cli-nmzhp1b5.js} +14 -12
  64. package/dist/{cli-p599f1qm.js → cli-p82cnnjq.js} +1 -1
  65. package/dist/{cli-xtjhfmpc.js → cli-pc6n7fre.js} +1 -1
  66. package/dist/{cli-vbyfwh92.js → cli-pjctyea3.js} +2 -2
  67. package/dist/{cli-nmbmy64z.js → cli-pq73h5mz.js} +3 -3
  68. package/dist/{cli-aa6yxc7m.js → cli-pz4rk021.js} +1 -1
  69. package/dist/{cli-7wcyk2s5.js → cli-q3tkegby.js} +1 -1
  70. package/dist/cli-qghe0h66.js +67 -0
  71. package/dist/{cli-6s7vdcta.js → cli-qh4d4tvk.js} +4 -4
  72. package/dist/{cli-mmwm7zbf.js → cli-rtt8g4v9.js} +1 -1
  73. package/dist/{cli-kg8hchyg.js → cli-sa02jsqt.js} +1 -1
  74. package/dist/{cli-1h3wcpbw.js → cli-snpj5zbj.js} +2 -2
  75. package/dist/{cli-a7v854rf.js → cli-tgrv9cjx.js} +263 -12
  76. package/dist/{cli-a47xy8eq.js → cli-vz22fyqv.js} +186 -54
  77. package/dist/{cli-1ayk1eas.js → cli-wk5yrfwd.js} +285 -42
  78. package/dist/{cli-1338796d.js → cli-ws19768n.js} +1755 -119
  79. package/dist/{cli-70r77n04.js → cli-xae4g51r.js} +1 -1
  80. package/dist/{cli-gf5hbmdr.js → cli-yjdgea32.js} +3 -3
  81. package/dist/{cli-xbkdm710.js → cli-yndmfxb3.js} +3 -3
  82. package/dist/{cli-rhw40jgb.js → cli-yyj3j5m4.js} +2 -2
  83. package/dist/cli.js +26 -25
  84. package/dist/{cloudflare-workers-ai-system-one-bm60fjm0.js → cloudflare-workers-ai-system-one-ef2a980q.js} +2 -2
  85. package/dist/{codex-adapter-q6k3gnyg.js → codex-adapter-cf2yxe9m.js} +3 -2
  86. package/dist/{codex-hook-9efm2x1m.js → codex-hook-a0c6g6t6.js} +1 -1
  87. package/dist/{codex-session-runner-3mwmk3d9.js → codex-session-runner-6vfzdgbd.js} +3 -2
  88. package/dist/{commands-evrenjn9.js → commands-t3mfy7vn.js} +9 -3
  89. package/dist/{controller-xjp8vswh.js → controller-q7xrbaz0.js} +8 -3
  90. package/dist/{controllers-connect-pp5jnxvc.js → controllers-connect-c6nm70kg.js} +20 -19
  91. package/dist/{cursor-adapter-yf47w4t0.js → cursor-adapter-ftd81ga8.js} +1 -1
  92. package/dist/{db-zxpw6aht.js → db-dk572x25.js} +3 -3
  93. package/dist/{db-rgfsh91f.js → db-vemtpmcc.js} +14 -4
  94. package/dist/{devtools-82fzqra8.js → devtools-qxfgbsqz.js} +1 -1
  95. package/dist/{dispatcher-smasgws7.js → dispatcher-kh7ecxh9.js} +17 -16
  96. package/dist/{docker-rrjhaqt9.js → docker-c1a3gqmf.js} +89 -22
  97. package/dist/{dsh-adapter-ftyy8dnp.js → dsh-adapter-v8628r89.js} +1 -1
  98. package/dist/{e2b-w1p0bnpm.js → e2b-fr12yccx.js} +5 -5
  99. package/dist/{e2b-qcaj675y.js → e2b-x6ywzak3.js} +3 -3
  100. package/dist/{easter-egg-3d-yyts7p16.js → easter-egg-3d-nenw4x63.js} +1 -1
  101. package/dist/{embedding-space-1z0mbg95.js → embedding-space-0jepb239.js} +6 -6
  102. package/dist/{embeddings-ygeg7tm7.js → embeddings-xxt49yxg.js} +1 -1
  103. package/dist/{execute-14qhqp4d.js → execute-1mhw6e4x.js} +1 -1
  104. package/dist/{external-channel-ttgrs3sx.js → external-channel-55v12e60.js} +2 -2
  105. package/dist/{fx-adapter-gt6pncj2.js → fx-adapter-bs8wqrk5.js} +6 -5
  106. package/dist/{google-generative-ai-yvmdxyxa.js → google-generative-ai-v94hr7h0.js} +5 -5
  107. package/dist/{google-vertex-jrtdh9gs.js → google-vertex-g7vr6cf8.js} +5 -5
  108. package/dist/{grok-adapter-sm4m5k4g.js → grok-adapter-gpjqmkcq.js} +6 -5
  109. package/dist/handlers-zxmp1ta4.js +32 -0
  110. package/dist/{heartbeat-qahz4p62.js → heartbeat-k42svnr0.js} +20 -19
  111. package/dist/{hook-4fd5wq5w.js → hook-114m7hap.js} +4 -3
  112. package/dist/{hook-kj3d398m.js → hook-6nphweh0.js} +24 -9
  113. package/dist/{http-1ted0j1k.js → http-r8cc4wz7.js} +1196 -211
  114. package/dist/index-a8phxqnk.js +37 -0
  115. package/dist/{index-q4b917zs.js → index-e3s8wmxy.js} +1 -1
  116. package/dist/{index-6rq3r5zb.js → index-fpk4bcd9.js} +1 -1
  117. package/dist/{index-5rk6rfzr.js → index-fvzke5kx.js} +1 -1
  118. package/dist/index-k6h00t4c.js +30 -0
  119. package/dist/{index-7v51awbn.js → index-nh3wyqdt.js} +1027 -577
  120. package/dist/{index-yqhz88yy.js → index-pxn30mse.js} +8 -8
  121. package/dist/index-xrz7te8h.js +33 -0
  122. package/dist/{keepalive-6cf311t5.js → keepalive-x060nt9p.js} +3 -3
  123. package/dist/lead-956sd2yc.js +71 -0
  124. package/dist/{maintenance-a4bm1bmt.js → maintenance-2xxfexhf.js} +1 -1
  125. package/dist/{mistral-conversations-4pmwj1c0.js → mistral-conversations-3gjpdb4b.js} +4 -4
  126. package/dist/{oauth-refresh-sweep-3h0dfw1k.js → oauth-refresh-sweep-fckc4ar5.js} +2 -2
  127. package/dist/{onboard-myn5hraa.js → onboard-zc48j237.js} +5 -5
  128. package/dist/{openai-codex-responses-44j573nm.js → openai-codex-responses-8pbxq9h5.js} +5 -5
  129. package/dist/{openai-completions-a4gee2a8.js → openai-completions-rm00s912.js} +4 -4
  130. package/dist/{openai-decisions-w4qkrcar.js → openai-decisions-8v957r2r.js} +1 -1
  131. package/dist/{openai-responses-rbp6h5kr.js → openai-responses-agxpf96k.js} +5 -5
  132. package/dist/{opencode-adapter-p2nkb5am.js → opencode-adapter-r690h1cr.js} +4 -3
  133. package/dist/{otel-impl-a1a0ef6b.js → otel-impl-r5xbh81q.js} +4 -4
  134. package/dist/{pi-mono-adapter-rzmpb72a.js → pi-mono-adapter-c7v8sdec.js} +7 -6
  135. package/dist/{pricing-refresh-83drrnxj.js → pricing-refresh-3pmre11v.js} +3 -3
  136. package/dist/{rbac-roles-s8y6ecb5.js → rbac-roles-3vbshwsb.js} +1 -1
  137. package/dist/{rbac-roles-0fb6e7pj.js → rbac-roles-910mmkgz.js} +2 -2
  138. package/dist/{render-v2-twmbgrz4.js → render-v2-pxykkbab.js} +3 -3
  139. package/dist/{resume-zfqc36xj.js → resume-adda0bwy.js} +6 -6
  140. package/dist/{runtime-qwggdpqr.js → runtime-fnbm7stk.js} +1 -1
  141. package/dist/{session-access-sc9pn9da.js → session-access-2fvamrdc.js} +4 -4
  142. package/dist/{session-source-backfill-qh07xr8g.js → session-source-backfill-08xvv0sm.js} +2 -2
  143. package/dist/{setup-4fr0m7a1.js → setup-1aqmvmh1.js} +3 -3
  144. package/dist/{typesafe-system-one-5mavm259.js → typesafe-system-one-0bmqcxfg.js} +2 -2
  145. package/dist/{virtual-modules-fh4ha15b.js → virtual-modules-d2pjk9j1.js} +5 -5
  146. package/dist/{work-objects-0wr8zxct.js → work-objects-9s0yrm5j.js} +20 -19
  147. package/dist/{worker-follow-up-ktyyazyp.js → worker-follow-up-3q78fqmr.js} +17 -16
  148. package/dist/worker-y15hy08h.js +71 -0
  149. package/openapi.json +289 -4
  150. package/package.json +9 -8
  151. package/src/apps/definition.ts +6 -5
  152. package/src/be/db/page-theme-tokens.ts +12 -0
  153. package/src/be/db/script-runs.ts +23 -0
  154. package/src/be/db/tasks/read.ts +131 -4
  155. package/src/be/db/tasks/write.ts +19 -0
  156. package/src/be/db-client.ts +114 -11
  157. package/src/be/db.ts +49 -6
  158. package/src/be/memory/providers/sqlite-store.ts +49 -12
  159. package/src/be/migrations/260_runtime_pool_cache.sql +20 -0
  160. package/src/be/migrations/262_session_visibility_indexes.sql +18 -0
  161. package/src/be/migrations/263_task_list_filter_index.sql +11 -0
  162. package/src/be/migrations/264_slack_thread_cover_index.sql +9 -0
  163. package/src/be/migrations/266_script_runs_source_task.sql +11 -0
  164. package/src/be/migrations/269_pages_design_system.sql +23 -0
  165. package/src/be/runtime-pools/cache-cleanup.ts +109 -0
  166. package/src/be/runtime-pools/db.ts +65 -6
  167. package/src/be/runtime-pools/remote-controller.ts +5 -0
  168. package/src/be/runtime-pools/supervisor.ts +33 -2
  169. package/src/be/scripts/typecheck.ts +22 -0
  170. package/src/be/seed/schedules-seeder.ts +0 -10
  171. package/src/be/seed-scripts/catalog/catalog-report.inline.ts +2 -0
  172. package/src/be/seed-scripts/catalog/catalog-report.ts +2 -0
  173. package/src/be/seed-scripts/catalog/memory-eval.ts +2 -0
  174. package/src/be/seed-scripts/catalog/swarm-update-check.ts +150 -0
  175. package/src/be/seed-scripts/catalog/task-failure-audit.ts +5 -1
  176. package/src/be/seed-scripts/index.ts +9 -0
  177. package/src/be/seed-skills/bundled-files.generated.json +3 -3
  178. package/src/be/swarm-config-guard.ts +32 -0
  179. package/src/be/task-auto-title-listener.ts +127 -0
  180. package/src/commands/controller.ts +4 -0
  181. package/src/commands/entrypoint-api-readiness.ts +74 -0
  182. package/src/commands/onboard/agent-fs-version.ts +1 -1
  183. package/src/commands/profile-sync.ts +151 -16
  184. package/src/commands/runner.ts +15 -7
  185. package/src/hooks/hook.ts +20 -9
  186. package/src/http/index.ts +4 -1
  187. package/src/http/list-total-cache.ts +14 -7
  188. package/src/http/memory.ts +38 -9
  189. package/src/http/pages-public.ts +93 -129
  190. package/src/http/pages.ts +94 -21
  191. package/src/http/script-runs.ts +174 -5
  192. package/src/http/status.ts +10 -2
  193. package/src/http/tasks.ts +4 -2
  194. package/src/pages/browser-sdk.ts +48 -21
  195. package/src/pages/color-literals.ts +115 -0
  196. package/src/pages/page-head.ts +766 -0
  197. package/src/pages/version.ts +3 -0
  198. package/src/providers/amp-adapter.ts +1 -1
  199. package/src/providers/codex-adapter.ts +1 -0
  200. package/src/providers/codex-app-server.ts +27 -1
  201. package/src/providers/fx-adapter.ts +1 -1
  202. package/src/providers/pi-mono-extension.ts +30 -15
  203. package/src/runtime-providers/controller-protocol.ts +14 -1
  204. package/src/runtime-providers/controller.ts +5 -0
  205. package/src/runtime-providers/docker.ts +108 -23
  206. package/src/runtime-providers/types.ts +16 -0
  207. package/src/script-workflows/workflow-ctx.ts +8 -0
  208. package/src/scripts-runtime/types/stdlib.d.ts +22 -0
  209. package/src/scripts-runtime/types/swarm-sdk.d.ts +22 -0
  210. package/src/server-user.ts +1 -0
  211. package/src/slack/commands.ts +5 -1
  212. package/src/slack/handlers.ts +37 -2
  213. package/src/slack/left-threads.ts +27 -4
  214. package/src/tasks/follow-up-gate.ts +318 -0
  215. package/src/tasks/follow-up-redelegation-guard.ts +155 -0
  216. package/src/tasks/worker-follow-up.ts +21 -1
  217. package/src/tests/claude-artifacts-read-script.test.ts +209 -0
  218. package/src/tests/codex-app-server.test.ts +59 -0
  219. package/src/tests/codex-gateway-route.test.ts +50 -1
  220. package/src/tests/create-page-tool.test.ts +501 -0
  221. package/src/tests/db-client-busy-retry.test.ts +81 -0
  222. package/src/tests/db-client.test.ts +21 -0
  223. package/src/tests/db-task-read-contract.test.ts +9 -2
  224. package/src/tests/db-task-write-contract.test.ts +7 -1
  225. package/src/tests/db-tasks-read.test.ts +21 -0
  226. package/src/tests/entrypoint-api-readiness.test.ts +416 -210
  227. package/src/tests/explicit-model-validation.test.ts +14 -8
  228. package/src/tests/fixtures/pages/head-v1.html +56 -0
  229. package/src/tests/follow-up-gate-shadow.test.ts +513 -0
  230. package/src/tests/follow-up-redelivery-guard.test.ts +432 -202
  231. package/src/tests/identity-field-budget.test.ts +1 -1
  232. package/src/tests/internal-ai/credentials.test.ts +62 -0
  233. package/src/tests/internal-ai/task-title.test.ts +283 -0
  234. package/src/tests/list-total-cache.test.ts +27 -2
  235. package/src/tests/memory-fts-populate.test.ts +11 -9
  236. package/src/tests/memory-retrieval-purge.test.ts +80 -0
  237. package/src/tests/merge-gate-detect-changes.test.ts +145 -0
  238. package/src/tests/migration-260-runtime-pool-cache.test.ts +118 -0
  239. package/src/tests/page-proxy.test.ts +1 -1
  240. package/src/tests/page-session-dev-cookie.test.ts +188 -0
  241. package/src/tests/pages-frame-ancestors.test.ts +28 -1
  242. package/src/tests/pages-head-v2.test.ts +534 -0
  243. package/src/tests/pages-http.test.ts +330 -0
  244. package/src/tests/pages-public-html.test.ts +43 -0
  245. package/src/tests/pages-storage.test.ts +182 -0
  246. package/src/tests/pages-versioning.test.ts +72 -0
  247. package/src/tests/profile-sync-stale-writeback.test.ts +334 -0
  248. package/src/tests/profile-sync.test.ts +11 -15
  249. package/src/tests/query-audit-plans.test.ts +27 -0
  250. package/src/tests/runtime-cache-cleanup.test.ts +367 -0
  251. package/src/tests/runtime-e2b-pools.test.ts +41 -0
  252. package/src/tests/runtime-pools-db.test.ts +98 -0
  253. package/src/tests/runtime-pools-http.test.ts +29 -0
  254. package/src/tests/runtime-providers-docker.test.ts +253 -0
  255. package/src/tests/runtime-remote-controller.test.ts +66 -2
  256. package/src/tests/runtime-supervisor.test.ts +66 -2
  257. package/src/tests/scheduled-task-required-params-migration.test.ts +3 -1
  258. package/src/tests/script-connections.test.ts +13 -0
  259. package/src/tests/script-runs-http.test.ts +684 -2
  260. package/src/tests/seed-registry.test.ts +28 -6
  261. package/src/tests/seed-schedules.test.ts +61 -8
  262. package/src/tests/seed-scripts.test.ts +168 -2
  263. package/src/tests/slack-leave-thread.test.ts +145 -5
  264. package/src/tests/slack-render-v2-query-plans.test.ts +24 -2
  265. package/src/tests/status.test.ts +11 -0
  266. package/src/tests/swarm-diff.test.ts +121 -2
  267. package/src/tests/task-auto-title-config-guard.test.ts +37 -0
  268. package/src/tests/task-auto-title-listener.test.ts +165 -0
  269. package/src/tests/tasks-readable-row.test.ts +155 -0
  270. package/src/tests/theme-import.test.ts +758 -0
  271. package/src/tests/themes-page-theme.test.ts +155 -0
  272. package/src/tests/themes-presets.test.ts +172 -0
  273. package/src/tests/workflow-script-principal.test.ts +2 -1
  274. package/src/themes/classics.ts +949 -0
  275. package/src/themes/import.ts +420 -0
  276. package/src/themes/page-fields.ts +40 -0
  277. package/src/themes/page-theme.ts +102 -0
  278. package/src/themes/presets.ts +561 -0
  279. package/src/themes/tokens.ts +218 -0
  280. package/src/tools/app-upsert.ts +1 -1
  281. package/src/tools/create-page.ts +118 -12
  282. package/src/tools/send-task.ts +35 -47
  283. package/src/tools/slack-leave-thread.ts +1 -1
  284. package/src/tools/task-dedup.ts +45 -21
  285. package/src/tools/utils.ts +33 -0
  286. package/src/types.ts +83 -3
  287. package/src/utils/constants.ts +19 -0
  288. package/src/utils/follow-up-gate-schedule-ids.ts +13 -0
  289. package/src/utils/internal-ai/credentials.ts +146 -93
  290. package/src/utils/internal-ai/index.ts +10 -0
  291. package/src/utils/internal-ai/models.ts +18 -0
  292. package/src/utils/internal-ai/task-title.ts +282 -0
  293. package/src/utils/page-session.ts +37 -0
  294. package/templates/schedules/daily-status-report/content.md +88 -2
  295. package/templates/schedules/daily-swarm-update-check/content.md +16 -40
  296. package/templates/skills/apps/SKILL.md +1 -1
  297. package/templates/skills/apps/content.md +1 -1
  298. package/templates/skills/composio-google-docs/content.md +10 -0
  299. package/templates/skills/heartbeat-runbook/content.md +1 -1
  300. package/templates/skills/pages/config.json +1 -1
  301. package/templates/skills/pages/content.md +539 -86
  302. package/templates/skills/pages/files/examples/annotated-pr.html +21 -12
  303. package/templates/skills/pages/files/examples/multiplayer-board.html +41 -41
  304. package/templates/skills/pages/files/examples/report-page.html +125 -124
  305. package/templates/skills/script-workflows/content.md +19 -1
  306. package/templates/skills/swarm-docs/content.md +1 -0
  307. package/templates/skills/swarm-scripts/SKILL.md +2 -0
  308. package/templates/skills/swarm-scripts/content.md +2 -0
  309. package/dist/cli-2pnswgh8.js +0 -189
  310. package/dist/cli-dznrgaep.js +0 -30
  311. package/dist/cli-w3hcrxht.js +0 -6985
  312. package/dist/handlers-vwmba1m2.js +0 -31
  313. package/dist/index-5r71rf98.js +0 -32
  314. package/dist/index-anmwxs87.js +0 -29
  315. package/dist/index-spp0ra5c.js +0 -36
  316. package/dist/lead-fjy418cw.js +0 -69
  317. package/dist/worker-0c5tyzqe.js +0 -69
  318. package/templates/schedules/daily-workflow-health-audit/config.json +0 -19
  319. package/templates/schedules/daily-workflow-health-audit/content.md +0 -191
@@ -1,9 +1,17 @@
1
1
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
2
  import { unlink } from "node:fs/promises";
3
- import { closeDb, getScheduledTasks, initDb, listWorkflows } from "../be/db";
3
+ import {
4
+ closeDb,
5
+ getScheduledTaskByName,
6
+ getScheduledTasks,
7
+ initDb,
8
+ listWorkflows,
9
+ } from "../be/db";
10
+ import { getScript } from "../be/scripts/db";
4
11
  import { runSeeder, SEEDERS } from "../be/seed";
5
12
  import { schedulesSeeder } from "../be/seed/schedules-seeder";
6
13
  import { workflowsSeeder } from "../be/seed/workflows-seeder";
14
+ import { scriptsSeeder } from "../be/seed-scripts";
7
15
 
8
16
  const TEST_DB_PATH = "./test-seed-registry.sqlite";
9
17
 
@@ -58,15 +66,29 @@ describe("seeder registry", () => {
58
66
  }
59
67
  });
60
68
 
61
- test("seeds all 23 automation templates on a fresh database and re-runs as a no-op", async () => {
69
+ test("seeds all 22 automation templates on a fresh database and re-runs as a no-op", async () => {
62
70
  expect(await listWorkflows()).toHaveLength(0);
63
71
  expect(await getScheduledTasks()).toHaveLength(0);
64
72
 
73
+ // Boot order: scripts seed before schedules, so a script-target schedule
74
+ // (daily-swarm-update-check) resolves its catalog script on a fresh install.
75
+ const order = SEEDERS.map((seeder) => seeder.kind);
76
+ expect(order.indexOf("script")).toBeLessThan(order.indexOf("schedule"));
77
+ expect(await getScript({ name: "swarm-update-check", scope: "global" })).toBeNull();
78
+ const scriptResult = await runSeeder(scriptsSeeder, {
79
+ quiet: true,
80
+ scriptEmbeddingMode: "skip",
81
+ });
65
82
  const workflowResult = await runSeeder(workflowsSeeder, { quiet: true });
66
83
  const scheduleResult = await runSeeder(schedulesSeeder, { quiet: true });
67
84
 
85
+ expect(scriptResult.failed).toEqual([]);
68
86
  expect(workflowResult).toMatchObject({ created: 11, failed: [] });
69
- expect(scheduleResult).toMatchObject({ created: 12, failed: [] });
87
+ expect(scheduleResult).toMatchObject({ created: 11, failed: [] });
88
+ expect(await getScheduledTaskByName("daily-swarm-update-check")).toMatchObject({
89
+ targetType: "script",
90
+ scriptName: "swarm-update-check",
91
+ });
70
92
  expect((await listWorkflows()).map((workflow) => workflow.name).sort()).toEqual(
71
93
  workflowsSeeder.items().map((item) => item.key),
72
94
  );
@@ -83,10 +105,10 @@ describe("seeder registry", () => {
83
105
  expect(await runSeeder(schedulesSeeder, { quiet: true })).toMatchObject({
84
106
  created: 0,
85
107
  updated: 0,
86
- skippedUnchanged: 12,
108
+ skippedUnchanged: 11,
87
109
  failed: [],
88
110
  });
89
111
  expect(await listWorkflows()).toHaveLength(11);
90
- expect(await getScheduledTasks()).toHaveLength(12);
91
- });
112
+ expect(await getScheduledTasks()).toHaveLength(11);
113
+ }, 60_000); // typechecks every catalog script with tsc
92
114
  });
@@ -8,6 +8,7 @@ import {
8
8
  initDb,
9
9
  updateScheduledTask,
10
10
  } from "../be/db";
11
+ import { upsertScriptByName } from "../be/scripts/db";
11
12
  import { runSeeder } from "../be/seed";
12
13
  import {
13
14
  createSchedulesSeeder,
@@ -15,6 +16,7 @@ import {
15
16
  type ScheduleTemplateSource,
16
17
  } from "../be/seed/schedules-seeder";
17
18
  import { createWorkflowsSeeder } from "../be/seed/workflows-seeder";
19
+ import { SEED_SCRIPTS } from "../be/seed-scripts";
18
20
 
19
21
  const TEST_DB_PATH = "./test-seed-schedules.sqlite";
20
22
 
@@ -95,6 +97,23 @@ function targetScheduleSource(target: Record<string, unknown>, prompt?: string)
95
97
  } satisfies ScheduleTemplateSource;
96
98
  }
97
99
 
100
+ /** Stand-in for the catalog script daily-swarm-update-check targets; boot seeds the real one first. */
101
+ async function seedUpdateCheckScript() {
102
+ await upsertScriptByName({
103
+ name: "swarm-update-check",
104
+ scope: "global",
105
+ scopeId: null,
106
+ source: "export default async function main() { return {}; }",
107
+ description: "test stand-in",
108
+ intent: "seed-schedules test fixture",
109
+ signatureJson: "{}",
110
+ fsMode: "none",
111
+ agentId: null,
112
+ isScratch: false,
113
+ typeChecked: true,
114
+ });
115
+ }
116
+
98
117
  async function makeWorkflow(name: string) {
99
118
  return createWorkflow({ name, definition: { nodes: [] } });
100
119
  }
@@ -118,9 +137,9 @@ afterEach(async () => {
118
137
  });
119
138
 
120
139
  describe("schedules seeder", () => {
121
- test("loads all twelve schedule templates, including the unfenced schedule block", () => {
140
+ test("loads all eleven schedule templates, including the unfenced schedule block", () => {
122
141
  const schedules = loadSeedSchedules();
123
- expect(schedules).toHaveLength(12);
142
+ expect(schedules).toHaveLength(11);
124
143
  // Zero-config, requires:[], placeholders:[], template enabled:true -> auto-enabled
125
144
  // under the default (unset -> on) SEED_AUTOMATIONS_ENABLED switch.
126
145
  expect(schedules.find((schedule) => schedule.name === "daily-status-report")).toMatchObject({
@@ -135,6 +154,11 @@ describe("schedules seeder", () => {
135
154
  enabled: true,
136
155
  requiredParams: [],
137
156
  requires: [],
157
+ // A script fire: no agent task, no prompt.
158
+ targetType: "script",
159
+ scriptName: "swarm-update-check",
160
+ scriptArgs: {},
161
+ taskTemplate: "",
138
162
  });
139
163
  expect(
140
164
  schedules.find((schedule) => schedule.name === "weekly-harness-upgrade-check"),
@@ -149,9 +173,10 @@ describe("schedules seeder", () => {
149
173
  "daily-blocker-digest",
150
174
  "daily-status-report",
151
175
  "daily-swarm-update-check",
152
- "daily-workflow-health-audit",
153
176
  "dreaming-daily",
154
177
  ]);
178
+ // The status report absorbed the workflow-health audit's six queries.
179
+ expect(schedules.map((schedule) => schedule.name)).not.toContain("daily-workflow-health-audit");
155
180
  // dreaming replaces Daily Evolution, which seeds off even though it is zero-config.
156
181
  expect(
157
182
  schedules.find((schedule) => schedule.name === "daily-compounding-reflection"),
@@ -160,20 +185,22 @@ describe("schedules seeder", () => {
160
185
 
161
186
  test("SEED_AUTOMATIONS_ENABLED=false still seeds all schedules disabled", async () => {
162
187
  process.env.SEED_AUTOMATIONS_ENABLED = "false";
163
- // dreaming-daily targets the seeded dreaming workflow.
188
+ // dreaming-daily targets the seeded dreaming workflow, daily-swarm-update-check a catalog script.
164
189
  await runSeeder(createWorkflowsSeeder(), { quiet: true });
190
+ await seedUpdateCheckScript();
165
191
  const result = await runSeeder(createSchedulesSeeder(), { quiet: true });
166
- expect(result).toMatchObject({ created: 12, failed: [] });
192
+ expect(result).toMatchObject({ created: 11, failed: [] });
167
193
  const schedules = await getScheduledTasks();
168
- expect(schedules).toHaveLength(12);
194
+ expect(schedules).toHaveLength(11);
169
195
  expect(schedules.every((schedule) => schedule.enabled === false)).toBe(true);
170
196
  });
171
197
 
172
198
  test("seeds a disabled schedule whose timezone still needs setup", async () => {
173
- // dreaming-daily targets the seeded dreaming workflow.
199
+ // dreaming-daily targets the seeded dreaming workflow, daily-swarm-update-check a catalog script.
174
200
  await runSeeder(createWorkflowsSeeder(), { quiet: true });
201
+ await seedUpdateCheckScript();
175
202
  const result = await runSeeder(createSchedulesSeeder(), { quiet: true });
176
- expect(result).toMatchObject({ created: 12, failed: [] });
203
+ expect(result).toMatchObject({ created: 11, failed: [] });
177
204
  expect(await getScheduledTaskByName("weekly-dependabot-triage")).toMatchObject({
178
205
  cronExpression: "40 3 * * 0",
179
206
  timezone: "{{TIMEZONE}}",
@@ -313,6 +340,32 @@ describe("schedules seeder", () => {
313
340
  expect(result.failed[0]?.error).toContain('Global script "no-such-script" not found');
314
341
  });
315
342
 
343
+ test("daily-swarm-update-check runs the catalog's swarm-update-check script, no agent task", async () => {
344
+ // The scripts catalog ships the target, so boot's script seeder creates it first.
345
+ expect(SEED_SCRIPTS.map((script) => script.name)).toContain("swarm-update-check");
346
+ const updateCheck = loadSeedSchedules().filter(
347
+ (schedule) => schedule.name === "daily-swarm-update-check",
348
+ );
349
+ expect(updateCheck).toHaveLength(1);
350
+
351
+ // Control: without the script the item fails instead of seeding an agent task.
352
+ const seeder = createSchedulesSeeder();
353
+ const before = await runSeeder(seeder, { quiet: true });
354
+ expect(before.failed.map((failure) => failure.key)).toContain("daily-swarm-update-check");
355
+ expect(await getScheduledTaskByName("daily-swarm-update-check")).toBeNull();
356
+
357
+ await seedUpdateCheckScript();
358
+ await runSeeder(seeder, { quiet: true });
359
+ const row = await getScheduledTaskByName("daily-swarm-update-check");
360
+ expect(row).toMatchObject({
361
+ targetType: "script",
362
+ scriptName: "swarm-update-check",
363
+ scriptArgs: {},
364
+ enabled: true,
365
+ });
366
+ expect(row?.taskTemplate).toBeUndefined();
367
+ });
368
+
316
369
  test("fails a workflow target whose workflow does not exist, without creating a row", async () => {
317
370
  const result = await runSeeder(
318
371
  createSchedulesSeeder([
@@ -20,6 +20,10 @@ import opsCatalogAudit, {
20
20
  renderPage as renderOpsCatalogAuditPage,
21
21
  } from "../be/seed-scripts/catalog/ops-catalog-audit";
22
22
  import scheduleHealth from "../be/seed-scripts/catalog/schedule-health";
23
+ import swarmUpdateCheck, {
24
+ compareVersions,
25
+ parseVersion,
26
+ } from "../be/seed-scripts/catalog/swarm-update-check";
23
27
  import taskContextGathering from "../be/seed-scripts/catalog/task-context-gathering";
24
28
  import taskFailureAudit from "../be/seed-scripts/catalog/task-failure-audit";
25
29
  import { extractScriptSignature } from "../scripts-runtime/extract-signature";
@@ -62,8 +66,8 @@ afterAll(async () => {
62
66
  });
63
67
 
64
68
  describe("seed-scripts catalog", () => {
65
- test("manifest holds 39 unique, well-described scripts", () => {
66
- expect(SEED_SCRIPTS.length).toBe(39);
69
+ test("manifest holds 40 unique, well-described scripts", () => {
70
+ expect(SEED_SCRIPTS.length).toBe(40);
67
71
  const names = SEED_SCRIPTS.map((s) => s.name);
68
72
  expect(new Set(names).size).toBe(names.length);
69
73
  for (const s of SEED_SCRIPTS) {
@@ -217,6 +221,32 @@ describe("seed-scripts catalog", () => {
217
221
  ).toHaveLength(200);
218
222
  });
219
223
 
224
+ test.each([
225
+ ["Incomplete response returned, reason: max_output_tokens", "output-limit"],
226
+ ["Stopped: max_tokens", "output-limit"],
227
+ ["Maximum output length exceeded", "output-limit"],
228
+ ["Output token limit reached", "output-limit"],
229
+ ["401 Unauthorized", "auth/credentials"],
230
+ ["invalid token", "auth/credentials"],
231
+ ["Resource not accessible by personal access token", "auth/credentials"],
232
+ ["Processed 100 tokens", "other"],
233
+ ["max_input_tokens exceeded", "other"],
234
+ ])("task-failure-audit classifies %s as %s", async (failureReason, expectedKey) => {
235
+ const result = await taskFailureAudit(
236
+ { publishPage: false },
237
+ {
238
+ swarm: {
239
+ async task_list() {
240
+ return { data: { tasks: [{ id: "failed-task", failureReason }] } };
241
+ },
242
+ },
243
+ },
244
+ );
245
+
246
+ expect(result.groups).toHaveLength(1);
247
+ expect(result.groups[0].key).toBe(expectedKey);
248
+ });
249
+
220
250
  test("task-failure-audit projects failure reasons into schedule-group samples", async () => {
221
251
  let projectionArgs: Record<string, unknown> | undefined;
222
252
  const result = await taskFailureAudit(
@@ -1974,3 +2004,139 @@ describe("dreaming-heartbeat check refuses a worker-chosen draftPath", () => {
1974
2004
  expect(out.reasons.join(" ")).not.toContain("draftPath");
1975
2005
  });
1976
2006
  });
2007
+
2008
+ describe("swarm-update-check", () => {
2009
+ const RELEASES = "https://api.github.com/repos/desplega-ai/agent-swarm/releases/latest";
2010
+
2011
+ /** Fake API + GitHub: `/health` answers `running`, the release endpoint `release`. */
2012
+ function fakeCtx(opts: {
2013
+ running?: string | null;
2014
+ release?: { status: number; body?: unknown } | "network-error";
2015
+ agents?: unknown[];
2016
+ }) {
2017
+ const fetches: { url: string; retries?: number }[] = [];
2018
+ const sent: any[] = [];
2019
+ const stdlib = {
2020
+ Redacted: { value: (v: unknown) => v },
2021
+ fetch: async (url: string, init: { retries?: number } = {}) => {
2022
+ fetches.push({ url, retries: init.retries });
2023
+ if (url === "http://api.test/health") {
2024
+ return new Response(
2025
+ JSON.stringify(opts.running === null ? {} : { version: opts.running }),
2026
+ );
2027
+ }
2028
+ if (url === RELEASES) {
2029
+ const release = opts.release ?? { status: 200, body: {} };
2030
+ if (release === "network-error") throw new Error("getaddrinfo ENOTFOUND");
2031
+ return new Response(JSON.stringify(release.body ?? {}), { status: release.status });
2032
+ }
2033
+ return new Response("{}", { status: 404 });
2034
+ },
2035
+ };
2036
+ const swarm = {
2037
+ config: { mcpBaseUrl: "http://api.test/" },
2038
+ async swarm_get() {
2039
+ return {
2040
+ success: true,
2041
+ status: 200,
2042
+ data: {
2043
+ agents: opts.agents ?? [
2044
+ { id: "worker-1", isLead: false },
2045
+ { id: "lead-1", isLead: true },
2046
+ ],
2047
+ },
2048
+ };
2049
+ },
2050
+ async task_send(args: unknown) {
2051
+ sent.push(args);
2052
+ return { success: true, status: 200, data: { success: true, task: { id: "notify-1" } } };
2053
+ },
2054
+ };
2055
+ return { ctx: { stdlib, swarm }, fetches, sent };
2056
+ }
2057
+
2058
+ const release = (tag: string) => ({
2059
+ status: 200,
2060
+ body: {
2061
+ tag_name: tag,
2062
+ html_url: `https://github.com/desplega-ai/agent-swarm/releases/tag/${tag}`,
2063
+ },
2064
+ });
2065
+
2066
+ test("compares versions numerically, never as strings", () => {
2067
+ expect(compareVersions(parseVersion("1.10.0")!, parseVersion("1.9.0")!)).toBeGreaterThan(0);
2068
+ expect(compareVersions(parseVersion("v2.0.0")!, parseVersion("2.0.0")!)).toBe(0);
2069
+ expect(parseVersion("1.2")).toBeNull();
2070
+ expect(parseVersion(undefined)).toBeNull();
2071
+ });
2072
+
2073
+ test("a newer release notifies the lead once, with the versions and release URL", async () => {
2074
+ const { ctx, sent } = fakeCtx({ running: "1.9.0", release: release("v1.10.0") });
2075
+ const result: any = await swarmUpdateCheck({}, ctx);
2076
+ expect(result).toMatchObject({
2077
+ checked: true,
2078
+ status: "update-available",
2079
+ running: "1.9.0",
2080
+ latest: "1.10.0",
2081
+ notified: true,
2082
+ leadTaskId: "notify-1",
2083
+ });
2084
+ expect(sent).toHaveLength(1);
2085
+ expect(sent[0]).toMatchObject({ agentId: "lead-1", routingReason: "skill" });
2086
+ expect(sent[0].task).toContain("running 1.9.0, latest 1.10.0");
2087
+ expect(sent[0].task).toContain(
2088
+ "https://github.com/desplega-ai/agent-swarm/releases/tag/v1.10.0",
2089
+ );
2090
+ expect(sent[0].task).toContain("admin delivery channel");
2091
+ });
2092
+
2093
+ test("control: up to date and older-than-running releases notify nobody", async () => {
2094
+ for (const tag of ["v1.10.0", "v1.9.9"]) {
2095
+ const { ctx, sent } = fakeCtx({ running: "1.10.0", release: release(tag) });
2096
+ const result: any = await swarmUpdateCheck({}, ctx);
2097
+ expect(result).toMatchObject({ checked: true, status: "up-to-date" });
2098
+ expect(sent).toHaveLength(0);
2099
+ }
2100
+ });
2101
+
2102
+ test("a missing running version is unknown, not up to date, and skips GitHub", async () => {
2103
+ const { ctx, sent, fetches } = fakeCtx({ running: null, release: release("v9.0.0") });
2104
+ const result: any = await swarmUpdateCheck({}, ctx);
2105
+ expect(result).toMatchObject({ checked: false, status: "unknown" });
2106
+ expect(result.message).toContain("Unable to determine running version");
2107
+ expect(fetches.map((f) => f.url)).not.toContain(RELEASES);
2108
+ expect(sent).toHaveLength(0);
2109
+ });
2110
+
2111
+ test("a failed GitHub call is reported after one attempt and notifies nobody", async () => {
2112
+ for (const failure of [
2113
+ { status: 403, body: { message: "rate limited" } },
2114
+ "network-error" as const,
2115
+ ]) {
2116
+ const { ctx, sent, fetches } = fakeCtx({ running: "1.0.0", release: failure });
2117
+ const result: any = await swarmUpdateCheck({}, ctx);
2118
+ expect(result).toMatchObject({ checked: false, status: "github-unreachable" });
2119
+ expect(result.message).toContain("Could not reach GitHub releases API");
2120
+ const github = fetches.filter((f) => f.url === RELEASES);
2121
+ expect(github).toHaveLength(1);
2122
+ expect(github[0]?.retries).toBe(1);
2123
+ expect(sent).toHaveLength(0);
2124
+ }
2125
+ });
2126
+
2127
+ test("dryRun reports the update without notifying", async () => {
2128
+ const { ctx, sent } = fakeCtx({ running: "1.0.0", release: release("v1.0.1") });
2129
+ const result: any = await swarmUpdateCheck({ dryRun: true }, ctx);
2130
+ expect(result).toMatchObject({ status: "update-available", notified: false });
2131
+ expect(sent).toHaveLength(0);
2132
+ });
2133
+
2134
+ test("an update with no lead to notify fails the run", async () => {
2135
+ const { ctx } = fakeCtx({
2136
+ running: "1.0.0",
2137
+ release: release("v1.0.1"),
2138
+ agents: [{ id: "worker-1", isLead: false }],
2139
+ });
2140
+ await expect(swarmUpdateCheck({}, ctx)).rejects.toThrow("no lead to notify");
2141
+ });
2142
+ });
@@ -9,6 +9,7 @@ import {
9
9
  createTaskExtended,
10
10
  createWorkflow,
11
11
  deleteSwarmConfig,
12
+ getKv,
12
13
  getSlackTasksInThread,
13
14
  initDb,
14
15
  updateWorkflow,
@@ -19,9 +20,14 @@ import { invokeToolInProcess, resetBridgeServerForTesting } from "../http/mcp-br
19
20
  import { handleTasks } from "../http/tasks";
20
21
  import { reservedNamespaceError } from "../kv-reserved-namespaces";
21
22
  import { createServer as createMcpServer } from "../server";
23
+ import { registerCommandHandler } from "../slack/commands";
22
24
  import { registerMessageHandler, resetSlackHandlerCachesForTesting } from "../slack/handlers";
23
- import { isSlackThreadLeft, SLACK_LEFT_THREAD_KV_NAMESPACE } from "../slack/left-threads";
24
- import { bufferThreadMessage, instantFlush } from "../slack/thread-buffer";
25
+ import {
26
+ isSlackThreadLeft,
27
+ SLACK_LEFT_THREAD_CONFIRMATION,
28
+ SLACK_LEFT_THREAD_KV_NAMESPACE,
29
+ } from "../slack/left-threads";
30
+ import { bufferThreadMessage, getBufferMessageCount, instantFlush } from "../slack/thread-buffer";
25
31
  import { slackContextKey } from "../tasks/context-key";
26
32
  import { registerSlackLeaveThreadTool } from "../tools/slack-leave-thread";
27
33
  import type { Agent, AgentTask } from "../types";
@@ -45,6 +51,8 @@ const previousEnv = {
45
51
 
46
52
  type Handler = (args: Record<string, unknown>) => Promise<void>;
47
53
  const handlers = new Map<string, Handler>();
54
+ const commands = new Map<string, Handler>();
55
+ const ephemerals: Array<Record<string, unknown>> = [];
48
56
 
49
57
  const client = {
50
58
  auth: { test: async () => ({ user_id: BOT_USER_ID, bot_id: "B_LEAVE_BOT" }) },
@@ -52,7 +60,13 @@ const client = {
52
60
  info: async () => ({ ok: true, channel: {} }),
53
61
  replies: async () => ({ ok: true, messages: [] }),
54
62
  },
55
- chat: { postMessage: async () => ({ ok: true, ts: "1.1" }) },
63
+ chat: {
64
+ postMessage: async () => ({ ok: true, ts: "1.1" }),
65
+ postEphemeral: async (args: Record<string, unknown>) => {
66
+ ephemerals.push(args);
67
+ return { ok: true };
68
+ },
69
+ },
56
70
  reactions: { add: async () => ({ ok: true }) },
57
71
  users: { info: async () => ({ ok: true, user: { profile: {} } }) },
58
72
  };
@@ -99,10 +113,10 @@ let lead: Agent;
99
113
  let worker: Agent;
100
114
  let otherWorker: Agent;
101
115
 
102
- async function seedThread(channel: string, threadTs: string) {
116
+ async function seedThread(channel: string, threadTs: string, opts: { unassigned?: boolean } = {}) {
103
117
  // As the Slack handlers create it: only Slack ingress writes slackTriggerMessageTs.
104
118
  return createTaskExtended("original ask", {
105
- agentId: worker.id,
119
+ agentId: opts.unassigned ? undefined : worker.id,
106
120
  source: "slack",
107
121
  slackChannelId: channel,
108
122
  slackThreadTs: threadTs,
@@ -158,6 +172,11 @@ beforeAll(async () => {
158
172
  handlers.set(type, handler);
159
173
  },
160
174
  } as never);
175
+ registerCommandHandler({
176
+ command: (name: string, handler: Handler) => {
177
+ commands.set(name, handler);
178
+ },
179
+ } as never);
161
180
 
162
181
  routeServer = createServer(async (req, res) => {
163
182
  setRequestAuth(req, { kind: "operator", fingerprint: "test-key" });
@@ -192,6 +211,7 @@ afterAll(async () => {
192
211
 
193
212
  beforeEach(() => {
194
213
  resetSlackHandlerCachesForTesting();
214
+ ephemerals.length = 0;
195
215
  });
196
216
 
197
217
  describe("slack-leave-thread", () => {
@@ -429,3 +449,123 @@ describe("SLACK_LEAVE_THREAD_DISABLE", () => {
429
449
  }
430
450
  });
431
451
  });
452
+
453
+ describe("!leave", () => {
454
+ test("leaves the thread in code: no task, no workflow event, an ephemeral confirmation", async () => {
455
+ const emitted: unknown[] = [];
456
+ const listener = (payload: unknown) => emitted.push(payload);
457
+ workflowEventBus.on("slack.message", listener);
458
+ try {
459
+ await seedThread("C_BANG_A", "3000.0001");
460
+ expect(await sendThreadReply("C_BANG_A", "3000.0001", "!leave")).toHaveLength(1);
461
+ expect(emitted).toHaveLength(0);
462
+ } finally {
463
+ workflowEventBus.off("slack.message", listener);
464
+ }
465
+
466
+ expect(await isSlackThreadLeft("C_BANG_A", "3000.0001")).toBe(true);
467
+ const entry = await getKv(SLACK_LEFT_THREAD_KV_NAMESPACE, "C_BANG_A:3000.0001");
468
+ expect(entry?.value).toMatchObject({ slackUserId: HUMAN });
469
+ expect(entry?.value).not.toHaveProperty("agentId");
470
+ expect(ephemerals).toEqual([
471
+ {
472
+ channel: "C_BANG_A",
473
+ user: HUMAN,
474
+ thread_ts: "3000.0001",
475
+ text: SLACK_LEFT_THREAD_CONFIRMATION,
476
+ },
477
+ ]);
478
+
479
+ expect(await sendThreadReply("C_BANG_A", "3000.0001", "chatting among ourselves")).toHaveLength(
480
+ 1,
481
+ );
482
+ expect(
483
+ await sendThreadReply("C_BANG_A", "3000.0001", `<@${BOT_USER_ID}> back please`),
484
+ ).toHaveLength(2);
485
+ expect(await isSlackThreadLeft("C_BANG_A", "3000.0001")).toBe(false);
486
+ });
487
+
488
+ test("an @mention before !leave leaves instead of rejoining, also in a left thread", async () => {
489
+ await seedThread("C_BANG_B", "3100.0001");
490
+ await sendThreadReply("C_BANG_B", "3100.0001", "!leave");
491
+ expect(await sendThreadReply("C_BANG_B", "3100.0001", `<@${BOT_USER_ID}> !leave`)).toHaveLength(
492
+ 1,
493
+ );
494
+ expect(await isSlackThreadLeft("C_BANG_B", "3100.0001")).toBe(true);
495
+ expect(ephemerals).toHaveLength(2);
496
+ });
497
+
498
+ test("with ADDITIVE_SLACK the !leave message is never buffered", async () => {
499
+ process.env.ADDITIVE_SLACK = "true";
500
+ try {
501
+ await seedThread("C_BANG_C", "3200.0001");
502
+ await sendThreadReply("C_BANG_C", "3200.0001", "!leave");
503
+ expect(getBufferMessageCount("C_BANG_C:3200.0001")).toBe(0);
504
+ expect(await isSlackThreadLeft("C_BANG_C", "3200.0001")).toBe(true);
505
+ } finally {
506
+ process.env.ADDITIVE_SLACK = "false";
507
+ }
508
+ });
509
+
510
+ test.each([
511
+ ["!leave", "3600.0001"],
512
+ [`<@${BOT_USER_ID}> !leave`, "3600.0002"],
513
+ ])("leaves a human-rooted thread whose only task is unassigned: %s", async (text, threadTs) => {
514
+ const emitted: unknown[] = [];
515
+ const listener = (payload: unknown) => emitted.push(payload);
516
+ workflowEventBus.on("slack.message", listener);
517
+ try {
518
+ const seeded = await seedThread("C_BANG_G", threadTs, { unassigned: true });
519
+ expect(seeded.agentId).toBeNull();
520
+ expect(await sendThreadReply("C_BANG_G", threadTs, text)).toHaveLength(1);
521
+ expect(emitted).toHaveLength(0);
522
+ } finally {
523
+ workflowEventBus.off("slack.message", listener);
524
+ }
525
+ expect(await isSlackThreadLeft("C_BANG_G", threadTs)).toBe(true);
526
+ expect(ephemerals).toHaveLength(1);
527
+ });
528
+
529
+ test("a task in another thread of the channel does not make !leave a command", async () => {
530
+ await seedThread("C_BANG_H", "3700.0001", { unassigned: true });
531
+ await sendThreadReply("C_BANG_H", "3700.0002", "!leave");
532
+ expect(await isSlackThreadLeft("C_BANG_H", "3700.0002")).toBe(false);
533
+ expect(ephemerals).toHaveLength(0);
534
+ });
535
+
536
+ test("is not a command outside a swarm thread, when addressed elsewhere, or with extra text", async () => {
537
+ await sendThreadReply("C_BANG_D", "3300.0001", "!leave");
538
+ expect(await isSlackThreadLeft("C_BANG_D", "3300.0001")).toBe(false);
539
+
540
+ await seedThread("C_BANG_E", "3400.0001");
541
+ await sendThreadReply("C_BANG_E", "3400.0001", "<@U_SOMEONE_ELSE> !leave");
542
+ await sendThreadReply("C_BANG_E", "3400.0001", "!leave it to me");
543
+ expect(await isSlackThreadLeft("C_BANG_E", "3400.0001")).toBe(false);
544
+ expect(ephemerals).toHaveLength(0);
545
+ });
546
+
547
+ test("SLACK_LEAVE_THREAD_DISABLE makes !leave an ordinary message and hides it from help", async () => {
548
+ async function helpText(): Promise<string> {
549
+ let text = "";
550
+ await commands.get("/agent-swarm-help")!({
551
+ ack: async () => {},
552
+ respond: async (reply: { blocks: Array<{ text: { text: string } }> }) => {
553
+ text = reply.blocks.map((b) => b.text.text).join("\n");
554
+ },
555
+ });
556
+ return text;
557
+ }
558
+ expect(await helpText()).toContain("`!leave` in a thread");
559
+
560
+ process.env.SLACK_LEAVE_THREAD_DISABLE = "true";
561
+ try {
562
+ await seedThread("C_BANG_F", "3500.0001");
563
+ expect(await sendThreadReply("C_BANG_F", "3500.0001", "!leave")).toHaveLength(2);
564
+ expect(await isSlackThreadLeft("C_BANG_F", "3500.0001")).toBe(false);
565
+ expect(ephemerals).toHaveLength(0);
566
+ expect(await helpText()).not.toContain("!leave");
567
+ } finally {
568
+ delete process.env.SLACK_LEAVE_THREAD_DISABLE;
569
+ }
570
+ });
571
+ });
@@ -1,3 +1,4 @@
1
+ import { Database } from "bun:sqlite";
1
2
  import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test";
2
3
  import {
3
4
  closeDb,
@@ -6,6 +7,7 @@ import {
6
7
  getDb,
7
8
  getDbClient,
8
9
  getResolvableDeferralOutcomes,
10
+ getSlackTreeMessages,
9
11
  initDb,
10
12
  recordSlackMessage,
11
13
  } from "../be/db";
@@ -93,12 +95,32 @@ describe("slack render-v2 watcher queries", () => {
93
95
  closeDb();
94
96
  });
95
97
 
98
+ test("the tree sweep tests thread tasks in the covering index, not task rows", async () => {
99
+ const sql = await capturedSql(() => getSlackTreeMessages());
100
+ const detail = plan(sql);
101
+ const uses = "SEARCH task USING COVERING INDEX idx_agent_tasks_slack_thread_cover";
102
+ expect(detail.split(uses).length - 1).toBe(3);
103
+ // Control: without migration 264 the same tests read the task rows.
104
+ const copy = Database.deserialize(getDb().serialize());
105
+ try {
106
+ copy.run("DROP INDEX idx_agent_tasks_slack_thread_cover");
107
+ const reverted = copy
108
+ .query<{ detail: string }, []>(`EXPLAIN QUERY PLAN ${sql}`)
109
+ .all()
110
+ .map((row) => row.detail)
111
+ .join("\n");
112
+ expect(reverted).not.toContain(uses);
113
+ } finally {
114
+ copy.close();
115
+ }
116
+ });
117
+
96
118
  test("deferral resolution drives from the chain, not from every task", async () => {
97
119
  const sql = await capturedSql(() => getResolvableDeferralOutcomes());
98
120
  const detail = plan(sql, [50]);
99
- // Either parentTaskId-led index is a seek on the chain's children.
121
+ // Any parentTaskId-led index is a seek on the chain's children.
100
122
  expect(detail).toMatch(
101
- /SEARCH wake USING INDEX idx_agent_tasks_parentTaskId(_source)? \(parentTaskId=\?\)/,
123
+ /SEARCH wake USING INDEX idx_agent_tasks_parentTaskId(_source|_id)? \(parentTaskId=\?\)/,
102
124
  );
103
125
  expect(detail).not.toContain("idx_agent_tasks_taskType");
104
126
  expect(detail).not.toContain("idx_agent_tasks_status_last_updated");
@@ -87,6 +87,8 @@ const ENV_KEYS_TO_RESET = [
87
87
  "SWARM_BRAND_COLOR",
88
88
  "SWARM_MARKETING_URL",
89
89
  "SWARM_HIDE_CLOUD_PROMO",
90
+ "APP_URL",
91
+ "DASHBOARD_URL",
90
92
  "HARNESS_PROVIDER",
91
93
  "ANTHROPIC_API_KEY",
92
94
  "CLAUDE_CODE_OAUTH_TOKEN",
@@ -191,6 +193,7 @@ describe("buildStatusPayload — identity", () => {
191
193
  marketing_url: null,
192
194
  hide_cloud_promo: false,
193
195
  org_id: null,
196
+ dashboard_url: null,
194
197
  });
195
198
  });
196
199
 
@@ -212,9 +215,17 @@ describe("buildStatusPayload — identity", () => {
212
215
  marketing_url: "https://swarm.acme.example",
213
216
  hide_cloud_promo: true,
214
217
  org_id: "org_acme_123",
218
+ dashboard_url: null,
215
219
  });
216
220
  });
217
221
 
222
+ test("reports the configured dashboard URL: the first APP_URL entry, else DASHBOARD_URL", async () => {
223
+ process.env.DASHBOARD_URL = "https://old.example.com";
224
+ expect((await buildStatusPayload()).identity.dashboard_url).toBe("https://old.example.com");
225
+ process.env.APP_URL = "https://swarm.example.com/, https://other.example.com";
226
+ expect((await buildStatusPayload()).identity.dashboard_url).toBe("https://swarm.example.com");
227
+ });
228
+
218
229
  test("treats SWARM_CLOUD=1 the same as 'true'", async () => {
219
230
  process.env.SWARM_CLOUD = "1";
220
231
  const payload = await buildStatusPayload();