@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
@@ -11,7 +11,14 @@ import {
11
11
  withSpan,
12
12
  withSpanContext,
13
13
  } from "../otel.ts";
14
- import { type BasePromptArgs, getBasePrompt } from "../prompts/base-prompt.ts";
14
+ import {
15
+ type BasePromptArgs,
16
+ getBasePrompt,
17
+ getTaskContextPrompt,
18
+ type RepoContext,
19
+ type SlackTaskContext,
20
+ type TaskContextPromptArgs,
21
+ } from "../prompts/base-prompt.ts";
15
22
  import {
16
23
  generateDefaultClaudeMd,
17
24
  generateDefaultIdentityMd,
@@ -40,6 +47,7 @@ import {
40
47
  type ProviderSession,
41
48
  type ProviderSessionConfig,
42
49
  } from "../providers/index.ts";
50
+ import { effectiveReasoningEffort } from "../providers/reasoning-effort.ts";
43
51
  import {
44
52
  createSkillInvokeTracker,
45
53
  isSkillLoaderCall,
@@ -48,6 +56,7 @@ import {
48
56
  } from "../providers/skill-invoke.ts";
49
57
  import type { SteerDeliveryResult } from "../providers/types.ts";
50
58
  import { WorkerRealtimeClient } from "../realtime/worker-client";
59
+ import { resumedTaskModelSelection, type TaskModelPinFields } from "../tasks/model-pin.ts";
51
60
  import { initTelemetry, telemetry } from "../telemetry.ts";
52
61
  import { mapTriggerSurface } from "../telemetry-context.ts";
53
62
  import {
@@ -66,12 +75,14 @@ import { getApiKey } from "../utils/api-key.ts";
66
75
  import { computeBudgetBackoffMs } from "../utils/budget-backoff.ts";
67
76
  import { ensure, initialize } from "../utils/business-use";
68
77
  import { isCodexAuthFailureReason } from "../utils/codex-auth-failure.ts";
78
+ import { isEnvExportRestrictedKey } from "../utils/config-env-export.ts";
69
79
  import { MASKED_SECRET_VALUE } from "../utils/config-secrets.ts";
70
80
  import { getMcpBaseUrl } from "../utils/constants.ts";
71
81
  import { getContextWindowSize } from "../utils/context-window.ts";
72
82
  import {
73
83
  type CredentialSelection,
74
84
  ModelWindowExhaustedError,
85
+ normalizeProviderKeyAliases,
75
86
  resolveCredentialPools,
76
87
  } from "../utils/credentials.ts";
77
88
  import { resolveCodexCreditsExhaustedCooldownMs } from "../utils/error-tracker.ts";
@@ -81,11 +92,16 @@ import { prettyPrintLine, prettyPrintStderr } from "../utils/pretty-print.ts";
81
92
  import { terminateRegisteredProcessGroups } from "../utils/process-group.ts";
82
93
  import { refreshRuntimeModelCatalog } from "../utils/runtime-model-catalog.ts";
83
94
  import { resolveScriptsOnlyMode } from "../utils/scripts-only-mode.ts";
84
- import { registerSensitiveKeyName, scrubSecrets } from "../utils/secret-scrubber.ts";
95
+ import {
96
+ registerEnvSecrets,
97
+ registerSensitiveKeyName,
98
+ scrubSecrets,
99
+ } from "../utils/secret-scrubber.ts";
85
100
  import { refreshSkillsIfChanged } from "../utils/skills-refresh.ts";
86
101
  import { guardSpawnModel } from "../utils/spawn-model-guard.ts";
87
102
  import { isSteeringEnabled } from "../utils/steering-enabled.ts";
88
103
  import { interpolate } from "../utils/template.ts";
104
+ import { vcsRepoName, vcsRepoUrlMatches } from "../utils/vcs-repo-match.ts";
89
105
  import {
90
106
  canonicalAzureDevOpsRepoUrl,
91
107
  isAzureDevOpsUrl,
@@ -181,17 +197,6 @@ function scheduleRunnerGc(reason: string): boolean {
181
197
  return true;
182
198
  }
183
199
 
184
- /** Save PM2 process list for persistence across container restarts */
185
- async function savePm2State(role: string): Promise<void> {
186
- try {
187
- console.log(`[${role}] Saving PM2 process list...`);
188
- await Bun.$`pm2 save`.quiet();
189
- console.log(`[${role}] PM2 state saved`);
190
- } catch {
191
- // PM2 not available or no processes - silently ignore
192
- }
193
- }
194
-
195
200
  /** Fetch repo config for a task's vcsRepo (e.g., "desplega-ai/agent-swarm") */
196
201
  export async function fetchRepoConfig(
197
202
  apiUrl: string,
@@ -208,11 +213,7 @@ export async function fetchRepoConfig(
208
213
  guidelines?: RepoGuidelines | null;
209
214
  } | null> {
210
215
  try {
211
- const requested = vcsRepo
212
- .trim()
213
- .replace(/\/+$/, "")
214
- .replace(/\.git$/, "");
215
- const repoName = requested.split("/").pop() || requested;
216
+ const repoName = vcsRepoName(vcsRepo);
216
217
  const resp = await fetch(`${apiUrl}/api/repos?name=${encodeURIComponent(repoName)}`, {
217
218
  headers: { Authorization: `Bearer ${apiKey}` },
218
219
  });
@@ -229,18 +230,7 @@ export async function fetchRepoConfig(
229
230
  }>;
230
231
  };
231
232
  if (requireExactMatch) {
232
- return (
233
- data.repos.find((r) => {
234
- const normalized = r.url
235
- .trim()
236
- .replace(/\/+$/, "")
237
- .replace(/\.git$/, "");
238
- if (normalized === requested) return true;
239
- // Qualified URLs must retain their host. Only shorthand references use suffix matching.
240
- if (requested.includes(":")) return false;
241
- return normalized.endsWith(`/${requested}`) || normalized.endsWith(`:${requested}`);
242
- }) ?? null
243
- );
233
+ return data.repos.find((r) => vcsRepoUrlMatches(r.url, vcsRepo)) ?? null;
244
234
  }
245
235
  return data.repos.find((r) => r.url.includes(vcsRepo)) ?? data.repos[0] ?? null;
246
236
  } catch {
@@ -846,6 +836,19 @@ const deploymentMemoryRaters = process.env.MEMORY_RATERS;
846
836
  */
847
837
  const rowOverriddenBootEnv = new Map<string, string | undefined>();
848
838
 
839
+ let lastWithheldEnvKeys = "";
840
+
841
+ /** Names only, and only when the set changes, so per-spawn reloads stay quiet. */
842
+ function logWithheldEnvKeys(keys: string[]): void {
843
+ const summary = [...keys].sort().join(", ");
844
+ if (summary === lastWithheldEnvKeys) return;
845
+ lastWithheldEnvKeys = summary;
846
+ if (!summary) return;
847
+ console.log(
848
+ `[env-reload] Kept ${keys.length} config entries with exportToEnv=false out of the harness env: ${summary}`,
849
+ );
850
+ }
851
+
849
852
  export async function fetchResolvedEnv(
850
853
  apiUrl: string,
851
854
  apiKey: string,
@@ -895,7 +898,12 @@ export async function fetchResolvedEnv(
895
898
  console.warn(`[env-reload] Failed to fetch config: ${response.status}`);
896
899
  } else {
897
900
  const data = (await response.json()) as {
898
- configs: Array<{ key: string; value: string; isSecret?: boolean }>;
901
+ configs: Array<{
902
+ key: string;
903
+ value: string;
904
+ isSecret?: boolean;
905
+ exportToEnv?: boolean;
906
+ }>;
899
907
  };
900
908
 
901
909
  // A deleted row restores the deployment value (including unset), while
@@ -914,7 +922,25 @@ export async function fetchResolvedEnv(
914
922
  scriptsOnlyConfigValue = data.configs.find(
915
923
  (config) => config.key === "SCRIPTS_ONLY_MCP",
916
924
  )?.value;
925
+ const withheldEnvKeys: string[] = [];
917
926
  for (const config of data.configs) {
927
+ // Every harness and its shell tool inherit this env. A row with
928
+ // exportToEnv=false stays out of it, and so does a copy of it in
929
+ // the base env (container env, or an older entrypoint's export).
930
+ // Checked before the masked-value fallback so a masked response
931
+ // can never keep a withheld boot value alive. An API without the
932
+ // field (pre-215) leaves it undefined, which keeps exporting.
933
+ if (config.exportToEnv === false || isEnvExportRestrictedKey(config.key)) {
934
+ if (config.isSecret) registerSensitiveKeyName(config.key);
935
+ delete env[config.key];
936
+ // Several adapters build the child env as
937
+ // `{ ...process.env, ...config.env }`, so a boot-time export
938
+ // revoked later must leave process.env too. It stays out until
939
+ // the worker restarts, even if the row is deleted afterwards.
940
+ if (baseEnv === process.env) delete process.env[config.key];
941
+ withheldEnvKeys.push(config.key);
942
+ continue;
943
+ }
918
944
  if (config.isSecret && config.value === MASKED_SECRET_VALUE) {
919
945
  skippedSecretKeys.push(config.key);
920
946
  continue;
@@ -957,6 +983,7 @@ export async function fetchResolvedEnv(
957
983
  scrubSecrets(`[env-reload] Skipped masked secrets: ${skippedSecretKeys.join(", ")}`),
958
984
  );
959
985
  }
986
+ logWithheldEnvKeys(withheldEnvKeys);
960
987
  console.log(`[env-reload] Loaded ${data.configs.length} config entries from API`);
961
988
  }
962
989
  // Non-secret stand-in for the pool rows skipped above, so credential
@@ -969,6 +996,16 @@ export async function fetchResolvedEnv(
969
996
  }
970
997
  }
971
998
 
999
+ // Registry key aliases (`HELMCODE_API_NAN` for `NAN_API_KEY`) are copied
1000
+ // into their canonical var once here, so pooling, credential checks, pi's
1001
+ // runtime key and the opencode key splice only ever read the canonical
1002
+ // name. An explicit canonical value wins.
1003
+ normalizeProviderKeyAliases(env);
1004
+ // Rows admitted after boot and pool members live only in this env, never in
1005
+ // process.env, so the scrubber's env pass cannot see them. Register them
1006
+ // before any adapter can echo one into a log or failure reason.
1007
+ registerEnvSecrets(env);
1008
+
972
1009
  // A task already has an adapter. Repository configuration must not select
973
1010
  // credentials or configure hooks for a different harness.
974
1011
  const resolvedProvider = sessionContext?.provider ?? resolveHarnessProvider(env, baseEnv);
@@ -1355,11 +1392,6 @@ const SWARM_TOOL_LABELS: Record<string, string | null> = {
1355
1392
  "create-channel": "📢 Creating channel",
1356
1393
  "list-channels": "📢 Listing channels",
1357
1394
  "delete-channel": "📢 Deleting channel",
1358
- // Services
1359
- "register-service": "🔌 Registering service",
1360
- "list-services": "🔌 Listing services",
1361
- "unregister-service": "🔌 Unregistering service",
1362
- "update-service-status": "🔌 Updating service status",
1363
1395
  // Reusable scripts
1364
1396
  "script-search": "📜 Searching scripts",
1365
1397
  "script-run": "📜 Running script",
@@ -2343,6 +2375,7 @@ export async function getPausedTasksFromAPI(config: ApiConfig): Promise<
2343
2375
  output?: string;
2344
2376
  status?: string;
2345
2377
  contextKey?: string;
2378
+ requestedBy?: Trigger["requestedBy"];
2346
2379
  }>
2347
2380
  > {
2348
2381
  const headers: Record<string, string> = {
@@ -2379,6 +2412,7 @@ export async function getPausedTasksFromAPI(config: ApiConfig): Promise<
2379
2412
  finishedAt?: string;
2380
2413
  output?: string;
2381
2414
  status?: string;
2415
+ requestedBy?: Trigger["requestedBy"];
2382
2416
  }>;
2383
2417
  };
2384
2418
  return data.tasks || [];
@@ -2537,7 +2571,6 @@ export function setupShutdownHandlers(
2537
2571
  });
2538
2572
  await closeAgent(apiConfig, role);
2539
2573
  }
2540
- await savePm2State(role);
2541
2574
  process.exit(exitCode);
2542
2575
  };
2543
2576
 
@@ -2805,10 +2838,13 @@ async function saveProviderSessionId(
2805
2838
  model?: string,
2806
2839
  harnessVariant?: string,
2807
2840
  harnessVariantMeta?: Record<string, unknown>,
2841
+ effort?: ReasoningEffort | null,
2808
2842
  ): Promise<void> {
2809
2843
  const headers: Record<string, string> = { "Content-Type": "application/json" };
2810
2844
  if (apiKey) headers.Authorization = `Bearer ${apiKey}`;
2811
2845
  const body: Record<string, unknown> = { claudeSessionId };
2846
+ // Devin's body variant has no effort field.
2847
+ if (effort !== undefined && provider !== "devin") body.effort = effort;
2812
2848
  if (provider !== undefined) body.provider = provider;
2813
2849
  if (providerMeta !== undefined) body.providerMeta = providerMeta;
2814
2850
  if (model !== undefined && model !== "") body.model = model;
@@ -3284,6 +3320,64 @@ export async function buildRequesterProfilePrompt(
3284
3320
  return result.skipped ? "" : result.text.trim();
3285
3321
  }
3286
3322
 
3323
+ /**
3324
+ * Warning appended to the task prompt when the task's `dir` was requested but
3325
+ * the session could not start there. Shared by the fresh and resume paths so
3326
+ * both send the model the same text.
3327
+ */
3328
+ export function missingTaskDirWarning(
3329
+ taskDir: string | undefined,
3330
+ effectiveCwd: string | undefined,
3331
+ ): string | undefined {
3332
+ return taskDir && !effectiveCwd
3333
+ ? `Note: The task requested working directory "${taskDir}" but it does not exist. Falling back to default directory.`
3334
+ : undefined;
3335
+ }
3336
+
3337
+ /** Per-task context that rides in the task prompt, never the system prompt. */
3338
+ export type TaskSessionContext = Omit<TaskContextPromptArgs, keyof BasePromptArgs> & {
3339
+ requestedBy?: RequesterProfile;
3340
+ /** Set when the task's `dir` does not exist and the session falls back. */
3341
+ cwdWarning?: string;
3342
+ };
3343
+
3344
+ /**
3345
+ * The two prompts of one task session. The system prompt depends only on the
3346
+ * agent (profile, harness, deployment), so it is byte-identical across that
3347
+ * agent's sessions and the provider reads it from cache instead of re-writing
3348
+ * it. Everything that varies per task (repository block, scripts-only Slack
3349
+ * note, requester profile, cwd warning) is appended to the task prompt.
3350
+ */
3351
+ export async function buildTaskSessionPrompts(input: {
3352
+ basePromptArgs: BasePromptArgs;
3353
+ additionalSystemPrompt?: string;
3354
+ taskPrompt: string;
3355
+ context: TaskSessionContext;
3356
+ }): Promise<{ systemPrompt: string; prompt: string }> {
3357
+ const { basePromptArgs, additionalSystemPrompt, context } = input;
3358
+ const basePrompt = await getBasePrompt(basePromptArgs);
3359
+ const systemPrompt = additionalSystemPrompt
3360
+ ? `${basePrompt}\n\n${additionalSystemPrompt}`
3361
+ : basePrompt;
3362
+
3363
+ const taskContext = [
3364
+ await getTaskContextPrompt({
3365
+ ...basePromptArgs,
3366
+ repoContext: context.repoContext,
3367
+ slackContext: context.slackContext,
3368
+ }),
3369
+ await buildRequesterProfilePrompt(context.requestedBy),
3370
+ context.cwdWarning?.trim(),
3371
+ ]
3372
+ .filter((part): part is string => Boolean(part))
3373
+ .join("\n\n");
3374
+
3375
+ return {
3376
+ systemPrompt,
3377
+ prompt: taskContext ? `${input.taskPrompt}\n\n${taskContext}` : input.taskPrompt,
3378
+ };
3379
+ }
3380
+
3287
3381
  /** A non-2xx answer from `POST /api/agents`; carries the status for retry triage. */
3288
3382
  export class AgentRegistrationHttpError extends Error {
3289
3383
  readonly status: number;
@@ -4128,6 +4222,9 @@ async function spawnProviderProcess(
4128
4222
  }
4129
4223
  if (spawnModel.warning) console.warn(spawnModel.warning);
4130
4224
  const model = spawnModel.model;
4225
+ // Recorded on the task at session start, like the model: what the adapter
4226
+ // applies, not just what was asked for.
4227
+ const sessionEffort = effectiveReasoningEffort(adapter.name, model, reasoningEffortOverride);
4131
4228
 
4132
4229
  // Resolve Codex OAuth pool slot BEFORE building ProviderSessionConfig so we
4133
4230
  // can pass codexSlot through and the adapter writes token refreshes back to
@@ -4431,6 +4528,7 @@ async function spawnProviderProcess(
4431
4528
  model,
4432
4529
  event.harnessVariant,
4433
4530
  event.harnessVariantMeta,
4531
+ sessionEffort,
4434
4532
  ).catch((err) => console.warn(`[runner] Failed to save session ID: ${err}`));
4435
4533
  } else {
4436
4534
  // Pool task: save provider session ID on active session so it can be
@@ -5690,17 +5788,13 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
5690
5788
  let agentSkillsSummary: { name: string; description: string }[] | undefined;
5691
5789
  let agentMcpServers: string[] | undefined;
5692
5790
 
5693
- // Per-task repo context — set when processing a task with githubRepo
5694
- let currentRepoContext: BasePromptArgs["repoContext"] | undefined;
5695
- // Slack context for current task (gates Slack instructions in prompt)
5696
- let currentTaskSlackContext: BasePromptArgs["slackContext"] | undefined;
5697
-
5698
5791
  // Generate base prompt (identity fields injected after profile fetch below).
5699
5792
  // Traits are read fresh on each call so a live adapter swap (Section 4)
5700
- // produces a prompt matching the new provider's capabilities.
5701
- const buildSystemPrompt = async () => {
5793
+ // produces a prompt matching the new provider's capabilities. Per-task
5794
+ // context never enters it; see buildTaskSessionPrompts.
5795
+ const basePromptArgs = (): BasePromptArgs => {
5702
5796
  const { traits } = adapter;
5703
- return getBasePrompt({
5797
+ return {
5704
5798
  role,
5705
5799
  agentId,
5706
5800
  capabilities,
@@ -5711,20 +5805,19 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
5711
5805
  name: agentProfileName,
5712
5806
  description: agentDescription,
5713
5807
  // SOUL.md is the persona and travels to every provider. base-prompt
5714
- // gates IDENTITY.md (inject when edited) and CLAUDE.md (local
5715
- // non-claude providers, inject when edited) itself.
5808
+ // gates IDENTITY.md and CLAUDE.md (inject when edited) itself.
5716
5809
  soulMd: agentSoulMd,
5717
5810
  identityMd: agentIdentityMd,
5718
5811
  claudeMd: agentClaudeMd,
5719
- repoContext: currentRepoContext,
5720
- slackContext: currentTaskSlackContext,
5721
5812
  ...(traits.hasMcp && {
5722
5813
  skillsSummary: agentSkillsSummary,
5723
5814
  mcpServers: agentMcpServers,
5724
5815
  }),
5725
- });
5816
+ };
5726
5817
  };
5727
5818
 
5819
+ const buildSystemPrompt = async () => getBasePrompt(basePromptArgs());
5820
+
5728
5821
  let basePrompt = await buildSystemPrompt();
5729
5822
 
5730
5823
  // Resolve additional system prompt: CLI flag > env var
@@ -6657,7 +6750,8 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
6657
6750
  }
6658
6751
  }
6659
6752
 
6660
- if (!resumeCwd && task.vcsRepo && apiUrl) {
6753
+ let resumeRepoContext: RepoContext | undefined;
6754
+ if (task.vcsRepo && apiUrl) {
6661
6755
  const repoConfig = await fetchRepoConfig(apiUrl, apiKey, task.vcsRepo);
6662
6756
  const effectiveConfig = repoConfig ?? {
6663
6757
  url: task.vcsRepo,
@@ -6668,11 +6762,34 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
6668
6762
  // This is the paused-task-resume-after-restart path — never a first
6669
6763
  // kickoff, so never force-reset the base branch here.
6670
6764
  const repoContext = await ensureRepoForTask(effectiveConfig, role, false);
6671
- if (repoContext?.clonePath) {
6765
+ resumeRepoContext = { ...repoContext, guidelines: repoConfig?.guidelines ?? null };
6766
+ if (!resumeCwd && repoContext?.clonePath) {
6672
6767
  resumeCwd = repoContext.clonePath;
6673
6768
  }
6674
6769
  }
6675
6770
 
6771
+ // Same split as the main task path: stable system prompt, per-task
6772
+ // context (repository, scripts-only Slack note, requester profile,
6773
+ // cwd warning) in the task prompt.
6774
+ const resumeSlackChannelId = (task as { slackChannelId?: string }).slackChannelId;
6775
+ const resumeSession = await buildTaskSessionPrompts({
6776
+ basePromptArgs: basePromptArgs(),
6777
+ additionalSystemPrompt,
6778
+ taskPrompt: resumePrompt,
6779
+ context: {
6780
+ repoContext: resumeRepoContext,
6781
+ slackContext: resumeSlackChannelId
6782
+ ? {
6783
+ channelId: resumeSlackChannelId,
6784
+ threadTs: (task as { slackThreadTs?: string }).slackThreadTs,
6785
+ }
6786
+ : undefined,
6787
+ requestedBy: task.requestedBy,
6788
+ cwdWarning: missingTaskDirWarning(task.dir, resumeCwd),
6789
+ },
6790
+ });
6791
+ resumePrompt = resumeSession.prompt;
6792
+
6676
6793
  // Per-task runner session ID so session logs are scoped to this task
6677
6794
  const resumeRunnerSessionId = crypto.randomUUID();
6678
6795
 
@@ -6693,7 +6810,7 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
6693
6810
  {
6694
6811
  prompt: resumePrompt,
6695
6812
  logFile,
6696
- systemPrompt: resolvedSystemPrompt,
6813
+ systemPrompt: resumeSession.systemPrompt,
6697
6814
  additionalArgs: opts.additionalArgs,
6698
6815
  // Native resume deprecated: always undefined. Follow-up continuity flows through
6699
6816
  // the context preamble injected above (see context-preamble.ts).
@@ -6706,10 +6823,11 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
6706
6823
  runnerSessionId: resumeRunnerSessionId,
6707
6824
  iteration,
6708
6825
  taskId: task.id,
6709
- model: (task as { model?: string }).model,
6826
+ // Only the task's own pin carries over. `model` and the claim
6827
+ // record describe the run before the pause; without a pin the
6828
+ // resumed run resolves from the agent's current config.
6829
+ ...resumedTaskModelSelection(task as TaskModelPinFields),
6710
6830
  modelTier: (task as { modelTier?: string }).modelTier,
6711
- resolvedModel: (task as { resolvedModel?: string }).resolvedModel,
6712
- modelSource: (task as { modelSource?: string }).modelSource,
6713
6831
  effort: (task as { effort?: ReasoningEffort }).effort,
6714
6832
  harnessProvider: state.harnessProvider,
6715
6833
  cwd: resumeCwd,
@@ -7102,11 +7220,12 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
7102
7220
  ?.slackChannelId;
7103
7221
  const taskSlackThreadTs = (trigger.task as { slackThreadTs?: string } | undefined)
7104
7222
  ?.slackThreadTs;
7105
- currentTaskSlackContext = taskSlackChannelId
7223
+ const taskSlackContext: SlackTaskContext | undefined = taskSlackChannelId
7106
7224
  ? { channelId: taskSlackChannelId, threadTs: taskSlackThreadTs }
7107
7225
  : undefined;
7108
7226
 
7109
7227
  // Handle repo context for tasks with vcsRepo (GitHub/GitLab)
7228
+ let currentRepoContext: RepoContext | undefined;
7110
7229
  const taskVcsRepo = (trigger.task as { vcsRepo?: string } | undefined)?.vcsRepo;
7111
7230
  if (taskVcsRepo && apiUrl) {
7112
7231
  const repoConfig = await fetchRepoConfig(apiUrl, apiKey, taskVcsRepo);
@@ -7130,8 +7249,6 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
7130
7249
  ...repoResult,
7131
7250
  guidelines: repoConfig?.guidelines ?? null,
7132
7251
  };
7133
- } else {
7134
- currentRepoContext = undefined;
7135
7252
  }
7136
7253
 
7137
7254
  // Resolve effective working directory (priority: task.dir > repoContext.clonePath > process.cwd())
@@ -7170,11 +7287,8 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
7170
7287
  " You can still access any path on the filesystem — this is just your starting directory.";
7171
7288
  }
7172
7289
 
7173
- // Warn in system prompt when task dir was specified but doesn't exist
7174
- let cwdWarning = "";
7175
- if (taskDir && !effectiveCwd) {
7176
- cwdWarning = `\n\nNote: The task requested working directory "${taskDir}" but it does not exist. Falling back to default directory.`;
7177
- }
7290
+ // Warn when task dir was specified but doesn't exist
7291
+ const cwdWarning = missingTaskDirWarning(taskDir, effectiveCwd);
7178
7292
 
7179
7293
  // Per-task skill hot-reload. Reuses the boot-time helper; signature
7180
7294
  // probe short-circuits when nothing changed. Skipped for
@@ -7220,13 +7334,21 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
7220
7334
  );
7221
7335
  }
7222
7336
 
7223
- // Rebuild system prompt with per-task repo context
7224
- const taskBasePrompt = await buildSystemPrompt();
7225
- const requesterProfilePrompt = await buildRequesterProfilePrompt(trigger.requestedBy);
7226
- const taskPromptParts = [taskBasePrompt, requesterProfilePrompt, additionalSystemPrompt]
7227
- .filter((part): part is string => Boolean(part))
7228
- .join("\n\n");
7229
- const taskSystemPrompt = taskPromptParts + cwdWarning;
7337
+ // Rebuild the system prompt (profile or skills may have changed) and
7338
+ // append the per-task context to the task prompt.
7339
+ const taskSession = await buildTaskSessionPrompts({
7340
+ basePromptArgs: basePromptArgs(),
7341
+ additionalSystemPrompt,
7342
+ taskPrompt: triggerPrompt,
7343
+ context: {
7344
+ repoContext: currentRepoContext,
7345
+ slackContext: taskSlackContext,
7346
+ requestedBy: trigger.requestedBy,
7347
+ cwdWarning,
7348
+ },
7349
+ });
7350
+ const taskSystemPrompt = taskSession.systemPrompt;
7351
+ triggerPrompt = taskSession.prompt;
7230
7352
 
7231
7353
  // The server refused the task's explicit model for this worker's CLI
7232
7354
  // version (harness_model_support = unsupported): fail fast, no spawn.
package/src/e2b/env.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { resolve } from "node:path";
2
+ import { openAICompatibleBaseUrlVars, openAICompatibleKeyVars } from "@desplega/model-catalog";
2
3
  import { getApiKey } from "../utils/api-key";
3
4
  import { isSensitiveKey, scrubSecrets } from "../utils/secret-scrubber";
4
5
 
@@ -20,6 +21,10 @@ export const DEFAULT_E2B_FORWARD_KEYS = [
20
21
  "CLAUDE_CODE_OAUTH_TOKEN",
21
22
  "OPENAI_API_KEY",
22
23
  "OPENROUTER_API_KEY",
24
+ // OpenAI-compatible registry providers: key vars (canonical and alias) and
25
+ // their `<ID>_BASE_URL` overrides.
26
+ ...openAICompatibleKeyVars(),
27
+ ...openAICompatibleBaseUrlVars(),
23
28
  "GITHUB_TOKEN",
24
29
  "GITLAB_TOKEN",
25
30
  "AZURE_DEVOPS_TOKEN",
@@ -1,4 +1,21 @@
1
1
  {
2
+ "comms-adherence-gate": {
3
+ "manifestFile": "manifest.json",
4
+ "manifest": {
5
+ "$schema": "../manifest.schema.json",
6
+ "name": "comms-adherence-gate",
7
+ "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",
8
+ "version": "1.5.0",
9
+ "runtime": "api",
10
+ "assets": {
11
+ "hooks": "hooks.ts"
12
+ }
13
+ },
14
+ "files": {
15
+ "hooks.ts": "import { type ApiCtx, block, type SwarmExtension } from \"swarm-extension\";\nimport { z } from \"zod\";\n\n/**\n * comms-adherence-gate\n *\n * When an agent calls `store-progress` with status \"completed\" on a task that\n * reaches a human in Slack, judge the `output` against the requesting user's\n * stored comms preferences (users.metadata.$.comms) with Jev, and against what\n * that user actually asked for in their latest message. An explicit request\n * (\"tell me a long joke\", \"explain in detail\") outranks the stored default, so\n * a requested long answer is not a verbosity failure.\n *\n * Transport (config.provider): Jev is served by TypeSafe directly and by\n * OpenRouter's Decisions endpoint. Both take the same {model, state, questions}\n * body and return the same `answers` shape. `auto` uses TYPESAFE_API_KEY when\n * set, else OPENROUTER_API_KEY.\n *\n * modes (config.mode):\n * off -> never evaluate\n * shadow -> evaluate + record, never block (default)\n * enforce -> record, and block the completion up to maxBlocksPerTask times\n * per task with actionable feedback\n *\n * Fails OPEN on every error: a judge outage must never wall the swarm.\n * Every verdict lands in KV namespace `ext:comms-gate`.\n */\n\nexport const config = z.object({\n mode: z.enum([\"off\", \"shadow\", \"enforce\"]).default(\"shadow\"),\n /** A dimension scoring below this fails. */\n threshold: z.number().min(0).max(1).default(0.5),\n timeoutMs: z.number().int().positive().default(4000),\n maxBlocksPerTask: z.number().int().min(0).default(1),\n provider: z.enum([\"auto\", \"typesafe\", \"openrouter\"]).default(\"auto\"),\n /** Jev model on the TypeSafe transport. */\n model: z.string().default(\"jev-latest\"),\n /** Jev model on the OpenRouter transport. */\n openrouterModel: z.string().default(\"typesafe/jev-1.13\"),\n /** Explicit TypeSafe key; otherwise TYPESAFE_API_KEY from the API process env. */\n apiKey: z.string().optional(),\n});\n\ntype Cfg = z.infer<typeof config>;\ntype Swarm = ApiCtx[\"swarm\"];\n\nconst NS = \"ext:comms-gate\";\nconst TYPESAFE_ENDPOINT = \"https://api.typesafe.ai/v1/systemone\";\nconst OPENROUTER_ENDPOINT = \"https://openrouter.ai/api/alpha/decisions\";\nconst UUID_RE = /^[0-9a-fA-F-]{36}$/;\n// The dispatcher cancels a handler after 5 s; leave room for the DB read and KV writes.\nconst MAX_TIMEOUT_MS = 4000;\n\nexport const cfgOf = (raw: unknown): Cfg => {\n const parsed = config.safeParse(raw ?? {});\n return parsed.success ? parsed.data : config.parse({});\n};\n\n// kv keys accept [a-zA-Z0-9._:/%-]\nconst safeKey = (raw: string | undefined | null): string =>\n (raw && raw.length > 0 ? raw : \"unknown\").replace(/[^a-zA-Z0-9._-]/g, \"_\").slice(0, 120);\n\n/** SDK envelopes: {success, data:{...}} or the bare payload. */\ntype Envelope = {\n data?: { rows?: unknown[]; value?: unknown };\n rows?: unknown[];\n value?: unknown;\n} | null;\n\nconst cell = (row: unknown, i: number, key: string): unknown =>\n Array.isArray(row) ? row[i] : (row as Record<string, unknown>)?.[key];\n\n/**\n * The newest human message sits at the END of a Slack task: the\n * `<thread_context>` blob comes first and can run to thousands of chars, so a\n * leading slice of `agent_tasks.task` shows the judge everything except the\n * actual request. Take the tail, then the part after the last `</thread_context>`.\n */\nconst CTX_END = \"</thread_context>\";\nexport const latestAsk = (tail: string): string => {\n const end = tail.lastIndexOf(CTX_END);\n let out = (end >= 0 ? tail.slice(end + CTX_END.length) : tail)\n .replace(/\\[Thread follow-up[^\\]]*\\]/g, \"\")\n .trim();\n // Some tasks end with the context block; fall back to the last messages in it.\n if (out.length < 8) out = (end >= 0 ? tail.slice(0, end) : tail).trim().slice(-800);\n return out.slice(0, 900);\n};\n\n// Preferences are the DEFAULT. A direct request outranks them.\nconst JUDGING_RULES = [\n \"The requester's explicit instruction in latest_request OUTRANKS requester_preferences. Preferences describe the DEFAULT; they never override a direct request.\",\n \"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.\",\n \"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.\",\n];\n\ntype Prefs = { verbosity?: unknown; tone?: unknown; language?: unknown } & Record<string, unknown>;\n\n/** One `noul` question per stated preference the gate knows how to judge. */\nexport const buildQuestions = (prefs: Prefs): Record<string, unknown> => {\n const questions: Record<string, unknown> = {};\n if (prefs.verbosity) {\n questions.verbosity = {\n type: \"noul\",\n instructions:\n \"Does agent_message respect the requester's verbosity preference, AS OVERRIDDEN by any explicit instruction in latest_request? Apply judging_rules.\",\n criteria: {\n 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.\",\n false:\n \"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.\",\n },\n };\n }\n if (prefs.tone) {\n questions.tone = {\n type: \"noul\",\n instructions:\n \"Does agent_message match the requester's stated tone preference, as overridden by any explicit instruction in latest_request? Apply judging_rules.\",\n criteria: {\n true: \"The tone matches what the requester asked for.\",\n false:\n \"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.\",\n },\n };\n }\n if (prefs.language) {\n questions.language = {\n type: \"noul\",\n instructions: \"Is agent_message written in the requester's stated language?\",\n criteria: {\n true: \"Written in the stated language.\",\n false: \"Written in a different language.\",\n },\n };\n }\n return questions;\n};\n\nconst secretCache = new Map<string, { value: string; at: number }>();\nconst KEY_TTL_MS = 5 * 60 * 1000;\nconst usable = (v: string): boolean => Boolean(v) && !v.startsWith(\"[REDACTED\");\nconst envOf = (name: string): string => String(process.env[name] ?? \"\");\n\n/**\n * Resolve a secret: explicit value, then the API process env. The API process loads\n * global swarm_config rows into process.env at boot and on config reload.\n * ctx.swarm.config_get cannot serve it: the SDK scrubs secrets to \"[REDACTED:<name>]\".\n */\nconst secret = (name: string, explicit?: string): string => {\n const cached = secretCache.get(name);\n if (cached && Date.now() - cached.at < KEY_TTL_MS) return cached.value;\n const value = [String(explicit ?? \"\"), envOf(name)].find(usable) ?? \"\";\n if (usable(value)) secretCache.set(name, { value, at: Date.now() });\n return value;\n};\n\n/** Test hook: forget cached secrets. */\nexport const resetKeyCache = (): void => {\n secretCache.clear();\n};\n\nexport type JevTransport = {\n provider: \"typesafe\" | \"openrouter\";\n endpoint: string;\n key: string;\n model: string;\n};\n\n/**\n * Pick the Jev transport. `auto` prefers TypeSafe and falls back to OpenRouter.\n * Throws when the chosen provider has no usable key; the handler fails open.\n */\nexport const resolveTransport = (cfg: Cfg): JevTransport => {\n const typesafe = (): JevTransport | null => {\n const key = secret(\"TYPESAFE_API_KEY\", cfg.apiKey);\n return usable(key)\n ? { provider: \"typesafe\", endpoint: TYPESAFE_ENDPOINT, key, model: cfg.model }\n : null;\n };\n const openrouter = (): JevTransport | null => {\n const key = secret(\"OPENROUTER_API_KEY\");\n return usable(key)\n ? { provider: \"openrouter\", endpoint: OPENROUTER_ENDPOINT, key, model: cfg.openrouterModel }\n : null;\n };\n const transport =\n cfg.provider === \"typesafe\"\n ? typesafe()\n : cfg.provider === \"openrouter\"\n ? openrouter()\n : (typesafe() ?? openrouter());\n if (transport) return transport;\n throw new Error(\n cfg.provider === \"typesafe\"\n ? \"no TYPESAFE_API_KEY\"\n : cfg.provider === \"openrouter\"\n ? \"no OPENROUTER_API_KEY\"\n : \"no TYPESAFE_API_KEY or OPENROUTER_API_KEY\",\n );\n};\n\n/** One Jev call; returns the `noul` score per question. Throws on any failure. */\nexport const askJev = async (\n state: Record<string, unknown>,\n questions: Record<string, unknown>,\n transport: JevTransport,\n cfg: Cfg,\n outer?: AbortSignal,\n): Promise<{ scores: Record<string, number>; latencyMs: number }> => {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), Math.min(cfg.timeoutMs, MAX_TIMEOUT_MS));\n const onOuterAbort = () => controller.abort();\n outer?.addEventListener(\"abort\", onOuterAbort);\n const t0 = Date.now();\n try {\n const res = await fetch(transport.endpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", Authorization: `Bearer ${transport.key}` },\n body: JSON.stringify({ state, model: transport.model, questions }),\n signal: controller.signal,\n });\n if (!res.ok) throw new Error(`${transport.provider} ${res.status}`);\n const body = (await res.json()) as { answers?: Record<string, { noul?: unknown }> } | null;\n const answers = body?.answers;\n if (!answers || typeof answers !== \"object\") throw new Error(\"no answers in response\");\n const scores: Record<string, number> = {};\n for (const [id, a] of Object.entries(answers)) {\n if (typeof a?.noul === \"number\") scores[id] = a.noul;\n }\n if (Object.keys(scores).length === 0) throw new Error(\"no scores in response\");\n return { scores, latencyMs: Date.now() - t0 };\n } finally {\n clearTimeout(timer);\n outer?.removeEventListener(\"abort\", onOuterAbort);\n }\n};\n\nconst SELFTEST = \"comms-gate-selftest\";\nconst SELFTEST_PREFS = {\n tone: \"direct, plain, no hedging\",\n language: \"English\",\n verbosity:\n \"very terse - answer in the first sentence, ~400 chars max. No preamble, no recap, no closing offer.\",\n};\nconst SELFTEST_BAD =\n \"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!\";\nconst SELFTEST_GOOD =\n \"Went with option B: it is the only one that keeps the migration reversible. PR #123.\";\n\n/**\n * Run the judge on a known-bad (positive control, must score below threshold) and a\n * known-good (negative control, must score at or above it) message and record both\n * under `selftest:last`. Proves key, egress and KV from inside the API runtime\n * without touching real traffic.\n */\nconst selfTest = async (swarm: Swarm, cfg: Cfg, signal?: AbortSignal): Promise<void> => {\n const started = Date.now();\n try {\n const transport = resolveTransport(cfg);\n const questions = { verbosity: buildQuestions(SELFTEST_PREFS).verbosity };\n const judge = (message: string) =>\n askJev(\n { requester_preferences: SELFTEST_PREFS, agent_message: message },\n questions,\n transport,\n cfg,\n signal,\n ).then((r) => r.scores.verbosity ?? null);\n const [bad, good] = await Promise.all([judge(SELFTEST_BAD), judge(SELFTEST_GOOD)]);\n await swarm.kv_set({\n namespace: NS,\n key: \"selftest:last\",\n value: {\n at: new Date().toISOString(),\n provider: transport.provider,\n threshold: cfg.threshold,\n positiveControlBadScore: bad,\n negativeControlGoodScore: good,\n controlsOk:\n typeof bad === \"number\" &&\n typeof good === \"number\" &&\n bad < cfg.threshold &&\n good >= cfg.threshold,\n latencyMs: Date.now() - started,\n },\n });\n } catch (err) {\n await swarm\n .kv_set({\n namespace: NS,\n key: \"selftest:last\",\n value: {\n at: new Date().toISOString(),\n error: String(err).slice(0, 400),\n latencyMs: Date.now() - started,\n },\n })\n .catch(() => {});\n }\n};\n\nconst TASK_SQL = `SELECT t.source, t.slackThreadTs, t.agentId, t.requestedByUserId,\n json_extract(u.metadata, '$.comms') AS comms, u.name,\n substr(t.task, 1, 400) AS askHead,\n substr(t.task, -1400) AS askTail\nFROM agent_tasks t\nLEFT JOIN users u ON u.id = t.requestedByUserId\nWHERE t.id = ?`;\n\nconst LEAD_SQL = \"SELECT isLead FROM agents WHERE id = ?\";\n\n/**\n * Mirrors the `task.progress.write` rule store-progress enforces\n * (src/rbac/legacy-policy.ts): the caller owns the task, the task is\n * unassigned, or the caller is a lead. Lead status comes from the caller's\n * agents row, never from tool args. Extension callers return before this.\n */\nconst mayWriteProgress = async (\n swarm: Swarm,\n callerId: string | undefined,\n taskAgentId: string,\n): Promise<boolean> => {\n if (!callerId) return false;\n if (!taskAgentId || taskAgentId === callerId) return true;\n const q = (await swarm.db_query({ sql: LEAD_SQL, params: [callerId] })) as Envelope;\n const row = (q?.data?.rows ?? q?.rows ?? [])[0];\n return Number(cell(row, 0, \"isLead\") ?? 0) === 1;\n};\n\nconst extension: SwarmExtension = (api) => {\n api.on(\"pre.tool.call\", async (event, ctx) => {\n if (event.tool !== \"store-progress\") return;\n if (event.requestInfo?.callOrigin === \"extension\") return;\n\n const cfg = cfgOf(ctx.config);\n if (cfg.mode === \"off\") return;\n\n const args = (event.args ?? {}) as Record<string, unknown>;\n\n // Self-test hatch: a progress call whose text starts with the sentinel runs\n // the controls and records them under `selftest:last`.\n if (args.status !== \"completed\") {\n if (typeof args.progress === \"string\" && args.progress.startsWith(SELFTEST)) {\n await selfTest(ctx.swarm, cfg, ctx.signal);\n }\n return;\n }\n\n const output = typeof args.output === \"string\" ? args.output.trim() : \"\";\n if (!output) return;\n\n const taskId = String(args.taskId ?? event.requestInfo?.sourceTaskId ?? \"\");\n if (!UUID_RE.test(taskId)) return;\n\n try {\n const q = (await ctx.swarm.db_query({ sql: TASK_SQL, params: [taskId] })) as Envelope;\n const row = (q?.data?.rows ?? q?.rows ?? [])[0];\n if (!row) return;\n\n const source = String(cell(row, 0, \"source\") ?? \"\");\n const slackThreadTs = cell(row, 1, \"slackThreadTs\");\n const agentId = String(cell(row, 2, \"agentId\") ?? \"\");\n const requesterId = String(cell(row, 3, \"requestedByUserId\") ?? \"\");\n const commsRaw = cell(row, 4, \"comms\");\n const requesterName = String(cell(row, 5, \"name\") ?? requesterId);\n const threadOpening = String(cell(row, 6, \"askHead\") ?? \"\");\n const ask = latestAsk(String(cell(row, 7, \"askTail\") ?? \"\"));\n\n // Judge only a completion store-progress will accept, so no agent can ship\n // another task's thread and preferences to the external judge.\n if (!(await mayWriteProgress(ctx.swarm, event.requestInfo?.agentId, agentId))) return;\n\n // Only where the text actually reaches a human in Slack.\n if (source !== \"slack\" && !slackThreadTs) return;\n if (!commsRaw) return; // no stated preferences -> nothing to judge against\n const prefs = (typeof commsRaw === \"string\" ? JSON.parse(commsRaw) : commsRaw) as Prefs;\n if (!prefs || typeof prefs !== \"object\") return;\n\n const questions = buildQuestions(prefs);\n if (Object.keys(questions).length === 0) return;\n\n const transport = resolveTransport(cfg);\n const { scores, latencyMs } = await askJev(\n {\n role: \"You audit an AI agent's final message to the human who requested the work.\",\n requester: requesterName,\n requester_preferences: prefs,\n latest_request: ask,\n thread_opening: threadOpening,\n judging_rules: JUDGING_RULES,\n agent_message: output,\n },\n questions,\n transport,\n cfg,\n ctx.signal,\n );\n\n const failed = Object.entries(scores)\n .filter(([, v]) => v < cfg.threshold)\n .map(([k]) => k);\n\n const verdict = {\n at: new Date().toISOString(),\n taskId,\n agentId,\n source,\n requester: requesterName,\n requesterId,\n provider: transport.provider,\n scores,\n failed,\n threshold: cfg.threshold,\n mode: cfg.mode,\n latencyMs,\n preferences: prefs,\n askPreview: ask.slice(0, 400),\n outputPreview: output.slice(0, 700),\n };\n\n await ctx.swarm.kv_incr({ namespace: NS, key: \"total\" });\n await ctx.swarm.kv_set({ namespace: NS, key: \"last\", value: verdict });\n\n if (failed.length === 0) {\n await ctx.swarm.kv_incr({ namespace: NS, key: \"pass\" });\n return;\n }\n\n await ctx.swarm.kv_incr({ namespace: NS, key: \"fail\" });\n for (const dim of failed) {\n await ctx.swarm.kv_incr({ namespace: NS, key: `dim:${safeKey(dim)}` });\n }\n\n const blocksKey = `blocks:${safeKey(taskId)}`;\n // kv_get answers {success, data:{value}}, and {success:false} when absent.\n const prior = (await ctx.swarm.kv_get({ namespace: NS, key: blocksKey })) as Envelope;\n const priorBlocks = Number(prior?.data?.value ?? prior?.value ?? 0) || 0;\n const willBlock = cfg.mode === \"enforce\" && priorBlocks < cfg.maxBlocksPerTask;\n\n await ctx.swarm.kv_set({\n namespace: NS,\n key: `fail:${Date.now()}:${safeKey(taskId)}`,\n value: { ...verdict, action: willBlock ? \"blocked\" : \"recorded\" },\n });\n\n if (!willBlock) {\n await ctx.swarm.kv_incr({\n namespace: NS,\n key: cfg.mode === \"enforce\" ? \"escaped\" : \"shadowed\",\n });\n return;\n }\n\n await ctx.swarm.kv_incr({ namespace: NS, key: \"blocked\" });\n await ctx.swarm.kv_incr({ namespace: NS, key: blocksKey });\n\n const detail = failed\n .map(\n (d) =>\n `${d} (${scores[d]!.toFixed(2)} vs ${cfg.threshold}) — requested: ${String(prefs[d] ?? \"\").slice(0, 220)}`,\n )\n .join(\"\\n\");\n\n return block(\n [\n `Comms gate: this output does not match ${requesterName}'s stated preferences.`,\n detail,\n \"Rewrite the output to match and call store-progress again. Do not change the facts, only how they are said.\",\n ].join(\"\\n\"),\n );\n } catch (err) {\n // Fail open, always.\n try {\n await ctx.swarm.kv_incr({ namespace: NS, key: \"error\" });\n await ctx.swarm.kv_set({\n namespace: NS,\n key: \"error:last\",\n value: { at: new Date().toISOString(), taskId, error: String(err).slice(0, 400) },\n });\n } catch {\n /* ignore */\n }\n ctx.log.warn(\"comms-adherence-gate: failing open\", { error: String(err) });\n return;\n }\n });\n};\n\nexport default extension;\n"
16
+ },
17
+ "readme": "# comms-adherence-gate\n\nAgents finish Slack tasks with a final message a human reads. This extension judges that message against the requester's stated comms preferences and their latest request with Jev, [TypeSafe](https://typesafe.ai)'s decision model. In `enforce` it blocks a non-adherent completion once and tells the agent what to fix.\n\nInstall config:\n\n```json\n{ \"mode\": \"shadow\" }\n```\n\nStart in `shadow`, read the verdicts in KV for a few days, then switch to `enforce`.\n\n## Modes\n\n| `mode` | Effect |\n|---|---|\n| `off` | Never evaluates. |\n| `shadow` (default) | Asks Jev and records the verdict. Never blocks. |\n| `enforce` | Asks Jev, records the verdict, and blocks the `store-progress` call when a dimension fails, at most `maxBlocksPerTask` times per task. The agent gets the failed dimensions, their scores and the stated preference, and calls again. |\n\n## What it gates\n\nHook: `pre.tool.call` on `store-progress` with `status: \"completed\"` and a non-empty `output`. Calls made by extensions are skipped. The task must:\n\n1. Come from Slack (`source = \"slack\"` or a `slackThreadTs`). Other tasks pass without a Jev call.\n2. Have a requester with stated preferences in `users.metadata.comms`. A requester with none passes. Set them with the `manage-user` tool.\n\n## Decision\n\nThe gate reads the task row and asks one `noul` question per stated preference it knows: `verbosity`, `tone`, `language`. Any other key in `comms` is context only.\n\nJev sees the preferences, the requester's latest message (the text after the last `</thread_context>`, from the last 1,400 characters of the task), the first 400 characters of the task, and the output. Its judging rules say an explicit request outranks the stored default. A requested long answer, story or document is not a verbosity failure.\n\nA dimension fails when its score is below `threshold`. Any failed dimension fails the output.\n\n## Jev transport\n\nJev is reachable two ways. Both take the same request and return the same answer.\n\n| `provider` | Endpoint | Key | Model |\n|---|---|---|---|\n| `typesafe` | `POST https://api.typesafe.ai/v1/systemone` | `TYPESAFE_API_KEY` (or `apiKey`) | `model`, default `jev-latest` |\n| `openrouter` | `POST https://openrouter.ai/api/alpha/decisions` | `OPENROUTER_API_KEY` | `openrouterModel`, default `typesafe/jev-1.13` |\n| `auto` (default) | TypeSafe when its key is set, else OpenRouter | | |\n\nEach verdict row records the `provider` that answered.\n\n## Fail open\n\nEvery error lets the completion through: a missing key, an HTTP error, a network error, a timeout, a malformed answer, or a failed DB read. The handler never throws, so an outage cannot auto-disable the extension. Each failure bumps the `error` counter and overwrites `error:last`.\n\n## Self-test\n\nCall `store-progress` with a non-completed status and `progress` starting with `comms-gate-selftest`. The gate judges a known-bad and a known-good message against a terse preference and writes `selftest:last`: both scores, the threshold, the provider, and `controlsOk` (bad below threshold and good at or above it). Real traffic is untouched.\n\n## Config\n\n| Key | Default | Notes |\n|---|---|---|\n| `mode` | `shadow` | `off`, `shadow`, or `enforce` |\n| `threshold` | `0.5` | A dimension scoring below this fails |\n| `timeoutMs` | `4000` | Jev call timeout, capped at 4000 (the dispatcher cancels a handler at 5 s) |\n| `maxBlocksPerTask` | `1` | Blocks per task in `enforce`; after that the gate records and lets it through |\n| `provider` | `auto` | `auto`, `typesafe`, or `openrouter`. See [Jev transport](#jev-transport) |\n| `model` | `jev-latest` | Jev model on TypeSafe |\n| `openrouterModel` | `typesafe/jev-1.13` | Jev model on OpenRouter |\n| `apiKey` | unset | Explicit TypeSafe key. Prefer the `TYPESAFE_API_KEY` secret below |\n\n## Required secrets\n\nRead from the API process environment at call time. Set them as global `swarm_config` secrets: the API process loads global rows into `process.env` at boot and on config reload.\n\n- `TYPESAFE_API_KEY` or `OPENROUTER_API_KEY`: one is required for any evaluation. Under `auto`, TypeSafe wins when both are set. Without either, every completion fails open.\n\nNo key is written to config, KV, logs, or a block reason.\n\n## KV namespace `ext:comms-gate`\n\n- `last`: the latest verdict, with `scores`, `failed`, `provider`, `latencyMs`, the preferences, a 400-character `askPreview` and a 700-character `outputPreview`.\n- `fail:<epochMs>:<taskId>`: one row per failed verdict, with `action` (`blocked` or `recorded`).\n- `blocks:<taskId>`: blocks issued for that task.\n- `error:last`, `selftest:last`.\n- Counters: `total`, `pass`, `fail`, `blocked`, `escaped` (enforce, block budget spent), `shadowed`, `error`, and `dim:<dimension>`.\n\n## Known limits\n\n- Only the last 1,400 characters of the task are searched for the latest request. A very long final message is cut to its last 900 characters.\n- The block budget is per task, so a retried completion after a block goes through once the budget is spent, even if it still fails.\n"
18
+ },
2
19
  "deploy-awareness": {
3
20
  "manifestFile": "manifest.json",
4
21
  "manifest": {