@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
@@ -0,0 +1,150 @@
1
+ import { z } from "zod";
2
+
3
+ export const argsSchema = z.object({
4
+ dryRun: z
5
+ .boolean()
6
+ .optional()
7
+ .describe("Compute the verdict but do not notify the lead when an update is available"),
8
+ });
9
+
10
+ const RELEASES_URL = "https://api.github.com/repos/desplega-ai/agent-swarm/releases/latest";
11
+
12
+ /** Parse `major.minor.patch` (a leading `v` and a pre-release/build suffix are ignored). */
13
+ export function parseVersion(value: unknown): [number, number, number] | null {
14
+ if (typeof value !== "string") return null;
15
+ const m = value
16
+ .trim()
17
+ .replace(/^v/i, "")
18
+ .match(/^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/);
19
+ if (!m) return null;
20
+ return [Number(m[1]), Number(m[2]), Number(m[3])];
21
+ }
22
+
23
+ /** Numeric compare of two parsed versions: negative, zero or positive. Never a string compare. */
24
+ export function compareVersions(a: [number, number, number], b: [number, number, number]): number {
25
+ for (let i = 0; i < 3; i++) {
26
+ const diff = (a[i] as number) - (b[i] as number);
27
+ if (diff !== 0) return diff;
28
+ }
29
+ return 0;
30
+ }
31
+
32
+ function errorText(error: unknown): string {
33
+ return String((error as any)?.message ?? error).slice(0, 200);
34
+ }
35
+
36
+ /** The version the API server is running, from its own unauthenticated `/health` route. */
37
+ async function runningVersion(ctx: any): Promise<string | null> {
38
+ try {
39
+ const base = ctx.stdlib.Redacted.value(ctx.swarm.config.mcpBaseUrl).replace(/\/+$/, "");
40
+ const res: any = await ctx.stdlib.fetch(base + "/health", { retries: 1, timeoutMs: 10_000 });
41
+ if (!res.ok) return null;
42
+ const body: any = await res.json();
43
+ return typeof body?.version === "string" && body.version.trim() ? body.version.trim() : null;
44
+ } catch {
45
+ return null;
46
+ }
47
+ }
48
+
49
+ async function resolveLeadId(ctx: any): Promise<string | null> {
50
+ const res: any = await ctx.swarm.swarm_get({});
51
+ const data = res?.data ?? res;
52
+ const agents: any[] = Array.isArray(data?.agents) ? data.agents : Array.isArray(data) ? data : [];
53
+ const lead = agents.find((agent) => agent?.isLead === true || agent?.isLead === 1);
54
+ return typeof lead?.id === "string" ? lead.id : null;
55
+ }
56
+
57
+ /**
58
+ * Compare the running agent-swarm version with the latest published GitHub
59
+ * release and, only when a newer release exists, hand the lead one task to
60
+ * tell the operator through its admin delivery channel.
61
+ */
62
+ export default async function swarmUpdateCheck(args: any, ctx: any) {
63
+ const parsed = argsSchema.safeParse(args ?? {});
64
+ if (!parsed.success) return { error: "invalid args: " + parsed.error.message };
65
+ const dryRun = parsed.data.dryRun === true;
66
+
67
+ const running = await runningVersion(ctx);
68
+ const runningParsed = parseVersion(running);
69
+ if (!running || !runningParsed) {
70
+ // Unknown is not "up to date": say so and stop, never guess a version.
71
+ return {
72
+ checked: false,
73
+ status: "unknown",
74
+ running,
75
+ message: `Unable to determine running version: ${running ? `unparseable "${running}"` : "not reported by /health"}.`,
76
+ };
77
+ }
78
+
79
+ let release: any;
80
+ try {
81
+ // One attempt: a failed GitHub call is reported, never retried in a loop.
82
+ const res: any = await ctx.stdlib.fetch(RELEASES_URL, {
83
+ retries: 1,
84
+ timeoutMs: 15_000,
85
+ headers: { Accept: "application/vnd.github+json", "User-Agent": "agent-swarm-update-check" },
86
+ });
87
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
88
+ release = await res.json();
89
+ } catch (error) {
90
+ return {
91
+ checked: false,
92
+ status: "github-unreachable",
93
+ running,
94
+ message: `Could not reach GitHub releases API: ${errorText(error)}`,
95
+ };
96
+ }
97
+
98
+ const latest = typeof release?.tag_name === "string" ? release.tag_name.replace(/^v/i, "") : null;
99
+ const latestParsed = parseVersion(latest);
100
+ if (!latest || !latestParsed) {
101
+ return {
102
+ checked: false,
103
+ status: "github-unreachable",
104
+ running,
105
+ message: `Could not read a version from the latest release tag: ${JSON.stringify(release?.tag_name ?? null)}.`,
106
+ };
107
+ }
108
+ const releaseUrl =
109
+ typeof release?.html_url === "string"
110
+ ? release.html_url
111
+ : `https://github.com/desplega-ai/agent-swarm/releases/tag/v${latest}`;
112
+
113
+ if (compareVersions(latestParsed, runningParsed) <= 0) {
114
+ return {
115
+ checked: true,
116
+ status: "up-to-date",
117
+ running,
118
+ latest,
119
+ message: `Up to date: running ${running}, latest published ${latest}.`,
120
+ };
121
+ }
122
+
123
+ const message = `Update available: running ${running}, latest ${latest} (${releaseUrl})`;
124
+ if (dryRun) {
125
+ return { checked: true, status: "update-available", running, latest, releaseUrl, notified: false, message };
126
+ }
127
+
128
+ const leadId = await resolveLeadId(ctx);
129
+ // A detected update that reaches nobody is a failed run, not a quiet success.
130
+ if (!leadId) throw new Error(`${message}, but the swarm has no lead to notify.`);
131
+ const sent: any = await ctx.swarm.task_send({
132
+ agentId: leadId,
133
+ routingReason: "skill",
134
+ routingNote: "Lead owns the operator's admin delivery channel for release notices.",
135
+ tags: ["swarm-update-check"],
136
+ task: [
137
+ `A newer agent-swarm release is published: running ${running}, latest ${latest}.`,
138
+ `Release: ${releaseUrl}`,
139
+ "",
140
+ "Post one short message to your configured admin delivery channel (the in-app fallback if none is configured) naming the running version, the new version and the release URL. Then complete with that message as the output.",
141
+ "Do not attempt the upgrade yourself: this notice only detects and reports.",
142
+ ].join("\n"),
143
+ });
144
+ const taskId = sent?.data?.task?.id ?? null;
145
+ if (sent?.success === false || sent?.data?.success === false || !taskId) {
146
+ const reason = sent?.data?.message ?? sent?.data?.error ?? `status ${sent?.status}`;
147
+ throw new Error(`${message}, but notifying the lead failed: ${errorText(reason)}`);
148
+ }
149
+ return { checked: true, status: "update-available", running, latest, releaseUrl, notified: true, leadTaskId: taskId, message };
150
+ }
@@ -30,7 +30,11 @@ const REASON_PATTERNS: any[] = [
30
30
  { key: "context-window", re: /context (window|limit|saturat)|peakcontext|compact/i },
31
31
  { key: "reboot-sweep", re: /reboot sweep/i },
32
32
  { key: "not-found", re: /not found|404|missing|no such/i },
33
- { key: "auth/credentials", re: /unauthorized|401|403|credential|token|forbidden/i },
33
+ {
34
+ key: "output-limit",
35
+ re: /\bmax_(?:output_)?tokens\b|\b(?:maximum|max) output (?:length|tokens)\b|\boutput (?:token|length) limit\b/i,
36
+ },
37
+ { key: "auth/credentials", re: /unauthorized|401|403|credential|\btoken\b|forbidden/i },
34
38
  { key: "ci/checks-failed", re: /\bci\b|check.?s? fail|lint|tsc|test.?s? fail/i },
35
39
  { key: "network", re: /network|econn|fetch failed|socket|dns|502|503|504/i },
36
40
  { key: "cancelled", re: /cancel|aborted/i },
@@ -60,6 +60,7 @@ import starHistoryRefreshSrc from "./catalog/star-history-refresh.ts" with { typ
60
60
  // @ts-expect-error Bun text imports synthesize a default string for this helper.
61
61
  import starHistoryRendererSrc from "./catalog/star-history-renderer.ts" with { type: "text" };
62
62
  import swarmOverviewSrc from "./catalog/swarm-overview.ts" with { type: "text" };
63
+ import swarmUpdateCheckSrc from "./catalog/swarm-update-check.ts" with { type: "text" };
63
64
  import taskContextGatheringSrc from "./catalog/task-context-gathering.ts" with { type: "text" };
64
65
  import taskFailureAuditSrc from "./catalog/task-failure-audit.ts" with { type: "text" };
65
66
  import textDiffSrc from "./catalog/text-diff.ts" with { type: "text" };
@@ -217,6 +218,14 @@ export const SEED_SCRIPTS: SeedScript[] = [
217
218
  "Task onboarding: one call instead of task_get plus multiple memory_search calls. Pass the task description split into 2-4 natural-language queries.",
218
219
  source: asText(taskContextGatheringSrc),
219
220
  },
221
+ {
222
+ name: "swarm-update-check",
223
+ description:
224
+ "Compare the running agent-swarm version (API /health) with the latest GitHub release of desplega-ai/agent-swarm as numeric major.minor.patch, and hand the lead one notify task only when a newer release exists.",
225
+ intent:
226
+ "Daily zero-config upgrade watchdog behind the daily-swarm-update-check schedule; dryRun reports the verdict without notifying.",
227
+ source: asText(swarmUpdateCheckSrc),
228
+ },
220
229
  {
221
230
  name: "schedule-health",
222
231
  description:
@@ -36,15 +36,15 @@
36
36
  "pages": [
37
37
  {
38
38
  "path": "examples/annotated-pr.html",
39
- "content": "<!doctype html>\n<!--\n Annotated-PR page using the auto-injected <swarm-diff> web component.\n\n No <script> import and no bundling: <swarm-diff>, <swarm-diff-jumps> and\n window.swarmUi.renderDiff are injected into every HTML page automatically.\n Each hunk gets id=\"swarm-diff-<file-slug>-<old_start>\" so <swarm-diff-jumps>\n can build the jump list and external links can deep-link a specific hunk.\n-->\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n <title>PR #1234 — console.log cleanup</title>\n</head>\n<body>\n <h1>PR #1234 — cleanup raw <code>console.log</code> calls</h1>\n <p>Replaces ad-hoc logging with the project logger.</p>\n\n <swarm-diff-jumps></swarm-diff-jumps>\n\n <swarm-diff file=\"src/foo.ts\" base-sha=\"abc123\" head-sha=\"def456\">\n { \"hunks\": [\n { \"old_start\": 10, \"old_lines\": 3, \"new_start\": 10, \"new_lines\": 4,\n \"lines\": [\n { \"type\": \"context\", \"text\": \" const x = 1;\" },\n { \"type\": \"del\", \"text\": \"- console.log(x);\" },\n { \"type\": \"add\", \"text\": \"+ logger.info({ x });\" },\n { \"type\": \"add\", \"text\": \"+ return x;\" }\n ],\n \"annotations\": [\n { \"line\": 12, \"severity\": \"warn\", \"text\": \"Avoid raw console.log\" }\n ]\n }\n ] }\n </swarm-diff>\n\n <swarm-diff file=\"src/bar.ts\" base-sha=\"abc123\" head-sha=\"def456\">\n { \"hunks\": [\n { \"old_start\": 5, \"old_lines\": 1, \"new_start\": 5, \"new_lines\": 1,\n \"lines\": [\n { \"type\": \"del\", \"text\": \"- console.error('boom');\" },\n { \"type\": \"add\", \"text\": \"+ logger.error('boom');\" }\n ]\n }\n ] }\n </swarm-diff>\n</body>\n</html>\n"
39
+ "content": "<!doctype html>\n<!--\n Annotated-PR page using the auto-injected <swarm-diff> web component.\n\n No <script> import and no bundling: <swarm-diff>, <swarm-diff-jumps> and\n window.swarmUi.renderDiff are injected into every HTML page automatically.\n\n Each \"text\" is the code line WITHOUT its +, - or space marker: the\n component draws the marker in its own column. old_lines counts the context\n and deleted lines, and new_lines counts the context and added lines. Inside\n the JSON, write < as \\u003c and & as \\u0026.\n\n Each hunk gets id=\"swarm-diff-<slug>\". The slug is <file>-<old_start> in\n lowercase, with each run of characters other than a-z and 0-9 changed to\n one \"-\". So src/foo.ts at old_start 10 gets id=\"swarm-diff-src-foo-ts-10\".\n <swarm-diff-jumps> builds its jump list from these ids, and external links\n can deep-link a specific hunk.\n-->\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n <title>PR #1234: console.log cleanup</title>\n</head>\n<body>\n <h1>PR #1234: cleanup raw <code>console.log</code> calls</h1>\n <p>Replaces ad-hoc logging with the project logger.</p>\n\n <swarm-diff-jumps></swarm-diff-jumps>\n\n <swarm-diff file=\"src/foo.ts\" base-sha=\"abc123\" head-sha=\"def456\">\n { \"hunks\": [\n { \"old_start\": 10, \"old_lines\": 2, \"new_start\": 10, \"new_lines\": 3,\n \"lines\": [\n { \"type\": \"context\", \"text\": \"export function load(id) {\" },\n { \"type\": \"del\", \"text\": \" console.log(id);\" },\n { \"type\": \"add\", \"text\": \" logger.info({ id });\" },\n { \"type\": \"add\", \"text\": \" return cache.get(id);\" }\n ],\n \"annotations\": [\n { \"line\": 12, \"severity\": \"warn\", \"text\": \"Returns undefined on a cache miss\" }\n ]\n }\n ] }\n </swarm-diff>\n\n <swarm-diff file=\"src/bar.ts\" base-sha=\"abc123\" head-sha=\"def456\">\n { \"hunks\": [\n { \"old_start\": 5, \"old_lines\": 1, \"new_start\": 5, \"new_lines\": 1,\n \"lines\": [\n { \"type\": \"del\", \"text\": \"console.error('boom');\" },\n { \"type\": \"add\", \"text\": \"logger.error('boom');\" }\n ]\n }\n ] }\n </swarm-diff>\n</body>\n</html>\n"
40
40
  },
41
41
  {
42
42
  "path": "examples/multiplayer-board.html",
43
- "content": "<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n <title>Shared board</title>\n <style>\n :root {\n color: #252522;\n background: #f7f6f3;\n font-family: \"SF Pro Display\", \"Helvetica Neue\", ui-sans-serif, system-ui, sans-serif;\n font-synthesis: none;\n }\n\n * { box-sizing: border-box; }\n\n body {\n margin: 0;\n min-width: 320px;\n padding: 0;\n background: #f7f6f3;\n color: #252522;\n font-family: \"SF Pro Display\", \"Helvetica Neue\", ui-sans-serif, system-ui, sans-serif;\n }\n\n button, input, textarea { font: inherit; }\n\n button {\n border: 1px solid #252522;\n border-radius: 8px;\n background: #252522;\n color: #fff;\n cursor: pointer;\n padding: 10px 14px;\n }\n\n button:hover { background: #3c3c37; }\n button:focus-visible, input:focus-visible, textarea:focus-visible { outline: 3px solid #e1a52c; outline-offset: 2px; }\n button:disabled { cursor: not-allowed; opacity: .5; }\n\n .page { max-width: 1120px; margin: 0 auto; padding: 32px 24px 56px; }\n\n header {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: 24px;\n border-bottom: 1px solid #d9d7d0;\n padding-bottom: 24px;\n }\n\n h1, h2, p { margin-top: 0; }\n h1 { font-size: clamp(2rem, 5vw, 3rem); letter-spacing: -.04em; margin-bottom: 8px; }\n h2 { font-size: 1.15rem; letter-spacing: -.02em; margin-bottom: 16px; }\n .lede, .muted { color: #67665f; }\n .lede { max-width: 46rem; margin-bottom: 0; }\n\n .viewer {\n min-width: 170px;\n border: 1px solid #d9d7d0;\n border-radius: 8px;\n background: #fff;\n padding: 12px;\n }\n\n .eyebrow {\n color: #67665f;\n display: block;\n font-size: .72rem;\n font-weight: 700;\n letter-spacing: .08em;\n margin-bottom: 4px;\n text-transform: uppercase;\n }\n\n .viewer-name { font-weight: 700; overflow-wrap: anywhere; }\n\n .notice {\n border: 1px solid #d9d7d0;\n border-left: 4px solid #e1a52c;\n border-radius: 8px;\n background: #fffdf8;\n margin: 24px 0;\n min-height: 47px;\n padding: 12px 14px;\n }\n\n .notice[data-tone=\"error\"] { border-left-color: #a34736; background: #fff9f7; }\n .notice[data-tone=\"stale\"] { border-left-color: #e1a52c; background: #fffdf8; }\n .notice:empty { display: none; }\n\n .grid { display: grid; gap: 24px; grid-template-columns: minmax(0, 1.65fr) minmax(260px, .85fr); }\n .panel { border: 1px solid #d9d7d0; border-radius: 12px; background: #fff; padding: 20px; }\n .panel + .panel { margin-top: 24px; }\n\n .add-row { display: grid; gap: 8px; grid-template-columns: minmax(0, 1fr) auto; margin-bottom: 16px; }\n input, textarea {\n border: 1px solid #bdbbb4;\n border-radius: 8px;\n background: #fff;\n color: inherit;\n padding: 10px 12px;\n width: 100%;\n }\n\n textarea { min-height: 94px; resize: vertical; }\n label { display: block; font-size: .88rem; font-weight: 650; margin-bottom: 8px; }\n\n .cards, .peers { list-style: none; margin: 0; padding: 0; }\n .cards { border-top: 1px solid #e7e5df; }\n .card {\n align-items: center;\n border-bottom: 1px solid #e7e5df;\n display: flex;\n gap: 12px;\n justify-content: space-between;\n padding: 13px 0;\n }\n\n .card-title { font-weight: 650; overflow-wrap: anywhere; }\n .card-status {\n background: #fff;\n border-color: #bdbbb4;\n color: #252522;\n flex: 0 0 auto;\n font-size: .82rem;\n padding: 7px 10px;\n }\n\n .card-status[data-done=\"true\"] { background: #f6efd9; border-color: #c89429; }\n .empty { color: #67665f; padding: 16px 0 4px; }\n .agent-note { background: #f7f6f3; border-radius: 8px; min-height: 46px; padding: 12px; white-space: pre-wrap; overflow-wrap: anywhere; }\n .agent-note:empty::before { color: #67665f; content: \"No note yet. A server-side agent can write one here.\"; }\n\n .peers { display: grid; gap: 8px; }\n .peer {\n border: 1px solid #e7e5df;\n border-radius: 8px;\n display: grid;\n gap: 3px;\n padding: 10px;\n }\n\n .peer-name { font-weight: 650; overflow-wrap: anywhere; }\n .room-meta { color: #67665f; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .75rem; margin: 0 0 16px; }\n .save-state { color: #67665f; font-size: .82rem; margin: 8px 0 0; min-height: 1.2em; }\n\n @media (max-width: 720px) {\n .page { padding: 24px 16px 40px; }\n header { align-items: stretch; flex-direction: column; }\n .viewer { min-width: 0; }\n .grid { grid-template-columns: 1fr; }\n .add-row { grid-template-columns: 1fr; }\n }\n </style>\n </head>\n <body>\n <swarm-cursors room=\"match-42\"></swarm-cursors>\n <main class=\"page\">\n <header>\n <div>\n <h1>Shared board</h1>\n <p class=\"lede\">A small board shared by everyone in match-42. Changes appear as peers make them.</p>\n </div>\n <section class=\"viewer\" aria-label=\"Current viewer\">\n <span class=\"eyebrow\">Connected as</span>\n <div class=\"viewer-name\" id=\"viewer-name\">Connecting…</div>\n </section>\n </header>\n\n <div id=\"notice\" class=\"notice\" role=\"status\" aria-live=\"polite\"></div>\n\n <div class=\"grid\">\n <section class=\"panel\" aria-labelledby=\"board-heading\">\n <h2 id=\"board-heading\">Match board</h2>\n <p class=\"room-meta\">Room: match-42 · Schema: 1</p>\n <form id=\"add-card-form\" class=\"add-row\">\n <label for=\"card-title\" class=\"sr-only\" hidden>New card title</label>\n <input id=\"card-title\" name=\"title\" maxlength=\"120\" placeholder=\"Write a card title\" required>\n <button type=\"submit\">Add card</button>\n </form>\n <ul id=\"cards\" class=\"cards\" aria-live=\"polite\"></ul>\n </section>\n\n <aside>\n <section class=\"panel\" aria-labelledby=\"peers-heading\">\n <h2 id=\"peers-heading\">Live peers</h2>\n <p class=\"muted\">Pointers use document-normalized coordinates and line up across viewports.</p>\n <ul id=\"peers\" class=\"peers\" aria-live=\"polite\"></ul>\n </section>\n\n <section class=\"panel\" aria-labelledby=\"agent-note-heading\">\n <h2 id=\"agent-note-heading\">Agent note</h2>\n <div id=\"agent-note\" class=\"agent-note\" aria-live=\"polite\"></div>\n </section>\n </aside>\n </div>\n\n <section class=\"panel\" aria-labelledby=\"lobby-heading\">\n <h2 id=\"lobby-heading\">Lobby</h2>\n <p class=\"room-meta\">Room: lobby · Schema: 1</p>\n <form id=\"lobby-form\">\n <label for=\"lobby-message\">Shared lobby message</label>\n <textarea id=\"lobby-message\" maxlength=\"1000\" placeholder=\"Leave a message for everyone in the lobby\"></textarea>\n <button type=\"submit\">Save lobby message</button>\n <p id=\"lobby-save-state\" class=\"save-state\" aria-live=\"polite\"></p>\n </form>\n </section>\n </main>\n\n <script>\n (() => {\n const ui = {\n viewerName: document.querySelector('#viewer-name'),\n notice: document.querySelector('#notice'),\n cards: document.querySelector('#cards'),\n peers: document.querySelector('#peers'),\n agentNote: document.querySelector('#agent-note'),\n addCardForm: document.querySelector('#add-card-form'),\n cardTitle: document.querySelector('#card-title'),\n lobbyForm: document.querySelector('#lobby-form'),\n lobbyMessage: document.querySelector('#lobby-message'),\n lobbySaveState: document.querySelector('#lobby-save-state')\n };\n\n let board;\n let lobby;\n let peers = [];\n let lobbyDirty = false;\n\n function message(error) {\n return error && error.message ? error.message : String(error);\n }\n\n function showNotice(text, tone) {\n ui.notice.textContent = text || '';\n ui.notice.dataset.tone = tone || '';\n }\n\n function markStale(room, roomName) {\n if (!room.stale) return false;\n showNotice(`${roomName} uses a different schema version. Its saved state remains visible, but this page cannot write changes.`, 'stale');\n return true;\n }\n\n function renderBoard(state) {\n const cards = state && state.cards && typeof state.cards === 'object' ? state.cards : {};\n ui.cards.replaceChildren();\n const entries = Object.entries(cards);\n\n if (!entries.length) {\n const empty = document.createElement('li');\n empty.className = 'empty';\n empty.textContent = 'No cards yet. Add the first card.';\n ui.cards.append(empty);\n }\n\n for (const [id, card] of entries) {\n const item = document.createElement('li');\n item.className = 'card';\n const title = document.createElement('span');\n title.className = 'card-title';\n title.textContent = card && typeof card.title === 'string' ? card.title : 'Untitled card';\n const toggle = document.createElement('button');\n const done = Boolean(card && card.done);\n toggle.className = 'card-status';\n toggle.dataset.done = String(done);\n toggle.type = 'button';\n toggle.textContent = done ? 'Done' : 'Open';\n toggle.setAttribute('aria-label', `Mark ${title.textContent} as ${done ? 'open' : 'done'}`);\n toggle.addEventListener('click', () => toggleCard(id));\n item.append(title, toggle);\n ui.cards.append(item);\n }\n\n ui.agentNote.textContent = state && typeof state.agentNote === 'string' ? state.agentNote : '';\n }\n\n function renderPeers(nextPeers) {\n peers = Array.isArray(nextPeers) ? nextPeers : [];\n ui.peers.replaceChildren();\n\n if (!peers.length) {\n const empty = document.createElement('li');\n empty.className = 'empty';\n empty.textContent = 'No other peers are connected.';\n ui.peers.append(empty);\n return;\n }\n\n for (const peer of peers) {\n const item = document.createElement('li');\n item.className = 'peer';\n const name = document.createElement('span');\n name.className = 'peer-name';\n name.textContent = peer.name || 'Guest';\n item.append(name);\n ui.peers.append(item);\n }\n }\n\n function renderLobby(state) {\n if (lobbyDirty) return;\n ui.lobbyMessage.value = state && typeof state.message === 'string' ? state.message : '';\n }\n\n async function addCard(title) {\n if (markStale(board, 'match-42')) return;\n try {\n await board.change(state => {\n state.cards ??= {};\n state.cards[crypto.randomUUID()] = { title, done: false };\n });\n ui.cardTitle.value = '';\n showNotice('Card added.', 'success');\n } catch (error) {\n showNotice(`Could not add the card: ${message(error)}`, 'error');\n }\n }\n\n async function toggleCard(id) {\n if (markStale(board, 'match-42')) return;\n try {\n await board.change(state => {\n const card = state.cards && state.cards[id];\n if (card) card.done = !card.done;\n });\n } catch (error) {\n showNotice(`Could not update the card: ${message(error)}`, 'error');\n }\n }\n\n async function saveLobbyMessage() {\n if (markStale(lobby, 'lobby')) return;\n try {\n const value = ui.lobbyMessage.value;\n await lobby.change(state => {\n state.message = value;\n });\n lobbyDirty = false;\n ui.lobbySaveState.textContent = 'Saved to the lobby room.';\n } catch (error) {\n ui.lobbySaveState.textContent = `Could not save: ${message(error)}`;\n showNotice(`Could not save the lobby message: ${message(error)}`, 'error');\n }\n }\n\n async function initialize() {\n try {\n board = await window.swarmSdk.room('match-42', { schemaVersion: 1 });\n lobby = await window.swarmSdk.room('lobby', { schemaVersion: 1 });\n\n const viewer = board.me || lobby.me || {};\n ui.viewerName.textContent = viewer.name || 'Guest';\n\n board.on('change', renderBoard);\n board.on('presence', renderPeers);\n board.on('error', error => showNotice(`match-42 error: ${message(error)}`, 'error'));\n board.on('reset', () => showNotice('match-42 was reset by another participant. The board has resynchronized.', 'stale'));\n lobby.on('change', renderLobby);\n lobby.on('error', error => showNotice(`lobby error: ${message(error)}`, 'error'));\n lobby.on('reset', () => showNotice('lobby was reset by another participant. The lobby has resynchronized.', 'stale'));\n\n renderBoard(board.state);\n renderPeers(board.presence.peers);\n renderLobby(lobby.state);\n markStale(board, 'match-42');\n markStale(lobby, 'lobby');\n\n if (!board.stale) {\n await board.change(state => {\n state.cards ??= {};\n state.agentNote ??= '';\n });\n }\n if (!lobby.stale) {\n await lobby.change(state => {\n state.message ??= '';\n state.activeMatch ??= 'match-42';\n });\n }\n } catch (error) {\n showNotice(`Could not connect to the shared rooms: ${message(error)}`, 'error');\n ui.viewerName.textContent = 'Unavailable';\n }\n }\n\n ui.addCardForm.addEventListener('submit', event => {\n event.preventDefault();\n const title = ui.cardTitle.value.trim();\n if (title) addCard(title);\n });\n ui.lobbyMessage.addEventListener('input', () => { lobbyDirty = true; });\n ui.lobbyForm.addEventListener('submit', event => {\n event.preventDefault();\n saveLobbyMessage();\n });\n window.addEventListener('pagehide', () => {\n if (board) board.close();\n if (lobby) lobby.close();\n });\n\n initialize();\n })();\n </script>\n </body>\n</html>\n"
43
+ "content": "<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n <title>Shared board</title>\n <style>\n :root {\n color: var(--color-foreground);\n background: var(--color-background);\n font-family: var(--font-sans);\n font-synthesis: none;\n }\n\n * { box-sizing: border-box; }\n\n body {\n margin: 0;\n min-width: 320px;\n padding: 0;\n background: var(--color-background);\n color: var(--color-foreground);\n font-family: var(--font-sans);\n }\n\n button, input, textarea { font: inherit; }\n\n button {\n border: 1px solid var(--color-primary);\n border-radius: var(--radius-md);\n background: var(--color-primary);\n color: var(--color-primary-foreground);\n cursor: pointer;\n padding: 10px 14px;\n }\n\n button:hover { background: color-mix(in oklab, var(--color-primary) 90%, transparent); }\n button:focus-visible, input:focus-visible, textarea:focus-visible { outline: 3px solid var(--color-ring); outline-offset: 2px; }\n button:disabled { cursor: not-allowed; opacity: .5; }\n\n .page { max-width: 1120px; margin: 0 auto; padding: 32px 24px 56px; }\n\n header {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: 24px;\n border-bottom: 1px solid var(--color-border);\n padding-bottom: 24px;\n }\n\n h1, h2, p { margin-top: 0; }\n h1 { font-size: clamp(2rem, 5vw, 3rem); letter-spacing: -.04em; margin-bottom: 8px; }\n h2 { font-size: 1.15rem; letter-spacing: -.02em; margin-bottom: 16px; }\n .lede, .muted { color: var(--color-muted-foreground); }\n .lede { max-width: 46rem; margin-bottom: 0; }\n\n .viewer {\n min-width: 170px;\n border: 1px solid var(--color-border);\n border-radius: var(--radius-md);\n background: var(--color-card);\n padding: 12px;\n }\n\n .eyebrow {\n color: var(--color-muted-foreground);\n display: block;\n font-size: .72rem;\n font-weight: 700;\n letter-spacing: .08em;\n margin-bottom: 4px;\n text-transform: uppercase;\n }\n\n .viewer-name { font-weight: 700; overflow-wrap: anywhere; }\n\n .notice {\n border: 1px solid var(--color-border);\n border-left: 4px solid var(--color-primary);\n border-radius: var(--radius-md);\n background: var(--color-card);\n margin: 24px 0;\n min-height: 47px;\n padding: 12px 14px;\n }\n\n .notice[data-tone=\"error\"] { border-left-color: var(--color-status-error); background: color-mix(in oklab, var(--color-status-error) 10%, var(--color-card)); }\n .notice[data-tone=\"stale\"] { border-left-color: var(--color-status-warning); background: color-mix(in oklab, var(--color-status-warning) 10%, var(--color-card)); }\n .notice:empty { display: none; }\n\n .grid { display: grid; gap: 24px; grid-template-columns: minmax(0, 1.65fr) minmax(260px, .85fr); }\n .panel { border: 1px solid var(--color-border); border-radius: var(--radius-xl); background: var(--color-card); padding: 20px; }\n .panel + .panel { margin-top: 24px; }\n\n .add-row { display: grid; gap: 8px; grid-template-columns: minmax(0, 1fr) auto; margin-bottom: 16px; }\n input, textarea {\n border: 1px solid var(--color-input);\n border-radius: var(--radius-md);\n background: var(--color-card);\n color: inherit;\n padding: 10px 12px;\n width: 100%;\n }\n\n textarea { min-height: 94px; resize: vertical; }\n label { display: block; font-size: .88rem; font-weight: 650; margin-bottom: 8px; }\n\n .cards, .peers { list-style: none; margin: 0; padding: 0; }\n .cards { border-top: 1px solid var(--color-border-subtle); }\n .card {\n align-items: center;\n border-bottom: 1px solid var(--color-border-subtle);\n display: flex;\n gap: 12px;\n justify-content: space-between;\n padding: 13px 0;\n }\n\n .card-title { font-weight: 650; overflow-wrap: anywhere; }\n .card-status {\n background: var(--color-card);\n border-color: var(--color-input);\n color: var(--color-foreground);\n flex: 0 0 auto;\n font-size: .82rem;\n padding: 7px 10px;\n }\n\n .card-status[data-done=\"true\"] { background: var(--color-status-success); border-color: var(--color-status-success-strong); color: var(--color-status-success-foreground); }\n .empty { color: var(--color-muted-foreground); padding: 16px 0 4px; }\n .agent-note { background: var(--color-muted); border-radius: var(--radius-md); min-height: 46px; padding: 12px; white-space: pre-wrap; overflow-wrap: anywhere; }\n .agent-note:empty::before { color: var(--color-muted-foreground); content: \"No note yet. A server-side agent can write one here.\"; }\n\n .peers { display: grid; gap: 8px; }\n .peer {\n border: 1px solid var(--color-border-subtle);\n border-radius: var(--radius-md);\n display: grid;\n gap: 3px;\n padding: 10px;\n }\n\n .peer-name { font-weight: 650; overflow-wrap: anywhere; }\n .room-meta { color: var(--color-muted-foreground); font-family: var(--font-mono); font-size: .75rem; margin: 0 0 16px; }\n .save-state { color: var(--color-muted-foreground); font-size: .82rem; margin: 8px 0 0; min-height: 1.2em; }\n\n @media (max-width: 720px) {\n .page { padding: 24px 16px 40px; }\n header { align-items: stretch; flex-direction: column; }\n .viewer { min-width: 0; }\n .grid { grid-template-columns: 1fr; }\n .add-row { grid-template-columns: 1fr; }\n }\n </style>\n </head>\n <body>\n <swarm-cursors room=\"match-42\"></swarm-cursors>\n <main class=\"page\">\n <header>\n <div>\n <h1>Shared board</h1>\n <p class=\"lede\">A small board shared by everyone in match-42. Changes appear as peers make them.</p>\n </div>\n <section class=\"viewer\" aria-label=\"Current viewer\">\n <span class=\"eyebrow\">Connected as</span>\n <div class=\"viewer-name\" id=\"viewer-name\">Connecting…</div>\n </section>\n </header>\n\n <div id=\"notice\" class=\"notice\" role=\"status\" aria-live=\"polite\"></div>\n\n <div class=\"grid\">\n <section class=\"panel\" aria-labelledby=\"board-heading\">\n <h2 id=\"board-heading\">Match board</h2>\n <p class=\"room-meta\">Room: match-42 · Schema: 1</p>\n <form id=\"add-card-form\" class=\"add-row\">\n <label for=\"card-title\" class=\"sr-only\" hidden>New card title</label>\n <input id=\"card-title\" name=\"title\" maxlength=\"120\" placeholder=\"Write a card title\" required>\n <button type=\"submit\">Add card</button>\n </form>\n <ul id=\"cards\" class=\"cards\" aria-live=\"polite\"></ul>\n </section>\n\n <aside>\n <section class=\"panel\" aria-labelledby=\"peers-heading\">\n <h2 id=\"peers-heading\">Live peers</h2>\n <p class=\"muted\">Pointers use document-normalized coordinates and line up across viewports.</p>\n <ul id=\"peers\" class=\"peers\" aria-live=\"polite\"></ul>\n </section>\n\n <section class=\"panel\" aria-labelledby=\"agent-note-heading\">\n <h2 id=\"agent-note-heading\">Agent note</h2>\n <div id=\"agent-note\" class=\"agent-note\" aria-live=\"polite\"></div>\n </section>\n </aside>\n </div>\n\n <section class=\"panel\" aria-labelledby=\"lobby-heading\">\n <h2 id=\"lobby-heading\">Lobby</h2>\n <p class=\"room-meta\">Room: lobby · Schema: 1</p>\n <form id=\"lobby-form\">\n <label for=\"lobby-message\">Shared lobby message</label>\n <textarea id=\"lobby-message\" maxlength=\"1000\" placeholder=\"Leave a message for everyone in the lobby\"></textarea>\n <button type=\"submit\">Save lobby message</button>\n <p id=\"lobby-save-state\" class=\"save-state\" aria-live=\"polite\"></p>\n </form>\n </section>\n </main>\n\n <script>\n (() => {\n const ui = {\n viewerName: document.querySelector('#viewer-name'),\n notice: document.querySelector('#notice'),\n cards: document.querySelector('#cards'),\n peers: document.querySelector('#peers'),\n agentNote: document.querySelector('#agent-note'),\n addCardForm: document.querySelector('#add-card-form'),\n cardTitle: document.querySelector('#card-title'),\n lobbyForm: document.querySelector('#lobby-form'),\n lobbyMessage: document.querySelector('#lobby-message'),\n lobbySaveState: document.querySelector('#lobby-save-state')\n };\n\n let board;\n let lobby;\n let peers = [];\n let lobbyDirty = false;\n\n function message(error) {\n return error && error.message ? error.message : String(error);\n }\n\n function showNotice(text, tone) {\n ui.notice.textContent = text || '';\n ui.notice.dataset.tone = tone || '';\n }\n\n function markStale(room, roomName) {\n if (!room.stale) return false;\n showNotice(`${roomName} uses a different schema version. Its saved state remains visible, but this page cannot write changes.`, 'stale');\n return true;\n }\n\n function renderBoard(state) {\n const cards = state && state.cards && typeof state.cards === 'object' ? state.cards : {};\n ui.cards.replaceChildren();\n const entries = Object.entries(cards);\n\n if (!entries.length) {\n const empty = document.createElement('li');\n empty.className = 'empty';\n empty.textContent = 'No cards yet. Add the first card.';\n ui.cards.append(empty);\n }\n\n for (const [id, card] of entries) {\n const item = document.createElement('li');\n item.className = 'card';\n const title = document.createElement('span');\n title.className = 'card-title';\n title.textContent = card && typeof card.title === 'string' ? card.title : 'Untitled card';\n const toggle = document.createElement('button');\n const done = Boolean(card && card.done);\n toggle.className = 'card-status';\n toggle.dataset.done = String(done);\n toggle.type = 'button';\n toggle.textContent = done ? 'Done' : 'Open';\n toggle.setAttribute('aria-label', `Mark ${title.textContent} as ${done ? 'open' : 'done'}`);\n toggle.addEventListener('click', () => toggleCard(id));\n item.append(title, toggle);\n ui.cards.append(item);\n }\n\n ui.agentNote.textContent = state && typeof state.agentNote === 'string' ? state.agentNote : '';\n }\n\n function renderPeers(nextPeers) {\n peers = Array.isArray(nextPeers) ? nextPeers : [];\n ui.peers.replaceChildren();\n\n if (!peers.length) {\n const empty = document.createElement('li');\n empty.className = 'empty';\n empty.textContent = 'No other peers are connected.';\n ui.peers.append(empty);\n return;\n }\n\n for (const peer of peers) {\n const item = document.createElement('li');\n item.className = 'peer';\n const name = document.createElement('span');\n name.className = 'peer-name';\n name.textContent = peer.name || 'Guest';\n item.append(name);\n ui.peers.append(item);\n }\n }\n\n function renderLobby(state) {\n if (lobbyDirty) return;\n ui.lobbyMessage.value = state && typeof state.message === 'string' ? state.message : '';\n }\n\n async function addCard(title) {\n if (markStale(board, 'match-42')) return;\n try {\n await board.change(state => {\n state.cards ??= {};\n state.cards[crypto.randomUUID()] = { title, done: false };\n });\n ui.cardTitle.value = '';\n showNotice('Card added.', 'success');\n } catch (error) {\n showNotice(`Could not add the card: ${message(error)}`, 'error');\n }\n }\n\n async function toggleCard(id) {\n if (markStale(board, 'match-42')) return;\n try {\n await board.change(state => {\n const card = state.cards && state.cards[id];\n if (card) card.done = !card.done;\n });\n } catch (error) {\n showNotice(`Could not update the card: ${message(error)}`, 'error');\n }\n }\n\n async function saveLobbyMessage() {\n if (markStale(lobby, 'lobby')) return;\n try {\n const value = ui.lobbyMessage.value;\n await lobby.change(state => {\n state.message = value;\n });\n lobbyDirty = false;\n ui.lobbySaveState.textContent = 'Saved to the lobby room.';\n } catch (error) {\n ui.lobbySaveState.textContent = `Could not save: ${message(error)}`;\n showNotice(`Could not save the lobby message: ${message(error)}`, 'error');\n }\n }\n\n async function initialize() {\n try {\n board = await window.swarmSdk.room('match-42', { schemaVersion: 1 });\n lobby = await window.swarmSdk.room('lobby', { schemaVersion: 1 });\n\n const viewer = board.me || lobby.me || {};\n ui.viewerName.textContent = viewer.name || 'Guest';\n\n board.on('change', renderBoard);\n board.on('presence', renderPeers);\n board.on('error', error => showNotice(`match-42 error: ${message(error)}`, 'error'));\n board.on('reset', () => showNotice('match-42 was reset by another participant. The board has resynchronized.', 'stale'));\n lobby.on('change', renderLobby);\n lobby.on('error', error => showNotice(`lobby error: ${message(error)}`, 'error'));\n lobby.on('reset', () => showNotice('lobby was reset by another participant. The lobby has resynchronized.', 'stale'));\n\n renderBoard(board.state);\n renderPeers(board.presence.peers);\n renderLobby(lobby.state);\n markStale(board, 'match-42');\n markStale(lobby, 'lobby');\n\n if (!board.stale) {\n await board.change(state => {\n state.cards ??= {};\n state.agentNote ??= '';\n });\n }\n if (!lobby.stale) {\n await lobby.change(state => {\n state.message ??= '';\n state.activeMatch ??= 'match-42';\n });\n }\n } catch (error) {\n showNotice(`Could not connect to the shared rooms: ${message(error)}`, 'error');\n ui.viewerName.textContent = 'Unavailable';\n }\n }\n\n ui.addCardForm.addEventListener('submit', event => {\n event.preventDefault();\n const title = ui.cardTitle.value.trim();\n if (title) addCard(title);\n });\n ui.lobbyMessage.addEventListener('input', () => { lobbyDirty = true; });\n ui.lobbyForm.addEventListener('submit', event => {\n event.preventDefault();\n saveLobbyMessage();\n });\n window.addEventListener('pagehide', () => {\n if (board) board.close();\n if (lobby) lobby.close();\n });\n\n initialize();\n })();\n </script>\n </body>\n</html>\n"
44
44
  },
45
45
  {
46
46
  "path": "examples/report-page.html",
47
- "content": "<!doctype html>\n<!--\n Reference report page — minimalist taste baseline + report density layer.\n\n Copy this as the starting point for any dense report/dashboard page, then\n replace the content. The CSS already encodes the baseline documented in the\n `pages` skill: warm monochrome canvas, single-column 1120px spine, 8/12/16/\n 24/32/48/72 spacing scale, tabular numerals, flat bordered cards, table\n horizontal scroll on mobile, and no decorative gradients or nested cards.\n\n Publish with:\n create_page({\n title: \"Q2 SEO Performance\",\n slug: \"q2-seo-performance\",\n description: \"Human-readable SEO performance report.\",\n contentType: \"text/html\",\n authMode: \"authed\",\n body: <this file's contents>,\n })\n-->\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n <title>Q2 SEO Performance</title>\n <style>\n :root {\n color-scheme: light;\n --bg: #f6f4f0;\n --panel: #ffffff;\n --ink: #18181b;\n --muted: #62646a;\n --line: #dedbd2;\n --accent: #2563eb;\n --danger: #b42318;\n --warn: #b54708;\n --ok: #067647;\n --radius: 8px;\n --shadow: 0 1px 2px rgba(24, 24, 27, 0.04);\n }\n * { box-sizing: border-box; }\n body {\n margin: 0;\n background: var(--bg);\n color: var(--ink);\n font-family: \"SF Pro Display\", \"Geist Sans\", \"Helvetica Neue\", ui-sans-serif, system-ui, sans-serif;\n font-size: 16px;\n line-height: 1.55;\n }\n main {\n width: min(1120px, calc(100% - 32px));\n margin: 0 auto;\n padding: 48px 0 72px;\n }\n header { margin-bottom: 32px; }\n .eyebrow {\n margin: 0 0 8px;\n color: var(--muted);\n font-size: 13px;\n font-weight: 700;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n }\n h1 {\n margin: 0;\n max-width: 780px;\n font-size: clamp(2rem, 4vw, 3rem);\n line-height: 1.05;\n letter-spacing: 0;\n }\n .lede {\n max-width: 760px;\n margin: 16px 0 0;\n color: var(--muted);\n font-size: 18px;\n }\n .metrics {\n display: grid;\n grid-template-columns: repeat(4, minmax(0, 1fr));\n gap: 12px;\n margin: 32px 0;\n }\n .metric, .section {\n background: var(--panel);\n border: 1px solid var(--line);\n border-radius: var(--radius);\n box-shadow: var(--shadow);\n }\n .metric { padding: 18px; }\n .metric strong {\n display: block;\n font-size: 32px;\n line-height: 1;\n font-variant-numeric: tabular-nums;\n }\n .metric span {\n display: block;\n margin-top: 8px;\n color: var(--muted);\n font-size: 13px;\n font-weight: 650;\n }\n .section {\n margin-top: 18px;\n padding: 24px;\n }\n h2 { margin: 0 0 12px; font-size: 24px; line-height: 1.2; }\n h3 { margin: 0 0 8px; font-size: 17px; line-height: 1.3; }\n p { margin: 0 0 12px; }\n .table-wrap { overflow-x: auto; border: 1px solid var(--line); border-radius: var(--radius); }\n table { width: 100%; border-collapse: collapse; min-width: 640px; }\n th, td { padding: 10px 12px; border-bottom: 1px solid var(--line); text-align: left; vertical-align: top; }\n th { color: var(--muted); font-size: 12px; text-transform: uppercase; letter-spacing: 0.06em; }\n td { font-size: 14px; font-variant-numeric: tabular-nums; }\n code, pre { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }\n details { margin-top: 24px; }\n summary { cursor: pointer; font-weight: 700; }\n pre { overflow: auto; padding: 16px; background: #111827; color: #f9fafb; border-radius: var(--radius); }\n @media (max-width: 760px) {\n main { width: min(100% - 24px, 1120px); padding-top: 32px; }\n .metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); }\n .section { padding: 18px; }\n }\n @media (max-width: 520px) {\n .metrics { grid-template-columns: 1fr; }\n .lede { font-size: 16px; }\n }\n </style>\n</head>\n<body>\n <main>\n <header>\n <p class=\"eyebrow\">Generated 2026-06-04</p>\n <h1>Q2 SEO Performance</h1>\n <p class=\"lede\">Organic traffic grew sharply, but the next gains depend on fixing thin page metadata and publishing three high-intent pillar pages.</p>\n </header>\n\n <section class=\"metrics\" aria-label=\"Key metrics\">\n <div class=\"metric\"><strong>18.6k</strong><span>Organic clicks</span></div>\n <div class=\"metric\"><strong>+50%</strong><span>Quarter over quarter</span></div>\n <div class=\"metric\"><strong>23</strong><span>Metadata fixes</span></div>\n <div class=\"metric\"><strong>3</strong><span>Priority pages</span></div>\n </section>\n\n <section class=\"section\">\n <h2>Summary</h2>\n <p>Start with the conclusion. Add tables only after the reader understands what changed and what to do next.</p>\n <div class=\"table-wrap\">\n <table>\n <thead><tr><th>Metric</th><th>Q1</th><th>Q2</th><th>Change</th></tr></thead>\n <tbody><tr><td>Organic clicks</td><td>12,400</td><td>18,600</td><td>+50%</td></tr></tbody>\n </table>\n </div>\n </section>\n\n <details>\n <summary>Raw evidence</summary>\n <pre>{ \"source\": \"search-console\", \"range\": \"2026-04-01/2026-06-30\" }</pre>\n </details>\n </main>\n</body>\n</html>\n"
47
+ "content": "<!doctype html>\n<!--\n Reference report page for the swarm design system (designVersion 2).\n\n Copy this as the starting point for a dense report or dashboard page, then\n replace the content. It uses only the swarm-* primitives, the theme tokens,\n and a few Tailwind layout utilities. It has zero color literals, so it\n follows the viewer's theme and dark mode inside the dashboard.\n\n Publish with:\n create_page({\n title: \"Q2 SEO Performance\",\n slug: \"q2-seo-performance\",\n description: \"Human-readable SEO performance report.\",\n contentType: \"text/html\",\n authMode: \"authed\",\n body: <this file's contents>,\n })\n-->\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n <title>Q2 SEO Performance</title>\n</head>\n<body>\n <main class=\"swarm-page\">\n <header>\n <p class=\"swarm-eyebrow\">Search Console · Generated 2026-06-04</p>\n <h1>Q2 SEO Performance</h1>\n <p class=\"swarm-lede\">Organic clicks grew 50% this quarter. The next gains depend on fixing thin page metadata and on three high-intent pillar pages.</p>\n <span class=\"swarm-badge\" data-tone=\"success\" data-dot>On track</span>\n </header>\n\n <section class=\"swarm-metrics\" aria-label=\"Key metrics\">\n <div class=\"swarm-metric\">\n <div class=\"swarm-metric-value\">18.6k</div>\n <div class=\"swarm-metric-label\">Organic clicks</div>\n </div>\n <div class=\"swarm-metric\" data-tone=\"success\">\n <div class=\"swarm-metric-value\">+50%</div>\n <div class=\"swarm-metric-label\">Quarter over quarter</div>\n </div>\n <div class=\"swarm-metric\" data-tone=\"warning\">\n <div class=\"swarm-metric-value\">23</div>\n <div class=\"swarm-metric-label\">Metadata fixes</div>\n </div>\n <div class=\"swarm-metric\">\n <div class=\"swarm-metric-value\">3</div>\n <div class=\"swarm-metric-label\">Priority pages</div>\n </div>\n </section>\n\n <section class=\"swarm-section\" aria-labelledby=\"findings-heading\">\n <h2 id=\"findings-heading\">Findings</h2>\n <div class=\"swarm-callout\" data-tone=\"warning\">\n <strong>Thin metadata caps growth.</strong> 23 ranking pages share a generic title and description. Fix them before new content ships.\n </div>\n <div class=\"swarm-callout\">\n <strong>One post drives the growth.</strong> The agent swarms post brought about a quarter of the new clicks.\n </div>\n </section>\n\n <section class=\"swarm-section\" aria-labelledby=\"pages-heading\">\n <h2 id=\"pages-heading\">Top pages</h2>\n <div class=\"swarm-toolbar\">\n <input class=\"swarm-input\" id=\"page-filter\" type=\"search\" placeholder=\"Filter pages\" aria-label=\"Filter pages\">\n <div class=\"swarm-segmented\" role=\"group\" aria-label=\"Status\">\n <button type=\"button\" data-status-filter=\"all\" aria-pressed=\"true\">All</button>\n <button type=\"button\" data-status-filter=\"fix\" aria-pressed=\"false\">Needs fix</button>\n </div>\n <button class=\"swarm-button\" id=\"filter-reset\" type=\"button\">Reset</button>\n </div>\n <div class=\"swarm-table-wrap\">\n <table class=\"swarm-table\" id=\"pages-table\">\n <thead>\n <tr><th>Page</th><th>Status</th><th class=\"num\">Q1 clicks</th><th class=\"num\">Q2 clicks</th><th class=\"num\">Change</th></tr>\n </thead>\n <tbody>\n <tr data-status=\"ok\"><td class=\"nowrap\">/pricing</td><td><span class=\"swarm-badge\" data-tone=\"success\">Healthy</span></td><td class=\"num\">4,100</td><td class=\"num\">6,900</td><td class=\"num\">+68%</td></tr>\n <tr data-status=\"fix\"><td class=\"nowrap\">/docs/install</td><td><span class=\"swarm-badge\" data-tone=\"warning\">Thin title</span></td><td class=\"num\">3,200</td><td class=\"num\">3,350</td><td class=\"num\">+5%</td></tr>\n <tr data-status=\"ok\"><td class=\"nowrap\">/blog/agent-swarms</td><td><span class=\"swarm-badge\" data-tone=\"success\">Healthy</span></td><td class=\"num\">1,500</td><td class=\"num\">3,100</td><td class=\"num\">+107%</td></tr>\n <tr data-status=\"fix\"><td class=\"nowrap\">/integrations/slack</td><td><span class=\"swarm-badge\" data-tone=\"error\">No description</span></td><td class=\"num\">1,900</td><td class=\"num\">1,700</td><td class=\"num text-status-error-strong\">-11%</td></tr>\n <tr data-status=\"review\"><td class=\"nowrap\">/changelog</td><td><span class=\"swarm-badge\">Not reviewed</span></td><td class=\"num\">400</td><td class=\"num\">1,150</td><td class=\"num\">+188%</td></tr>\n </tbody>\n </table>\n </div>\n <p class=\"text-muted-foreground\" id=\"filter-empty\" hidden>No pages match the filter.</p>\n </section>\n\n <section class=\"swarm-section\" aria-labelledby=\"actions-heading\">\n <h2 id=\"actions-heading\">Next actions</h2>\n <div class=\"grid gap-4 md:grid-cols-2\">\n <article class=\"swarm-panel\">\n <h3>Fix thin metadata</h3>\n <dl class=\"swarm-kv\">\n <dt>Owner</dt><dd>Content team</dd>\n <dt>Pages</dt><dd>23</dd>\n <dt>Due</dt><dd>2026-07-15</dd>\n </dl>\n </article>\n <article class=\"swarm-panel\">\n <h3>Publish three pillar pages</h3>\n <dl class=\"swarm-kv\">\n <dt>Owner</dt><dd>Growth team</dd>\n <dt>Topics</dt><dd>Agent orchestration, task routing, swarm costs</dd>\n <dt>Due</dt><dd>2026-08-01</dd>\n </dl>\n </article>\n </div>\n <div class=\"mt-4\">\n <a class=\"swarm-button\" data-variant=\"primary\" href=\"https://search.google.com/search-console\">Open Search Console</a>\n </div>\n </section>\n\n <details>\n <summary>Raw evidence</summary>\n <pre>{ \"source\": \"search-console\", \"range\": \"2026-04-01/2026-06-30\" }</pre>\n </details>\n </main>\n\n <script>\n (() => {\n const filter = document.getElementById(\"page-filter\");\n const reset = document.getElementById(\"filter-reset\");\n const empty = document.getElementById(\"filter-empty\");\n const statusButtons = document.querySelectorAll(\"[data-status-filter]\");\n const rows = document.querySelectorAll(\"#pages-table tbody tr\");\n let status = \"all\";\n\n function applyFilter() {\n const query = filter.value.trim().toLowerCase();\n let shown = 0;\n for (const row of rows) {\n const visible =\n row.textContent.toLowerCase().includes(query) &&\n (status === \"all\" || row.dataset.status === status);\n row.style.display = visible ? \"\" : \"none\";\n if (visible) shown += 1;\n }\n empty.hidden = shown > 0;\n }\n\n function setStatus(next) {\n status = next;\n for (const button of statusButtons) {\n button.setAttribute(\"aria-pressed\", String(button.dataset.statusFilter === next));\n }\n applyFilter();\n }\n\n for (const button of statusButtons) {\n button.addEventListener(\"click\", () => setStatus(button.dataset.statusFilter));\n }\n filter.addEventListener(\"input\", applyFilter);\n reset.addEventListener(\"click\", () => {\n filter.value = \"\";\n setStatus(\"all\");\n });\n })();\n </script>\n</body>\n</html>\n"
48
48
  }
49
49
  ],
50
50
  "planning": [
@@ -12,6 +12,11 @@ import {
12
12
  import { envExportRestrictedError, isEnvExportRestrictedKey } from "../utils/config-env-export";
13
13
  import { validateEgressAllowlist } from "../utils/egress-guard";
14
14
  import { parseEnabledTools } from "../utils/enabled-tools";
15
+ import {
16
+ isFollowUpGateScheduleId,
17
+ parseFollowUpGateScheduleIds,
18
+ } from "../utils/follow-up-gate-schedule-ids";
19
+ import { kindForModelString } from "../utils/internal-ai/models";
15
20
  import { normalizeOrgWebsite } from "../utils/org-website";
16
21
  import { parseTaskToolManifest } from "../utils/task-tool-manifest";
17
22
  import { isTierConfigKey, validateTierConfigValue } from "./model-tier-keys";
@@ -306,6 +311,24 @@ export function validateRuntimeTokenTtlHours(value: unknown): string | null {
306
311
  : "Invalid RUNTIME_TOKEN_TTL_HOURS (must be greater than zero and at most 168)";
307
312
  }
308
313
 
314
+ /**
315
+ * `TASK_AUTO_TITLE_MODEL` pins the task-title model: `provider/model-id` for a
316
+ * provider the internal-ai layer can call, or a bare `claude -p` alias. Empty
317
+ * clears the pin. The model id itself is checked at call time (an unknown id
318
+ * logs and falls through to the next candidate).
319
+ */
320
+ export function validateTaskAutoTitleModel(value: unknown): string | null {
321
+ if (typeof value !== "string") return "Invalid TASK_AUTO_TITLE_MODEL (must be a string)";
322
+ const model = value.trim();
323
+ if (!model) return null;
324
+ if (/\s/.test(model) || model.endsWith("/")) {
325
+ return "Invalid TASK_AUTO_TITLE_MODEL (use provider/model-id or a claude -p alias)";
326
+ }
327
+ return kindForModelString(model)
328
+ ? null
329
+ : "Invalid TASK_AUTO_TITLE_MODEL (provider must be openrouter, anthropic, openai, or openai-codex)";
330
+ }
331
+
309
332
  export const validateControllerTokenTtlDays = boundedIntegerValidators(
310
333
  ["RUNTIME_CONTROLLER_TOKEN_TTL_DAYS"],
311
334
  1,
@@ -428,6 +451,13 @@ const VALIDATED_KEYS: Record<string, ConfigValidator> = {
428
451
  // model's weekly window (Fable/Opus/Sonnet). fallback: legacy random pick.
429
452
  ...enumValidator("MODEL_WINDOW_EXHAUSTED_POLICY", ["fail", "fallback"]),
430
453
  ...enumValidator("HEARTBEAT_CHECKLIST_GATE_MODE", ["shadow", "enforce"]),
454
+ // `enforce` is deliberately absent: the follow-up gate only records decisions.
455
+ ...enumValidator("FOLLOW_UP_GATE_MODE", ["off", "shadow"]),
456
+ FOLLOW_UP_GATE_SCHEDULE_IDS: (value: unknown) => {
457
+ const message = "Invalid FOLLOW_UP_GATE_SCHEDULE_IDS (must be comma-separated schedule UUIDs)";
458
+ if (typeof value !== "string") return message;
459
+ return parseFollowUpGateScheduleIds(value).every(isFollowUpGateScheduleId) ? null : message;
460
+ },
431
461
  ...enumValidator("SYSTEM_ONE_DEFAULT_PROVIDER", [...SYSTEM_ONE_DEFAULT_PROVIDER_OPTIONS]),
432
462
  // Codex credits-exhausted cooldown (ms). Permissive on range here (positive
433
463
  // integer) — the worker clamps to [5m, 7d] via resolveCodexCreditsExhaustedCooldownMs.
@@ -525,7 +555,9 @@ const VALIDATED_KEYS: Record<string, ConfigValidator> = {
525
555
  "MODEL_AUTO_UPGRADE",
526
556
  "COMB_ENABLED",
527
557
  "AGENT_FS_PROXY",
558
+ "TASK_AUTO_TITLE_ENABLED",
528
559
  ]),
560
+ TASK_AUTO_TITLE_MODEL: validateTaskAutoTitleModel,
529
561
  ...enumValidator("SLACK_MODE", ["socket", "http"]),
530
562
  ...enumValidator("SLACK_THREAD_STEERING", ["off", "lead", "all"]),
531
563
  ...enumValidator("SLACK_THREAD_STEERING_MODE", ["steer", "queue"]),
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Automatic task titles.
3
+ *
4
+ * Listens for `task.created` (emitted after commit by `createTaskExtended`),
5
+ * asks the cheapest configured model for a short title, and stores it on the
6
+ * task when nobody set one first. The sessions sidebar and task lists read
7
+ * `title` and fall back to the prompt, so a failed or skipped generation
8
+ * changes nothing visible.
9
+ *
10
+ * `TASK_AUTO_TITLE_ENABLED=false` turns the listener off at the next event;
11
+ * `TASK_AUTO_TITLE_MODEL` pins one model (see `buildTaskTitlePlan`).
12
+ */
13
+
14
+ import { getApiKey } from "../utils/api-key";
15
+ import { getMcpBaseUrl } from "../utils/constants";
16
+ import { parseEnvFlag } from "../utils/env-flag";
17
+ import { generateTaskTitle, TASK_TITLE_TIMEOUT_MS } from "../utils/internal-ai/task-title";
18
+ import { scrubSecrets } from "../utils/secret-scrubber";
19
+ import { workflowEventBus } from "../workflows/event-bus";
20
+ import { getTaskById } from "./db/tasks/read";
21
+ import { setTaskTitleIfUnset } from "./db/tasks/write";
22
+
23
+ /** Titles generated at once. Bursts (a workflow fan-out) queue behind this. */
24
+ export const TASK_AUTO_TITLE_CONCURRENCY = 3;
25
+
26
+ export function isTaskAutoTitleEnabled(env: NodeJS.ProcessEnv = process.env): boolean {
27
+ return parseEnvFlag(env.TASK_AUTO_TITLE_ENABLED, true);
28
+ }
29
+
30
+ export interface TaskAutoTitleListenerOptions {
31
+ /** Defaults to `process.env`; read on every event so a config reload applies. */
32
+ env?: NodeJS.ProcessEnv;
33
+ /** Test injection: replaces the model call. */
34
+ generate?: typeof generateTaskTitle;
35
+ }
36
+
37
+ export interface TaskAutoTitleListener {
38
+ stop(): void;
39
+ /** Resolves once every queued task has been processed. */
40
+ idle(): Promise<void>;
41
+ }
42
+
43
+ export function startTaskAutoTitleListener(
44
+ options: TaskAutoTitleListenerOptions = {},
45
+ ): TaskAutoTitleListener {
46
+ const env = options.env ?? process.env;
47
+ const generate = options.generate ?? generateTaskTitle;
48
+ const queue: string[] = [];
49
+ let active = 0;
50
+ let idleWaiters: Array<() => void> = [];
51
+
52
+ const settleIdle = () => {
53
+ if (active > 0 || queue.length > 0) return;
54
+ const waiters = idleWaiters;
55
+ idleWaiters = [];
56
+ for (const resolve of waiters) resolve();
57
+ };
58
+
59
+ const pump = () => {
60
+ while (active < TASK_AUTO_TITLE_CONCURRENCY) {
61
+ const taskId = queue.shift();
62
+ if (!taskId) break;
63
+ active++;
64
+ void titleTask(taskId, env, generate)
65
+ .catch((error) => console.error("[task-title] Failed:", scrubSecrets(String(error))))
66
+ .finally(() => {
67
+ active--;
68
+ pump();
69
+ settleIdle();
70
+ });
71
+ }
72
+ };
73
+
74
+ const onTaskCreated = (data: unknown) => {
75
+ if (!isTaskAutoTitleEnabled(env)) return;
76
+ const taskId = (data as { taskId?: unknown }).taskId;
77
+ if (typeof taskId !== "string" || !taskId) return;
78
+ queue.push(taskId);
79
+ pump();
80
+ };
81
+
82
+ workflowEventBus.on("task.created", onTaskCreated);
83
+ return {
84
+ stop: () => workflowEventBus.off("task.created", onTaskCreated),
85
+ idle: () =>
86
+ new Promise<void>((resolve) => {
87
+ idleWaiters.push(resolve);
88
+ settleIdle();
89
+ }),
90
+ };
91
+ }
92
+
93
+ async function titleTask(
94
+ taskId: string,
95
+ env: NodeJS.ProcessEnv,
96
+ generate: typeof generateTaskTitle,
97
+ ): Promise<void> {
98
+ const task = await getTaskById(taskId);
99
+ if (!task || task.title) return;
100
+ const shortId = taskId.slice(0, 8);
101
+ const startedAt = Date.now();
102
+ const result = await generate({
103
+ task: task.task,
104
+ env,
105
+ // Loopback context for the codex OAuth probe; only used when no API key is set.
106
+ apiUrl: getMcpBaseUrl(),
107
+ apiKey: getApiKey(),
108
+ callerTag: `task-title:${task.source}`,
109
+ signal: AbortSignal.timeout(TASK_TITLE_TIMEOUT_MS),
110
+ });
111
+ if (!result.ok) {
112
+ if (result.reason !== "short-task") {
113
+ console.log(`[task-title] ${shortId}: no title (${result.reason})`);
114
+ }
115
+ return;
116
+ }
117
+ const updated = await setTaskTitleIfUnset(taskId, result.title);
118
+ const elapsed = Date.now() - startedAt;
119
+ // The title derives from task text, so a pasted token could reach the log.
120
+ console.log(
121
+ scrubSecrets(
122
+ `[task-title] ${shortId}: "${result.title}" via ${result.model} in ${elapsed}ms${
123
+ updated ? "" : " (kept the existing title)"
124
+ }`,
125
+ ),
126
+ );
127
+ }
@@ -15,6 +15,10 @@ async function executeCommand(
15
15
  command: ControllerCommand,
16
16
  ): Promise<unknown> {
17
17
  switch (command.op) {
18
+ case "release-cache":
19
+ if (!controller.releaseCache) throw new Error("Controller does not support cache cleanup");
20
+ await controller.releaseCache(command.payload);
21
+ return {};
18
22
  case "provision":
19
23
  return await controller.provision(command.payload);
20
24
  case "terminate":
@@ -0,0 +1,74 @@
1
+ /** Worker boot step; deliberately standalone so both worker images can run it before setup. */
2
+ export interface ApiReadinessDependencies {
3
+ fetch: (url: string, init: RequestInit) => Promise<Response>;
4
+ now: () => number;
5
+ sleep: (ms: number) => Promise<void>;
6
+ log: (message: string) => void;
7
+ }
8
+
9
+ const defaults: ApiReadinessDependencies = {
10
+ fetch: (url, init) => fetch(url, init),
11
+ now: () => performance.now(),
12
+ sleep: (ms) => Bun.sleep(ms),
13
+ log: (message) => console.log(message),
14
+ };
15
+
16
+ /** Poll the unauthenticated health endpoint before any provider reads configuration. */
17
+ export async function waitForApiReady(
18
+ baseUrl: string,
19
+ timeoutValue = "90",
20
+ dependencies: Partial<ApiReadinessDependencies> = {},
21
+ ): Promise<boolean> {
22
+ const { fetch: probe, now, sleep, log } = { ...defaults, ...dependencies };
23
+ const raw = timeoutValue || "90";
24
+ const seconds = Number(raw);
25
+ if (!/^\d+$/.test(raw) || !Number.isSafeInteger(seconds) || seconds <= 0) {
26
+ log(
27
+ `[entrypoint] FATAL: WORKER_API_READY_TIMEOUT_SECONDS must be a positive integer, got '${raw}'; exiting.`,
28
+ );
29
+ return false;
30
+ }
31
+
32
+ const url = `${baseUrl.replace(/\/$/, "")}/health`;
33
+ const deadline = now() + seconds * 1000;
34
+ log(`[entrypoint] Waiting for API readiness at ${url} (timeout ${raw}s)...`);
35
+ while (true) {
36
+ const controller = new AbortController();
37
+ const timer = setTimeout(
38
+ () => controller.abort(),
39
+ Math.min(3000, Math.max(1, deadline - now())),
40
+ );
41
+ try {
42
+ const response = await probe(url, { redirect: "manual", signal: controller.signal });
43
+ // curl -o /dev/null waited for the whole response, without retaining its body.
44
+ if (response.body) {
45
+ for await (const _chunk of response.body) {
46
+ // Discard health output, including error bodies, within the request timeout.
47
+ }
48
+ }
49
+ // Match curl -f without -L: redirects are successful and are not followed.
50
+ if (response.status > 0 && response.status < 400 && now() <= deadline) {
51
+ log(`[entrypoint] API ready at ${url}`);
52
+ return true;
53
+ }
54
+ } catch {
55
+ // The API can still be booting; retry connection, HTTP and timeout failures alike.
56
+ } finally {
57
+ clearTimeout(timer);
58
+ }
59
+ if (now() >= deadline) {
60
+ log(`[entrypoint] FATAL: API readiness timed out after ${raw}s waiting for ${url}; exiting.`);
61
+ return false;
62
+ }
63
+ await sleep(Math.min(1000, deadline - now()));
64
+ }
65
+ }
66
+
67
+ if (import.meta.main) {
68
+ process.exitCode = (await waitForApiReady(
69
+ Bun.argv[2] ?? "http://host.docker.internal:3013",
70
+ Bun.argv[3],
71
+ ))
72
+ ? 0
73
+ : 1;
74
+ }
@@ -2,4 +2,4 @@
2
2
  * agent-fs image tag bundled by `agent-swarm onboard --yes`.
3
3
  * `bun run bump:agent-fs` rewrites it together with the other pins.
4
4
  */
5
- export const AGENT_FS_VERSION = "0.18.13";
5
+ export const AGENT_FS_VERSION = "0.18.14";