@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
@@ -15,6 +15,7 @@ import crypto from "node:crypto";
15
15
  import { unlink } from "node:fs/promises";
16
16
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
17
17
  import { closeDb, getPageBySlug, getPageVersions, initDb } from "../be/db";
18
+ import { COLOR_LITERAL_THRESHOLD, countColorLiterals } from "../pages/color-literals";
18
19
  import { registerCreatePageTool } from "../tools/create-page";
19
20
 
20
21
  const TEST_DB_PATH = "./test-create-page-tool.sqlite";
@@ -211,6 +212,506 @@ describe("create_page MCP tool", () => {
211
212
  expect(row?.passwordHash).not.toBe("example-open-sesame");
212
213
  expect(await Bun.password.verify("example-open-sesame", row!.passwordHash!)).toBe(true);
213
214
  });
215
+
216
+ describe("design fields", () => {
217
+ type ToolResult = {
218
+ isError?: boolean;
219
+ content: Array<{ text: string }>;
220
+ structuredContent: {
221
+ id?: string;
222
+ message: string;
223
+ details?: string;
224
+ nudge?: string;
225
+ designVersion?: number;
226
+ ignoredTokens?: string[];
227
+ colorLiteralCount?: number;
228
+ usesDesignSystem?: boolean;
229
+ lightOnlyColors?: boolean;
230
+ };
231
+ };
232
+
233
+ const NUDGE_TEXT = (count: number) =>
234
+ `This page hardcodes ${count} color values. Use theme tokens (var(--color-primary), bg-primary, text-muted-foreground) so theme and dark-mode switches restyle it. Until then the page stays light in a dark dashboard.`;
235
+ const V1_NUDGE =
236
+ "This page is on designVersion 1 (the original look), so design system classes, tokens and themes have no effect. Pass designVersion: 2.";
237
+ const LIGHT_ONLY_NUDGE =
238
+ "Your themeTokens set colors for light mode only. Dark mode keeps the preset's dark colors; add a dark map (or a .dark block) to restyle it too.";
239
+ const IGNORED_MESSAGE =
240
+ "Some theme tokens were ignored (outside the page token contract or referencing a variable outside it).";
241
+
242
+ const uniqueSlug = () => `design-${crypto.randomUUID().slice(0, 8)}`;
243
+
244
+ /** An HTML page whose <style> block holds exactly `count` color literals. */
245
+ const styledPage = (count: number) =>
246
+ `<h1>Report</h1><style>${Array.from(
247
+ { length: count },
248
+ (_, i) => `.c${i}{color:rgb(${i} 0 0)}`,
249
+ ).join("")}</style>`;
250
+
251
+ async function call(args: Record<string, unknown>): Promise<ToolResult> {
252
+ const tool = buildServer();
253
+ return (await tool.handler(
254
+ {
255
+ title: "Design Page",
256
+ slug: uniqueSlug(),
257
+ body: "<h1>hi</h1>",
258
+ contentType: "text/html",
259
+ authMode: "public",
260
+ ...args,
261
+ },
262
+ fakeMeta,
263
+ )) as ToolResult;
264
+ }
265
+
266
+ test("a new page gets design version 2 with no theme and no tokens", async () => {
267
+ const slug = uniqueSlug();
268
+ const result = await call({ slug });
269
+ expect(result.isError).toBe(false);
270
+ const row = await getPageBySlug(agentId, slug);
271
+ expect(row?.designVersion).toBe(2);
272
+ expect(row?.theme).toBeNull();
273
+ expect(row?.themeTokens).toBeNull();
274
+ expect(result.structuredContent.ignoredTokens).toBeUndefined();
275
+ });
276
+
277
+ test("an explicit design version and a preset theme are stored on create", async () => {
278
+ const slug = uniqueSlug();
279
+ await call({ slug, theme: "paper", designVersion: 1 });
280
+ const row = await getPageBySlug(agentId, slug);
281
+ expect(row?.theme).toBe("paper");
282
+ expect(row?.designVersion).toBe(1);
283
+ });
284
+
285
+ test("an unknown theme returns an error that lists the presets and saves nothing", async () => {
286
+ const slug = uniqueSlug();
287
+ const result = await call({ slug, theme: "neon" });
288
+ expect(result.isError).toBe(true);
289
+ expect(result.structuredContent.message).toContain('Unknown theme "neon"');
290
+ for (const id of ["hive", "paper", "github"]) {
291
+ expect(result.structuredContent.message).toContain(id);
292
+ }
293
+ expect(await getPageBySlug(agentId, slug)).toBeNull();
294
+ });
295
+
296
+ test("shadcn-style tokens are stored normalized and the ignored names are reported", async () => {
297
+ const slug = uniqueSlug();
298
+ const result = await call({
299
+ slug,
300
+ themeTokens: {
301
+ light: {
302
+ "--primary": "oklch(0.55 0.2 260)",
303
+ background: "oklch(1 0 0)",
304
+ radius: "0.5rem",
305
+ "--sidebar": "oklch(0.9 0 0)",
306
+ "--shadow-2xs": "0 1px 2px #0000",
307
+ },
308
+ dark: { "--primary": "oklch(0.7 0.15 260)" },
309
+ },
310
+ });
311
+ expect(result.isError).toBe(false);
312
+ expect(result.structuredContent.ignoredTokens).toEqual(["--sidebar", "--shadow-2xs"]);
313
+ expect(result.structuredContent.message).toContain(IGNORED_MESSAGE);
314
+ expect(result.structuredContent.details).toContain(
315
+ "Ignored theme tokens: --sidebar, --shadow-2xs",
316
+ );
317
+ expect(result.content[0]!.text).toContain("--shadow-2xs");
318
+
319
+ const row = await getPageBySlug(agentId, slug);
320
+ expect(row?.themeTokens).toEqual({
321
+ light: {
322
+ "--color-primary": "oklch(0.55 0.2 260)",
323
+ "--color-background": "oklch(1 0 0)",
324
+ "--radius-sm": "calc(0.5rem - 4px)",
325
+ "--radius-md": "calc(0.5rem - 2px)",
326
+ "--radius-lg": "0.5rem",
327
+ "--radius-xl": "calc(0.5rem + 4px)",
328
+ },
329
+ dark: { "--color-primary": "oklch(0.7 0.15 260)" },
330
+ });
331
+ });
332
+
333
+ test("tokens with nothing to ignore report nothing", async () => {
334
+ const result = await call({
335
+ themeTokens: { light: { "--color-primary": "oklch(0.55 0.2 260)" } },
336
+ });
337
+ expect(result.isError).toBe(false);
338
+ expect(result.structuredContent.ignoredTokens).toBeUndefined();
339
+ expect(result.structuredContent.message).not.toContain("ignored");
340
+ expect(result.structuredContent.details).not.toContain("Ignored");
341
+ });
342
+
343
+ test("the css format works", async () => {
344
+ const slug = uniqueSlug();
345
+ const result = await call({
346
+ slug,
347
+ themeTokens: {
348
+ format: "css",
349
+ css: ":root { --primary: 222.2 47.4% 11.2%; --tracking-normal: 0em; } .dark { --primary: 210 40% 98%; }",
350
+ },
351
+ });
352
+ expect(result.isError).toBe(false);
353
+ expect(result.structuredContent.ignoredTokens).toEqual(["--tracking-normal"]);
354
+ const row = await getPageBySlug(agentId, slug);
355
+ expect(row?.themeTokens).toEqual({
356
+ light: { "--color-primary": "hsl(222.2 47.4% 11.2%)" },
357
+ dark: { "--color-primary": "hsl(210 40% 98%)" },
358
+ });
359
+ });
360
+
361
+ test("bad themeTokens return an error that names the problem and save nothing", async () => {
362
+ const slug = uniqueSlug();
363
+ const unsafe = await call({
364
+ slug,
365
+ themeTokens: { light: { "--color-primary": "red; } body { display: none" } },
366
+ });
367
+ expect(unsafe.isError).toBe(true);
368
+ expect(unsafe.structuredContent.message).toContain("Invalid themeTokens");
369
+ expect(unsafe.structuredContent.message).toContain("--color-primary");
370
+
371
+ const missingCss = await call({ slug, themeTokens: { format: "css" } });
372
+ expect(missingCss.isError).toBe(true);
373
+ expect(missingCss.structuredContent.message).toContain("Invalid themeTokens");
374
+
375
+ expect(await getPageBySlug(agentId, slug)).toBeNull();
376
+ });
377
+
378
+ test("an upsert keeps the design fields it omits and clears them with null", async () => {
379
+ const slug = uniqueSlug();
380
+ const tokens = { light: { "--color-ring": "oklch(0.6 0.2 30)" } };
381
+ await call({ slug, theme: "paper", themeTokens: tokens, designVersion: 1 });
382
+ const stored = {
383
+ theme: "paper",
384
+ themeTokens: { light: { "--color-ring": "oklch(0.6 0.2 30)" }, dark: {} },
385
+ };
386
+
387
+ // Omitted fields keep the stored values, including an older design version.
388
+ await call({ slug, body: "<h1>v2</h1>" });
389
+ let row = await getPageBySlug(agentId, slug);
390
+ expect(row?.body).toBe("<h1>v2</h1>");
391
+ expect(row).toMatchObject({ ...stored, designVersion: 1 });
392
+
393
+ // A given version moves the page. The theme and the tokens stay.
394
+ await call({ slug, body: "<h1>v3</h1>", designVersion: 2 });
395
+ row = await getPageBySlug(agentId, slug);
396
+ expect(row).toMatchObject({ ...stored, designVersion: 2 });
397
+
398
+ // A new theme replaces the old one. The tokens stay.
399
+ await call({ slug, body: "<h1>v4</h1>", theme: "github" });
400
+ row = await getPageBySlug(agentId, slug);
401
+ expect(row).toMatchObject({ theme: "github", themeTokens: stored.themeTokens });
402
+
403
+ // null clears one field at a time.
404
+ await call({ slug, body: "<h1>v5</h1>", theme: null });
405
+ row = await getPageBySlug(agentId, slug);
406
+ expect(row?.theme).toBeNull();
407
+ expect(row?.themeTokens).toEqual(stored.themeTokens);
408
+ await call({ slug, body: "<h1>v6</h1>", themeTokens: null });
409
+ row = await getPageBySlug(agentId, slug);
410
+ expect(row?.themeTokens).toBeNull();
411
+ expect(row?.designVersion).toBe(2);
412
+ });
413
+
414
+ test("a rejected upsert leaves the page and its history alone", async () => {
415
+ const slug = uniqueSlug();
416
+ const first = await call({ slug, body: "<h1>kept</h1>", theme: "paper" });
417
+ const result = await call({ slug, body: "<h1>lost</h1>", theme: "neon" });
418
+ expect(result.isError).toBe(true);
419
+
420
+ const row = await getPageBySlug(agentId, slug);
421
+ expect(row?.body).toBe("<h1>kept</h1>");
422
+ expect(row?.theme).toBe("paper");
423
+ expect(await getPageVersions(first.structuredContent.id!)).toHaveLength(0);
424
+ });
425
+
426
+ test("a reference outside the contract is reported with the reference", async () => {
427
+ const result = await call({
428
+ themeTokens: {
429
+ format: "css",
430
+ css: ":root { --primary: oklch(0.55 0.2 260); --font-sans: var(--font-geist-sans); } .dark { --primary: oklch(0.7 0.15 260); }",
431
+ },
432
+ });
433
+ expect(result.isError).toBe(false);
434
+ expect(result.structuredContent.ignoredTokens).toEqual([
435
+ "--font-sans (references --font-geist-sans)",
436
+ ]);
437
+ expect(result.structuredContent.message).toContain(IGNORED_MESSAGE);
438
+ expect(result.structuredContent.details).toContain(
439
+ "Ignored theme tokens: --font-sans (references --font-geist-sans)",
440
+ );
441
+ });
442
+
443
+ test("the result reports the saved design version in data and in the message", async () => {
444
+ const slug = uniqueSlug();
445
+ const created = await call({ slug });
446
+ expect(created.structuredContent.designVersion).toBe(2);
447
+ expect(created.structuredContent.message).toContain("designVersion=2)");
448
+
449
+ const legacy = await call({ designVersion: 1 });
450
+ expect(legacy.structuredContent.designVersion).toBe(1);
451
+ expect(legacy.structuredContent.message).toContain("designVersion=1)");
452
+
453
+ const json = await call({ body: '{"type":"text"}', contentType: "application/json" });
454
+ expect(json.structuredContent.designVersion).toBe(2);
455
+ expect(json.structuredContent.usesDesignSystem).toBeUndefined();
456
+ expect(json.structuredContent.lightOnlyColors).toBeUndefined();
457
+ });
458
+
459
+ test("a design version 1 HTML page that uses the design system gets the version nudge", async () => {
460
+ const designSystemCalls: Array<Record<string, unknown>> = [
461
+ { body: '<main class="swarm-page"><h1>Report</h1></main>' },
462
+ { body: "<p class='lead swarm-lede'>x</p>" },
463
+ { body: "<style>.a{color:var(--color-primary)}</style>" },
464
+ { theme: "paper" },
465
+ { themeTokens: { light: { "--radius-lg": "8px" } } },
466
+ ];
467
+ for (const extra of designSystemCalls) {
468
+ const result = await call({ designVersion: 1, ...extra });
469
+ expect(result.isError).toBe(false);
470
+ expect(result.structuredContent.usesDesignSystem).toBe(true);
471
+ expect(result.structuredContent.nudge).toBe(V1_NUDGE);
472
+ }
473
+ });
474
+
475
+ test("the version nudge stays quiet on plain v1 pages, on v2 pages and on other content types", async () => {
476
+ const plain = await call({
477
+ designVersion: 1,
478
+ // <swarm-diff> works on both design versions, and a cleared theme is not a theme.
479
+ body: '<h1>PR</h1><swarm-diff>{"files":[]}</swarm-diff><p class="not-swarm-x">x</p>',
480
+ theme: null,
481
+ themeTokens: null,
482
+ });
483
+ expect(plain.structuredContent.usesDesignSystem).toBe(false);
484
+ expect(plain.structuredContent.nudge).toBeUndefined();
485
+
486
+ const v2 = await call({ body: '<main class="swarm-page">x</main>', theme: "paper" });
487
+ expect(v2.structuredContent.usesDesignSystem).toBe(true);
488
+ expect(v2.structuredContent.nudge).toBeUndefined();
489
+
490
+ const svg = await call({
491
+ designVersion: 1,
492
+ body: '<svg xmlns="http://www.w3.org/2000/svg" class="swarm-page"/>',
493
+ contentType: "image/svg+xml",
494
+ theme: "paper",
495
+ });
496
+ expect(svg.structuredContent.usesDesignSystem).toBeUndefined();
497
+ expect(svg.structuredContent.nudge).toBeUndefined();
498
+ });
499
+
500
+ test("an upsert that keeps a design version 1 page gets the version nudge", async () => {
501
+ const slug = uniqueSlug();
502
+ await call({ slug, body: "<h1>old</h1>", designVersion: 1 });
503
+ const rewritten = await call({ slug, body: '<main class="swarm-page">new</main>' });
504
+ expect(rewritten.structuredContent.designVersion).toBe(1);
505
+ expect(rewritten.structuredContent.nudge).toBe(V1_NUDGE);
506
+
507
+ const moved = await call({
508
+ slug,
509
+ body: '<main class="swarm-page">new</main>',
510
+ designVersion: 2,
511
+ });
512
+ expect(moved.structuredContent.designVersion).toBe(2);
513
+ expect(moved.structuredContent.nudge).toBeUndefined();
514
+ });
515
+
516
+ test("stored tokens with light colors and no dark colors get the light-only nudge", async () => {
517
+ const slug = uniqueSlug();
518
+ const lightOnly = await call({
519
+ slug,
520
+ themeTokens: { light: { "--color-primary": "oklch(0.55 0.2 260)", "--radius-lg": "8px" } },
521
+ });
522
+ expect(lightOnly.structuredContent.lightOnlyColors).toBe(true);
523
+ expect(lightOnly.structuredContent.nudge).toBe(LIGHT_ONLY_NUDGE);
524
+
525
+ // The note follows the stored tokens, so an upsert that keeps them repeats it.
526
+ const kept = await call({ slug, body: "<h1>again</h1>" });
527
+ expect(kept.structuredContent.lightOnlyColors).toBe(true);
528
+ expect(kept.structuredContent.nudge).toBe(LIGHT_ONLY_NUDGE);
529
+
530
+ const quiet: Array<Record<string, unknown>> = [
531
+ { themeTokens: { light: { "--font-sans": "Inter", "--radius-lg": "8px" } } },
532
+ {
533
+ themeTokens: {
534
+ light: { "--color-primary": "oklch(0.55 0.2 260)" },
535
+ dark: { "--color-primary": "oklch(0.7 0.15 260)" },
536
+ },
537
+ },
538
+ { themeTokens: { dark: { "--color-primary": "oklch(0.7 0.15 260)" } } },
539
+ {},
540
+ ];
541
+ for (const extra of quiet) {
542
+ const result = await call(extra);
543
+ expect(result.structuredContent.lightOnlyColors).toBe(false);
544
+ expect(result.structuredContent.nudge).toBeUndefined();
545
+ }
546
+ });
547
+
548
+ test("the nudge joins every sentence that applies, in a fixed order", async () => {
549
+ const lightOnly = { light: { "--color-primary": "oklch(0.55 0.2 260)" } };
550
+ const legacy = await call({ designVersion: 1, themeTokens: lightOnly });
551
+ expect(legacy.structuredContent.nudge).toBe(`${V1_NUDGE} ${LIGHT_ONLY_NUDGE}`);
552
+
553
+ const current = await call({ body: styledPage(9), themeTokens: lightOnly });
554
+ expect(current.structuredContent.nudge).toBe(`${LIGHT_ONLY_NUDGE} ${NUDGE_TEXT(9)}`);
555
+ expect(current.content[0]!.text).toContain(`${LIGHT_ONLY_NUDGE} ${NUDGE_TEXT(9)}`);
556
+ });
557
+
558
+ test("the color nudge fires at 8 color literals and not at 7", async () => {
559
+ // The page server shares this threshold: from 8 on, dark mode keeps the page light.
560
+ expect(COLOR_LITERAL_THRESHOLD).toBe(8);
561
+ const seven = await call({ body: styledPage(7) });
562
+ expect(seven.structuredContent.colorLiteralCount).toBe(7);
563
+ expect(seven.structuredContent.nudge).toBeUndefined();
564
+
565
+ const eight = await call({ body: styledPage(8) });
566
+ expect(eight.structuredContent.colorLiteralCount).toBe(8);
567
+ expect(eight.structuredContent.nudge).toBe(NUDGE_TEXT(8));
568
+ expect(eight.content[0]!.text).toContain(NUDGE_TEXT(8));
569
+ });
570
+
571
+ test("the color count covers style attributes and ignores colors elsewhere", async () => {
572
+ const result = await call({
573
+ body: [
574
+ "<style>.a{color:#fff;background:#0f172a}</style>",
575
+ '<p style="color:#2563eb;border:1px solid hsl(0 0% 90%)">a</p>',
576
+ "<p style='fill:oklch(0.5 0.2 30)'>b</p>",
577
+ // Not counted: text, script, and attributes that are not style.
578
+ "<p>Use #fff or rgb(0 0 0) for white.</p>",
579
+ '<script>const c = "#ff0000";</script>',
580
+ '<svg fill="#fff"><circle stroke="rgb(0 0 0)"/></svg>',
581
+ ].join(""),
582
+ });
583
+ expect(result.structuredContent.colorLiteralCount).toBe(5);
584
+ });
585
+
586
+ test("pages on design version 1, and pages that are not HTML, get no color count or nudge", async () => {
587
+ const legacy = await call({ body: styledPage(20), designVersion: 1 });
588
+ expect(legacy.structuredContent.colorLiteralCount).toBeUndefined();
589
+ expect(legacy.structuredContent.nudge).toBeUndefined();
590
+
591
+ const json = await call({
592
+ body: JSON.stringify({ note: styledPage(20) }),
593
+ contentType: "application/json",
594
+ });
595
+ expect(json.structuredContent.colorLiteralCount).toBeUndefined();
596
+ expect(json.structuredContent.nudge).toBeUndefined();
597
+
598
+ const svg = await call({
599
+ body: `<svg xmlns="http://www.w3.org/2000/svg">${styledPage(20)}</svg>`,
600
+ contentType: "image/svg+xml",
601
+ });
602
+ expect(svg.structuredContent.colorLiteralCount).toBeUndefined();
603
+ expect(svg.structuredContent.nudge).toBeUndefined();
604
+ });
605
+
606
+ test("an upsert is judged by the design version the page keeps", async () => {
607
+ const slug = uniqueSlug();
608
+ await call({ slug, body: "<h1>old</h1>", designVersion: 1 });
609
+ const kept = await call({ slug, body: styledPage(20) });
610
+ expect(kept.structuredContent.colorLiteralCount).toBeUndefined();
611
+ expect(kept.structuredContent.nudge).toBeUndefined();
612
+
613
+ const moved = await call({ slug, body: styledPage(20), designVersion: 2 });
614
+ expect(moved.structuredContent.colorLiteralCount).toBe(20);
615
+ expect(moved.structuredContent.nudge).toBe(NUDGE_TEXT(20));
616
+ });
617
+
618
+ test("a failed save carries no nudge", async () => {
619
+ const result = await call({ body: styledPage(20), theme: "neon" });
620
+ expect(result.isError).toBe(true);
621
+ expect(result.structuredContent.nudge).toBeUndefined();
622
+ });
623
+ });
624
+ });
625
+
626
+ describe("countColorLiterals", () => {
627
+ test("counts every color literal form in a style block", () => {
628
+ const css =
629
+ ".a{color:#fff;background:#ffffff;border-color:#ffffff80;outline-color:#fffa}" +
630
+ ".b{color:rgb(0 0 0);fill:rgba(0,0,0,.5);stroke:hsl(0 0% 0%);x:hsla(0,0%,0%,.5);y:oklch(0.5 0.2 30)}";
631
+ expect(countColorLiterals(`<style>${css}</style>`)).toBe(9);
632
+ });
633
+
634
+ test("counts style attributes with either quote and every style block", () => {
635
+ const html =
636
+ "<p style=\"color:#fff\">a</p><p style='background:rgb(0 0 0)'>b</p>" +
637
+ "<style>.a{color:#000}</style><style media=print>.b{color:#111}</style>";
638
+ expect(countColorLiterals(html)).toBe(4);
639
+ });
640
+
641
+ test("is case-insensitive", () => {
642
+ expect(countColorLiterals("<STYLE>.a{color:#FFF;b:RGB(0 0 0)}</STYLE>")).toBe(2);
643
+ });
644
+
645
+ test("counts an unclosed style block to the end of the body", () => {
646
+ expect(countColorLiterals("<style>.a{color:#fff}.b{color:#000}")).toBe(2);
647
+ });
648
+
649
+ test("ignores colors outside style regions", () => {
650
+ const html =
651
+ "<p>#fff rgb(0 0 0)</p><script>var c = '#ff0000'</script>" +
652
+ '<svg fill="#fff"></svg><p data-style="color:#fff"></p>';
653
+ expect(countColorLiterals(html)).toBe(0);
654
+ });
655
+
656
+ test("ignores hex-like text that is not a color", () => {
657
+ const css = ".a{width:#12345}.b{x:#1234567}#header{}.c{content:'#xyz'}.d{color:var(--c)}";
658
+ expect(countColorLiterals(`<style>${css}</style>`)).toBe(0);
659
+ });
660
+
661
+ test("counts white and black as values in style regions, not inside names", () => {
662
+ const css =
663
+ ".a{color:white;background:black;border:1px solid white}" +
664
+ ".b{white-space:nowrap;--black:1;font-family:'Archivo Black'}.white{}#black{}";
665
+ expect(countColorLiterals(`<style>${css}</style>`)).toBe(3);
666
+ expect(countColorLiterals('<p style="background: black">x</p>')).toBe(1);
667
+ expect(countColorLiterals("<p>white on black</p>")).toBe(0);
668
+ });
669
+
670
+ test("counts color classes in class attributes", () => {
671
+ const palette =
672
+ '<p class="bg-blue-500 hover:text-gray-600/80 dark:border-zinc-800 !ring-red-50">a</p>' +
673
+ "<p class='from-sky-400 via-violet-500 to-rose-950 shadow-slate-900 divide-stone-100'>b</p>" +
674
+ '<p class="outline-amber-300 decoration-lime-200 accent-teal-700 caret-cyan-600">c</p>' +
675
+ '<p class="placeholder-neutral-400 fill-emerald-500 stroke-indigo-500 text-fuchsia-500">d</p>' +
676
+ '<p class="bg-orange-100 text-yellow-800 border-green-300 bg-purple-50 text-pink-400">e</p>';
677
+ expect(countColorLiterals(palette)).toBe(22);
678
+ expect(countColorLiterals('<p class="bg-white text-black bg-white/80 text-white">x</p>')).toBe(
679
+ 4,
680
+ );
681
+ expect(
682
+ countColorLiterals(
683
+ '<p class="bg-[#fff] text-[rgb(0_0_0)] border-[hsl(0_0%_90%)] ring-[oklch(0.5_0.2_30)]">x</p>',
684
+ ),
685
+ ).toBe(4);
686
+ });
687
+
688
+ test("ignores token classes, other class names, and color classes outside class attributes", () => {
689
+ const html =
690
+ '<p class="bg-card text-muted-foreground border-border bg-primary swarm-panel">a</p>' +
691
+ '<p class="border-white ring-black my-bg-red-500 bg-red text-red bg-blue-500x">b</p>' +
692
+ '<p data-class="bg-red-500">c</p><p>bg-red-500 text-white</p>' +
693
+ "<style>.bg-white{display:block}</style>";
694
+ expect(countColorLiterals(html)).toBe(0);
695
+ });
696
+
697
+ test("returns 0 for a body without style regions", () => {
698
+ expect(countColorLiterals("")).toBe(0);
699
+ expect(countColorLiterals("<h1>plain</h1>")).toBe(0);
700
+ });
701
+
702
+ test("stays fast on a body of many unterminated tags", () => {
703
+ // 20,000 "<style " tags with no ">" cost about a second with an unbounded
704
+ // attribute match and about 2 ms with the capped one.
705
+ const started = performance.now();
706
+ expect(countColorLiterals("<style ".repeat(20_000))).toBe(0);
707
+ expect(countColorLiterals(' style="'.repeat(20_000))).toBe(0);
708
+ expect(countColorLiterals(' class="'.repeat(20_000))).toBe(0);
709
+ expect(countColorLiterals(`<p class="${"bg-red-".repeat(20_000)}">`)).toBe(0);
710
+ expect(countColorLiterals(`<p class="bg-red-${"5".repeat(100_000)}x">`)).toBe(0);
711
+ expect(countColorLiterals(`<p class="${"-[".repeat(50_000)}">`)).toBe(0);
712
+ expect(countColorLiterals(`<style>${"whites ".repeat(20_000)}</style>`)).toBe(0);
713
+ expect(performance.now() - started).toBeLessThan(500);
714
+ });
214
715
  });
215
716
 
216
717
  describe("create_page MCP tool capability gating", () => {
@@ -306,4 +306,85 @@ describe("db-client SQLITE_BUSY retry", () => {
306
306
  expect(rows.map((r) => r.name)).toEqual(["pre-existing"]);
307
307
  await released;
308
308
  });
309
+ test("a crowd of queued writers does not spin the event loop while a holder persists", async () => {
310
+ // Regression for the 31/10 stalls: a long external holder (a Litestream
311
+ // snapshot) with many writers queued. Each writer used to spin its full
312
+ // attemptSpinMs inside the FIFO lock on every attempt, so a round of
313
+ // attempts blocked the loop for writers x spin and the nominal retry
314
+ // budget stretched to minutes.
315
+ const crowdClient = createBunSqliteClient(() => main, {
316
+ maxWaitMs: 4_000,
317
+ backoffMs: [25, 50, 100, 250, ...Array<number>(20).fill(250)],
318
+ attemptSpinMs: 50,
319
+ finalAttemptSpinMs: 50,
320
+ });
321
+ const writers = 60;
322
+ const released = holdWriteLock(1_500);
323
+
324
+ let maxLagMs = 0;
325
+ let last = performance.now();
326
+ const ticker = setInterval(() => {
327
+ const now = performance.now();
328
+ maxLagMs = Math.max(maxLagMs, now - last - 10);
329
+ last = now;
330
+ }, 10);
331
+
332
+ const startedAt = performance.now();
333
+ const results = await Promise.allSettled(
334
+ Array.from({ length: writers }, (_, i) =>
335
+ crowdClient.run("INSERT INTO items (name) VALUES (?)", [`crowd-${i}`]),
336
+ ),
337
+ );
338
+ const wallMs = performance.now() - startedAt;
339
+ clearInterval(ticker);
340
+ await released;
341
+
342
+ expect(results.every((r) => r.status === "fulfilled")).toBe(true);
343
+ // Ungated: 60 writers x 50ms = 3s of blocked loop per round of attempts.
344
+ expect(maxLagMs).toBeLessThan(500);
345
+ // Ungated the crowd needed several seconds of spin rounds after release.
346
+ expect(wallMs).toBeLessThan(3_000);
347
+ const rows = await crowdClient.query<{ n: number }>(
348
+ "SELECT COUNT(*) AS n FROM items WHERE name LIKE 'crowd-%'",
349
+ );
350
+ expect(rows[0]?.n).toBe(writers);
351
+ });
352
+
353
+ test("a held lock past the budget fails fast for the whole crowd", async () => {
354
+ const crowdClient = createBunSqliteClient(() => main, {
355
+ maxWaitMs: 400,
356
+ backoffMs: [25, 50, 100, 250],
357
+ attemptSpinMs: 50,
358
+ finalAttemptSpinMs: 50,
359
+ });
360
+ const released = holdWriteLock(2_500);
361
+ const startedAt = performance.now();
362
+ const results = await Promise.allSettled(
363
+ Array.from({ length: 40 }, (_, i) =>
364
+ crowdClient.run("INSERT INTO items (name) VALUES (?)", [`doomed-${i}`]),
365
+ ),
366
+ );
367
+ const wallMs = performance.now() - startedAt;
368
+ await released;
369
+ expect(results.every((r) => r.status === "rejected")).toBe(true);
370
+ for (const r of results) {
371
+ expect((r as PromiseRejectedResult).reason.code).toBe("SQLITE_BUSY");
372
+ }
373
+ // Ungated: 40 writers x 5 spins x 50ms = 10s. Gated: well under 2s.
374
+ expect(wallMs).toBeLessThan(2_000);
375
+ });
376
+
377
+ test("the gate closes once a blocked write lands, so the next BUSY probes afresh", async () => {
378
+ const released = holdWriteLock(150);
379
+ await client.run("INSERT INTO items (name) VALUES (?)", ["after-hold"]);
380
+ await released;
381
+ // New episode: a second, short hold is still bridged.
382
+ const second = holdWriteLock(100);
383
+ await client.run("INSERT INTO items (name) VALUES (?)", ["after-second-hold"]);
384
+ await second;
385
+ const rows = await client.query<{ name: string }>(
386
+ "SELECT name FROM items WHERE name LIKE 'after-%' ORDER BY name",
387
+ );
388
+ expect(rows.map((r) => r.name)).toEqual(["after-hold", "after-second-hold"]);
389
+ });
309
390
  });
@@ -475,6 +475,27 @@ describe("db-client slow statement warning", () => {
475
475
  }
476
476
  });
477
477
 
478
+ test("does not count a slow database open against the first statement", async () => {
479
+ // The first call opens the database (and applies migrations) through the
480
+ // client's getter. That cost is boot work, not the statement's.
481
+ let opened = false;
482
+ const lazy = createBunSqliteClient(() => {
483
+ if (!opened) {
484
+ opened = true;
485
+ Bun.sleepSync(150);
486
+ }
487
+ return raw;
488
+ });
489
+ const warn = spyOn(console, "warn").mockImplementation(() => {});
490
+ try {
491
+ await lazy.query("SELECT name FROM items");
492
+ expect(opened).toBe(true);
493
+ expect(slowWarnings(warn)).toHaveLength(0);
494
+ } finally {
495
+ warn.mockRestore();
496
+ }
497
+ });
498
+
478
499
  test("does not log fast statements", async () => {
479
500
  const warn = spyOn(console, "warn").mockImplementation(() => {});
480
501
  try {
@@ -144,6 +144,7 @@ value:completeApprovalCancellationNotificationClaim
144
144
  value:completeTask
145
145
  value:computeContentHash
146
146
  value:countActiveScriptRuns
147
+ value:countAgentDispatchesInThread
147
148
  value:countAllMetrics
148
149
  value:countAllPages
149
150
  value:countKv
@@ -218,9 +219,10 @@ value:extensionAgentAssignmentError
218
219
  value:failPendingResumeIfUnclaimed
219
220
  value:failTask
220
221
  value:findActiveTasksByVcs
221
- value:findCompletedTaskInThread
222
+ value:findCompletedTasksInThread
222
223
  value:findExistingLinearTrackerContextWork
223
224
  value:findRecentCancelledTaskInThread
225
+ value:findRecentFailedTaskInThread
224
226
  value:findRecentSimilarTasks
225
227
  value:findTaskByAgentMailThread
226
228
  value:findTaskByGitHub
@@ -369,6 +371,7 @@ value:getScheduledTasks
369
371
  value:getScriptRun
370
372
  value:getScriptRunByIdempotencyKey
371
373
  value:getScriptRunExecutionArgs
374
+ value:getScriptRunFollowUpGuard
372
375
  value:getScriptRunJournalStep
373
376
  value:getServiceByAgentAndName
374
377
  value:getServiceById
@@ -561,7 +564,9 @@ value:setApiKeyName
561
564
  value:setApiKeyPlan
562
565
  value:setBudgetRefusalFollowUpTaskId
563
566
  value:setFavorite
567
+ value:setScriptRunFollowUpGuard
564
568
  value:setSlackMessageTracking
569
+ value:setTaskTitleIfUnset
565
570
  value:setUserFavorite
566
571
  value:settleSupersededTaskDependents
567
572
  value:shouldBlockPolling
@@ -622,10 +627,12 @@ type:PendingChatRelay
622
627
  type:TaskFilters
623
628
  value:SUPERSEDED_RESUME_REPAIR_WINDOW_MS
624
629
  value:TASK_LINEAGE_LIMITS
630
+ value:countAgentDispatchesInThread
625
631
  value:findActiveTasksByVcs
626
- value:findCompletedTaskInThread
632
+ value:findCompletedTasksInThread
627
633
  value:findExistingLinearTrackerContextWork
628
634
  value:findRecentCancelledTaskInThread
635
+ value:findRecentFailedTaskInThread
629
636
  value:findTaskByGitHub
630
637
  value:findTaskByVcs
631
638
  value:getAgentCurrentTask