@desplega.ai/agent-swarm 2.7.6 → 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 (244) hide show
  1. package/dist/{acp-adapter-bp91d37f.js → acp-adapter-7b4vk3h5.js} +5 -4
  2. package/dist/{actions-yj0eeaqh.js → actions-3tt153af.js} +18 -18
  3. package/dist/{amp-adapter-4pg9h34q.js → amp-adapter-njpts0yr.js} +1 -1
  4. package/dist/{anthropic-messages-x4q6rfq5.js → anthropic-messages-xey2skwx.js} +7 -7
  5. package/dist/{app-909e97cr.js → app-nky4378f.js} +3 -3
  6. package/dist/{assistant-qdyb5fyy.js → assistant-jnb669pa.js} +19 -19
  7. package/dist/{azure-openai-responses-s1w5n70z.js → azure-openai-responses-26t7j9ra.js} +5 -5
  8. package/dist/{boot-reembed-fk0s9z18.js → boot-reembed-9mgvaztt.js} +1 -1
  9. package/dist/{boot-reembed-gs1rzrnh.js → boot-reembed-qs2djdej.js} +1 -1
  10. package/dist/{boot-scrub-sweep-4qfd4kfx.js → boot-scrub-sweep-yjpdj7s1.js} +3 -3
  11. package/dist/{boot-seeds-yr00k13d.js → boot-seeds-5cm6ambd.js} +4 -4
  12. package/dist/{boxd-vdmfssd9.js → boxd-nv507va8.js} +1 -1
  13. package/dist/{claude-adapter-qta1f1xn.js → claude-adapter-fn287zmm.js} +5 -4
  14. package/dist/{claude-managed-setup-sc6j4dm0.js → claude-managed-setup-wtdz2mj2.js} +1 -1
  15. package/dist/{claude-sdk-session-zgrz6yvc.js → claude-sdk-session-x91kqyhm.js} +8288 -8224
  16. package/dist/{cli-wp9sp1qb.js → cli-08bmz9z9.js} +1 -1
  17. package/dist/{cli-yy3rhsdw.js → cli-1c6tzt8g.js} +7 -7
  18. package/dist/{cli-q0k1w5zd.js → cli-35rsp8ye.js} +1 -1
  19. package/dist/{cli-xnrwazav.js → cli-3fg0bdcv.js} +1 -1
  20. package/dist/{cli-89knwags.js → cli-5b456bpf.js} +2 -2
  21. package/dist/{cli-drr5v6av.js → cli-5jnhv72d.js} +1 -1
  22. package/dist/{cli-dxae2yg7.js → cli-5zx699j9.js} +2 -2
  23. package/dist/{cli-0at8w34w.js → cli-6g0hb4cp.js} +8 -8
  24. package/dist/{cli-e3r2neck.js → cli-6pfd54vx.js} +5 -5
  25. package/dist/{cli-bsn8nhrq.js → cli-6vcp76qk.js} +3 -3
  26. package/dist/{cli-ekwks2vs.js → cli-72v799xn.js} +1 -1
  27. package/dist/cli-82j36f2m.js +45 -0
  28. package/dist/{cli-yx4kfngt.js → cli-8305gbq0.js} +29 -16
  29. package/dist/{cli-4necwtsv.js → cli-8sckt74b.js} +8 -8
  30. package/dist/{cli-vvpx3gsx.js → cli-95jzxcp3.js} +1 -1
  31. package/dist/{cli-hr2e2fft.js → cli-adeegh7d.js} +7 -7
  32. package/dist/{cli-m48g6p95.js → cli-afkqkrtw.js} +3 -3
  33. package/dist/{cli-0qatpy8q.js → cli-ayayvfg4.js} +24 -4
  34. package/dist/{cli-bqt1mptt.js → cli-az48n0nr.js} +4 -4
  35. package/dist/{cli-zm72anhg.js → cli-bfv7vdg6.js} +9 -9
  36. package/dist/{cli-hgaazk69.js → cli-caj33vcq.js} +5 -5
  37. package/dist/{cli-3ypk9vk1.js → cli-cb2a8pna.js} +1 -1
  38. package/dist/{cli-waqbxg08.js → cli-cppkf404.js} +17 -17
  39. package/dist/{cli-yn4kxs06.js → cli-ddaanqkd.js} +4 -4
  40. package/dist/{cli-386tyxxg.js → cli-e0kmphd6.js} +2 -2
  41. package/dist/{cli-54t00gt8.js → cli-e46hg0xw.js} +1 -1
  42. package/dist/{cli-jm2vfpfd.js → cli-e5bzrn01.js} +1 -1
  43. package/dist/{cli-436532ck.js → cli-excnnjg2.js} +165 -153
  44. package/dist/{cli-71ptav7x.js → cli-ferkkrg9.js} +1 -1
  45. package/dist/{cli-vpf89f34.js → cli-fmjd0t1y.js} +3 -3
  46. package/dist/{cli-pd7vagab.js → cli-fyt2g4wd.js} +3 -3
  47. package/dist/{cli-gdhpcbhf.js → cli-g08zdaec.js} +1 -1
  48. package/dist/{cli-ehq736cn.js → cli-g7s47cc5.js} +4 -4
  49. package/dist/{cli-svj5090z.js → cli-gz6q4eve.js} +3 -3
  50. package/dist/{cli-8snvgs5r.js → cli-je518txm.js} +10 -10
  51. package/dist/{cli-rmmmrjkd.js → cli-jtjtabxe.js} +1 -1
  52. package/dist/{cli-611a1294.js → cli-k0wwfqbg.js} +18 -1
  53. package/dist/{cli-es6va8jk.js → cli-k1nje3v3.js} +38 -43
  54. package/dist/{cli-p5gp8pcx.js → cli-k8196sh2.js} +6 -6
  55. package/dist/{cli-nwj0ee2w.js → cli-k899ehvh.js} +2 -2
  56. package/dist/{cli-decfb6v8.js → cli-m0da8j35.js} +23 -9
  57. package/dist/{cli-he9wchme.js → cli-mkk9rnzz.js} +4 -4
  58. package/dist/{cli-nhrvpaj7.js → cli-n6bv12jn.js} +510 -4
  59. package/dist/{cli-8bc1w0tw.js → cli-nmzhp1b5.js} +14 -12
  60. package/dist/{cli-0yv5z2pg.js → cli-p82cnnjq.js} +1 -1
  61. package/dist/{cli-c60vetmm.js → cli-pc6n7fre.js} +1 -1
  62. package/dist/{cli-xds5f77d.js → cli-pjctyea3.js} +2 -2
  63. package/dist/{cli-xbbndvjy.js → cli-pq73h5mz.js} +3 -3
  64. package/dist/{cli-aa6yxc7m.js → cli-pz4rk021.js} +1 -1
  65. package/dist/{cli-xnqgpshy.js → cli-q3tkegby.js} +1 -1
  66. package/dist/cli-qghe0h66.js +67 -0
  67. package/dist/{cli-zbbwez0r.js → cli-qh4d4tvk.js} +4 -4
  68. package/dist/{cli-w6dt9ged.js → cli-rtt8g4v9.js} +1 -1
  69. package/dist/{cli-1h3wcpbw.js → cli-snpj5zbj.js} +2 -2
  70. package/dist/{cli-a7v854rf.js → cli-tgrv9cjx.js} +263 -12
  71. package/dist/{cli-faxe6thg.js → cli-vz22fyqv.js} +186 -54
  72. package/dist/{cli-k2gzwsj2.js → cli-wk5yrfwd.js} +160 -26
  73. package/dist/{cli-shpbx5ee.js → cli-ws19768n.js} +1641 -49
  74. package/dist/{cli-70r77n04.js → cli-xae4g51r.js} +1 -1
  75. package/dist/{cli-kqdg1bsb.js → cli-yndmfxb3.js} +2 -2
  76. package/dist/{cli-bjb3z51z.js → cli-yyj3j5m4.js} +2 -2
  77. package/dist/cli.js +21 -21
  78. package/dist/{cloudflare-workers-ai-system-one-bm60fjm0.js → cloudflare-workers-ai-system-one-ef2a980q.js} +2 -2
  79. package/dist/{codex-adapter-ayd1fb6h.js → codex-adapter-cf2yxe9m.js} +3 -2
  80. package/dist/{codex-hook-9efm2x1m.js → codex-hook-a0c6g6t6.js} +1 -1
  81. package/dist/{codex-session-runner-jesa32xc.js → codex-session-runner-6vfzdgbd.js} +3 -2
  82. package/dist/{commands-hanewgf2.js → commands-t3mfy7vn.js} +3 -3
  83. package/dist/{controller-dt1n3c1q.js → controller-q7xrbaz0.js} +3 -3
  84. package/dist/{controllers-connect-xx8x64n5.js → controllers-connect-c6nm70kg.js} +19 -19
  85. package/dist/{cursor-adapter-9s4dwwdw.js → cursor-adapter-ftd81ga8.js} +1 -1
  86. package/dist/{db-3vttcbv4.js → db-dk572x25.js} +3 -3
  87. package/dist/{db-9dv0g1ab.js → db-vemtpmcc.js} +4 -2
  88. package/dist/{dispatcher-tx9hd1sv.js → dispatcher-kh7ecxh9.js} +17 -17
  89. package/dist/{dsh-adapter-007y4ejs.js → dsh-adapter-v8628r89.js} +1 -1
  90. package/dist/{e2b-we6hs6q5.js → e2b-fr12yccx.js} +1 -1
  91. package/dist/{e2b-a19d28rz.js → e2b-x6ywzak3.js} +1 -1
  92. package/dist/{easter-egg-3d-rgbrqb8s.js → easter-egg-3d-nenw4x63.js} +1 -1
  93. package/dist/{embedding-space-ny1e2940.js → embedding-space-0jepb239.js} +6 -6
  94. package/dist/{embeddings-2t386t13.js → embeddings-xxt49yxg.js} +1 -1
  95. package/dist/{execute-pksh0ceh.js → execute-1mhw6e4x.js} +1 -1
  96. package/dist/{external-channel-phgmhmd7.js → external-channel-55v12e60.js} +2 -2
  97. package/dist/{fx-adapter-fy8rr921.js → fx-adapter-bs8wqrk5.js} +6 -5
  98. package/dist/{google-generative-ai-8y3m3k4b.js → google-generative-ai-v94hr7h0.js} +5 -5
  99. package/dist/{google-vertex-w3v98n7b.js → google-vertex-g7vr6cf8.js} +5 -5
  100. package/dist/{grok-adapter-q0abvpaj.js → grok-adapter-gpjqmkcq.js} +6 -5
  101. package/dist/handlers-zxmp1ta4.js +32 -0
  102. package/dist/{heartbeat-kadyek8p.js → heartbeat-k42svnr0.js} +20 -20
  103. package/dist/{hook-9k551xnj.js → hook-114m7hap.js} +4 -3
  104. package/dist/{hook-5zs3msf6.js → hook-6nphweh0.js} +24 -9
  105. package/dist/{http-mhz9vw1f.js → http-r8cc4wz7.js} +1025 -173
  106. package/dist/index-a8phxqnk.js +37 -0
  107. package/dist/{index-vdygbq10.js → index-e3s8wmxy.js} +1 -1
  108. package/dist/{index-q361cg2f.js → index-fpk4bcd9.js} +1 -1
  109. package/dist/index-k6h00t4c.js +30 -0
  110. package/dist/{index-rvr00sg7.js → index-nh3wyqdt.js} +1017 -574
  111. package/dist/index-xrz7te8h.js +33 -0
  112. package/dist/{keepalive-w56f32cg.js → keepalive-x060nt9p.js} +3 -3
  113. package/dist/lead-956sd2yc.js +71 -0
  114. package/dist/{maintenance-7mdgxcea.js → maintenance-2xxfexhf.js} +1 -1
  115. package/dist/{mistral-conversations-4pmwj1c0.js → mistral-conversations-3gjpdb4b.js} +4 -4
  116. package/dist/{oauth-refresh-sweep-29txy9vk.js → oauth-refresh-sweep-fckc4ar5.js} +2 -2
  117. package/dist/{onboard-kpsrkarj.js → onboard-zc48j237.js} +3 -3
  118. package/dist/{openai-codex-responses-44j573nm.js → openai-codex-responses-8pbxq9h5.js} +5 -5
  119. package/dist/{openai-completions-a4gee2a8.js → openai-completions-rm00s912.js} +4 -4
  120. package/dist/{openai-decisions-w4qkrcar.js → openai-decisions-8v957r2r.js} +1 -1
  121. package/dist/{openai-responses-rbp6h5kr.js → openai-responses-agxpf96k.js} +5 -5
  122. package/dist/{opencode-adapter-j25vm3rr.js → opencode-adapter-r690h1cr.js} +4 -3
  123. package/dist/{otel-impl-6kkct7a8.js → otel-impl-r5xbh81q.js} +4 -4
  124. package/dist/{pi-mono-adapter-exhkzhn2.js → pi-mono-adapter-c7v8sdec.js} +7 -6
  125. package/dist/{pricing-refresh-n2x7jh6k.js → pricing-refresh-3pmre11v.js} +3 -3
  126. package/dist/{rbac-roles-81ypx6es.js → rbac-roles-3vbshwsb.js} +1 -1
  127. package/dist/{rbac-roles-b5htkjby.js → rbac-roles-910mmkgz.js} +2 -2
  128. package/dist/{render-v2-qn6jmpk4.js → render-v2-pxykkbab.js} +3 -3
  129. package/dist/{resume-wd30wx23.js → resume-adda0bwy.js} +6 -6
  130. package/dist/{runtime-4hsg7h60.js → runtime-fnbm7stk.js} +1 -1
  131. package/dist/{session-access-5qvfevhk.js → session-access-2fvamrdc.js} +4 -4
  132. package/dist/{session-source-backfill-n1czavbr.js → session-source-backfill-08xvv0sm.js} +2 -2
  133. package/dist/{setup-dnb56qy5.js → setup-1aqmvmh1.js} +1 -1
  134. package/dist/{typesafe-system-one-5mavm259.js → typesafe-system-one-0bmqcxfg.js} +2 -2
  135. package/dist/{virtual-modules-76xxv9fs.js → virtual-modules-d2pjk9j1.js} +5 -5
  136. package/dist/{work-objects-4n21tfkm.js → work-objects-9s0yrm5j.js} +20 -20
  137. package/dist/{worker-follow-up-77hvzxww.js → worker-follow-up-3q78fqmr.js} +17 -17
  138. package/dist/worker-y15hy08h.js +71 -0
  139. package/openapi.json +197 -4
  140. package/package.json +5 -5
  141. package/src/apps/definition.ts +6 -5
  142. package/src/be/db/page-theme-tokens.ts +12 -0
  143. package/src/be/db/tasks/read.ts +9 -1
  144. package/src/be/db/tasks/write.ts +19 -0
  145. package/src/be/db-client.ts +107 -7
  146. package/src/be/db.ts +34 -2
  147. package/src/be/migrations/269_pages_design_system.sql +23 -0
  148. package/src/be/seed/schedules-seeder.ts +0 -10
  149. package/src/be/seed-scripts/catalog/catalog-report.inline.ts +2 -0
  150. package/src/be/seed-scripts/catalog/catalog-report.ts +2 -0
  151. package/src/be/seed-scripts/catalog/memory-eval.ts +2 -0
  152. package/src/be/seed-scripts/catalog/swarm-update-check.ts +150 -0
  153. package/src/be/seed-scripts/index.ts +9 -0
  154. package/src/be/seed-skills/bundled-files.generated.json +3 -3
  155. package/src/be/swarm-config-guard.ts +32 -0
  156. package/src/be/task-auto-title-listener.ts +127 -0
  157. package/src/commands/onboard/agent-fs-version.ts +1 -1
  158. package/src/commands/profile-sync.ts +151 -16
  159. package/src/commands/runner.ts +15 -7
  160. package/src/hooks/hook.ts +20 -9
  161. package/src/http/index.ts +3 -0
  162. package/src/http/pages-public.ts +93 -129
  163. package/src/http/pages.ts +94 -21
  164. package/src/pages/browser-sdk.ts +48 -21
  165. package/src/pages/color-literals.ts +115 -0
  166. package/src/pages/page-head.ts +766 -0
  167. package/src/pages/version.ts +3 -0
  168. package/src/providers/amp-adapter.ts +1 -1
  169. package/src/providers/codex-adapter.ts +1 -0
  170. package/src/providers/codex-app-server.ts +27 -1
  171. package/src/providers/pi-mono-extension.ts +30 -15
  172. package/src/tasks/follow-up-gate.ts +318 -0
  173. package/src/tasks/worker-follow-up.ts +21 -1
  174. package/src/tests/codex-app-server.test.ts +59 -0
  175. package/src/tests/codex-gateway-route.test.ts +50 -1
  176. package/src/tests/create-page-tool.test.ts +501 -0
  177. package/src/tests/db-client-busy-retry.test.ts +81 -0
  178. package/src/tests/db-task-read-contract.test.ts +1 -0
  179. package/src/tests/db-task-write-contract.test.ts +2 -0
  180. package/src/tests/db-tasks-read.test.ts +21 -0
  181. package/src/tests/fixtures/pages/head-v1.html +56 -0
  182. package/src/tests/follow-up-gate-shadow.test.ts +513 -0
  183. package/src/tests/identity-field-budget.test.ts +1 -1
  184. package/src/tests/internal-ai/credentials.test.ts +62 -0
  185. package/src/tests/internal-ai/task-title.test.ts +283 -0
  186. package/src/tests/merge-gate-detect-changes.test.ts +145 -0
  187. package/src/tests/page-proxy.test.ts +1 -1
  188. package/src/tests/page-session-dev-cookie.test.ts +188 -0
  189. package/src/tests/pages-frame-ancestors.test.ts +28 -1
  190. package/src/tests/pages-head-v2.test.ts +534 -0
  191. package/src/tests/pages-http.test.ts +330 -0
  192. package/src/tests/pages-public-html.test.ts +43 -0
  193. package/src/tests/pages-storage.test.ts +182 -0
  194. package/src/tests/pages-versioning.test.ts +72 -0
  195. package/src/tests/profile-sync-stale-writeback.test.ts +334 -0
  196. package/src/tests/profile-sync.test.ts +11 -15
  197. package/src/tests/scheduled-task-required-params-migration.test.ts +3 -1
  198. package/src/tests/script-connections.test.ts +2 -0
  199. package/src/tests/seed-registry.test.ts +28 -6
  200. package/src/tests/seed-schedules.test.ts +61 -8
  201. package/src/tests/seed-scripts.test.ts +142 -2
  202. package/src/tests/swarm-diff.test.ts +121 -2
  203. package/src/tests/task-auto-title-config-guard.test.ts +37 -0
  204. package/src/tests/task-auto-title-listener.test.ts +165 -0
  205. package/src/tests/theme-import.test.ts +758 -0
  206. package/src/tests/themes-page-theme.test.ts +155 -0
  207. package/src/tests/themes-presets.test.ts +172 -0
  208. package/src/tests/workflow-script-principal.test.ts +2 -1
  209. package/src/themes/classics.ts +949 -0
  210. package/src/themes/import.ts +420 -0
  211. package/src/themes/page-fields.ts +40 -0
  212. package/src/themes/page-theme.ts +102 -0
  213. package/src/themes/presets.ts +561 -0
  214. package/src/themes/tokens.ts +218 -0
  215. package/src/tools/app-upsert.ts +1 -1
  216. package/src/tools/create-page.ts +118 -12
  217. package/src/tools/utils.ts +33 -0
  218. package/src/types.ts +45 -3
  219. package/src/utils/constants.ts +19 -0
  220. package/src/utils/follow-up-gate-schedule-ids.ts +13 -0
  221. package/src/utils/internal-ai/credentials.ts +146 -93
  222. package/src/utils/internal-ai/index.ts +10 -0
  223. package/src/utils/internal-ai/models.ts +18 -0
  224. package/src/utils/internal-ai/task-title.ts +282 -0
  225. package/src/utils/page-session.ts +37 -0
  226. package/templates/schedules/daily-status-report/content.md +88 -2
  227. package/templates/schedules/daily-swarm-update-check/content.md +16 -40
  228. package/templates/skills/apps/SKILL.md +1 -1
  229. package/templates/skills/apps/content.md +1 -1
  230. package/templates/skills/heartbeat-runbook/content.md +1 -1
  231. package/templates/skills/pages/config.json +1 -1
  232. package/templates/skills/pages/content.md +539 -86
  233. package/templates/skills/pages/files/examples/annotated-pr.html +21 -12
  234. package/templates/skills/pages/files/examples/multiplayer-board.html +41 -41
  235. package/templates/skills/pages/files/examples/report-page.html +125 -124
  236. package/dist/cli-2pnswgh8.js +0 -189
  237. package/dist/handlers-by3sfxyx.js +0 -32
  238. package/dist/index-4f2pvxae.js +0 -37
  239. package/dist/index-98gz7tmp.js +0 -30
  240. package/dist/index-k08z2cak.js +0 -33
  241. package/dist/lead-enq35nqp.js +0 -70
  242. package/dist/worker-gg1576j3.js +0 -70
  243. package/templates/schedules/daily-workflow-health-audit/config.json +0 -19
  244. package/templates/schedules/daily-workflow-health-audit/content.md +0 -191
@@ -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) {
@@ -2000,3 +2004,139 @@ describe("dreaming-heartbeat check refuses a worker-chosen draftPath", () => {
2000
2004
  expect(out.reasons.join(" ")).not.toContain("draftPath");
2001
2005
  });
2002
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
+ });
@@ -28,6 +28,16 @@ const EXAMPLE_HUNK = {
28
28
  ],
29
29
  };
30
30
 
31
+ /** The rendered diff rows: old-side and new-side gutter numbers plus the row HTML. */
32
+ function diffRows(html: string): { old: string; new: string; html: string }[] {
33
+ return [...html.matchAll(/<tr\b[^>]*>([\s\S]*?)<\/tr>/g)].map(([, row]) => {
34
+ const [oldCell = "", newCell = ""] = [
35
+ ...row!.matchAll(/<td class="swarm-diff-gutter"[^>]*>([^<]*)<\/td>/g),
36
+ ].map(([, cell]) => cell!);
37
+ return { old: oldCell, new: newCell, html: row! };
38
+ });
39
+ }
40
+
31
41
  type StubInstance = {
32
42
  innerHTML: string;
33
43
  textContent: string;
@@ -55,12 +65,15 @@ function makeRig(
55
65
  ): {
56
66
  Ctor: new () => StubInstance;
57
67
  CursorsCtor: new () => StubInstance;
68
+ JumpsCtor: new () => StubInstance;
58
69
  SdkCtor?: new () => { room: (name?: string, options?: unknown) => Promise<unknown> };
59
70
  body: StubInstance;
60
71
  setText: (el: StubInstance, attrs: Record<string, string>, text: string) => void;
61
72
  fireDocumentEvent: (name: string, event: unknown) => void;
62
73
  fireNextFrame: () => void;
63
74
  flushMicrotasks: () => Promise<void>;
75
+ /** Sets what `document.querySelectorAll` returns (the jump list's anchors). */
76
+ setAnchors: (anchors: unknown[]) => void;
64
77
  } {
65
78
  const registry = new Map<string, new () => StubInstance>();
66
79
  const documentListeners = new Map<string, ((event: unknown) => void)[]>();
@@ -143,12 +156,13 @@ function makeRig(
143
156
 
144
157
  // Provide `document` stubs the jump-list path uses.
145
158
  const body = new StubHTMLElement();
159
+ let anchors: unknown[] = [];
146
160
  const doc = {
147
161
  readyState,
148
162
  documentElement: { scrollWidth: 1000, scrollHeight: 500, clientWidth: 1000, clientHeight: 500 },
149
163
  body,
150
164
  createElement: () => new StubHTMLElement(),
151
- querySelectorAll: () => [],
165
+ querySelectorAll: () => anchors,
152
166
  addEventListener: (name: string, handler: (event: unknown) => void) => {
153
167
  const handlers = documentListeners.get(name) ?? [];
154
168
  handlers.push(handler);
@@ -176,7 +190,7 @@ function makeRig(
176
190
  "CustomEvent",
177
191
  "queueMicrotask",
178
192
  "console",
179
- `${SWARM_UI_JS}\nreturn { diff: window.customElements.get('swarm-diff'), cursors: window.customElements.get('swarm-cursors'), sdk: window.SwarmSDK };`,
193
+ `${SWARM_UI_JS}\nreturn { diff: window.customElements.get('swarm-diff'), cursors: window.customElements.get('swarm-cursors'), jumps: window.customElements.get('swarm-diff-jumps'), sdk: window.SwarmSDK };`,
180
194
  );
181
195
  const constructors = factory(
182
196
  win,
@@ -189,16 +203,20 @@ function makeRig(
189
203
  ) as {
190
204
  diff?: new () => StubInstance;
191
205
  cursors?: new () => StubInstance;
206
+ jumps?: new () => StubInstance;
192
207
  sdk?: new () => { room: (name?: string, options?: unknown) => Promise<unknown> };
193
208
  };
194
209
  const Ctor = constructors.diff;
195
210
  if (!Ctor) throw new Error("custom element did not register");
196
211
  const CursorsCtor = constructors.cursors;
197
212
  if (!CursorsCtor) throw new Error("swarm-cursors custom element did not register");
213
+ const JumpsCtor = constructors.jumps;
214
+ if (!JumpsCtor) throw new Error("swarm-diff-jumps custom element did not register");
198
215
 
199
216
  return {
200
217
  Ctor,
201
218
  CursorsCtor,
219
+ JumpsCtor,
202
220
  SdkCtor: constructors.sdk,
203
221
  body,
204
222
  setText(el, attrs, text) {
@@ -220,6 +238,9 @@ function makeRig(
220
238
  // Drain the chained async room-open and render callbacks.
221
239
  for (let i = 0; i < 5; i++) await Promise.resolve();
222
240
  },
241
+ setAnchors(list) {
242
+ anchors = list;
243
+ },
223
244
  };
224
245
  }
225
246
 
@@ -517,6 +538,58 @@ describe("<swarm-diff> render", () => {
517
538
  expect(html).toContain("Avoid raw console.log");
518
539
  });
519
540
 
541
+ test("shows an annotation once, on its new-side line, not on a context row with that old-side number", async () => {
542
+ // Three added lines shift the new side: the context row `});` is old 63,
543
+ // new 66, and the annotation names new line 63.
544
+ const shifted = {
545
+ hunks: [
546
+ {
547
+ old_start: 60,
548
+ old_lines: 4,
549
+ new_start: 60,
550
+ new_lines: 7,
551
+ lines: [
552
+ { type: "context", text: "start();" },
553
+ { type: "add", text: "one();" },
554
+ { type: "add", text: "two();" },
555
+ { type: "add", text: "three();" },
556
+ { type: "context", text: "four();" },
557
+ { type: "context", text: "five();" },
558
+ { type: "context", text: "});" },
559
+ ],
560
+ annotations: [{ line: 63, severity: "warn", text: "Note on new line 63" }],
561
+ },
562
+ ],
563
+ };
564
+ const html = await renderViaStub(JSON.stringify(shifted), { file: "src/a.ts" });
565
+ const rows = diffRows(html);
566
+ expect(rows.find((row) => row.html.includes("});"))).toMatchObject({ old: "63", new: "66" });
567
+ const annotated = rows.filter((row) => row.html.includes("Note on new line 63"));
568
+ expect(annotated.map((row) => [row.old, row.new])).toEqual([["", "63"]]);
569
+ });
570
+
571
+ test("matches a deleted row's annotation on its old-side number", async () => {
572
+ const deleted = {
573
+ hunks: [
574
+ {
575
+ old_start: 10,
576
+ old_lines: 3,
577
+ new_start: 20,
578
+ new_lines: 2,
579
+ lines: [
580
+ { type: "context", text: "keep();" },
581
+ { type: "del", text: "drop();" },
582
+ { type: "context", text: "keepToo();" },
583
+ ],
584
+ annotations: [{ line: 11, severity: "error", text: "Why drop this?" }],
585
+ },
586
+ ],
587
+ };
588
+ const html = await renderViaStub(JSON.stringify(deleted), { file: "src/b.ts" });
589
+ const annotated = diffRows(html).filter((row) => row.html.includes("Why drop this?"));
590
+ expect(annotated.map((row) => [row.old, row.new])).toEqual([["11", ""]]);
591
+ });
592
+
520
593
  test("handles empty/missing JSON body gracefully (no rows, no throw)", async () => {
521
594
  const html = await renderViaStub("", { file: "empty.ts" });
522
595
  // Should still render an outer container with the file name.
@@ -545,6 +618,52 @@ describe("<swarm-diff> render", () => {
545
618
  });
546
619
  });
547
620
 
621
+ describe("<swarm-diff-jumps> labels", () => {
622
+ const hunk = (oldStart: number, newStart: number) => ({
623
+ old_start: oldStart,
624
+ old_lines: 1,
625
+ new_start: newStart,
626
+ new_lines: 1,
627
+ lines: [{ type: "context", text: "x();" }],
628
+ });
629
+
630
+ test("a file with more than one hunk shows each hunk's new-side start line", async () => {
631
+ const rig = makeRig();
632
+ const render = async (file: string, hunks: unknown[]) => {
633
+ const el = new rig.Ctor();
634
+ rig.setText(el, { file }, JSON.stringify({ hunks }));
635
+ el.connectedCallback?.();
636
+ await rig.flushMicrotasks();
637
+ return el;
638
+ };
639
+ // The anchors the jump list finds in the document, built from the rendered HTML.
640
+ const anchorsOf = (diff: StubInstance) =>
641
+ [
642
+ ...diff.innerHTML.matchAll(
643
+ /<a id="([^"]*)" class="swarm-diff-anchor" data-hunk="([^"]*)" data-new-start="([^"]*)"><\/a>/g,
644
+ ),
645
+ ].map(([, id, slug, newStart]) => {
646
+ const attrs: Record<string, string> = { "data-hunk": slug!, "data-new-start": newStart! };
647
+ return { id, getAttribute: (name: string) => attrs[name] ?? null, closest: () => diff };
648
+ });
649
+ const x = await render("src/x.ts", [hunk(10, 58), hunk(100, 120)]);
650
+ const y = await render("src/y.ts", [hunk(5, 5)]);
651
+ rig.setAnchors([...anchorsOf(x), ...anchorsOf(y)]);
652
+
653
+ const jumps = new rig.JumpsCtor();
654
+ jumps.connectedCallback?.();
655
+ await rig.flushMicrotasks();
656
+ const links = [
657
+ ...jumps.innerHTML.matchAll(/<a href="#([^"]*)" target="_self"[^>]*>([^<]*)<\/a>/g),
658
+ ].map(([, href, label]) => [href, label]);
659
+ expect(links).toEqual([
660
+ ["swarm-diff-src-x-ts-10", "src/x.ts:58"],
661
+ ["swarm-diff-src-x-ts-100", "src/x.ts:120"],
662
+ ["swarm-diff-src-y-ts-5", "src/y.ts"],
663
+ ]);
664
+ });
665
+ });
666
+
548
667
  describe("<swarm-diff> parse-order regression (Bug #479-1)", () => {
549
668
  // Real browsers fire connectedCallback when the parser sees the opening
550
669
  // tag, BEFORE the JSON text children are parsed. The element MUST defer
@@ -0,0 +1,37 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { validateConfigValue } from "../be/swarm-config-guard";
3
+
4
+ describe("task auto-title config validation", () => {
5
+ test("TASK_AUTO_TITLE_ENABLED accepts boolean literals only", () => {
6
+ expect(validateConfigValue("TASK_AUTO_TITLE_ENABLED", "true")).toBeNull();
7
+ expect(validateConfigValue("TASK_AUTO_TITLE_ENABLED", "false")).toBeNull();
8
+ expect(validateConfigValue("TASK_AUTO_TITLE_ENABLED", "maybe")).toMatch(
9
+ /TASK_AUTO_TITLE_ENABLED/,
10
+ );
11
+ });
12
+
13
+ test("TASK_AUTO_TITLE_MODEL accepts provider/model-id for a callable provider, a claude alias, or empty", () => {
14
+ for (const value of [
15
+ "openrouter/deepseek/deepseek-v4.1-flash",
16
+ "anthropic/claude-haiku-5-5",
17
+ "openai/gpt-5-nano",
18
+ "openai-codex/gpt-6-luna",
19
+ "haiku",
20
+ "",
21
+ " anthropic/claude-haiku-5-5 ",
22
+ ]) {
23
+ expect(validateConfigValue("TASK_AUTO_TITLE_MODEL", value)).toBeNull();
24
+ }
25
+ });
26
+
27
+ test("TASK_AUTO_TITLE_MODEL rejects unsupported providers and malformed strings", () => {
28
+ expect(validateConfigValue("TASK_AUTO_TITLE_MODEL", "google/gemini-2.5-flash-lite")).toMatch(
29
+ /provider must be/,
30
+ );
31
+ expect(validateConfigValue("TASK_AUTO_TITLE_MODEL", "openai/")).toMatch(/provider\/model-id/);
32
+ expect(validateConfigValue("TASK_AUTO_TITLE_MODEL", "openai/gpt 5")).toMatch(
33
+ /provider\/model-id/,
34
+ );
35
+ expect(validateConfigValue("TASK_AUTO_TITLE_MODEL", 42)).toMatch(/must be a string/);
36
+ });
37
+ });
@@ -0,0 +1,165 @@
1
+ import { afterAll, afterEach, beforeAll, describe, expect, test } from "bun:test";
2
+ import { unlink } from "node:fs/promises";
3
+ import {
4
+ closeDb,
5
+ createAgent,
6
+ createTaskExtended,
7
+ getTaskById,
8
+ initDb,
9
+ updateTaskTitle,
10
+ } from "../be/db";
11
+ import { setTaskTitleIfUnset } from "../be/db/tasks/write";
12
+ import {
13
+ isTaskAutoTitleEnabled,
14
+ startTaskAutoTitleListener,
15
+ type TaskAutoTitleListener,
16
+ } from "../be/task-auto-title-listener";
17
+ import type { generateTaskTitle } from "../utils/internal-ai/task-title";
18
+ import { workflowEventBus } from "../workflows/event-bus";
19
+
20
+ const TEST_DB_PATH = "./test-task-auto-title-listener.sqlite";
21
+
22
+ /**
23
+ * `createTaskExtended` emits `task.created` from an after-commit hook that
24
+ * imports the event bus asynchronously, so the event lands after the call
25
+ * returns. Create the task through this helper to await that emission.
26
+ */
27
+ async function createTaskAndAwaitEvent(task: string): Promise<string> {
28
+ const created = new Promise<void>((resolve) => {
29
+ const handler = () => {
30
+ workflowEventBus.off("task.created", handler);
31
+ resolve();
32
+ };
33
+ workflowEventBus.on("task.created", handler);
34
+ });
35
+ const row = await createTaskExtended(task, { agentId, source: "api" });
36
+ await created;
37
+ return row.id;
38
+ }
39
+ const LONG_TASK =
40
+ "Investigate why the nightly Playwright suite times out on the sessions page after the sidebar redesign and fix the flake.";
41
+
42
+ let agentId: string;
43
+ let listener: TaskAutoTitleListener | null = null;
44
+
45
+ type Generate = typeof generateTaskTitle;
46
+
47
+ /** Generator stub that returns `title` and records every prompt it saw. */
48
+ function generator(title: string | null, seen: string[] = []) {
49
+ const generate: Generate = async (opts) => {
50
+ seen.push(opts.task);
51
+ return title
52
+ ? { ok: true, title, model: "test/model", kind: "openrouter" }
53
+ : { ok: false, reason: "all-models-failed" };
54
+ };
55
+ return { generate, seen };
56
+ }
57
+
58
+ beforeAll(async () => {
59
+ initDb(TEST_DB_PATH);
60
+ agentId = (await createAgent({ name: "auto-title-worker", isLead: false, status: "idle" })).id;
61
+ });
62
+
63
+ afterEach(() => {
64
+ listener?.stop();
65
+ listener = null;
66
+ });
67
+
68
+ afterAll(async () => {
69
+ closeDb();
70
+ for (const suffix of ["", "-wal", "-shm"]) {
71
+ try {
72
+ await unlink(`${TEST_DB_PATH}${suffix}`);
73
+ } catch {}
74
+ }
75
+ });
76
+
77
+ describe("isTaskAutoTitleEnabled", () => {
78
+ test("defaults on and honours false-ish values", () => {
79
+ expect(isTaskAutoTitleEnabled({})).toBe(true);
80
+ expect(isTaskAutoTitleEnabled({ TASK_AUTO_TITLE_ENABLED: "" })).toBe(true);
81
+ expect(isTaskAutoTitleEnabled({ TASK_AUTO_TITLE_ENABLED: "false" })).toBe(false);
82
+ expect(isTaskAutoTitleEnabled({ TASK_AUTO_TITLE_ENABLED: "0" })).toBe(false);
83
+ expect(isTaskAutoTitleEnabled({ TASK_AUTO_TITLE_ENABLED: "true" })).toBe(true);
84
+ });
85
+ });
86
+
87
+ describe("setTaskTitleIfUnset", () => {
88
+ test("writes once and never replaces an existing title", async () => {
89
+ const task = await createTaskExtended(LONG_TASK, { agentId, source: "api" });
90
+ expect(task.title).toBeUndefined();
91
+
92
+ const first = await setTaskTitleIfUnset(task.id, " Generated title ");
93
+ expect(first?.title).toBe("Generated title");
94
+
95
+ const second = await setTaskTitleIfUnset(task.id, "Another title");
96
+ expect(second).toBeNull();
97
+ expect((await getTaskById(task.id))?.title).toBe("Generated title");
98
+
99
+ expect(await setTaskTitleIfUnset(task.id, " ")).toBeNull();
100
+ });
101
+ });
102
+
103
+ describe("startTaskAutoTitleListener", () => {
104
+ test("titles a new task after commit", async () => {
105
+ const { generate, seen } = generator("Fix sessions page Playwright flake");
106
+ listener = startTaskAutoTitleListener({ env: {}, generate });
107
+
108
+ const taskId = await createTaskAndAwaitEvent(LONG_TASK);
109
+ await listener.idle();
110
+
111
+ expect(seen).toEqual([LONG_TASK]);
112
+ expect((await getTaskById(taskId))?.title).toBe("Fix sessions page Playwright flake");
113
+ });
114
+
115
+ test("keeps a title a person set while the model was running", async () => {
116
+ let release: () => void = () => {};
117
+ const gate = new Promise<void>((resolve) => {
118
+ release = resolve;
119
+ });
120
+ const generate: Generate = async () => {
121
+ await gate;
122
+ return { ok: true, title: "Model title", model: "test/model", kind: "openrouter" };
123
+ };
124
+ listener = startTaskAutoTitleListener({ env: {}, generate });
125
+
126
+ const taskId = await createTaskAndAwaitEvent(LONG_TASK);
127
+ await updateTaskTitle(taskId, "Human title");
128
+ release();
129
+ await listener.idle();
130
+
131
+ expect((await getTaskById(taskId))?.title).toBe("Human title");
132
+ });
133
+
134
+ test("leaves the task untouched when generation fails", async () => {
135
+ const { generate, seen } = generator(null);
136
+ listener = startTaskAutoTitleListener({ env: {}, generate });
137
+
138
+ const taskId = await createTaskAndAwaitEvent(LONG_TASK);
139
+ await listener.idle();
140
+
141
+ expect(seen.length).toBe(1);
142
+ expect((await getTaskById(taskId))?.title).toBeUndefined();
143
+ });
144
+
145
+ test("does nothing when TASK_AUTO_TITLE_ENABLED is false", async () => {
146
+ const { generate, seen } = generator("Should not land");
147
+ listener = startTaskAutoTitleListener({ env: { TASK_AUTO_TITLE_ENABLED: "false" }, generate });
148
+
149
+ const taskId = await createTaskAndAwaitEvent(LONG_TASK);
150
+ await listener.idle();
151
+
152
+ expect(seen).toEqual([]);
153
+ expect((await getTaskById(taskId))?.title).toBeUndefined();
154
+ });
155
+
156
+ test("stop() detaches from the event bus", async () => {
157
+ const { generate, seen } = generator("Late title");
158
+ const handle = startTaskAutoTitleListener({ env: {}, generate });
159
+ handle.stop();
160
+
161
+ await createTaskAndAwaitEvent(LONG_TASK);
162
+ await handle.idle();
163
+ expect(seen).toEqual([]);
164
+ });
165
+ });