@desplega.ai/agent-swarm 1.173.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (511) hide show
  1. package/README.md +1 -1
  2. package/dist/{acp-adapter-ccm9xrep.js → acp-adapter-kr4abrsk.js} +17 -16
  3. package/dist/{actions-sgeyv08k.js → actions-eh4g6w2r.js} +39 -34
  4. package/dist/{amp-adapter-gpxp3kc9.js → amp-adapter-7h7wdt8q.js} +8 -7
  5. package/dist/{app-km9qnv2f.js → app-7k8s9w5p.js} +18 -14
  6. package/dist/{assistant-5r8tcvvd.js → assistant-wswt2y7p.js} +40 -35
  7. package/dist/{boot-reembed-16rv9843.js → boot-reembed-9apm9qw6.js} +10 -9
  8. package/dist/{boot-reembed-swwzdgd7.js → boot-reembed-whzbdthj.js} +10 -9
  9. package/dist/{boot-scrub-sweep-zkr6vkqb.js → boot-scrub-sweep-ndzmvmgt.js} +11 -10
  10. package/dist/{boot-seeds-d8sp49t2.js → boot-seeds-e9sta0yt.js} +16 -13
  11. package/dist/{claude-adapter-yzzzh8jp.js → claude-adapter-4mqd3r6r.js} +19 -18
  12. package/dist/{claude-managed-adapter-xgxhqrt2.js → claude-managed-adapter-9wswb52m.js} +7 -5
  13. package/dist/{claude-managed-setup-bnmjarjc.js → claude-managed-setup-tpe1effx.js} +6 -4
  14. package/dist/{claude-sdk-session-4whxere4.js → claude-sdk-session-q49ghmh9.js} +12402 -11850
  15. package/dist/{cli-ne7smj1b.js → cli-01xvj1rq.js} +10 -10
  16. package/dist/{cli-nwrdx840.js → cli-035vekcw.js} +3 -3
  17. package/dist/{cli-9xf7fdtp.js → cli-0pd8hzhc.js} +29 -6
  18. package/dist/{cli-tmhsxhb8.js → cli-0veb92a3.js} +28 -6
  19. package/dist/{cli-c3rk1v0x.js → cli-10jesr25.js} +3 -3
  20. package/dist/{cli-6thk4jz3.js → cli-1y7yywmj.js} +6 -6
  21. package/dist/{cli-6r2f7edk.js → cli-20krdhz5.js} +47 -19
  22. package/dist/{cli-rfs4vddv.js → cli-29rja109.js} +39 -20
  23. package/dist/cli-2m1ktfzz.js +17795 -0
  24. package/dist/{cli-na2pxe4f.js → cli-2pwa0q4d.js} +15 -4
  25. package/dist/{cli-7a93cv8c.js → cli-4mengw46.js} +441 -43
  26. package/dist/{cli-94b48jhk.js → cli-5twdke5x.js} +16 -14
  27. package/dist/{cli-7a0sde2x.js → cli-6cagzc96.js} +9 -0
  28. package/dist/cli-6cvfxs8r.js +47 -0
  29. package/dist/{cli-w1apjecb.js → cli-711vwbxn.js} +2 -2
  30. package/dist/{cli-r4fqfpwf.js → cli-78vv7446.js} +50 -27
  31. package/dist/{cli-srqq3gc5.js → cli-7n6s5man.js} +5 -5
  32. package/dist/{cli-m89t9jdf.js → cli-7x0g4sn1.js} +15416 -16024
  33. package/dist/{cli-2bw3w47j.js → cli-8wwxw8a3.js} +1 -1
  34. package/dist/{cli-yatxqynm.js → cli-9ha6hs2d.js} +1 -290
  35. package/dist/{cli-ywm1azg9.js → cli-9jzdj90z.js} +8 -4
  36. package/dist/cli-9traj3dn.js +638 -0
  37. package/dist/{cli-smcrrmhy.js → cli-agzebbd5.js} +9 -9
  38. package/dist/{cli-c5km5znr.js → cli-c8r3kzzb.js} +1 -1
  39. package/dist/cli-cvv9k0hh.js +10143 -0
  40. package/dist/{cli-anzdk0z4.js → cli-d9286m9h.js} +2 -2
  41. package/dist/cli-e3g5mbkz.js +776 -0
  42. package/dist/cli-f527ce74.js +781 -0
  43. package/dist/{cli-gmdajfw5.js → cli-gxzkg1de.js} +5020 -1729
  44. package/dist/{cli-451ks6aw.js → cli-h1arqn31.js} +1 -1
  45. package/dist/cli-hjwmhcg1.js +10622 -0
  46. package/dist/{cli-jpc4r224.js → cli-hqr88ew6.js} +19 -3
  47. package/dist/{cli-h7k0xtry.js → cli-hvqbtna4.js} +4 -1256
  48. package/dist/{cli-cd0hv9nh.js → cli-j67mh2vg.js} +2 -2
  49. package/dist/cli-jta2eyzc.js +619 -0
  50. package/dist/{cli-wd0hz6ah.js → cli-kbrt8gqa.js} +9 -91
  51. package/dist/cli-kceah1eh.js +92 -0
  52. package/dist/{cli-d603qf86.js → cli-ksphgh48.js} +10 -8
  53. package/dist/{cli-3nxdmjrc.js → cli-mgz4nbgg.js} +4 -4
  54. package/dist/{cli-saqyage0.js → cli-mz5as5ha.js} +179 -27
  55. package/dist/cli-n2wq4j7m.js +6985 -0
  56. package/dist/{cli-ppy4nnnh.js → cli-n8ndw8j7.js} +2 -2
  57. package/dist/{cli-1w0vj3gw.js → cli-naqzr17j.js} +7 -7
  58. package/dist/{cli-0a7mm3zp.js → cli-nm1p8jfr.js} +25 -21
  59. package/dist/{cli-wrh08zjd.js → cli-p3zbc1rw.js} +5 -18
  60. package/dist/{cli-hsem16hb.js → cli-pe05f8yk.js} +29 -6
  61. package/dist/{cli-5wpw4rab.js → cli-qmzsbkvy.js} +5 -5
  62. package/dist/{cli-bv2etw2y.js → cli-qnm4e9e4.js} +99 -590
  63. package/dist/{cli-1x7da0de.js → cli-qqy4q48b.js} +8 -8
  64. package/dist/{cli-r44t9wqc.js → cli-r106725k.js} +2 -2
  65. package/dist/{cli-5er5gjbm.js → cli-r35rw5de.js} +15 -15
  66. package/dist/{cli-v3zhnxsb.js → cli-r610p9bs.js} +2 -2
  67. package/dist/{cli-7jykv7ef.js → cli-rc6pf5kv.js} +1 -1
  68. package/dist/{cli-x119kdba.js → cli-rdxm77k3.js} +1 -1
  69. package/dist/cli-rhs9pz33.js +294 -0
  70. package/dist/{cli-0p3enk35.js → cli-s69nfyeh.js} +9 -1
  71. package/dist/{cli-0kxf81jj.js → cli-sawv63e8.js} +2 -2
  72. package/dist/{cli-e3pmyt39.js → cli-sg19gbf0.js} +1 -1
  73. package/dist/{cli-d161g69q.js → cli-shk7r5cs.js} +57 -6
  74. package/dist/{cli-7pz3b7x5.js → cli-sksetb2q.js} +36 -1
  75. package/dist/{cli-014vns2y.js → cli-stfs0k5s.js} +208 -525
  76. package/dist/{cli-db3m6zdt.js → cli-t02z59f2.js} +1 -1
  77. package/dist/{cli-b07hr3bt.js → cli-txqb9s38.js} +2 -2
  78. package/dist/{cli-s8z6x4b4.js → cli-v7g551te.js} +225 -113
  79. package/dist/{cli-rfz2ve55.js → cli-vgqzp4em.js} +3341 -955
  80. package/dist/{cli-7avy4v8w.js → cli-w1q7h2r2.js} +5 -3
  81. package/dist/{cli-rcp5aqdw.js → cli-w7m81tb7.js} +1 -1
  82. package/dist/cli-wfnemr1t.js +86 -0
  83. package/dist/cli-wmgrkscj.js +23254 -0
  84. package/dist/{cli-qm1448f9.js → cli-wms6x9db.js} +8 -8
  85. package/dist/{cli-tadfgd0t.js → cli-x1r545x8.js} +33 -4
  86. package/dist/{cli-xc7p7s3g.js → cli-xczsgshn.js} +6 -6
  87. package/dist/{cli-fmhj1ehj.js → cli-y3167wt1.js} +882 -271
  88. package/dist/{cli-r0byd61h.js → cli-yjmqpxt3.js} +17 -16
  89. package/dist/{cli-81r1wjyv.js → cli-z780ec7c.js} +1 -1
  90. package/dist/{cli-phcqp1b9.js → cli-zm9t2vde.js} +2 -2
  91. package/dist/cli.js +34 -49
  92. package/dist/{codex-adapter-r3dn4fp5.js → codex-adapter-4z16v9n0.js} +15 -14
  93. package/dist/{codex-login-841kv5fk.js → codex-login-pkepy2j1.js} +3 -2
  94. package/dist/{codex-session-runner-fwt793h4.js → codex-session-runner-rvms9y2x.js} +15 -14
  95. package/dist/{commands-z8fq9qna.js → commands-2a9rtyat.js} +11 -10
  96. package/dist/{controller-hq5ggpcp.js → controller-3k3madbn.js} +5 -4
  97. package/dist/{controllers-connect-3mf2btyj.js → controllers-connect-v9yn8zb9.js} +42 -37
  98. package/dist/{cursor-adapter-q8pgv5qa.js → cursor-adapter-98gbhyzn.js} +7 -6
  99. package/dist/{db-qfpq4bt7.js → db-eywwsnjw.js} +30 -9
  100. package/dist/{db-yyhqqrbf.js → db-ncypk9np.js} +12 -11
  101. package/dist/{devin-adapter-xgtj680j.js → devin-adapter-zeq8b9gz.js} +3 -2
  102. package/dist/{dispatcher-x0x7qfcq.js → dispatcher-w94kc575.js} +38 -33
  103. package/dist/{docker-f503k9p0.js → docker-b1e4cqab.js} +3 -2
  104. package/dist/{dsh-adapter-34etvb5m.js → dsh-adapter-47ztej36.js} +7 -6
  105. package/dist/{e2b-66xra4az.js → e2b-bbt8ey45.js} +4 -3
  106. package/dist/{e2b-xwhyj8cw.js → e2b-dp804sen.js} +4 -3
  107. package/dist/{event-bus-vjtv82gm.js → event-bus-j2jdg3qb.js} +3 -2
  108. package/dist/{external-channel-h1fjecwe.js → external-channel-zrkg8ar9.js} +11 -10
  109. package/dist/{google-generative-ai-szdxbpqg.js → google-generative-ai-g8db7x0f.js} +4 -2
  110. package/dist/{google-vertex-qzw3p36d.js → google-vertex-r2692q0r.js} +4 -2
  111. package/dist/{grok-adapter-e7mr287e.js → grok-adapter-s9ncp9vy.js} +18 -17
  112. package/dist/{handlers-znb9n7d4.js → handlers-kqftvjgw.js} +41 -36
  113. package/dist/{heartbeat-bm4qkn8d.js → heartbeat-9q96p8v9.js} +43 -38
  114. package/dist/{hook-254r4mza.js → hook-56qe10dp.js} +11 -23
  115. package/dist/{hook-xaa3q80s.js → hook-yy6rw42k.js} +12 -11
  116. package/dist/{http-3dgrgp8y.js → http-r2dnb8f8.js} +2411 -275
  117. package/dist/{index-fx3jqq1e.js → index-3gtmkhc6.js} +38 -33
  118. package/dist/{index-bdsqa799.js → index-3k5hkz7r.js} +43 -38
  119. package/dist/index-52j1aja4.js +3465 -0
  120. package/dist/{index-g2byveez.js → index-5e101240.js} +38 -33
  121. package/dist/{index-2ybk7khy.js → index-5wyfjc2c.js} +845 -683
  122. package/dist/index-9gw58n4k.js +193 -0
  123. package/dist/index-9j87mcm8.js +56305 -0
  124. package/dist/index-bsmcresk.js +79424 -0
  125. package/dist/index-mh85qhec.js +475 -0
  126. package/dist/{index-jd7sejm2.js → index-vxvhxxbn.js} +10 -9
  127. package/dist/index-we27kd31.js +12810 -0
  128. package/dist/{keepalive-6ywhpja1.js → keepalive-psj6j54c.js} +12 -11
  129. package/dist/{lead-cw45n21s.js → lead-hajz4688.js} +61 -56
  130. package/dist/{maintenance-ss1a1ge9.js → maintenance-ps02sb5m.js} +10 -9
  131. package/dist/{node-fkajaq96.js → node-mwb5r697.js} +2 -1
  132. package/dist/{oauth-refresh-sweep-3c6f8vyy.js → oauth-refresh-sweep-0284h93b.js} +11 -10
  133. package/dist/{onboard-8hzdhc1k.js → onboard-rbf6bg01.js} +11 -3
  134. package/dist/{opencode-adapter-qbchbcfz.js → opencode-adapter-13dcatgq.js} +18 -13
  135. package/dist/{otel-impl-e6mnvc4v.js → otel-impl-zvqcbhp7.js} +242 -868
  136. package/dist/{pi-mono-adapter-3c9g9snh.js → pi-mono-adapter-xxy47q4v.js} +244 -106
  137. package/dist/{pricing-refresh-gedkjdw0.js → pricing-refresh-kefx9v3t.js} +12 -11
  138. package/dist/{rbac-roles-dt4vh1yy.js → rbac-roles-3m36rxm2.js} +11 -10
  139. package/dist/{rbac-roles-eszdg6pm.js → rbac-roles-z76xv0hq.js} +12 -11
  140. package/dist/{render-v2-907cy6sa.js → render-v2-wz73bs25.js} +18 -14
  141. package/dist/{resume-42stcaz0.js → resume-k4wzge8w.js} +25 -21
  142. package/dist/{runtime-qfry88v2.js → runtime-sa2vr16g.js} +2 -2
  143. package/dist/{setup-adbc1c10.js → setup-2fp2pypp.js} +2 -2
  144. package/dist/{virtual-modules-rtmt494q.js → virtual-modules-9gkxqrap.js} +6 -5
  145. package/dist/{work-objects-wpj8ck95.js → work-objects-6k0ywz3m.js} +41 -36
  146. package/dist/{worker-bzdra9rf.js → worker-5v3rxsdh.js} +61 -56
  147. package/dist/{x-qfmygqqv.js → x-4mtdeym1.js} +3 -2
  148. package/openapi.json +1279 -329
  149. package/package.json +15 -15
  150. package/src/agentmail/handlers.ts +21 -11
  151. package/src/apps/definition.ts +16 -0
  152. package/src/azure-devops/api.ts +52 -2
  153. package/src/azure-devops/boards-sync.ts +50 -20
  154. package/src/azure-devops/index.ts +1 -0
  155. package/src/azure-devops/outbound.ts +95 -0
  156. package/src/be/boot-seeds.ts +11 -0
  157. package/src/be/config-refs.ts +60 -0
  158. package/src/be/db/model-profiles.ts +315 -0
  159. package/src/be/db/page-versions.ts +15 -0
  160. package/src/be/db/runtime.ts +35 -8
  161. package/src/be/db/skill-name-resolution.ts +36 -0
  162. package/src/be/db/tasks/read.ts +237 -0
  163. package/src/be/db/tasks/write.ts +7 -0
  164. package/src/be/db.ts +87 -36
  165. package/src/be/migrations/215_swarm_config_export_to_env.sql +34 -0
  166. package/src/be/migrations/216_task_resolved_effort.sql +5 -0
  167. package/src/be/migrations/217_chat_sdk_state.sql +104 -0
  168. package/src/be/migrations/219_relabel_pi_context_compactions.sql +22 -0
  169. package/src/be/migrations/220_model_profiles.sql +137 -0
  170. package/src/be/model-catalog-store.ts +5 -1
  171. package/src/be/model-tier-resolution.ts +155 -10
  172. package/src/be/model-validation.ts +12 -2
  173. package/src/be/models-catalog.ts +26 -1
  174. package/src/be/modelsdev-cache.json +198 -63
  175. package/src/be/modelsdev-cache.ts +9 -1
  176. package/src/be/script-credential-broker.ts +39 -2
  177. package/src/be/scripts/run-saved.ts +1 -0
  178. package/src/be/scripts/typecheck.ts +22 -8
  179. package/src/be/secret-registry.ts +6 -2
  180. package/src/be/seed/runner.ts +13 -1
  181. package/src/be/seed/state-db.ts +5 -0
  182. package/src/be/seed/types.ts +6 -0
  183. package/src/be/seed-model-profiles.ts +41 -0
  184. package/src/be/seed-pricing.ts +19 -2
  185. package/src/be/seed-scripts/catalog/heartbeat-jev-gate.ts +368 -0
  186. package/src/be/seed-scripts/catalog/ops-catalog-audit.inline.ts +1 -1
  187. package/src/be/seed-scripts/catalog/ops-catalog-audit.ts +1 -1
  188. package/src/be/seed-scripts/index.ts +9 -0
  189. package/src/be/seed-skills/bundled-files.generated.json +0 -18
  190. package/src/be/seed-skills/index.ts +104 -3
  191. package/src/be/swarm-config-guard.ts +53 -0
  192. package/src/cli.tsx +0 -16
  193. package/src/commands/onboard/non-interactive.ts +4 -2
  194. package/src/commands/provider-credentials.ts +52 -6
  195. package/src/commands/runner.ts +193 -71
  196. package/src/e2b/env.ts +5 -0
  197. package/src/extensions/catalog.generated.json +17 -0
  198. package/src/extensions/contract-types.generated.ts +1 -1
  199. package/src/extensions/contract.ts +45 -0
  200. package/src/extensions/dispatcher.ts +7 -0
  201. package/src/extensions/loader.ts +10 -5
  202. package/src/extensions/post-bridge.ts +40 -0
  203. package/src/github/handlers.ts +11 -4
  204. package/src/heartbeat/heartbeat.ts +2 -1
  205. package/src/heartbeat/templates.ts +8 -5
  206. package/src/hooks/hook.ts +0 -26
  207. package/src/http/agents.ts +49 -2
  208. package/src/http/all-routes.ts +1 -1
  209. package/src/http/apps.ts +30 -3
  210. package/src/http/config.ts +62 -19
  211. package/src/http/core.ts +8 -0
  212. package/src/http/index.ts +25 -3
  213. package/src/http/integrations.ts +88 -1
  214. package/src/http/mcp.ts +15 -5
  215. package/src/http/model-profiles.ts +303 -0
  216. package/src/http/page-proxy.ts +2 -4
  217. package/src/http/pages-public.ts +13 -7
  218. package/src/http/pages.ts +3 -13
  219. package/src/http/poll.ts +42 -71
  220. package/src/http/requested-by.ts +54 -0
  221. package/src/http/script-runs.ts +1 -1
  222. package/src/http/scripts.ts +26 -2
  223. package/src/http/stats.ts +1 -30
  224. package/src/http/tasks.ts +79 -5
  225. package/src/http/utils.ts +14 -0
  226. package/src/http/webhooks.ts +160 -3
  227. package/src/http/x.ts +1 -0
  228. package/src/integrations/chat-sdk/adapters.ts +129 -0
  229. package/src/integrations/chat-sdk/attachments.ts +232 -0
  230. package/src/integrations/chat-sdk/background.ts +36 -0
  231. package/src/integrations/chat-sdk/commands.ts +194 -0
  232. package/src/integrations/chat-sdk/config.ts +198 -0
  233. package/src/integrations/chat-sdk/discord-api.ts +63 -0
  234. package/src/integrations/chat-sdk/discord-gateway.ts +185 -0
  235. package/src/integrations/chat-sdk/handlers.ts +28 -0
  236. package/src/integrations/chat-sdk/inbound.ts +426 -0
  237. package/src/integrations/chat-sdk/limits.ts +48 -0
  238. package/src/integrations/chat-sdk/logger.ts +81 -0
  239. package/src/integrations/chat-sdk/outbound.ts +123 -0
  240. package/src/integrations/chat-sdk/relay.ts +172 -0
  241. package/src/integrations/chat-sdk/router.ts +169 -0
  242. package/src/integrations/chat-sdk/runtime.ts +351 -0
  243. package/src/integrations/chat-sdk/schema.ts +245 -0
  244. package/src/integrations/chat-sdk/state.ts +243 -0
  245. package/src/integrations/chat-sdk/status.ts +244 -0
  246. package/src/integrations/chat-sdk/steering.ts +58 -0
  247. package/src/integrations/chat-sdk/teams-origin.ts +77 -0
  248. package/src/integrations/chat-sdk/telegram-api.ts +56 -0
  249. package/src/integrations/chat-sdk/templates.ts +16 -0
  250. package/src/integrations/chat-sdk/test-hooks.ts +37 -0
  251. package/src/integrations/chat-sdk/thread-buffer.ts +100 -0
  252. package/src/integrations/chat-sdk/typing.ts +65 -0
  253. package/src/{artifact-sdk → pages}/feedback-overlay.ts +65 -6
  254. package/src/pages/version.ts +4 -5
  255. package/src/prompts/base-prompt.ts +111 -45
  256. package/src/providers/acp-target-catalog.ts +6 -0
  257. package/src/providers/acp-targets.ts +56 -3
  258. package/src/providers/amp-adapter.ts +1 -1
  259. package/src/providers/claude-adapter.ts +1 -1
  260. package/src/providers/claude-flag-settings.ts +17 -5
  261. package/src/providers/claude-managed-adapter.ts +1 -1
  262. package/src/providers/claude-session-events.ts +6 -1
  263. package/src/providers/dsh-adapter.ts +1 -1
  264. package/src/providers/opencode-adapter.ts +243 -26
  265. package/src/providers/opencode-swarm-plugin.ts +31 -0
  266. package/src/providers/pi-mono-adapter.ts +398 -153
  267. package/src/providers/pi-mono-extension.ts +17 -3
  268. package/src/providers/pricing-sources.md +33 -2
  269. package/src/providers/reasoning-effort.ts +37 -0
  270. package/src/providers/types.ts +6 -2
  271. package/src/rbac/legacy-policy.ts +16 -2
  272. package/src/rbac/permissions.ts +26 -0
  273. package/src/scheduler/deferred-task-waits.ts +18 -5
  274. package/src/scheduler/scheduler.ts +11 -3
  275. package/src/scripts-runtime/executors/quickjs-prelude.ts +75 -6
  276. package/src/scripts-runtime/executors/quickjs-runner.ts +85 -8
  277. package/src/scripts-runtime/executors/quickjs-web-shims.ts +1041 -0
  278. package/src/scripts-runtime/extract-args-schema.ts +9 -8
  279. package/src/scripts-runtime/loader.ts +19 -7
  280. package/src/scripts-runtime/quickjs-compat.ts +464 -0
  281. package/src/scripts-runtime/sdk-allowlist.ts +12 -6
  282. package/src/scripts-runtime/swarm-sdk.ts +0 -11
  283. package/src/scripts-runtime/types/stdlib.d.ts +39 -15
  284. package/src/scripts-runtime/types/swarm-extension.d.ts +66 -1
  285. package/src/scripts-runtime/types/swarm-sdk.d.ts +39 -15
  286. package/src/server.ts +62 -19
  287. package/src/slack/blocks.ts +4 -2
  288. package/src/slack/event-dedup.ts +1 -1
  289. package/src/slack/reaction-shortcode.ts +14 -0
  290. package/src/slack/render-v2.ts +12 -7
  291. package/src/slack/responses.ts +5 -4
  292. package/src/slack/thread-buffer.ts +41 -22
  293. package/src/tasks/additive-buffer.ts +4 -2
  294. package/src/tasks/context-key.ts +60 -1
  295. package/src/tasks/model-pin.ts +67 -0
  296. package/src/tasks/worker-follow-up.ts +15 -33
  297. package/src/tests/acp-adapter.test.ts +42 -0
  298. package/src/tests/agentmail-filters.test.ts +47 -0
  299. package/src/tests/api-drain-worker-e2e.test.ts +1 -6
  300. package/src/tests/apps-spike2.test.ts +35 -0
  301. package/src/tests/azure-devops-outbound.test.ts +420 -0
  302. package/src/tests/base-prompt.test.ts +58 -54
  303. package/src/tests/chat-sdk-commands.test.ts +191 -0
  304. package/src/tests/chat-sdk-config.test.ts +334 -0
  305. package/src/tests/chat-sdk-discord-api.test.ts +68 -0
  306. package/src/tests/chat-sdk-discord-gateway.test.ts +252 -0
  307. package/src/tests/chat-sdk-discord-webhook.test.ts +126 -0
  308. package/src/tests/chat-sdk-fields-sync.test.ts +50 -0
  309. package/src/tests/chat-sdk-harness.ts +146 -0
  310. package/src/tests/chat-sdk-inbound.test.ts +363 -0
  311. package/src/tests/chat-sdk-relay.test.ts +470 -0
  312. package/src/tests/chat-sdk-reload.test.ts +128 -0
  313. package/src/tests/chat-sdk-shutdown.test.ts +220 -0
  314. package/src/tests/chat-sdk-slack.test.ts +268 -0
  315. package/src/tests/chat-sdk-state.test.ts +163 -0
  316. package/src/tests/chat-sdk-status-route.test.ts +382 -0
  317. package/src/tests/chat-sdk-teams.test.ts +277 -0
  318. package/src/tests/chat-sdk-tools.test.ts +520 -0
  319. package/src/tests/chat-sdk-typing.test.ts +120 -0
  320. package/src/tests/chat-sdk-webhook-telegram.test.ts +200 -0
  321. package/src/tests/claude-adapter-binary.test.ts +9 -4
  322. package/src/tests/claude-flag-settings.test.ts +11 -3
  323. package/src/tests/claude-managed-adapter.test.ts +1 -1
  324. package/src/tests/claude-sdk-lifecycle.test.ts +9 -3
  325. package/src/tests/claude-thinking-tokens.test.ts +57 -0
  326. package/src/tests/claude-worker-parity.test.ts +2 -7
  327. package/src/tests/config-env-export-chat-sdk.test.ts +11 -0
  328. package/src/tests/config-refs.test.ts +66 -0
  329. package/src/tests/context-key.test.ts +19 -0
  330. package/src/tests/cors-allowlist.test.ts +25 -0
  331. package/src/tests/create-page-tool.test.ts +18 -0
  332. package/src/tests/credential-check.test.ts +139 -0
  333. package/src/tests/credentials.test.ts +21 -0
  334. package/src/tests/db-runtime.test.ts +61 -0
  335. package/src/tests/db-task-read-contract.test.ts +22 -0
  336. package/src/tests/db-task-write-contract.test.ts +11 -0
  337. package/src/tests/deferred-task-wake.test.ts +78 -1
  338. package/src/tests/docs-index-footprint.test.js +3 -3
  339. package/src/tests/dsh-adapter.test.ts +1 -1
  340. package/src/tests/enabled-capabilities.test.ts +20 -0
  341. package/src/tests/entrypoint-api-readiness.test.ts +14 -8
  342. package/src/tests/entrypoint-config-env-export.test.ts +5 -2
  343. package/src/tests/entrypoint-export-to-env.test.ts +78 -0
  344. package/src/tests/env-flag.test.ts +1 -0
  345. package/src/tests/explicit-model-validation.test.ts +105 -1
  346. package/src/tests/extensions-catalog.test.ts +1 -0
  347. package/src/tests/extensions-example-comms-adherence-gate.test.ts +413 -0
  348. package/src/tests/extensions-example-route-chat-channel.test.ts +154 -0
  349. package/src/tests/extensions-loader.test.ts +15 -0
  350. package/src/tests/extensions-post-bridge.test.ts +51 -0
  351. package/src/tests/fetch-resolved-env.test.ts +128 -0
  352. package/src/tests/fixtures/chat-sdk/telegram-private-text.json +10 -0
  353. package/src/tests/fixtures/extensions/route-chat-channel-to-agent/hooks.ts +27 -0
  354. package/src/tests/fixtures/extensions/route-chat-channel-to-agent/manifest.json +9 -0
  355. package/src/tests/fixtures/openai-compatible-registry-extension.child.ts +146 -0
  356. package/src/tests/fixtures/runner-legacy-resume-prompt.ts +122 -0
  357. package/src/tests/github-handlers.test.ts +35 -0
  358. package/src/tests/harness-provider-resolution.test.ts +12 -0
  359. package/src/tests/heartbeat-checklist.test.ts +3 -2
  360. package/src/tests/heartbeat-jev-gate-credentials.test.ts +168 -0
  361. package/src/tests/heartbeat-jev-gate.test.ts +385 -0
  362. package/src/tests/hook-runtime-close.test.ts +0 -2
  363. package/src/tests/http-api-integration.test.ts +0 -25
  364. package/src/tests/migration-046-budgets.test.ts +17 -16
  365. package/src/tests/migration-217-chat-sdk-state.test.ts +113 -0
  366. package/src/tests/model-profile-backcompat.test.ts +327 -0
  367. package/src/tests/model-profile-providers.test.ts +302 -0
  368. package/src/tests/model-profile-runtime-secrets.test.ts +223 -0
  369. package/src/tests/model-profiles-api.test.ts +614 -0
  370. package/src/tests/model-tier-resolution.test.ts +93 -0
  371. package/src/tests/models-catalog.test.ts +42 -1
  372. package/src/tests/openai-compatible-providers.test.ts +134 -0
  373. package/src/tests/openai-compatible-registry-extension.test.ts +42 -0
  374. package/src/tests/opencode-adapter.test.ts +216 -2
  375. package/src/tests/opencode-helmcode-sdk.test.ts +201 -0
  376. package/src/tests/opencode-shell-env.test.ts +145 -0
  377. package/src/tests/openrouter-base-url.test.ts +101 -151
  378. package/src/tests/page-proxy.test.ts +1 -1
  379. package/src/tests/pages-browser-sdk.test.ts +76 -0
  380. package/src/tests/pages-feedback-keys.test.ts +121 -0
  381. package/src/tests/pages-feedback.test.ts +4 -1
  382. package/src/tests/paused-tasks-requested-by.test.ts +102 -0
  383. package/src/tests/pi-context-compaction.test.ts +193 -0
  384. package/src/tests/pi-custom-provider-fake-server.test.ts +442 -0
  385. package/src/tests/pi-mono-adapter.test.ts +385 -5
  386. package/src/tests/pi-shell-env.test.ts +170 -0
  387. package/src/tests/profile-claim.test.ts +269 -0
  388. package/src/tests/prompt-template-remaining.test.ts +35 -0
  389. package/src/tests/quickjs-compat.test.ts +213 -0
  390. package/src/tests/rbac-engine.test.ts +32 -3
  391. package/src/tests/runner-exit-after-tasks.test.ts +0 -4
  392. package/src/tests/runner-legacy-resume-prompt.test.ts +64 -0
  393. package/src/tests/script-connections.test.ts +6 -0
  394. package/src/tests/script-executor-conformance.test.ts +252 -1
  395. package/src/tests/script-executor-quickjs.test.ts +96 -0
  396. package/src/tests/script-runs-http.test.ts +89 -0
  397. package/src/tests/scripts-http.test.ts +217 -1
  398. package/src/tests/scripts-only-gating.test.ts +2 -3
  399. package/src/tests/scripts-runtime.test.ts +58 -40
  400. package/src/tests/secret-scrubber.test.ts +96 -0
  401. package/src/tests/seed-model-profiles.test.ts +174 -0
  402. package/src/tests/seed-scripts.test.ts +2 -2
  403. package/src/tests/seed-skills-bundled-files.test.ts +45 -53
  404. package/src/tests/seed-skills-retire.test.ts +182 -0
  405. package/src/tests/send-task-requester-gate.test.ts +58 -0
  406. package/src/tests/session-cost-recompute-unit.test.ts +46 -0
  407. package/src/tests/skill-get-name-resolution.test.ts +137 -0
  408. package/src/tests/skill-invocations.test.ts +4 -2
  409. package/src/tests/slack-blocks.test.ts +36 -0
  410. package/src/tests/slack-inline-output.test.ts +14 -0
  411. package/src/tests/slack-reaction-config.test.ts +20 -0
  412. package/src/tests/slack-render-v2.test.ts +37 -0
  413. package/src/tests/slack-thread-buffer.test.ts +98 -1
  414. package/src/tests/sonnet-5-5-registry.test.ts +9 -1
  415. package/src/tests/swarm-config-export-to-env.test.ts +267 -0
  416. package/src/tests/swarm-config-reserved-keys.test.ts +16 -0
  417. package/src/tests/swarm-diff.test.ts +1 -1
  418. package/src/tests/task-model-effort-rerun.test.ts +164 -0
  419. package/src/tests/task-session-prompts.test.ts +132 -0
  420. package/src/tests/task-tool-manifest.test.ts +14 -0
  421. package/src/tests/task-tools-ctx.test.ts +2 -0
  422. package/src/tests/tool-annotations.test.ts +6 -6
  423. package/src/tests/ui-logs-parser.test.ts +43 -0
  424. package/src/tests/used-by-route.test.ts +6 -0
  425. package/src/tests/vercel-ai-gateway.test.ts +149 -0
  426. package/src/tests/workflow-llm.test.ts +61 -0
  427. package/src/tests/workflow-swarm-script.test.ts +38 -0
  428. package/src/tests/workflow-wait-filter.test.ts +5 -3
  429. package/src/tools/chat-access.ts +41 -0
  430. package/src/tools/chat-open-dm.ts +59 -0
  431. package/src/tools/chat-post.ts +74 -0
  432. package/src/tools/chat-read-thread.ts +100 -0
  433. package/src/tools/chat-reply.ts +75 -0
  434. package/src/tools/create-page.ts +3 -13
  435. package/src/tools/model-catalog/index.ts +2 -1
  436. package/src/tools/model-profiles/list-model-profiles.ts +54 -0
  437. package/src/tools/register-chat-channel.ts +139 -0
  438. package/src/tools/request-human-input.ts +11 -3
  439. package/src/tools/script-apis.ts +5 -1
  440. package/src/tools/script-upsert.ts +4 -1
  441. package/src/tools/send-task.ts +48 -1
  442. package/src/tools/skills/skill-get.ts +17 -9
  443. package/src/tools/swarm-config/get-config.ts +2 -1
  444. package/src/tools/swarm-config/list-config.ts +2 -1
  445. package/src/tools/swarm-config/set-config.ts +16 -3
  446. package/src/tools/system-one-decide.ts +104 -0
  447. package/src/tools/task-action.ts +22 -1
  448. package/src/tools/tool-config.ts +14 -6
  449. package/src/types.ts +48 -3
  450. package/src/utils/config-env-export.ts +74 -0
  451. package/src/utils/credentials.test.ts +92 -0
  452. package/src/utils/credentials.ts +52 -4
  453. package/src/utils/harness-provider.ts +15 -6
  454. package/src/utils/runtime-model-catalog.ts +11 -1
  455. package/src/utils/secret-scrubber.ts +86 -10
  456. package/src/utils/task-tool-manifest.ts +19 -2
  457. package/src/utils/vcs-repo-match.ts +29 -0
  458. package/src/workflows/executors/raw-llm.ts +8 -3
  459. package/src/workflows/executors/swarm-script.ts +1 -0
  460. package/src/workflows/executors/system-one-decision.ts +21 -11
  461. package/src/workflows/executors/workflow-llm.ts +22 -0
  462. package/src/workflows/index.ts +21 -12
  463. package/templates/extensions/README.md +1 -0
  464. package/templates/extensions/comms-adherence-gate/README.md +88 -0
  465. package/templates/extensions/comms-adherence-gate/hooks.ts +489 -0
  466. package/templates/extensions/comms-adherence-gate/manifest.json +8 -0
  467. package/templates/skills/agent-browser/SKILL.md +1 -1
  468. package/templates/skills/agent-browser/content.md +1 -1
  469. package/templates/skills/apps/SKILL.md +8 -1
  470. package/templates/skills/apps/content.md +8 -1
  471. package/templates/skills/chat-sdk/config.json +14 -0
  472. package/templates/skills/chat-sdk/content.md +84 -0
  473. package/templates/skills/composio-gmail/content.md +7 -0
  474. package/templates/skills/composio-google-docs/content.md +1 -1
  475. package/templates/skills/email-domain-health/config.json +14 -0
  476. package/templates/skills/email-domain-health/content.md +131 -0
  477. package/templates/skills/kv-storage/content.md +2 -2
  478. package/templates/skills/pages/content.md +8 -10
  479. package/templates/skills/swarm-extensions/content.md +2 -0
  480. package/templates/skills/swarm-scripts/SKILL.md +4 -2
  481. package/templates/skills/swarm-scripts/config.json +1 -1
  482. package/templates/skills/swarm-scripts/content.md +4 -2
  483. package/templates/skills/workflow-iterate/content.md +38 -0
  484. package/dist/artifact-eczsknc3.js +0 -3843
  485. package/dist/cli-42qhyvwx.js +0 -33
  486. package/dist/cli-bd957dgb.js +0 -153
  487. package/dist/cli-q51yprfq.js +0 -1104
  488. package/dist/cli-t1ze64h3.js +0 -811
  489. package/dist/cli-tcc4wd5d.js +0 -56471
  490. package/dist/index-9ch1n761.js +0 -1102
  491. package/src/artifact-sdk/index.ts +0 -2
  492. package/src/artifact-sdk/localtunnel.d.ts +0 -20
  493. package/src/artifact-sdk/port.ts +0 -12
  494. package/src/artifact-sdk/server.ts +0 -183
  495. package/src/artifact-sdk/tunnel.ts +0 -19
  496. package/src/commands/artifact.ts +0 -249
  497. package/src/http/ecosystem.ts +0 -77
  498. package/src/tests/artifact-commands.test.ts +0 -92
  499. package/src/tests/artifact-sdk.test.ts +0 -793
  500. package/src/tests/register-service-security.test.ts +0 -194
  501. package/src/tools/list-services.ts +0 -107
  502. package/src/tools/register-service.ts +0 -177
  503. package/src/tools/unregister-service.ts +0 -73
  504. package/src/tools/update-service-status.ts +0 -96
  505. package/templates/skills/artifacts/config.json +0 -14
  506. package/templates/skills/artifacts/content.md +0 -344
  507. package/templates/skills/artifacts/files/examples/approval-flow.ts +0 -34
  508. package/templates/skills/artifacts/files/examples/hono-dashboard.ts +0 -31
  509. package/templates/skills/artifacts/files/examples/multi-artifact.ts +0 -20
  510. package/templates/skills/artifacts/files/examples/static-report.sh +0 -17
  511. /package/src/{artifact-sdk → pages}/browser-sdk.ts +0 -0
@@ -0,0 +1,489 @@
1
+ import { type ApiCtx, block, type SwarmExtension } from "swarm-extension";
2
+ import { z } from "zod";
3
+
4
+ /**
5
+ * comms-adherence-gate
6
+ *
7
+ * When an agent calls `store-progress` with status "completed" on a task that
8
+ * reaches a human in Slack, judge the `output` against the requesting user's
9
+ * stored comms preferences (users.metadata.$.comms) with Jev, and against what
10
+ * that user actually asked for in their latest message. An explicit request
11
+ * ("tell me a long joke", "explain in detail") outranks the stored default, so
12
+ * a requested long answer is not a verbosity failure.
13
+ *
14
+ * Transport (config.provider): Jev is served by TypeSafe directly and by
15
+ * OpenRouter's Decisions endpoint. Both take the same {model, state, questions}
16
+ * body and return the same `answers` shape. `auto` uses TYPESAFE_API_KEY when
17
+ * set, else OPENROUTER_API_KEY.
18
+ *
19
+ * modes (config.mode):
20
+ * off -> never evaluate
21
+ * shadow -> evaluate + record, never block (default)
22
+ * enforce -> record, and block the completion up to maxBlocksPerTask times
23
+ * per task with actionable feedback
24
+ *
25
+ * Fails OPEN on every error: a judge outage must never wall the swarm.
26
+ * Every verdict lands in KV namespace `ext:comms-gate`.
27
+ */
28
+
29
+ export const config = z.object({
30
+ mode: z.enum(["off", "shadow", "enforce"]).default("shadow"),
31
+ /** A dimension scoring below this fails. */
32
+ threshold: z.number().min(0).max(1).default(0.5),
33
+ timeoutMs: z.number().int().positive().default(4000),
34
+ maxBlocksPerTask: z.number().int().min(0).default(1),
35
+ provider: z.enum(["auto", "typesafe", "openrouter"]).default("auto"),
36
+ /** Jev model on the TypeSafe transport. */
37
+ model: z.string().default("jev-latest"),
38
+ /** Jev model on the OpenRouter transport. */
39
+ openrouterModel: z.string().default("typesafe/jev-1.13"),
40
+ /** Explicit TypeSafe key; otherwise TYPESAFE_API_KEY from the API process env. */
41
+ apiKey: z.string().optional(),
42
+ });
43
+
44
+ type Cfg = z.infer<typeof config>;
45
+ type Swarm = ApiCtx["swarm"];
46
+
47
+ const NS = "ext:comms-gate";
48
+ const TYPESAFE_ENDPOINT = "https://api.typesafe.ai/v1/systemone";
49
+ const OPENROUTER_ENDPOINT = "https://openrouter.ai/api/alpha/decisions";
50
+ const UUID_RE = /^[0-9a-fA-F-]{36}$/;
51
+ // The dispatcher cancels a handler after 5 s; leave room for the DB read and KV writes.
52
+ const MAX_TIMEOUT_MS = 4000;
53
+
54
+ export const cfgOf = (raw: unknown): Cfg => {
55
+ const parsed = config.safeParse(raw ?? {});
56
+ return parsed.success ? parsed.data : config.parse({});
57
+ };
58
+
59
+ // kv keys accept [a-zA-Z0-9._:/%-]
60
+ const safeKey = (raw: string | undefined | null): string =>
61
+ (raw && raw.length > 0 ? raw : "unknown").replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 120);
62
+
63
+ /** SDK envelopes: {success, data:{...}} or the bare payload. */
64
+ type Envelope = {
65
+ data?: { rows?: unknown[]; value?: unknown };
66
+ rows?: unknown[];
67
+ value?: unknown;
68
+ } | null;
69
+
70
+ const cell = (row: unknown, i: number, key: string): unknown =>
71
+ Array.isArray(row) ? row[i] : (row as Record<string, unknown>)?.[key];
72
+
73
+ /**
74
+ * The newest human message sits at the END of a Slack task: the
75
+ * `<thread_context>` blob comes first and can run to thousands of chars, so a
76
+ * leading slice of `agent_tasks.task` shows the judge everything except the
77
+ * actual request. Take the tail, then the part after the last `</thread_context>`.
78
+ */
79
+ const CTX_END = "</thread_context>";
80
+ export const latestAsk = (tail: string): string => {
81
+ const end = tail.lastIndexOf(CTX_END);
82
+ let out = (end >= 0 ? tail.slice(end + CTX_END.length) : tail)
83
+ .replace(/\[Thread follow-up[^\]]*\]/g, "")
84
+ .trim();
85
+ // Some tasks end with the context block; fall back to the last messages in it.
86
+ if (out.length < 8) out = (end >= 0 ? tail.slice(0, end) : tail).trim().slice(-800);
87
+ return out.slice(0, 900);
88
+ };
89
+
90
+ // Preferences are the DEFAULT. A direct request outranks them.
91
+ const JUDGING_RULES = [
92
+ "The requester's explicit instruction in latest_request OUTRANKS requester_preferences. Preferences describe the DEFAULT; they never override a direct request.",
93
+ "If latest_request asks for something long, detailed, creative, a story, a joke, a full explanation or a document, then length and structure serving that request are CORRECT and must NOT be penalized.",
94
+ "Judge whether agent_message serves what latest_request actually asked for, written in the requester's manner. Do not judge it against the default preference when the request overrides it.",
95
+ ];
96
+
97
+ type Prefs = { verbosity?: unknown; tone?: unknown; language?: unknown } & Record<string, unknown>;
98
+
99
+ /** One `noul` question per stated preference the gate knows how to judge. */
100
+ export const buildQuestions = (prefs: Prefs): Record<string, unknown> => {
101
+ const questions: Record<string, unknown> = {};
102
+ if (prefs.verbosity) {
103
+ questions.verbosity = {
104
+ type: "noul",
105
+ instructions:
106
+ "Does agent_message respect the requester's verbosity preference, AS OVERRIDDEN by any explicit instruction in latest_request? Apply judging_rules.",
107
+ criteria: {
108
+ true: "Length and structure fit what latest_request asked for (or the stated preference when the request does not override it), and it leads with the result.",
109
+ false:
110
+ "Too long for what was asked, or it opens with preamble, recaps the request, narrates the process, or ends with an offer to elaborate. Length explicitly requested in latest_request is NOT a failure.",
111
+ },
112
+ };
113
+ }
114
+ if (prefs.tone) {
115
+ questions.tone = {
116
+ type: "noul",
117
+ instructions:
118
+ "Does agent_message match the requester's stated tone preference, as overridden by any explicit instruction in latest_request? Apply judging_rules.",
119
+ criteria: {
120
+ true: "The tone matches what the requester asked for.",
121
+ false:
122
+ "The tone conflicts with it (for example hedging, flattery or filler when the requester asked for direct and plain). A voice appropriate to an explicitly requested creative or narrative form is NOT a failure.",
123
+ },
124
+ };
125
+ }
126
+ if (prefs.language) {
127
+ questions.language = {
128
+ type: "noul",
129
+ instructions: "Is agent_message written in the requester's stated language?",
130
+ criteria: {
131
+ true: "Written in the stated language.",
132
+ false: "Written in a different language.",
133
+ },
134
+ };
135
+ }
136
+ return questions;
137
+ };
138
+
139
+ const secretCache = new Map<string, { value: string; at: number }>();
140
+ const KEY_TTL_MS = 5 * 60 * 1000;
141
+ const usable = (v: string): boolean => Boolean(v) && !v.startsWith("[REDACTED");
142
+ const envOf = (name: string): string => String(process.env[name] ?? "");
143
+
144
+ /**
145
+ * Resolve a secret: explicit value, then the API process env. The API process loads
146
+ * global swarm_config rows into process.env at boot and on config reload.
147
+ * ctx.swarm.config_get cannot serve it: the SDK scrubs secrets to "[REDACTED:<name>]".
148
+ */
149
+ const secret = (name: string, explicit?: string): string => {
150
+ const cached = secretCache.get(name);
151
+ if (cached && Date.now() - cached.at < KEY_TTL_MS) return cached.value;
152
+ const value = [String(explicit ?? ""), envOf(name)].find(usable) ?? "";
153
+ if (usable(value)) secretCache.set(name, { value, at: Date.now() });
154
+ return value;
155
+ };
156
+
157
+ /** Test hook: forget cached secrets. */
158
+ export const resetKeyCache = (): void => {
159
+ secretCache.clear();
160
+ };
161
+
162
+ export type JevTransport = {
163
+ provider: "typesafe" | "openrouter";
164
+ endpoint: string;
165
+ key: string;
166
+ model: string;
167
+ };
168
+
169
+ /**
170
+ * Pick the Jev transport. `auto` prefers TypeSafe and falls back to OpenRouter.
171
+ * Throws when the chosen provider has no usable key; the handler fails open.
172
+ */
173
+ export const resolveTransport = (cfg: Cfg): JevTransport => {
174
+ const typesafe = (): JevTransport | null => {
175
+ const key = secret("TYPESAFE_API_KEY", cfg.apiKey);
176
+ return usable(key)
177
+ ? { provider: "typesafe", endpoint: TYPESAFE_ENDPOINT, key, model: cfg.model }
178
+ : null;
179
+ };
180
+ const openrouter = (): JevTransport | null => {
181
+ const key = secret("OPENROUTER_API_KEY");
182
+ return usable(key)
183
+ ? { provider: "openrouter", endpoint: OPENROUTER_ENDPOINT, key, model: cfg.openrouterModel }
184
+ : null;
185
+ };
186
+ const transport =
187
+ cfg.provider === "typesafe"
188
+ ? typesafe()
189
+ : cfg.provider === "openrouter"
190
+ ? openrouter()
191
+ : (typesafe() ?? openrouter());
192
+ if (transport) return transport;
193
+ throw new Error(
194
+ cfg.provider === "typesafe"
195
+ ? "no TYPESAFE_API_KEY"
196
+ : cfg.provider === "openrouter"
197
+ ? "no OPENROUTER_API_KEY"
198
+ : "no TYPESAFE_API_KEY or OPENROUTER_API_KEY",
199
+ );
200
+ };
201
+
202
+ /** One Jev call; returns the `noul` score per question. Throws on any failure. */
203
+ export const askJev = async (
204
+ state: Record<string, unknown>,
205
+ questions: Record<string, unknown>,
206
+ transport: JevTransport,
207
+ cfg: Cfg,
208
+ outer?: AbortSignal,
209
+ ): Promise<{ scores: Record<string, number>; latencyMs: number }> => {
210
+ const controller = new AbortController();
211
+ const timer = setTimeout(() => controller.abort(), Math.min(cfg.timeoutMs, MAX_TIMEOUT_MS));
212
+ const onOuterAbort = () => controller.abort();
213
+ outer?.addEventListener("abort", onOuterAbort);
214
+ const t0 = Date.now();
215
+ try {
216
+ const res = await fetch(transport.endpoint, {
217
+ method: "POST",
218
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${transport.key}` },
219
+ body: JSON.stringify({ state, model: transport.model, questions }),
220
+ signal: controller.signal,
221
+ });
222
+ if (!res.ok) throw new Error(`${transport.provider} ${res.status}`);
223
+ const body = (await res.json()) as { answers?: Record<string, { noul?: unknown }> } | null;
224
+ const answers = body?.answers;
225
+ if (!answers || typeof answers !== "object") throw new Error("no answers in response");
226
+ const scores: Record<string, number> = {};
227
+ for (const [id, a] of Object.entries(answers)) {
228
+ if (typeof a?.noul === "number") scores[id] = a.noul;
229
+ }
230
+ if (Object.keys(scores).length === 0) throw new Error("no scores in response");
231
+ return { scores, latencyMs: Date.now() - t0 };
232
+ } finally {
233
+ clearTimeout(timer);
234
+ outer?.removeEventListener("abort", onOuterAbort);
235
+ }
236
+ };
237
+
238
+ const SELFTEST = "comms-gate-selftest";
239
+ const SELFTEST_PREFS = {
240
+ tone: "direct, plain, no hedging",
241
+ language: "English",
242
+ verbosity:
243
+ "very terse - answer in the first sentence, ~400 chars max. No preamble, no recap, no closing offer.",
244
+ };
245
+ const SELFTEST_BAD =
246
+ "Great question! Let me walk you through what I did here. First I looked at the task, then I reviewed the relevant files, and after careful consideration I decided to go with option B. I hope this helps - let me know if you would like me to elaborate further on any of the above!";
247
+ const SELFTEST_GOOD =
248
+ "Went with option B: it is the only one that keeps the migration reversible. PR #123.";
249
+
250
+ /**
251
+ * Run the judge on a known-bad (positive control, must score below threshold) and a
252
+ * known-good (negative control, must score at or above it) message and record both
253
+ * under `selftest:last`. Proves key, egress and KV from inside the API runtime
254
+ * without touching real traffic.
255
+ */
256
+ const selfTest = async (swarm: Swarm, cfg: Cfg, signal?: AbortSignal): Promise<void> => {
257
+ const started = Date.now();
258
+ try {
259
+ const transport = resolveTransport(cfg);
260
+ const questions = { verbosity: buildQuestions(SELFTEST_PREFS).verbosity };
261
+ const judge = (message: string) =>
262
+ askJev(
263
+ { requester_preferences: SELFTEST_PREFS, agent_message: message },
264
+ questions,
265
+ transport,
266
+ cfg,
267
+ signal,
268
+ ).then((r) => r.scores.verbosity ?? null);
269
+ const [bad, good] = await Promise.all([judge(SELFTEST_BAD), judge(SELFTEST_GOOD)]);
270
+ await swarm.kv_set({
271
+ namespace: NS,
272
+ key: "selftest:last",
273
+ value: {
274
+ at: new Date().toISOString(),
275
+ provider: transport.provider,
276
+ threshold: cfg.threshold,
277
+ positiveControlBadScore: bad,
278
+ negativeControlGoodScore: good,
279
+ controlsOk:
280
+ typeof bad === "number" &&
281
+ typeof good === "number" &&
282
+ bad < cfg.threshold &&
283
+ good >= cfg.threshold,
284
+ latencyMs: Date.now() - started,
285
+ },
286
+ });
287
+ } catch (err) {
288
+ await swarm
289
+ .kv_set({
290
+ namespace: NS,
291
+ key: "selftest:last",
292
+ value: {
293
+ at: new Date().toISOString(),
294
+ error: String(err).slice(0, 400),
295
+ latencyMs: Date.now() - started,
296
+ },
297
+ })
298
+ .catch(() => {});
299
+ }
300
+ };
301
+
302
+ const TASK_SQL = `SELECT t.source, t.slackThreadTs, t.agentId, t.requestedByUserId,
303
+ json_extract(u.metadata, '$.comms') AS comms, u.name,
304
+ substr(t.task, 1, 400) AS askHead,
305
+ substr(t.task, -1400) AS askTail
306
+ FROM agent_tasks t
307
+ LEFT JOIN users u ON u.id = t.requestedByUserId
308
+ WHERE t.id = ?`;
309
+
310
+ const LEAD_SQL = "SELECT isLead FROM agents WHERE id = ?";
311
+
312
+ /**
313
+ * Mirrors the `task.progress.write` rule store-progress enforces
314
+ * (src/rbac/legacy-policy.ts): the caller owns the task, the task is
315
+ * unassigned, or the caller is a lead. Lead status comes from the caller's
316
+ * agents row, never from tool args. Extension callers return before this.
317
+ */
318
+ const mayWriteProgress = async (
319
+ swarm: Swarm,
320
+ callerId: string | undefined,
321
+ taskAgentId: string,
322
+ ): Promise<boolean> => {
323
+ if (!callerId) return false;
324
+ if (!taskAgentId || taskAgentId === callerId) return true;
325
+ const q = (await swarm.db_query({ sql: LEAD_SQL, params: [callerId] })) as Envelope;
326
+ const row = (q?.data?.rows ?? q?.rows ?? [])[0];
327
+ return Number(cell(row, 0, "isLead") ?? 0) === 1;
328
+ };
329
+
330
+ const extension: SwarmExtension = (api) => {
331
+ api.on("pre.tool.call", async (event, ctx) => {
332
+ if (event.tool !== "store-progress") return;
333
+ if (event.requestInfo?.callOrigin === "extension") return;
334
+
335
+ const cfg = cfgOf(ctx.config);
336
+ if (cfg.mode === "off") return;
337
+
338
+ const args = (event.args ?? {}) as Record<string, unknown>;
339
+
340
+ // Self-test hatch: a progress call whose text starts with the sentinel runs
341
+ // the controls and records them under `selftest:last`.
342
+ if (args.status !== "completed") {
343
+ if (typeof args.progress === "string" && args.progress.startsWith(SELFTEST)) {
344
+ await selfTest(ctx.swarm, cfg, ctx.signal);
345
+ }
346
+ return;
347
+ }
348
+
349
+ const output = typeof args.output === "string" ? args.output.trim() : "";
350
+ if (!output) return;
351
+
352
+ const taskId = String(args.taskId ?? event.requestInfo?.sourceTaskId ?? "");
353
+ if (!UUID_RE.test(taskId)) return;
354
+
355
+ try {
356
+ const q = (await ctx.swarm.db_query({ sql: TASK_SQL, params: [taskId] })) as Envelope;
357
+ const row = (q?.data?.rows ?? q?.rows ?? [])[0];
358
+ if (!row) return;
359
+
360
+ const source = String(cell(row, 0, "source") ?? "");
361
+ const slackThreadTs = cell(row, 1, "slackThreadTs");
362
+ const agentId = String(cell(row, 2, "agentId") ?? "");
363
+ const requesterId = String(cell(row, 3, "requestedByUserId") ?? "");
364
+ const commsRaw = cell(row, 4, "comms");
365
+ const requesterName = String(cell(row, 5, "name") ?? requesterId);
366
+ const threadOpening = String(cell(row, 6, "askHead") ?? "");
367
+ const ask = latestAsk(String(cell(row, 7, "askTail") ?? ""));
368
+
369
+ // Judge only a completion store-progress will accept, so no agent can ship
370
+ // another task's thread and preferences to the external judge.
371
+ if (!(await mayWriteProgress(ctx.swarm, event.requestInfo?.agentId, agentId))) return;
372
+
373
+ // Only where the text actually reaches a human in Slack.
374
+ if (source !== "slack" && !slackThreadTs) return;
375
+ if (!commsRaw) return; // no stated preferences -> nothing to judge against
376
+ const prefs = (typeof commsRaw === "string" ? JSON.parse(commsRaw) : commsRaw) as Prefs;
377
+ if (!prefs || typeof prefs !== "object") return;
378
+
379
+ const questions = buildQuestions(prefs);
380
+ if (Object.keys(questions).length === 0) return;
381
+
382
+ const transport = resolveTransport(cfg);
383
+ const { scores, latencyMs } = await askJev(
384
+ {
385
+ role: "You audit an AI agent's final message to the human who requested the work.",
386
+ requester: requesterName,
387
+ requester_preferences: prefs,
388
+ latest_request: ask,
389
+ thread_opening: threadOpening,
390
+ judging_rules: JUDGING_RULES,
391
+ agent_message: output,
392
+ },
393
+ questions,
394
+ transport,
395
+ cfg,
396
+ ctx.signal,
397
+ );
398
+
399
+ const failed = Object.entries(scores)
400
+ .filter(([, v]) => v < cfg.threshold)
401
+ .map(([k]) => k);
402
+
403
+ const verdict = {
404
+ at: new Date().toISOString(),
405
+ taskId,
406
+ agentId,
407
+ source,
408
+ requester: requesterName,
409
+ requesterId,
410
+ provider: transport.provider,
411
+ scores,
412
+ failed,
413
+ threshold: cfg.threshold,
414
+ mode: cfg.mode,
415
+ latencyMs,
416
+ preferences: prefs,
417
+ askPreview: ask.slice(0, 400),
418
+ outputPreview: output.slice(0, 700),
419
+ };
420
+
421
+ await ctx.swarm.kv_incr({ namespace: NS, key: "total" });
422
+ await ctx.swarm.kv_set({ namespace: NS, key: "last", value: verdict });
423
+
424
+ if (failed.length === 0) {
425
+ await ctx.swarm.kv_incr({ namespace: NS, key: "pass" });
426
+ return;
427
+ }
428
+
429
+ await ctx.swarm.kv_incr({ namespace: NS, key: "fail" });
430
+ for (const dim of failed) {
431
+ await ctx.swarm.kv_incr({ namespace: NS, key: `dim:${safeKey(dim)}` });
432
+ }
433
+
434
+ const blocksKey = `blocks:${safeKey(taskId)}`;
435
+ // kv_get answers {success, data:{value}}, and {success:false} when absent.
436
+ const prior = (await ctx.swarm.kv_get({ namespace: NS, key: blocksKey })) as Envelope;
437
+ const priorBlocks = Number(prior?.data?.value ?? prior?.value ?? 0) || 0;
438
+ const willBlock = cfg.mode === "enforce" && priorBlocks < cfg.maxBlocksPerTask;
439
+
440
+ await ctx.swarm.kv_set({
441
+ namespace: NS,
442
+ key: `fail:${Date.now()}:${safeKey(taskId)}`,
443
+ value: { ...verdict, action: willBlock ? "blocked" : "recorded" },
444
+ });
445
+
446
+ if (!willBlock) {
447
+ await ctx.swarm.kv_incr({
448
+ namespace: NS,
449
+ key: cfg.mode === "enforce" ? "escaped" : "shadowed",
450
+ });
451
+ return;
452
+ }
453
+
454
+ await ctx.swarm.kv_incr({ namespace: NS, key: "blocked" });
455
+ await ctx.swarm.kv_incr({ namespace: NS, key: blocksKey });
456
+
457
+ const detail = failed
458
+ .map(
459
+ (d) =>
460
+ `${d} (${scores[d]!.toFixed(2)} vs ${cfg.threshold}) — requested: ${String(prefs[d] ?? "").slice(0, 220)}`,
461
+ )
462
+ .join("\n");
463
+
464
+ return block(
465
+ [
466
+ `Comms gate: this output does not match ${requesterName}'s stated preferences.`,
467
+ detail,
468
+ "Rewrite the output to match and call store-progress again. Do not change the facts, only how they are said.",
469
+ ].join("\n"),
470
+ );
471
+ } catch (err) {
472
+ // Fail open, always.
473
+ try {
474
+ await ctx.swarm.kv_incr({ namespace: NS, key: "error" });
475
+ await ctx.swarm.kv_set({
476
+ namespace: NS,
477
+ key: "error:last",
478
+ value: { at: new Date().toISOString(), taskId, error: String(err).slice(0, 400) },
479
+ });
480
+ } catch {
481
+ /* ignore */
482
+ }
483
+ ctx.log.warn("comms-adherence-gate: failing open", { error: String(err) });
484
+ return;
485
+ }
486
+ });
487
+ };
488
+
489
+ export default extension;
@@ -0,0 +1,8 @@
1
+ {
2
+ "$schema": "../manifest.schema.json",
3
+ "name": "comms-adherence-gate",
4
+ "description": "Judges every Slack-facing store-progress completion against the requester's comms preferences and latest request with Jev (TypeSafe or OpenRouter), and in enforce blocks a non-adherent completion once per task",
5
+ "version": "1.5.0",
6
+ "runtime": "api",
7
+ "assets": { "hooks": "hooks.ts" }
8
+ }
@@ -84,7 +84,7 @@ agent-fs share-create thoughts/<agent-id>/qa/<topic>-screenshots/<name>.png --js
84
84
  - In `store-progress`, list the upload in the `attachments` field with `kind: "agent-fs"` and the path, and paste the `share-create` link for people who need to open the file.
85
85
  - If `agent-fs auth whoami` fails, report the local path, say the upload was skipped, and continue.
86
86
 
87
- The `artifacts` skill holds the full agent-fs recipe and the naming conventions.
87
+ The `agent-fs` skill holds the full agent-fs recipe.
88
88
 
89
89
  ## Gotchas
90
90
 
@@ -79,7 +79,7 @@ agent-fs share-create thoughts/<agent-id>/qa/<topic>-screenshots/<name>.png --js
79
79
  - In `store-progress`, list the upload in the `attachments` field with `kind: "agent-fs"` and the path, and paste the `share-create` link for people who need to open the file.
80
80
  - If `agent-fs auth whoami` fails, report the local path, say the upload was skipped, and continue.
81
81
 
82
- The `artifacts` skill holds the full agent-fs recipe and the naming conventions.
82
+ The `agent-fs` skill holds the full agent-fs recipe.
83
83
 
84
84
  ## Gotchas
85
85
 
@@ -5,7 +5,7 @@ description: Build and iterate on schema-backed Swarm Apps with live models, nam
5
5
 
6
6
  # Swarm Apps
7
7
 
8
- Swarm Apps are persistent, agent-authored internal applications: a schema-backed row store, named queries, custom script/task actions, and a validated json-render interface served at `/apps/<id>`. Use an app when people need to view and change live records; use `create_page` for a static snapshot and `artifacts` for custom server logic or a UI outside this catalog.
8
+ Swarm Apps are persistent, agent-authored internal applications: a schema-backed row store, named queries, custom script/task actions, and a validated json-render interface served at `/apps/<id>`. Use an app when people need to view and change live records; use `create_page` for a static snapshot.
9
9
 
10
10
  > **Capability gate**: app tools are available when the server's `CAPABILITIES` includes `pages` (for example `CAPABILITIES=core,task-pool,pages`). There is no separate `apps` capability. If these tools are absent from your MCP list, this is why.
11
11
 
@@ -740,3 +740,10 @@ Create this with `app-upsert`:
740
740
  ```
741
741
 
742
742
  The tool returns `{ appId, url: "/apps/<id>" }`. Open that URL to verify the live UI, then continue with the read-patch-validate loop above.
743
+
744
+ ## View and screenshot a live app
745
+
746
+ - An app renders at `<dashboard>/apps/<id>`. Two view modes are set by query string: `?mode=full` (full-viewport overlay with a slim header) and `?mode=chromeless` (only the rendered page, the embed surface). `app.navigate` keeps only `?mode`.
747
+ - Logged-in browser: a fresh dashboard has no connection and redirects to /setup. Seed the localStorage key `agent-swarm-connections` with `{connections:[{id,name,apiUrl,apiKey}],activeId}`. Build the value from an env var and pipe it to `agent-browser eval --stdin`, so the key never reaches a command line. Reload, then pick a user in the "Who are you?" dialog.
748
+ - Apps scroll inside an inner canvas, so a full-page screenshot captures only the viewport. Scroll the canvas or raise the viewport height. For mobile, set a 375px-wide viewport before the screenshot.
749
+ - App definitions: `GET /api/apps` and `GET /api/apps/<id>` with the API key as Bearer.
@@ -1,6 +1,6 @@
1
1
  # Swarm Apps
2
2
 
3
- Swarm Apps are persistent, agent-authored internal applications: a schema-backed row store, named queries, custom script/task actions, and a validated json-render interface served at `/apps/<id>`. Use an app when people need to view and change live records; use `create_page` for a static snapshot and `artifacts` for custom server logic or a UI outside this catalog.
3
+ Swarm Apps are persistent, agent-authored internal applications: a schema-backed row store, named queries, custom script/task actions, and a validated json-render interface served at `/apps/<id>`. Use an app when people need to view and change live records; use `create_page` for a static snapshot.
4
4
 
5
5
  > **Capability gate**: app tools are available when the server's `CAPABILITIES` includes `pages` (for example `CAPABILITIES=core,task-pool,pages`). There is no separate `apps` capability. If these tools are absent from your MCP list, this is why.
6
6
 
@@ -735,3 +735,10 @@ Create this with `app-upsert`:
735
735
  ```
736
736
 
737
737
  The tool returns `{ appId, url: "/apps/<id>" }`. Open that URL to verify the live UI, then continue with the read-patch-validate loop above.
738
+
739
+ ## View and screenshot a live app
740
+
741
+ - An app renders at `<dashboard>/apps/<id>`. Two view modes are set by query string: `?mode=full` (full-viewport overlay with a slim header) and `?mode=chromeless` (only the rendered page, the embed surface). `app.navigate` keeps only `?mode`.
742
+ - Logged-in browser: a fresh dashboard has no connection and redirects to /setup. Seed the localStorage key `agent-swarm-connections` with `{connections:[{id,name,apiUrl,apiKey}],activeId}`. Build the value from an env var and pipe it to `agent-browser eval --stdin`, so the key never reaches a command line. Reload, then pick a user in the "Who are you?" dialog.
743
+ - Apps scroll inside an inner canvas, so a full-page screenshot captures only the viewport. Scroll the canvas or raise the viewport height. For mobile, set a 375px-wide viewport before the screenshot.
744
+ - App definitions: `GET /api/apps` and `GET /api/apps/<id>` with the API key as Bearer.
@@ -0,0 +1,14 @@
1
+ {
2
+ "kind": "skill",
3
+ "name": "chat-sdk",
4
+ "displayName": "Chat SDK",
5
+ "slug": "chat-sdk",
6
+ "title": "Chat SDK",
7
+ "description": "Chat threads from Telegram, Slack (Chat SDK), Discord, and Teams. Use when a task carries a task:chat-sdk contextKey or a user asked through Telegram, Slack, Discord, or Teams. Covers what the swarm posts for you, `chat-reply`, markdown and length per platform, attachments, `chat-read-thread` limits, and channel routes with `register-chat-channel`.",
8
+ "version": "1.0.0",
9
+ "category": "skills",
10
+ "placeholders": [],
11
+ "runAllSeedersCandidate": true,
12
+ "systemDefault": false,
13
+ "tags": ["chat", "telegram", "discord", "teams", "slack", "communication"]
14
+ }
@@ -0,0 +1,84 @@
1
+ # Chat SDK
2
+
3
+ The swarm receives messages from Telegram, Slack, Discord, and Microsoft Teams through the Chat SDK channel. A person mentions the bot or sends it a direct message, and the swarm creates a task for it. This skill tells you how to work on such a task.
4
+
5
+ ## How you know you are on a chat task
6
+
7
+ - The task has `source: "chat"`, or it is a lead follow-up of one.
8
+ - The task `contextKey` starts with `task:chat-sdk:`. The full form is `task:chat-sdk:<adapter>:<encoded thread id>`, for example `task:chat-sdk:telegram:555001`.
9
+ - The task text holds the user's message. Later replies in the same thread arrive as follow-up tasks with the same `contextKey`, or as steering messages on your running task.
10
+ - The user has four slash commands that never reach you as a task: `/help`, `/status`, `/cancel` and `/reset`. `/cancel` ends the thread's active tasks, yours included, with status `cancelled`. After `/reset` the next task has no parent task and no earlier thread context, so treat it as a new conversation.
11
+
12
+ Use `get-task-details` to read the `contextKey` when you are not sure.
13
+
14
+ ## Reply with `chat-reply`
15
+
16
+ `chat-reply { text }` posts to the thread your current task came from. It finds the thread from your task, so you do not pass ids.
17
+
18
+ - Use it to ask the user a question you cannot answer yourself. Ask once, with all your questions together.
19
+ - Use it for one short progress note when work takes more than a few minutes, such as "Started. This takes about 10 minutes."
20
+ - Do not use it for receipts ("Got it", "On it"), and do not send many progress notes.
21
+ - It works only on a task with a `task:chat-sdk:` contextKey. A session bound to a non-chat task does not see `chat-reply` at all, because the tool is left out of its tool list.
22
+
23
+ ## The final summary is posted automatically
24
+
25
+ When your task ends, the swarm posts the task output to the thread:
26
+
27
+ - Completed: your `store-progress` output.
28
+ - Failed: a "Task failed." line, then the failure reason.
29
+ - Cancelled: a "Task cancelled." line, then the reason when there is one.
30
+
31
+ Do not post your final answer with `chat-reply` as well. The user would get it twice. Put the answer in the `store-progress` output and stop.
32
+
33
+ The post is durable. If the platform is down, the swarm retries with backoff for up to 24 hours.
34
+
35
+ ## Markdown per platform
36
+
37
+ Write plain, short markdown: bold, italic, inline code, code blocks, bullet lists, and links. Avoid tables, nested lists, and HTML.
38
+
39
+ | Platform | What renders | Limit per message |
40
+ |---|---|---|
41
+ | Telegram | Bold, italic, code, code blocks, links, lists. No tables. | 4096 characters |
42
+ | Discord | Bold, italic, code, code blocks, lists, headings, links. No tables. | 2000 characters |
43
+ | Slack | Converted to Slack mrkdwn: bold, italic, code, lists, links. Tables do not render well. | 4000 characters |
44
+ | Teams | Basic markdown: bold, italic, code, lists, links. | 20000 characters |
45
+
46
+ The swarm splits longer text into several messages at line breaks. Long answers are hard to read in chat, so keep the answer short and put long material in an artifact or page, then post the link.
47
+
48
+ ## Attachments arrive as task attachments
49
+
50
+ Files and images the user sends in the thread are stored as attachments of the task. The task text names them. Download them with the `download-task-attachment` skill. You cannot send files back to the thread in this version. Share a link instead.
51
+
52
+ ## `chat-read-thread` limits per platform
53
+
54
+ `chat-read-thread { threadId, limit? }` returns the most recent messages of a thread, oldest first. The thread id is the adapter name plus the URI-decoded remainder of the contextKey. Telegram ids have no escaping, so `telegram:555001` stays as it is. Slack, Discord, and Teams ids contain `%3A` in the contextKey, so do not build the id by hand. `chat-reply` resolves the thread itself. For `chat-read-thread`, use the thread id returned by `chat-reply`, `chat-post`, or `chat-open-dm`. You can read the thread of your current task. Any other thread needs lead privileges.
55
+
56
+ - Telegram has no history API. The SDK keeps the Telegram thread history it saw in the swarm database, capped at 100 messages and 7 days.
57
+ - Teams needs the `Chat.Read.*` Graph permission on the bot. Without it, the call fails.
58
+ - Slack and Discord return the platform history.
59
+
60
+ The task text already holds the earlier swarm messages of the thread, so read the thread only when you need more.
61
+
62
+ ## Other tools
63
+
64
+ - `chat-post { adapter, threadId | channelId, text }` posts outside your task's thread. Lead only. Use it only when the task asks you to message another thread or channel.
65
+ - `chat-open-dm { adapter, userId }` returns the direct-message thread id for a user. Lead only. Then post to it with `chat-post`.
66
+
67
+ ## Leads: `register-chat-channel`
68
+
69
+ By default every message addressed to the swarm goes to the lead. A lead can route one channel to a fixed agent or workflow:
70
+
71
+ ```
72
+ register-chat-channel { adapter: "telegram", channelId: "telegram:-100123", agentId: "<agent id>" }
73
+ register-chat-channel { adapter: "slack", channelId: "slack:C123", workflowId: "<workflow id>" }
74
+ unregister-chat-channel { adapter: "telegram", channelId: "telegram:-100123" }
75
+ ```
76
+
77
+ Give exactly one of `agentId` or `workflowId`. A `swarm#<agentId>` token in a message still wins over the route. A workflow mapping fires only for a bot mention or a DM; a plain reply in the subscribed thread does not start another run. Only the lead can call these tools.
78
+
79
+ ## Etiquette
80
+
81
+ - Keep messages short. Chat is read on phones.
82
+ - Never post secrets, tokens, internal URLs with credentials, or private data from other users. The swarm scrubs known secrets, but do not depend on it.
83
+ - When you ask a question in a group, start with the user's name so they see it.
84
+ - One question message is better than several.