@desplega.ai/agent-swarm 1.173.0 → 2.0.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 (432) hide show
  1. package/README.md +1 -1
  2. package/dist/{acp-adapter-ccm9xrep.js → acp-adapter-sj2g0m84.js} +17 -16
  3. package/dist/{actions-sgeyv08k.js → actions-fegk44gr.js} +39 -34
  4. package/dist/{amp-adapter-gpxp3kc9.js → amp-adapter-ad8ft89x.js} +8 -7
  5. package/dist/{app-km9qnv2f.js → app-kfvgbvzy.js} +18 -14
  6. package/dist/{assistant-5r8tcvvd.js → assistant-z7zdy9ja.js} +40 -35
  7. package/dist/{boot-reembed-swwzdgd7.js → boot-reembed-5c2z0mbe.js} +10 -9
  8. package/dist/{boot-reembed-16rv9843.js → boot-reembed-j8xa181e.js} +10 -9
  9. package/dist/{boot-scrub-sweep-zkr6vkqb.js → boot-scrub-sweep-hnkn3rgt.js} +11 -10
  10. package/dist/{boot-seeds-d8sp49t2.js → boot-seeds-kwjq4d2z.js} +14 -13
  11. package/dist/{claude-adapter-yzzzh8jp.js → claude-adapter-9vmvpvgd.js} +19 -18
  12. package/dist/{claude-managed-adapter-xgxhqrt2.js → claude-managed-adapter-k9qae05j.js} +7 -5
  13. package/dist/{claude-managed-setup-bnmjarjc.js → claude-managed-setup-q54zx5q2.js} +6 -4
  14. package/dist/{claude-sdk-session-4whxere4.js → claude-sdk-session-9jye6cxr.js} +11881 -11472
  15. package/dist/{cli-94b48jhk.js → cli-03gedhng.js} +16 -14
  16. package/dist/{cli-1w0vj3gw.js → cli-1e4b6bzh.js} +7 -7
  17. package/dist/{cli-d603qf86.js → cli-29jc5q7w.js} +10 -8
  18. package/dist/cli-2m1ktfzz.js +17795 -0
  19. package/dist/{cli-x119kdba.js → cli-3dj8k4vy.js} +1 -1
  20. package/dist/{cli-srqq3gc5.js → cli-3fbmmawm.js} +5 -5
  21. package/dist/{cli-gmdajfw5.js → cli-4jxm2qtz.js} +4987 -1708
  22. package/dist/{cli-tadfgd0t.js → cli-5hkm7whv.js} +33 -4
  23. package/dist/{cli-42qhyvwx.js → cli-5t4f29ez.js} +3 -3
  24. package/dist/{cli-7a0sde2x.js → cli-6cagzc96.js} +9 -0
  25. package/dist/cli-6cvfxs8r.js +47 -0
  26. package/dist/{cli-0kxf81jj.js → cli-6jyj59nb.js} +2 -2
  27. package/dist/{cli-w1apjecb.js → cli-6razj6t4.js} +2 -2
  28. package/dist/{cli-anzdk0z4.js → cli-74ahz61x.js} +2 -2
  29. package/dist/{cli-m89t9jdf.js → cli-7x0g4sn1.js} +15416 -16024
  30. package/dist/{cli-yatxqynm.js → cli-9ha6hs2d.js} +1 -290
  31. package/dist/{cli-81r1wjyv.js → cli-9nfx44eh.js} +1 -1
  32. package/dist/cli-9traj3dn.js +638 -0
  33. package/dist/{cli-smcrrmhy.js → cli-agzebbd5.js} +9 -9
  34. package/dist/{cli-na2pxe4f.js → cli-apx87jne.js} +2 -2
  35. package/dist/{cli-rfz2ve55.js → cli-bzszjf2p.js} +2855 -862
  36. package/dist/{cli-c5km5znr.js → cli-c8r3kzzb.js} +1 -1
  37. package/dist/{cli-hsem16hb.js → cli-cgtksawx.js} +7 -4
  38. package/dist/{cli-db3m6zdt.js → cli-d2qr7aj4.js} +1 -1
  39. package/dist/{cli-rfs4vddv.js → cli-d5f9ed5c.js} +5 -5
  40. package/dist/cli-e3g5mbkz.js +776 -0
  41. package/dist/{cli-7jykv7ef.js → cli-e6mpjhed.js} +1 -1
  42. package/dist/{cli-7a93cv8c.js → cli-ee9gtz70.js} +98 -23
  43. package/dist/{cli-3nxdmjrc.js → cli-eejsgvm4.js} +4 -4
  44. package/dist/{cli-fmhj1ehj.js → cli-ef1hg1wv.js} +470 -51
  45. package/dist/cli-f527ce74.js +781 -0
  46. package/dist/{cli-ppy4nnnh.js → cli-f7kr7svz.js} +2 -2
  47. package/dist/cli-hjwmhcg1.js +10622 -0
  48. package/dist/{cli-2bw3w47j.js → cli-hnhtk6cj.js} +1 -1
  49. package/dist/{cli-ywm1azg9.js → cli-hnmhtzvh.js} +6 -4
  50. package/dist/{cli-h7k0xtry.js → cli-hvqbtna4.js} +4 -1256
  51. package/dist/{cli-s8z6x4b4.js → cli-j0pfptdp.js} +218 -102
  52. package/dist/cli-jta2eyzc.js +619 -0
  53. package/dist/{cli-5er5gjbm.js → cli-k382tshb.js} +10 -11
  54. package/dist/{cli-v3zhnxsb.js → cli-k3dp5tpf.js} +2 -2
  55. package/dist/{cli-wd0hz6ah.js → cli-kbrt8gqa.js} +9 -91
  56. package/dist/cli-kceah1eh.js +92 -0
  57. package/dist/{cli-ne7smj1b.js → cli-ke896fcf.js} +10 -10
  58. package/dist/{cli-xc7p7s3g.js → cli-kt6zqg0e.js} +6 -6
  59. package/dist/{cli-wrh08zjd.js → cli-m50t4309.js} +5 -18
  60. package/dist/cli-n2wq4j7m.js +6985 -0
  61. package/dist/{cli-bv2etw2y.js → cli-n3hzg89b.js} +46 -15
  62. package/dist/{cli-tmhsxhb8.js → cli-n5vwg842.js} +23 -6
  63. package/dist/{cli-qm1448f9.js → cli-ne236afy.js} +8 -8
  64. package/dist/{cli-d161g69q.js → cli-p1x85d2t.js} +50 -2
  65. package/dist/{cli-7pz3b7x5.js → cli-przpmdgr.js} +25 -0
  66. package/dist/{cli-0a7mm3zp.js → cli-q0ke1yww.js} +25 -21
  67. package/dist/{cli-e3pmyt39.js → cli-q50942rs.js} +1 -1
  68. package/dist/{cli-r44t9wqc.js → cli-r106725k.js} +2 -2
  69. package/dist/{cli-phcqp1b9.js → cli-r2pnvj8g.js} +2 -2
  70. package/dist/{cli-jpc4r224.js → cli-r62a35ea.js} +19 -3
  71. package/dist/{cli-cd0hv9nh.js → cli-ra0xzggj.js} +2 -2
  72. package/dist/cli-rhs9pz33.js +294 -0
  73. package/dist/{cli-r4fqfpwf.js → cli-s2sp957d.js} +50 -27
  74. package/dist/{cli-1x7da0de.js → cli-s4setq91.js} +8 -8
  75. package/dist/{cli-6thk4jz3.js → cli-se8w6wny.js} +6 -6
  76. package/dist/cli-skxvvpap.js +10140 -0
  77. package/dist/{cli-014vns2y.js → cli-stfs0k5s.js} +208 -525
  78. package/dist/{cli-0p3enk35.js → cli-tqaazemc.js} +9 -1
  79. package/dist/{cli-nwrdx840.js → cli-tr4sxp68.js} +3 -3
  80. package/dist/{cli-r0byd61h.js → cli-tsde1hdr.js} +15 -15
  81. package/dist/{cli-b07hr3bt.js → cli-txqb9s38.js} +2 -2
  82. package/dist/{cli-6r2f7edk.js → cli-v1yb0rqb.js} +6 -6
  83. package/dist/{cli-saqyage0.js → cli-vatyhshy.js} +179 -27
  84. package/dist/{cli-5wpw4rab.js → cli-vdach5a9.js} +5 -5
  85. package/dist/{cli-7avy4v8w.js → cli-w1q7h2r2.js} +5 -3
  86. package/dist/{cli-rcp5aqdw.js → cli-w7m81tb7.js} +1 -1
  87. package/dist/{cli-c3rk1v0x.js → cli-wayw93nt.js} +3 -3
  88. package/dist/cli-wmgrkscj.js +23254 -0
  89. package/dist/{cli-9xf7fdtp.js → cli-z5jjpaxw.js} +29 -6
  90. package/dist/{cli-451ks6aw.js → cli-zkpks3r3.js} +1 -1
  91. package/dist/cli.js +29 -45
  92. package/dist/{codex-adapter-r3dn4fp5.js → codex-adapter-y81sssk0.js} +15 -14
  93. package/dist/{codex-login-841kv5fk.js → codex-login-f2rcfpp9.js} +3 -2
  94. package/dist/{codex-session-runner-fwt793h4.js → codex-session-runner-8vx4j6a9.js} +15 -14
  95. package/dist/{commands-z8fq9qna.js → commands-kcpvpcrr.js} +11 -10
  96. package/dist/{controller-hq5ggpcp.js → controller-ywrmt6yd.js} +5 -4
  97. package/dist/{controllers-connect-3mf2btyj.js → controllers-connect-kwpvj3rf.js} +42 -37
  98. package/dist/{cursor-adapter-q8pgv5qa.js → cursor-adapter-cehv1qns.js} +7 -6
  99. package/dist/{db-yyhqqrbf.js → db-k8ankj20.js} +12 -11
  100. package/dist/{db-qfpq4bt7.js → db-ww5d7jq3.js} +28 -9
  101. package/dist/{devin-adapter-xgtj680j.js → devin-adapter-x1022qb1.js} +3 -2
  102. package/dist/{dispatcher-x0x7qfcq.js → dispatcher-e8gq5m68.js} +38 -33
  103. package/dist/{docker-f503k9p0.js → docker-ff6y1190.js} +3 -2
  104. package/dist/{dsh-adapter-34etvb5m.js → dsh-adapter-3epjm3af.js} +7 -6
  105. package/dist/{e2b-66xra4az.js → e2b-8ygwhg0f.js} +4 -3
  106. package/dist/{e2b-xwhyj8cw.js → e2b-swhnpw3n.js} +4 -3
  107. package/dist/{event-bus-vjtv82gm.js → event-bus-6axnngdj.js} +3 -2
  108. package/dist/{external-channel-h1fjecwe.js → external-channel-h3359cwn.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-0d4x7gkx.js} +18 -17
  112. package/dist/{handlers-znb9n7d4.js → handlers-an1gzv21.js} +41 -36
  113. package/dist/{heartbeat-bm4qkn8d.js → heartbeat-6dakeb61.js} +43 -38
  114. package/dist/{hook-254r4mza.js → hook-6v6cv6d5.js} +11 -23
  115. package/dist/{hook-xaa3q80s.js → hook-sa5xye4y.js} +12 -11
  116. package/dist/{http-3dgrgp8y.js → http-r7f3eky0.js} +1499 -251
  117. package/dist/index-52j1aja4.js +3465 -0
  118. package/dist/{index-fx3jqq1e.js → index-7ep4gzdk.js} +38 -33
  119. package/dist/index-9gw58n4k.js +193 -0
  120. package/dist/index-9j87mcm8.js +56305 -0
  121. package/dist/index-bsmcresk.js +79424 -0
  122. package/dist/index-mh85qhec.js +475 -0
  123. package/dist/{index-g2byveez.js → index-rmpw9a8q.js} +38 -33
  124. package/dist/{index-2ybk7khy.js → index-s2wxgzr0.js} +468 -683
  125. package/dist/{index-bdsqa799.js → index-v1v6n7hg.js} +43 -38
  126. package/dist/index-we27kd31.js +12810 -0
  127. package/dist/{index-jd7sejm2.js → index-zk96aq1k.js} +10 -9
  128. package/dist/{keepalive-6ywhpja1.js → keepalive-be9vwy5h.js} +12 -11
  129. package/dist/{lead-cw45n21s.js → lead-0861cwrf.js} +61 -56
  130. package/dist/{maintenance-ss1a1ge9.js → maintenance-mzzgc8bn.js} +10 -9
  131. package/dist/{node-fkajaq96.js → node-mn1zs7an.js} +2 -1
  132. package/dist/{oauth-refresh-sweep-3c6f8vyy.js → oauth-refresh-sweep-cpdnfwdj.js} +11 -10
  133. package/dist/{onboard-8hzdhc1k.js → onboard-fh8qs4r3.js} +11 -3
  134. package/dist/{opencode-adapter-qbchbcfz.js → opencode-adapter-nm42wwj8.js} +18 -13
  135. package/dist/{otel-impl-e6mnvc4v.js → otel-impl-x6c1c9g4.js} +242 -868
  136. package/dist/{pi-mono-adapter-3c9g9snh.js → pi-mono-adapter-9n2ya240.js} +244 -106
  137. package/dist/{pricing-refresh-gedkjdw0.js → pricing-refresh-qb0rv0ap.js} +12 -11
  138. package/dist/{rbac-roles-dt4vh1yy.js → rbac-roles-1gzdwbh1.js} +11 -10
  139. package/dist/{rbac-roles-eszdg6pm.js → rbac-roles-3mraey52.js} +12 -11
  140. package/dist/{render-v2-907cy6sa.js → render-v2-agpa45zz.js} +18 -14
  141. package/dist/{resume-42stcaz0.js → resume-x0ydy9s9.js} +25 -21
  142. package/dist/{runtime-qfry88v2.js → runtime-sa2vr16g.js} +2 -2
  143. package/dist/{setup-adbc1c10.js → setup-as9ga0d6.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-y5g1y7ev.js} +41 -36
  146. package/dist/{worker-bzdra9rf.js → worker-860c3e38.js} +61 -56
  147. package/dist/{x-qfmygqqv.js → x-m477fr0h.js} +3 -2
  148. package/openapi.json +523 -344
  149. package/package.json +10 -11
  150. package/src/be/config-refs.ts +60 -0
  151. package/src/be/db/page-versions.ts +15 -0
  152. package/src/be/db/runtime.ts +35 -8
  153. package/src/be/db/skill-name-resolution.ts +36 -0
  154. package/src/be/db/tasks/read.ts +230 -0
  155. package/src/be/db/tasks/write.ts +7 -0
  156. package/src/be/db.ts +52 -7
  157. package/src/be/migrations/215_swarm_config_export_to_env.sql +34 -0
  158. package/src/be/migrations/216_task_resolved_effort.sql +5 -0
  159. package/src/be/migrations/217_chat_sdk_state.sql +104 -0
  160. package/src/be/model-catalog-store.ts +5 -1
  161. package/src/be/model-tier-resolution.ts +12 -1
  162. package/src/be/model-validation.ts +5 -2
  163. package/src/be/models-catalog.ts +26 -1
  164. package/src/be/modelsdev-cache.json +198 -63
  165. package/src/be/modelsdev-cache.ts +9 -1
  166. package/src/be/script-credential-broker.ts +39 -2
  167. package/src/be/scripts/run-saved.ts +1 -0
  168. package/src/be/scripts/typecheck.ts +10 -8
  169. package/src/be/seed/runner.ts +13 -1
  170. package/src/be/seed/state-db.ts +5 -0
  171. package/src/be/seed/types.ts +6 -0
  172. package/src/be/seed-pricing.ts +19 -2
  173. package/src/be/seed-scripts/catalog/ops-catalog-audit.inline.ts +1 -1
  174. package/src/be/seed-scripts/catalog/ops-catalog-audit.ts +1 -1
  175. package/src/be/seed-skills/bundled-files.generated.json +0 -18
  176. package/src/be/seed-skills/index.ts +104 -3
  177. package/src/be/swarm-config-guard.ts +52 -0
  178. package/src/cli.tsx +0 -16
  179. package/src/commands/onboard/non-interactive.ts +4 -2
  180. package/src/commands/provider-credentials.ts +52 -6
  181. package/src/commands/runner.ts +190 -54
  182. package/src/e2b/env.ts +5 -0
  183. package/src/extensions/contract-types.generated.ts +1 -1
  184. package/src/extensions/contract.ts +45 -0
  185. package/src/extensions/dispatcher.ts +7 -0
  186. package/src/extensions/post-bridge.ts +40 -0
  187. package/src/github/handlers.ts +11 -4
  188. package/src/heartbeat/templates.ts +8 -5
  189. package/src/hooks/hook.ts +0 -26
  190. package/src/http/all-routes.ts +0 -1
  191. package/src/http/config.ts +36 -17
  192. package/src/http/core.ts +4 -0
  193. package/src/http/index.ts +14 -2
  194. package/src/http/integrations.ts +88 -1
  195. package/src/http/mcp.ts +15 -5
  196. package/src/http/page-proxy.ts +2 -4
  197. package/src/http/pages-public.ts +3 -3
  198. package/src/http/pages.ts +3 -13
  199. package/src/http/poll.ts +28 -68
  200. package/src/http/requested-by.ts +54 -0
  201. package/src/http/scripts.ts +1 -0
  202. package/src/http/stats.ts +1 -30
  203. package/src/http/tasks.ts +61 -5
  204. package/src/http/utils.ts +14 -0
  205. package/src/http/webhooks.ts +159 -1
  206. package/src/http/x.ts +1 -0
  207. package/src/integrations/chat-sdk/adapters.ts +129 -0
  208. package/src/integrations/chat-sdk/attachments.ts +232 -0
  209. package/src/integrations/chat-sdk/background.ts +36 -0
  210. package/src/integrations/chat-sdk/commands.ts +194 -0
  211. package/src/integrations/chat-sdk/config.ts +198 -0
  212. package/src/integrations/chat-sdk/discord-api.ts +63 -0
  213. package/src/integrations/chat-sdk/discord-gateway.ts +185 -0
  214. package/src/integrations/chat-sdk/handlers.ts +28 -0
  215. package/src/integrations/chat-sdk/inbound.ts +426 -0
  216. package/src/integrations/chat-sdk/limits.ts +48 -0
  217. package/src/integrations/chat-sdk/logger.ts +81 -0
  218. package/src/integrations/chat-sdk/outbound.ts +123 -0
  219. package/src/integrations/chat-sdk/relay.ts +172 -0
  220. package/src/integrations/chat-sdk/router.ts +169 -0
  221. package/src/integrations/chat-sdk/runtime.ts +351 -0
  222. package/src/integrations/chat-sdk/schema.ts +245 -0
  223. package/src/integrations/chat-sdk/state.ts +243 -0
  224. package/src/integrations/chat-sdk/status.ts +244 -0
  225. package/src/integrations/chat-sdk/steering.ts +58 -0
  226. package/src/integrations/chat-sdk/teams-origin.ts +77 -0
  227. package/src/integrations/chat-sdk/telegram-api.ts +56 -0
  228. package/src/integrations/chat-sdk/templates.ts +16 -0
  229. package/src/integrations/chat-sdk/test-hooks.ts +37 -0
  230. package/src/integrations/chat-sdk/thread-buffer.ts +100 -0
  231. package/src/integrations/chat-sdk/typing.ts +65 -0
  232. package/src/pages/version.ts +4 -5
  233. package/src/prompts/base-prompt.ts +111 -45
  234. package/src/providers/acp-target-catalog.ts +6 -0
  235. package/src/providers/acp-targets.ts +55 -2
  236. package/src/providers/amp-adapter.ts +1 -1
  237. package/src/providers/claude-adapter.ts +1 -1
  238. package/src/providers/claude-flag-settings.ts +17 -5
  239. package/src/providers/claude-managed-adapter.ts +1 -1
  240. package/src/providers/claude-session-events.ts +6 -1
  241. package/src/providers/dsh-adapter.ts +1 -1
  242. package/src/providers/opencode-adapter.ts +243 -26
  243. package/src/providers/opencode-swarm-plugin.ts +31 -0
  244. package/src/providers/pi-mono-adapter.ts +398 -153
  245. package/src/providers/pricing-sources.md +33 -2
  246. package/src/providers/reasoning-effort.ts +27 -0
  247. package/src/providers/types.ts +6 -2
  248. package/src/rbac/legacy-policy.ts +8 -2
  249. package/src/rbac/permissions.ts +21 -0
  250. package/src/scheduler/scheduler.ts +4 -2
  251. package/src/scripts-runtime/loader.ts +19 -7
  252. package/src/scripts-runtime/sdk-allowlist.ts +8 -6
  253. package/src/scripts-runtime/swarm-sdk.ts +0 -11
  254. package/src/scripts-runtime/types/stdlib.d.ts +27 -15
  255. package/src/scripts-runtime/types/swarm-extension.d.ts +63 -1
  256. package/src/scripts-runtime/types/swarm-sdk.d.ts +27 -15
  257. package/src/server.ts +52 -18
  258. package/src/slack/event-dedup.ts +1 -1
  259. package/src/tasks/additive-buffer.ts +4 -2
  260. package/src/tasks/context-key.ts +60 -1
  261. package/src/tasks/model-pin.ts +67 -0
  262. package/src/tasks/worker-follow-up.ts +4 -31
  263. package/src/tests/acp-adapter.test.ts +42 -0
  264. package/src/tests/api-drain-worker-e2e.test.ts +1 -6
  265. package/src/tests/base-prompt.test.ts +58 -54
  266. package/src/tests/chat-sdk-commands.test.ts +191 -0
  267. package/src/tests/chat-sdk-config.test.ts +334 -0
  268. package/src/tests/chat-sdk-discord-api.test.ts +68 -0
  269. package/src/tests/chat-sdk-discord-gateway.test.ts +252 -0
  270. package/src/tests/chat-sdk-discord-webhook.test.ts +126 -0
  271. package/src/tests/chat-sdk-fields-sync.test.ts +50 -0
  272. package/src/tests/chat-sdk-harness.ts +146 -0
  273. package/src/tests/chat-sdk-inbound.test.ts +363 -0
  274. package/src/tests/chat-sdk-relay.test.ts +470 -0
  275. package/src/tests/chat-sdk-reload.test.ts +128 -0
  276. package/src/tests/chat-sdk-shutdown.test.ts +220 -0
  277. package/src/tests/chat-sdk-slack.test.ts +268 -0
  278. package/src/tests/chat-sdk-state.test.ts +163 -0
  279. package/src/tests/chat-sdk-status-route.test.ts +382 -0
  280. package/src/tests/chat-sdk-teams.test.ts +277 -0
  281. package/src/tests/chat-sdk-tools.test.ts +520 -0
  282. package/src/tests/chat-sdk-typing.test.ts +120 -0
  283. package/src/tests/chat-sdk-webhook-telegram.test.ts +200 -0
  284. package/src/tests/claude-adapter-binary.test.ts +9 -4
  285. package/src/tests/claude-flag-settings.test.ts +11 -3
  286. package/src/tests/claude-managed-adapter.test.ts +1 -1
  287. package/src/tests/claude-sdk-lifecycle.test.ts +9 -3
  288. package/src/tests/claude-thinking-tokens.test.ts +57 -0
  289. package/src/tests/claude-worker-parity.test.ts +2 -7
  290. package/src/tests/config-env-export-chat-sdk.test.ts +11 -0
  291. package/src/tests/config-refs.test.ts +66 -0
  292. package/src/tests/context-key.test.ts +19 -0
  293. package/src/tests/cors-allowlist.test.ts +25 -0
  294. package/src/tests/create-page-tool.test.ts +18 -0
  295. package/src/tests/credential-check.test.ts +139 -0
  296. package/src/tests/credentials.test.ts +21 -0
  297. package/src/tests/db-runtime.test.ts +61 -0
  298. package/src/tests/db-task-read-contract.test.ts +20 -0
  299. package/src/tests/db-task-write-contract.test.ts +10 -0
  300. package/src/tests/docs-index-footprint.test.js +3 -3
  301. package/src/tests/dsh-adapter.test.ts +1 -1
  302. package/src/tests/enabled-capabilities.test.ts +20 -0
  303. package/src/tests/entrypoint-api-readiness.test.ts +14 -8
  304. package/src/tests/entrypoint-config-env-export.test.ts +5 -2
  305. package/src/tests/entrypoint-export-to-env.test.ts +78 -0
  306. package/src/tests/explicit-model-validation.test.ts +105 -1
  307. package/src/tests/extensions-example-route-chat-channel.test.ts +154 -0
  308. package/src/tests/extensions-post-bridge.test.ts +51 -0
  309. package/src/tests/fetch-resolved-env.test.ts +128 -0
  310. package/src/tests/fixtures/chat-sdk/telegram-private-text.json +10 -0
  311. package/src/tests/fixtures/extensions/route-chat-channel-to-agent/hooks.ts +27 -0
  312. package/src/tests/fixtures/extensions/route-chat-channel-to-agent/manifest.json +9 -0
  313. package/src/tests/fixtures/openai-compatible-registry-extension.child.ts +146 -0
  314. package/src/tests/fixtures/runner-legacy-resume-prompt.ts +122 -0
  315. package/src/tests/github-handlers.test.ts +35 -0
  316. package/src/tests/harness-provider-resolution.test.ts +12 -0
  317. package/src/tests/heartbeat-checklist.test.ts +3 -2
  318. package/src/tests/hook-runtime-close.test.ts +0 -2
  319. package/src/tests/http-api-integration.test.ts +0 -25
  320. package/src/tests/migration-046-budgets.test.ts +17 -16
  321. package/src/tests/migration-217-chat-sdk-state.test.ts +113 -0
  322. package/src/tests/model-tier-resolution.test.ts +93 -0
  323. package/src/tests/models-catalog.test.ts +42 -1
  324. package/src/tests/openai-compatible-providers.test.ts +134 -0
  325. package/src/tests/openai-compatible-registry-extension.test.ts +42 -0
  326. package/src/tests/opencode-adapter.test.ts +216 -2
  327. package/src/tests/opencode-helmcode-sdk.test.ts +201 -0
  328. package/src/tests/opencode-shell-env.test.ts +145 -0
  329. package/src/tests/openrouter-base-url.test.ts +101 -151
  330. package/src/tests/page-proxy.test.ts +1 -1
  331. package/src/tests/pages-browser-sdk.test.ts +76 -0
  332. package/src/tests/pages-feedback.test.ts +1 -1
  333. package/src/tests/paused-tasks-requested-by.test.ts +102 -0
  334. package/src/tests/pi-custom-provider-fake-server.test.ts +442 -0
  335. package/src/tests/pi-mono-adapter.test.ts +385 -5
  336. package/src/tests/pi-shell-env.test.ts +170 -0
  337. package/src/tests/prompt-template-remaining.test.ts +35 -0
  338. package/src/tests/rbac-engine.test.ts +11 -3
  339. package/src/tests/runner-exit-after-tasks.test.ts +0 -4
  340. package/src/tests/runner-legacy-resume-prompt.test.ts +64 -0
  341. package/src/tests/script-connections.test.ts +2 -0
  342. package/src/tests/scripts-http.test.ts +201 -1
  343. package/src/tests/scripts-only-gating.test.ts +2 -3
  344. package/src/tests/scripts-runtime.test.ts +58 -40
  345. package/src/tests/secret-scrubber.test.ts +86 -0
  346. package/src/tests/seed-skills-bundled-files.test.ts +45 -53
  347. package/src/tests/seed-skills-retire.test.ts +182 -0
  348. package/src/tests/send-task-requester-gate.test.ts +58 -0
  349. package/src/tests/session-cost-recompute-unit.test.ts +46 -0
  350. package/src/tests/skill-get-name-resolution.test.ts +137 -0
  351. package/src/tests/sonnet-5-5-registry.test.ts +9 -1
  352. package/src/tests/swarm-config-export-to-env.test.ts +267 -0
  353. package/src/tests/swarm-config-reserved-keys.test.ts +16 -0
  354. package/src/tests/swarm-diff.test.ts +1 -1
  355. package/src/tests/task-model-effort-rerun.test.ts +164 -0
  356. package/src/tests/task-session-prompts.test.ts +132 -0
  357. package/src/tests/task-tool-manifest.test.ts +14 -0
  358. package/src/tests/task-tools-ctx.test.ts +2 -0
  359. package/src/tests/tool-annotations.test.ts +6 -6
  360. package/src/tests/ui-logs-parser.test.ts +43 -0
  361. package/src/tests/used-by-route.test.ts +6 -0
  362. package/src/tests/vercel-ai-gateway.test.ts +149 -0
  363. package/src/tests/workflow-swarm-script.test.ts +38 -0
  364. package/src/tests/workflow-wait-filter.test.ts +5 -3
  365. package/src/tools/chat-access.ts +41 -0
  366. package/src/tools/chat-open-dm.ts +59 -0
  367. package/src/tools/chat-post.ts +74 -0
  368. package/src/tools/chat-read-thread.ts +100 -0
  369. package/src/tools/chat-reply.ts +75 -0
  370. package/src/tools/create-page.ts +3 -13
  371. package/src/tools/model-catalog/index.ts +2 -1
  372. package/src/tools/register-chat-channel.ts +139 -0
  373. package/src/tools/send-task.ts +24 -0
  374. package/src/tools/skills/skill-get.ts +17 -9
  375. package/src/tools/swarm-config/get-config.ts +2 -1
  376. package/src/tools/swarm-config/list-config.ts +2 -1
  377. package/src/tools/swarm-config/set-config.ts +16 -3
  378. package/src/tools/tool-config.ts +8 -6
  379. package/src/types.ts +12 -2
  380. package/src/utils/config-env-export.ts +74 -0
  381. package/src/utils/credentials.test.ts +92 -0
  382. package/src/utils/credentials.ts +52 -4
  383. package/src/utils/harness-provider.ts +15 -6
  384. package/src/utils/runtime-model-catalog.ts +11 -1
  385. package/src/utils/secret-scrubber.ts +71 -2
  386. package/src/utils/task-tool-manifest.ts +19 -2
  387. package/src/workflows/executors/swarm-script.ts +1 -0
  388. package/templates/skills/agent-browser/SKILL.md +1 -1
  389. package/templates/skills/agent-browser/content.md +1 -1
  390. package/templates/skills/apps/SKILL.md +8 -1
  391. package/templates/skills/apps/content.md +8 -1
  392. package/templates/skills/chat-sdk/config.json +14 -0
  393. package/templates/skills/chat-sdk/content.md +84 -0
  394. package/templates/skills/composio-gmail/content.md +7 -0
  395. package/templates/skills/composio-google-docs/content.md +1 -1
  396. package/templates/skills/email-domain-health/config.json +14 -0
  397. package/templates/skills/email-domain-health/content.md +131 -0
  398. package/templates/skills/kv-storage/content.md +2 -2
  399. package/templates/skills/pages/content.md +8 -10
  400. package/templates/skills/swarm-extensions/content.md +2 -0
  401. package/templates/skills/swarm-scripts/SKILL.md +4 -2
  402. package/templates/skills/swarm-scripts/config.json +1 -1
  403. package/templates/skills/swarm-scripts/content.md +4 -2
  404. package/templates/skills/workflow-iterate/content.md +38 -0
  405. package/dist/artifact-eczsknc3.js +0 -3843
  406. package/dist/cli-bd957dgb.js +0 -153
  407. package/dist/cli-q51yprfq.js +0 -1104
  408. package/dist/cli-t1ze64h3.js +0 -811
  409. package/dist/cli-tcc4wd5d.js +0 -56471
  410. package/dist/index-9ch1n761.js +0 -1102
  411. package/src/artifact-sdk/index.ts +0 -2
  412. package/src/artifact-sdk/localtunnel.d.ts +0 -20
  413. package/src/artifact-sdk/port.ts +0 -12
  414. package/src/artifact-sdk/server.ts +0 -183
  415. package/src/artifact-sdk/tunnel.ts +0 -19
  416. package/src/commands/artifact.ts +0 -249
  417. package/src/http/ecosystem.ts +0 -77
  418. package/src/tests/artifact-commands.test.ts +0 -92
  419. package/src/tests/artifact-sdk.test.ts +0 -793
  420. package/src/tests/register-service-security.test.ts +0 -194
  421. package/src/tools/list-services.ts +0 -107
  422. package/src/tools/register-service.ts +0 -177
  423. package/src/tools/unregister-service.ts +0 -73
  424. package/src/tools/update-service-status.ts +0 -96
  425. package/templates/skills/artifacts/config.json +0 -14
  426. package/templates/skills/artifacts/content.md +0 -344
  427. package/templates/skills/artifacts/files/examples/approval-flow.ts +0 -34
  428. package/templates/skills/artifacts/files/examples/hono-dashboard.ts +0 -31
  429. package/templates/skills/artifacts/files/examples/multi-artifact.ts +0 -20
  430. package/templates/skills/artifacts/files/examples/static-report.sh +0 -17
  431. /package/src/{artifact-sdk → pages}/browser-sdk.ts +0 -0
  432. /package/src/{artifact-sdk → pages}/feedback-overlay.ts +0 -0
@@ -1,13 +1,26 @@
1
1
  import { afterAll, afterEach, beforeAll, describe, expect, spyOn, test } from "bun:test";
2
- import { existsSync, mkdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
2
+ import {
3
+ existsSync,
4
+ mkdirSync,
5
+ mkdtempSync,
6
+ readFileSync,
7
+ rmSync,
8
+ symlinkSync,
9
+ writeFileSync,
10
+ } from "node:fs";
11
+ import { tmpdir } from "node:os";
3
12
  import { join } from "node:path";
13
+ import { OPENAI_COMPATIBLE_PROVIDERS } from "@desplega/model-catalog";
4
14
  import * as piCodingAgent from "@earendil-works/pi-coding-agent";
5
15
  import { configureHttpResolver, resetHttpResolver } from "../prompts/resolver";
6
16
  import {
17
+ checkPiMonoCredentials,
7
18
  createPiRuntimeAuth,
8
19
  extractPiAssistantText,
9
20
  PiMonoAdapter,
10
21
  resolveModel,
22
+ type SessionModelsJson,
23
+ writeSessionModelsJson,
11
24
  } from "../providers/pi-mono-adapter";
12
25
  import type { ProviderSessionConfig } from "../providers/types";
13
26
 
@@ -151,8 +164,8 @@ describe("PiMonoAdapter.createSession — OPENROUTER_BASE_URL gateway", () => {
151
164
  const model = capturedOptions?.model as { baseUrl?: string; id?: string } | undefined;
152
165
  expect(model?.id).toBe("google/gemini-3-flash-preview");
153
166
  expect(model?.baseUrl).toBe(GATEWAY);
154
- // The override was materialized for the runtime to load.
155
- expect(existsSync(join(agentDir, "models.json"))).toBe(true);
167
+ // The override lives in the session's own copy, never the shared file.
168
+ expect(existsSync(join(agentDir, "models.json"))).toBe(false);
156
169
  });
157
170
 
158
171
  test("session model keeps the default openrouter.ai baseUrl when env is unset", async () => {
@@ -310,7 +323,7 @@ describe("PiMonoAdapter.createSession — MCP setup", () => {
310
323
  const adapter = new PiMonoAdapter();
311
324
  await adapter.createSession(makeMcpConfig(`http://localhost:${server.port}`));
312
325
  const options = createAgentSessionSpy.mock.calls[0]?.[0];
313
- expect(options?.customTools?.map((tool) => tool.name)).toEqual(["get-tasks"]);
326
+ expect(options?.customTools?.map((tool) => tool.name)).toEqual(["get-tasks", "bash"]);
314
327
  expect(initializeSessions).toEqual([null, null]);
315
328
  expect(new Set(seen)).toEqual(new Set(["pi-retry-runtime"]));
316
329
  } finally {
@@ -330,7 +343,9 @@ describe("PiMonoAdapter.createSession — MCP setup", () => {
330
343
  await adapter.createSession(makeMcpConfig(`http://localhost:${server.port}`));
331
344
  expect(initializeSessions).toEqual([null, null, null]);
332
345
  expect(failures).toEqual(["initialize"]);
333
- expect(createAgentSessionSpy.mock.calls[0]?.[0]?.customTools).toEqual([]);
346
+ expect(
347
+ createAgentSessionSpy.mock.calls[0]?.[0]?.customTools?.map((tool) => tool.name),
348
+ ).toEqual(["bash"]);
334
349
  } finally {
335
350
  server.stop(true);
336
351
  }
@@ -542,6 +557,371 @@ describe("createPiRuntimeAuth", () => {
542
557
  });
543
558
  });
544
559
 
560
+ // ─── NaN / Helmcode custom providers (models.json, runtime keys, resolve) ───
561
+
562
+ const NAN_ENV = { NAN_API_KEY: "example-nan-picked" };
563
+ const HELMCODE_ENV = { HELMCODE_API_KEY: "example-helmcode-picked" };
564
+ async function readModelsJson(path: string | undefined): Promise<Record<string, any>> {
565
+ if (!path) throw new Error("expected a per-session models.json");
566
+ return JSON.parse(await Bun.file(path).text());
567
+ }
568
+
569
+ function withTempDir(prefix: string, fn: (dir: string) => Promise<void>): () => Promise<void> {
570
+ return async () => {
571
+ const dir = mkdtempSync(join(tmpdir(), prefix));
572
+ try {
573
+ await fn(dir);
574
+ } finally {
575
+ rmSync(dir, { recursive: true, force: true });
576
+ }
577
+ };
578
+ }
579
+
580
+ /** Compose a session's models.json and always remove it afterwards. */
581
+ async function withSessionModels(
582
+ dir: string,
583
+ env: Record<string, string | undefined>,
584
+ model: string | undefined,
585
+ fn: (session: SessionModelsJson) => Promise<void>,
586
+ ): Promise<void> {
587
+ const session = await writeSessionModelsJson(dir, env, model);
588
+ try {
589
+ await fn(session);
590
+ } finally {
591
+ await session.dispose();
592
+ }
593
+ }
594
+
595
+ describe("writeSessionModelsJson (pi)", () => {
596
+ test(
597
+ "writes both blocks with $VAR apiKeys to a private file, never the key value or the shared file",
598
+ withTempDir("pi-custom-fresh-", async (dir) => {
599
+ await withSessionModels(dir, { ...NAN_ENV, ...HELMCODE_ENV }, undefined, async (s) => {
600
+ expect(s.modelsPath).toBeDefined();
601
+ expect(s.modelsPath?.startsWith(dir)).toBe(false);
602
+ expect(existsSync(join(dir, "models.json"))).toBe(false);
603
+ const raw = await Bun.file(s.modelsPath as string).text();
604
+ expect(raw).not.toContain("example-nan-picked");
605
+ expect(raw).not.toContain("example-helmcode-picked");
606
+ const written = JSON.parse(raw);
607
+ expect(written.providers.nan).toMatchObject({
608
+ baseUrl: OPENAI_COMPATIBLE_PROVIDERS.nan.baseUrl,
609
+ api: "openai-completions",
610
+ apiKey: "$NAN_API_KEY",
611
+ });
612
+ expect(written.providers.helmcode).toMatchObject({
613
+ baseUrl: OPENAI_COMPATIBLE_PROVIDERS.helmcode.baseUrl,
614
+ api: "openai-completions",
615
+ apiKey: "$HELMCODE_API_KEY",
616
+ });
617
+ expect(written.providers.helmcode.models.map((m: { id: string }) => m.id)).toEqual(
618
+ OPENAI_COMPATIBLE_PROVIDERS.helmcode.models.map((m) => m.id),
619
+ );
620
+ const glm = written.providers.nan.models.find((m: { id: string }) => m.id === "glm5.3");
621
+ expect(glm).toMatchObject({
622
+ contextWindow: 1000000,
623
+ maxTokens: 131072,
624
+ reasoning: true,
625
+ compat: { supportsDeveloperRole: true },
626
+ });
627
+ });
628
+ }),
629
+ );
630
+
631
+ test(
632
+ "dispose removes the private file",
633
+ withTempDir("pi-custom-dispose-", async (dir) => {
634
+ const s = await writeSessionModelsJson(dir, NAN_ENV);
635
+ expect(existsSync(s.modelsPath as string)).toBe(true);
636
+ await s.dispose();
637
+ await s.dispose();
638
+ expect(existsSync(s.modelsPath as string)).toBe(false);
639
+ }),
640
+ );
641
+
642
+ test(
643
+ "only the HELMCODE_API_NAN alias set: nan block still uses $NAN_API_KEY",
644
+ withTempDir("pi-custom-alias-", async (dir) => {
645
+ await withSessionModels(dir, { HELMCODE_API_NAN: "example-alias" }, undefined, async (s) => {
646
+ const written = await readModelsJson(s.modelsPath);
647
+ expect(written.providers.nan.apiKey).toBe("$NAN_API_KEY");
648
+ expect(written.providers.helmcode).toBeUndefined();
649
+ expect(JSON.stringify(written)).not.toContain("example-alias");
650
+ });
651
+ }),
652
+ );
653
+
654
+ test(
655
+ "no key, default gateway, no marker: the session loads the shared file",
656
+ withTempDir("pi-custom-noop-", async (dir) => {
657
+ const userConfig = { providers: { nan: { baseUrl: "https://mine.example/v1" } } };
658
+ await Bun.write(join(dir, "models.json"), JSON.stringify(userConfig));
659
+ const s = await writeSessionModelsJson(dir, {});
660
+ expect(s.modelsPath).toBeUndefined();
661
+ expect(JSON.parse(readFileSync(join(dir, "models.json"), "utf-8"))).toEqual(userConfig);
662
+ }),
663
+ );
664
+
665
+ test(
666
+ "a user-authored providers.nan in the shared file wins; helmcode is still added",
667
+ withTempDir("pi-custom-user-", async (dir) => {
668
+ const userNan = { baseUrl: "https://mine.example/v1", apiKey: "$MY_VAR", models: [] };
669
+ const shared = JSON.stringify({ providers: { nan: userNan } });
670
+ await Bun.write(join(dir, "models.json"), shared);
671
+ await withSessionModels(dir, { ...NAN_ENV, ...HELMCODE_ENV }, undefined, async (s) => {
672
+ const written = await readModelsJson(s.modelsPath);
673
+ expect(written.providers.nan).toEqual(userNan);
674
+ expect(written.providers.helmcode.apiKey).toBe("$HELMCODE_API_KEY");
675
+ });
676
+ expect(readFileSync(join(dir, "models.json"), "utf-8")).toBe(shared);
677
+ }),
678
+ );
679
+
680
+ test(
681
+ "keeps other providers and top-level keys; the gateway comes from this session's env",
682
+ withTempDir("pi-custom-merge-", async (dir) => {
683
+ const gateway = "https://control-plane.example/proxy/v1";
684
+ await Bun.write(
685
+ join(dir, "models.json"),
686
+ JSON.stringify({
687
+ $schema: "https://pi.dev/schemas/models.schema.json",
688
+ providers: {
689
+ custom: { baseUrl: "https://custom.example/v1", api: "openai-completions" },
690
+ openrouter: { baseUrl: gateway },
691
+ },
692
+ }),
693
+ );
694
+ // An earlier version wrote that gateway into the shared file.
695
+ await Bun.write(
696
+ join(dir, ".agent-swarm-openrouter-override.json"),
697
+ JSON.stringify({ baseUrl: gateway }),
698
+ );
699
+ const sharedBefore = readFileSync(join(dir, "models.json"), "utf-8");
700
+ await withSessionModels(dir, NAN_ENV, undefined, async (s) => {
701
+ // This session uses the default gateway: the leftover override
702
+ // recorded in the legacy marker is reverted in its copy.
703
+ const written = await readModelsJson(s.modelsPath);
704
+ expect(written.$schema).toBe("https://pi.dev/schemas/models.schema.json");
705
+ expect(written.providers.openrouter).toBeUndefined();
706
+ expect(written.providers.custom.baseUrl).toBe("https://custom.example/v1");
707
+ expect(written.providers.nan.apiKey).toBe("$NAN_API_KEY");
708
+ });
709
+ const other = "https://other-gateway.example/v1";
710
+ await withSessionModels(dir, { OPENROUTER_BASE_URL: other }, undefined, async (s) => {
711
+ expect((await readModelsJson(s.modelsPath)).providers.openrouter).toEqual({
712
+ baseUrl: other,
713
+ });
714
+ });
715
+ // The shared file is never written by the composer.
716
+ expect(readFileSync(join(dir, "models.json"), "utf-8")).toBe(sharedBefore);
717
+ }),
718
+ );
719
+
720
+ test(
721
+ "sibling sessions get their own endpoints, providers and model ids",
722
+ withTempDir("pi-custom-siblings-", async (dir) => {
723
+ const a = await writeSessionModelsJson(
724
+ dir,
725
+ { HELMCODE_API_KEY: "a", HELMCODE_BASE_URL: "http://127.0.0.1:1/v1", ...NAN_ENV },
726
+ "nan/glm5.2",
727
+ );
728
+ const b = await writeSessionModelsJson(
729
+ dir,
730
+ { HELMCODE_API_KEY: "b", HELMCODE_BASE_URL: "http://127.0.0.1:2/v1" },
731
+ "helmcode/glm5.3",
732
+ );
733
+ try {
734
+ expect(a.modelsPath).not.toBe(b.modelsPath);
735
+ const first = await readModelsJson(a.modelsPath);
736
+ const second = await readModelsJson(b.modelsPath);
737
+ expect(first.providers.helmcode.baseUrl).toBe("http://127.0.0.1:1/v1");
738
+ expect(second.providers.helmcode.baseUrl).toBe("http://127.0.0.1:2/v1");
739
+ // B has no NaN key: no nan block, even while A's session holds one.
740
+ expect(first.providers.nan.models.map((m: { id: string }) => m.id)).toContain("glm5.2");
741
+ expect(second.providers.nan).toBeUndefined();
742
+ } finally {
743
+ await a.dispose();
744
+ await b.dispose();
745
+ }
746
+ }),
747
+ );
748
+
749
+ test(
750
+ "corrupt shared models.json: the copy holds our blocks only, the shared file is untouched",
751
+ withTempDir("pi-custom-corrupt-", async (dir) => {
752
+ await Bun.write(join(dir, "models.json"), "{not json");
753
+ const warn = spyOn(console, "warn").mockImplementation(() => {});
754
+ try {
755
+ await withSessionModels(dir, NAN_ENV, undefined, async (s) => {
756
+ expect((await readModelsJson(s.modelsPath)).providers.nan.apiKey).toBe("$NAN_API_KEY");
757
+ });
758
+ } finally {
759
+ warn.mockRestore();
760
+ }
761
+ expect(readFileSync(join(dir, "models.json"), "utf-8")).toBe("{not json");
762
+ }),
763
+ );
764
+
765
+ test(
766
+ "ModelRuntime resolves the written blocks as nan/<id> and helmcode/<id>",
767
+ withTempDir("pi-custom-runtime-", async (dir) => {
768
+ await withSessionModels(dir, { ...NAN_ENV, ...HELMCODE_ENV }, undefined, async (s) => {
769
+ const runtime = await piCodingAgent.ModelRuntime.create({
770
+ authPath: join(dir, "auth.json"),
771
+ modelsPath: s.modelsPath,
772
+ });
773
+ expect(runtime.getError()).toBeUndefined();
774
+ expect(runtime.getModel("nan", "qwen3.8-flash")?.baseUrl).toBe(
775
+ OPENAI_COMPATIBLE_PROVIDERS.nan.baseUrl,
776
+ );
777
+ expect(runtime.getModel("helmcode", "glm5.3")?.contextWindow).toBe(1048576);
778
+ });
779
+ }),
780
+ );
781
+ });
782
+
783
+ describe("createPiRuntimeAuth — NaN and Helmcode", () => {
784
+ test(
785
+ "HELMCODE_API_NAN alone sets the nan runtime key; NAN_API_KEY wins when both are set",
786
+ withTempDir("pi-custom-auth-", async (dir) => {
787
+ const models = await writeSessionModelsJson(dir, { NAN_API_KEY: "x", HELMCODE_API_KEY: "y" });
788
+ // pi's own agent-dir env var points auth.json at the temp dir.
789
+ const origAgentDir = process.env.PI_CODING_AGENT_DIR;
790
+ process.env.PI_CODING_AGENT_DIR = dir;
791
+ try {
792
+ const path = models.modelsPath;
793
+ const aliasOnly = await createPiRuntimeAuth({ HELMCODE_API_NAN: "a" }, path);
794
+ await expect(aliasOnly.getAuth("nan")).resolves.toMatchObject({ auth: { apiKey: "a" } });
795
+
796
+ const both = await createPiRuntimeAuth({ HELMCODE_API_NAN: "a", NAN_API_KEY: "b" }, path);
797
+ await expect(both.getAuth("nan")).resolves.toMatchObject({ auth: { apiKey: "b" } });
798
+
799
+ const helm = await createPiRuntimeAuth({ HELMCODE_API_KEY: "h" }, path);
800
+ await expect(helm.getAuth("helmcode")).resolves.toMatchObject({ auth: { apiKey: "h" } });
801
+ } finally {
802
+ await models.dispose();
803
+ if (origAgentDir === undefined) delete process.env.PI_CODING_AGENT_DIR;
804
+ else process.env.PI_CODING_AGENT_DIR = origAgentDir;
805
+ }
806
+
807
+ // Runtime keys never land in auth.json.
808
+ const auth = (await Bun.file(join(dir, "auth.json")).exists())
809
+ ? await Bun.file(join(dir, "auth.json")).text()
810
+ : "";
811
+ expect(auth).not.toContain('"a"');
812
+ expect(auth).not.toContain('"b"');
813
+ expect(auth).not.toContain('"h"');
814
+ }),
815
+ );
816
+ });
817
+
818
+ describe("resolveModel — models.json custom providers", () => {
819
+ test(
820
+ "falls back to the runtime for nan/qwen3.8-flash and helmcode/glm5.3",
821
+ withTempDir("pi-custom-resolve-", async (dir) => {
822
+ const models = await writeSessionModelsJson(dir, { ...NAN_ENV, ...HELMCODE_ENV });
823
+ const runtime = await piCodingAgent.ModelRuntime.create({
824
+ authPath: join(dir, "auth.json"),
825
+ modelsPath: models.modelsPath,
826
+ });
827
+ await models.dispose();
828
+ const nan = resolveModel("nan/qwen3.8-flash", NAN_ENV, runtime);
829
+ expect(nan?.provider).toBe("nan");
830
+ expect(nan?.id).toBe("qwen3.8-flash");
831
+ const helm = resolveModel("helmcode/glm5.3", HELMCODE_ENV, runtime);
832
+ expect(helm?.provider).toBe("helmcode");
833
+ expect(helm?.id).toBe("glm5.3");
834
+ expect(helm?.baseUrl).toBe(OPENAI_COMPATIBLE_PROVIDERS.helmcode.baseUrl);
835
+ }),
836
+ );
837
+
838
+ test(
839
+ "a nan/<id> outside the table (nan/glm5.2) gets a bare entry and resolves",
840
+ withTempDir("pi-custom-bare-", async (dir) => {
841
+ await withSessionModels(dir, NAN_ENV, "nan/glm5.2", async (s) => {
842
+ const written = await readModelsJson(s.modelsPath);
843
+ const ids = written.providers.nan.models.map((m: { id: string }) => m.id);
844
+ expect(ids).toContain("glm5.2");
845
+ expect(ids).toContain("glm5.3");
846
+ const runtime = await piCodingAgent.ModelRuntime.create({
847
+ authPath: join(dir, "auth.json"),
848
+ modelsPath: s.modelsPath,
849
+ });
850
+ const model = resolveModel("nan/glm5.2", NAN_ENV, runtime);
851
+ expect(model?.provider).toBe("nan");
852
+ expect(model?.id).toBe("glm5.2");
853
+ expect(model?.baseUrl).toBe(OPENAI_COMPATIBLE_PROVIDERS.nan.baseUrl);
854
+ });
855
+
856
+ // The bare entry belongs to that session: the next task's model never sees it.
857
+ await withSessionModels(dir, NAN_ENV, "nan/glm5.3", async (s) => {
858
+ const next = await readModelsJson(s.modelsPath);
859
+ expect(next.providers.nan.models.map((m: { id: string }) => m.id)).not.toContain("glm5.2");
860
+ });
861
+ }),
862
+ );
863
+
864
+ test("builtin hits are unchanged when a runtime is passed", async () => {
865
+ const runtime = { getModel: () => undefined };
866
+ const model = resolveModel("anthropic/claude-sonnet-5", {}, runtime);
867
+ expect(model?.provider).toBe("anthropic");
868
+ });
869
+
870
+ test("an unknown provider returns undefined and logs a scrubbed warning", () => {
871
+ const warn = spyOn(console, "warn").mockImplementation(() => {});
872
+ try {
873
+ const runtime = { getModel: () => undefined };
874
+ expect(resolveModel("nope/model-x", {}, runtime)).toBeUndefined();
875
+ expect(warn).toHaveBeenCalledTimes(1);
876
+ expect(String(warn.mock.calls[0]?.[0])).toContain('"nope/model-x"');
877
+ expect(String(warn.mock.calls[0]?.[0])).toContain("default model");
878
+ } finally {
879
+ warn.mockRestore();
880
+ }
881
+ });
882
+
883
+ test("without a runtime, nan/<id> does not resolve (pi-ai has no builtin nan)", () => {
884
+ const warn = spyOn(console, "warn").mockImplementation(() => {});
885
+ try {
886
+ expect(resolveModel("nan/glm5.3", NAN_ENV)).toBeUndefined();
887
+ } finally {
888
+ warn.mockRestore();
889
+ }
890
+ });
891
+ });
892
+
893
+ describe("checkPiMonoCredentials — NaN and Helmcode", () => {
894
+ const opts = { homeDir: "/home/worker", fs: { existsSync: () => false } };
895
+
896
+ test("MODEL_OVERRIDE=nan/* accepts NAN_API_KEY or the HELMCODE_API_NAN alias", async () => {
897
+ const env = { MODEL_OVERRIDE: "nan/glm5.3" };
898
+ expect((await checkPiMonoCredentials(env, opts)).ready).toBe(false);
899
+ expect((await checkPiMonoCredentials({ ...env, NAN_API_KEY: "x" }, opts)).ready).toBe(true);
900
+ expect((await checkPiMonoCredentials({ ...env, HELMCODE_API_NAN: "x" }, opts)).ready).toBe(
901
+ true,
902
+ );
903
+ // An OpenRouter key does not satisfy a nan/* model.
904
+ expect((await checkPiMonoCredentials({ ...env, OPENROUTER_API_KEY: "x" }, opts)).ready).toBe(
905
+ false,
906
+ );
907
+ });
908
+
909
+ test("MODEL_OVERRIDE=helmcode/* requires HELMCODE_API_KEY", async () => {
910
+ const env = { MODEL_OVERRIDE: "helmcode/glm5.3" };
911
+ const missing = await checkPiMonoCredentials({ ...env, NAN_API_KEY: "x" }, opts);
912
+ expect(missing.ready).toBe(false);
913
+ expect(missing.missing).toContain("HELMCODE_API_KEY");
914
+ expect((await checkPiMonoCredentials({ ...env, HELMCODE_API_KEY: "x" }, opts)).ready).toBe(
915
+ true,
916
+ );
917
+ });
918
+
919
+ test("permissive case accepts a NaN-only or Helmcode-only worker", async () => {
920
+ expect((await checkPiMonoCredentials({ HELMCODE_API_NAN: "x" }, opts)).ready).toBe(true);
921
+ expect((await checkPiMonoCredentials({ HELMCODE_API_KEY: "x" }, opts)).ready).toBe(true);
922
+ });
923
+ });
924
+
545
925
  describe("Pi-mono event normalization", () => {
546
926
  test("extractPiAssistantText ignores user messages", () => {
547
927
  const text = extractPiAssistantText({
@@ -0,0 +1,170 @@
1
+ import {
2
+ afterAll,
3
+ afterEach,
4
+ beforeAll,
5
+ beforeEach,
6
+ describe,
7
+ expect,
8
+ spyOn,
9
+ test,
10
+ } from "bun:test";
11
+ import { mkdtempSync, rmSync } from "node:fs";
12
+ import { tmpdir } from "node:os";
13
+ import { join } from "node:path";
14
+ import type { AgentSession } from "@earendil-works/pi-coding-agent";
15
+ import { fetchResolvedEnv } from "../commands/runner";
16
+ import { buildPiShellEnv, PiMonoAdapter } from "../providers/pi-mono-adapter";
17
+
18
+ /**
19
+ * pi's Bash tool must run with the env the runner resolved for this task,
20
+ * not the worker's process.env: a row admitted after boot (exportToEnv
21
+ * flipped on) must reach the shell, and a row revoked after boot (flipped off)
22
+ * must leave it, even though process.env still holds the boot value.
23
+ */
24
+
25
+ type ConfigRow = { key: string; value: string; isSecret: boolean; exportToEnv: boolean };
26
+
27
+ let server: ReturnType<typeof Bun.serve>;
28
+ let apiUrl = "";
29
+ let rows: ConfigRow[] = [];
30
+
31
+ beforeAll(() => {
32
+ server = Bun.serve({
33
+ port: 0,
34
+ async fetch(req) {
35
+ const url = new URL(req.url);
36
+ if (url.pathname === "/api/config/resolved") return Response.json({ configs: rows });
37
+ if (url.pathname !== "/mcp") return Response.json({ servers: [] });
38
+ const body = (await req.json()) as { id?: number; method?: string };
39
+ const result =
40
+ body.method === "tools/list"
41
+ ? { tools: [] }
42
+ : { protocolVersion: "2025-03-26", capabilities: {} };
43
+ return Response.json({ jsonrpc: "2.0", id: body.id ?? 1, result });
44
+ },
45
+ });
46
+ apiUrl = `http://localhost:${server.port}`;
47
+ });
48
+
49
+ afterAll(() => {
50
+ server.stop(true);
51
+ });
52
+
53
+ describe("pi Bash tool env", () => {
54
+ const envKeys = [
55
+ "PI_CODING_AGENT_DIR",
56
+ "OPENROUTER_API_KEY",
57
+ "OPENROUTER_BASE_URL",
58
+ "SHELL_REVOKED_CANARY",
59
+ "SHELL_ADMITTED_CANARY",
60
+ ] as const;
61
+ const saved: Record<string, string | undefined> = {};
62
+ let agentDir = "";
63
+ let log: ReturnType<typeof spyOn>;
64
+
65
+ beforeEach(() => {
66
+ for (const key of envKeys) saved[key] = process.env[key];
67
+ agentDir = mkdtempSync(join(tmpdir(), "pi-shell-env-"));
68
+ process.env.PI_CODING_AGENT_DIR = agentDir;
69
+ process.env.OPENROUTER_API_KEY = "example-test-key";
70
+ process.env.OPENROUTER_BASE_URL = "http://127.0.0.1:9/api/v1";
71
+ log = spyOn(console, "log").mockImplementation(() => {});
72
+ });
73
+ afterEach(() => {
74
+ log.mockRestore();
75
+ for (const key of envKeys) {
76
+ if (saved[key] === undefined) delete process.env[key];
77
+ else process.env[key] = saved[key];
78
+ }
79
+ rmSync(agentDir, { recursive: true, force: true });
80
+ });
81
+
82
+ async function sessionWithEnv(env: Record<string, string | undefined>): Promise<AgentSession> {
83
+ const session = await new PiMonoAdapter().createSession({
84
+ prompt: "hello",
85
+ systemPrompt: "test",
86
+ model: "openrouter/google/gemini-3-flash-preview",
87
+ role: "worker",
88
+ agentId: "test-agent",
89
+ taskId: "test-task",
90
+ apiUrl,
91
+ apiKey: "example-test-key",
92
+ cwd: agentDir,
93
+ logFile: join(agentDir, `session-${crypto.randomUUID()}.log`),
94
+ env,
95
+ });
96
+ return (session as unknown as { agentSession: AgentSession }).agentSession;
97
+ }
98
+
99
+ async function shell(session: AgentSession, command: string): Promise<string> {
100
+ const bash = session.getToolDefinition("bash");
101
+ if (!bash) throw new Error("bash tool missing");
102
+ const result = await bash.execute(
103
+ "call-1",
104
+ { command },
105
+ undefined,
106
+ undefined,
107
+ undefined as never,
108
+ );
109
+ return result.content
110
+ .map((part) => ("text" in part ? part.text : ""))
111
+ .join("")
112
+ .trim();
113
+ }
114
+
115
+ test("admission and revocation after boot reach the shell", async () => {
116
+ // Boot exported SHELL_REVOKED_CANARY and withheld SHELL_ADMITTED_CANARY; the
117
+ // operator then flipped both rows.
118
+ process.env.SHELL_REVOKED_CANARY = "boot-revoked";
119
+ delete process.env.SHELL_ADMITTED_CANARY;
120
+ rows = [
121
+ { key: "SHELL_REVOKED_CANARY", value: "row-revoked", isSecret: true, exportToEnv: false },
122
+ { key: "SHELL_ADMITTED_CANARY", value: "row-admitted", isSecret: true, exportToEnv: true },
123
+ ];
124
+ const { env } = await fetchResolvedEnv(apiUrl, "key", "agent-1", process.env);
125
+ const session = await sessionWithEnv(env);
126
+ try {
127
+ const out = await shell(
128
+ session,
129
+ // The shell's own environment: /proc on Linux, `env` on macOS (no procfs).
130
+ 'echo "admitted=$SHELL_ADMITTED_CANARY revoked=$SHELL_REVOKED_CANARY."; { if [ -r /proc/$$/environ ]; then tr "\\0" "\\n" < /proc/$$/environ; else env; fi; } | grep -c "^SHELL_REVOKED_CANARY=" || true',
131
+ );
132
+ expect(out).toContain("admitted=row-admitted revoked=.");
133
+ expect(out.split("\n").at(-1)).toBe("0");
134
+ } finally {
135
+ session.dispose();
136
+ }
137
+ });
138
+
139
+ test("concurrent sessions keep their own env", async () => {
140
+ const first = await sessionWithEnv({ ...process.env, SHELL_ADMITTED_CANARY: "first" });
141
+ const second = await sessionWithEnv({ ...process.env, SHELL_ADMITTED_CANARY: undefined });
142
+ try {
143
+ const [a, b] = await Promise.all([
144
+ shell(first, 'echo "v=$SHELL_ADMITTED_CANARY"'),
145
+ shell(second, 'echo "v=$SHELL_ADMITTED_CANARY"'),
146
+ ]);
147
+ expect(a).toBe("v=first");
148
+ expect(b).toBe("v=");
149
+ expect(process.env.SHELL_ADMITTED_CANARY).toBeUndefined();
150
+ } finally {
151
+ first.dispose();
152
+ second.dispose();
153
+ }
154
+ });
155
+ });
156
+
157
+ describe("buildPiShellEnv", () => {
158
+ test("keeps the session env and takes only PATH and pi's session keys from its spawn env", () => {
159
+ const env = buildPiShellEnv(
160
+ { HOME: "/home/worker", PATH: "/usr/bin", TASK_ONLY: "yes" },
161
+ { PATH: "/pi/bin:/usr/bin", PI_SESSION_ID: "s1", PI_OTHER: "leak", PROCESS_ONLY: "leak" },
162
+ );
163
+ expect(env).toEqual({
164
+ HOME: "/home/worker",
165
+ PATH: "/pi/bin:/usr/bin",
166
+ TASK_ONLY: "yes",
167
+ PI_SESSION_ID: "s1",
168
+ });
169
+ });
170
+ });
@@ -12,6 +12,7 @@ import "../heartbeat/templates";
12
12
  import "../tools/templates";
13
13
  import "../commands/templates";
14
14
  import "../slack/templates";
15
+ import "../integrations/chat-sdk/templates";
15
16
  import "../prompts/session-templates";
16
17
 
17
18
  const TEST_DB_PATH = "./test-prompt-remaining.sqlite";
@@ -33,6 +34,7 @@ async function ensureTemplatesRegistered(): Promise<void> {
33
34
  await import(`../tools/templates?t=${ts}`);
34
35
  await import(`../commands/templates?t=${ts}`);
35
36
  await import(`../slack/templates?t=${ts}`);
37
+ await import(`../integrations/chat-sdk/templates?t=${ts}`);
36
38
  await import(`../prompts/session-templates?t=${ts}`);
37
39
  }
38
40
 
@@ -115,6 +117,22 @@ describe("template registration — all sources", () => {
115
117
  expect(eventTypes).toContain("heartbeat.checklist");
116
118
  });
117
119
 
120
+ test("Heartbeat checklist puts the live status after the standing orders and instructions", () => {
121
+ // The status changes every tick; the orders and instructions usually do
122
+ // not, so they lead the text and the status goes last.
123
+ const { text } = resolveTemplate("heartbeat.checklist", {
124
+ system_status: "STATUS_MARKER",
125
+ heartbeat_content: "ORDERS_MARKER",
126
+ });
127
+ const orders = text.indexOf("ORDERS_MARKER");
128
+ const instructions = text.indexOf("## Instructions");
129
+ const status = text.indexOf("## Current System Status");
130
+ expect(orders).toBeGreaterThan(-1);
131
+ expect(instructions).toBeGreaterThan(orders);
132
+ expect(status).toBeGreaterThan(instructions);
133
+ expect(text.trimEnd()).toEndWith("STATUS_MARKER");
134
+ });
135
+
118
136
  test("Task lifecycle templates are registered (3 task_lifecycle)", () => {
119
137
  const all = getAllTemplateDefinitions();
120
138
  const eventTypes = all.map((d) => d.eventType);
@@ -143,6 +161,11 @@ describe("template registration — all sources", () => {
143
161
  expect(eventTypes).toContain("slack.assistant.offline");
144
162
  expect(eventTypes).toContain("slack.message.thread_context");
145
163
  });
164
+
165
+ test("Chat SDK templates are registered (1 system)", () => {
166
+ const eventTypes = getAllTemplateDefinitions().map((d) => d.eventType);
167
+ expect(eventTypes).toContain("chat.message.thread_context");
168
+ });
146
169
  });
147
170
 
148
171
  // ============================================================================
@@ -608,4 +631,16 @@ Alice: Hello
608
631
 
609
632
  expect(result.text).toBe(expected);
610
633
  });
634
+
635
+ test("chat.message.thread_context produces the same block as Slack", () => {
636
+ const result = resolveTemplate("chat.message.thread_context", {
637
+ thread_messages: "Alice: Hello\n[Agent]: Hi there!",
638
+ });
639
+
640
+ expect(result.skipped).toBe(false);
641
+ expect(result.text).toBe(`<thread_context>
642
+ Alice: Hello
643
+ [Agent]: Hi there!
644
+ </thread_context>`);
645
+ });
611
646
  });