@desplega.ai/agent-swarm 2.5.4 → 2.7.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 (354) hide show
  1. package/README.md +4 -2
  2. package/dist/{acp-adapter-ek38cwsv.js → acp-adapter-ma1js5n7.js} +9 -9
  3. package/dist/{actions-92mepq03.js → actions-1vhmtvg9.js} +21 -21
  4. package/dist/{amp-adapter-9gaxg015.js → amp-adapter-sxqqvmdk.js} +4 -4
  5. package/dist/{app-ctn8gsgx.js → app-mwx1d5a9.js} +3 -3
  6. package/dist/{assistant-zyr5kz47.js → assistant-yx1p6g82.js} +30 -26
  7. package/dist/{boot-reembed-w75maga2.js → boot-reembed-5h37a2c2.js} +1 -1
  8. package/dist/{boot-reembed-dwzfttdr.js → boot-reembed-7h78vmvv.js} +2 -1
  9. package/dist/{boot-scrub-sweep-e9kq43kf.js → boot-scrub-sweep-8mawd4gw.js} +4 -4
  10. package/dist/{boot-seeds-x28v3fhb.js → boot-seeds-ca84dhnn.js} +4 -4
  11. package/dist/{claude-adapter-f0afqnne.js → claude-adapter-wqt4hfkq.js} +9 -9
  12. package/dist/{claude-managed-adapter-x81a7r65.js → claude-managed-adapter-6fjqtr2z.js} +2 -2
  13. package/dist/{claude-managed-setup-bqzwjd9x.js → claude-managed-setup-rqv98sea.js} +10 -3
  14. package/dist/{claude-sdk-session-7b7whkaq.js → claude-sdk-session-f29f3b2x.js} +11 -11
  15. package/dist/{cli-sxgtf5fw.js → cli-07q03xm2.js} +1 -1
  16. package/dist/{cli-kxpfx2xr.js → cli-0z1xjzwa.js} +2 -2
  17. package/dist/{cli-g9vd9ece.js → cli-1hhbzkbb.js} +14 -16
  18. package/dist/{cli-qqzmjxkf.js → cli-1m9x8dr5.js} +11 -6
  19. package/dist/{cli-jbnn4vxb.js → cli-2pvzmas1.js} +3 -3
  20. package/dist/{cli-x77gen2p.js → cli-2y7v133v.js} +7 -7
  21. package/dist/{cli-dew6x255.js → cli-39aet655.js} +2 -2
  22. package/dist/{cli-g1cw7xtt.js → cli-39rrw8yp.js} +1 -1
  23. package/dist/{cli-jb30dzvk.js → cli-48gxjmcz.js} +530 -190
  24. package/dist/{cli-pzatvw94.js → cli-54sj64fc.js} +2 -2
  25. package/dist/{cli-bvpgr9ny.js → cli-60sty27p.js} +5 -5
  26. package/dist/{cli-asq5ydm4.js → cli-6a9qfa8b.js} +411 -69
  27. package/dist/{cli-277c31dw.js → cli-6mgdba42.js} +225 -3
  28. package/dist/{cli-19202kew.js → cli-6srv653c.js} +5 -5
  29. package/dist/{cli-n4c8e3s5.js → cli-708wdy32.js} +1 -1
  30. package/dist/{cli-pasyvjmm.js → cli-737pyqxr.js} +1 -1
  31. package/dist/{cli-6x9ma5w5.js → cli-803188pb.js} +1 -1
  32. package/dist/{cli-hh5we352.js → cli-86ehn4s5.js} +7 -7
  33. package/dist/{cli-9s05422w.js → cli-8mbc9za9.js} +1 -1
  34. package/dist/{cli-ff1tm9n9.js → cli-8wmj18cz.js} +1 -1
  35. package/dist/{cli-1r5ad7j5.js → cli-9gsm2rzp.js} +12 -12
  36. package/dist/{cli-mz9afxxf.js → cli-9pshx6rd.js} +17 -3
  37. package/dist/{cli-s682zgqz.js → cli-a6ds2epe.js} +1 -1
  38. package/dist/{cli-6a1xg1p7.js → cli-a89zdqz9.js} +2 -2
  39. package/dist/{cli-03vpq45j.js → cli-ay40dnj3.js} +2 -2
  40. package/dist/{cli-tsma7p8v.js → cli-aypd09nf.js} +2 -2
  41. package/dist/{cli-ethgy7ha.js → cli-b3yzyc57.js} +2 -2
  42. package/dist/{cli-bn0rewsy.js → cli-c081bj3b.js} +2 -2
  43. package/dist/{cli-mmhj8vd7.js → cli-c668vvcf.js} +11 -2
  44. package/dist/{cli-sbwsxvtd.js → cli-d20qvpr2.js} +2 -2
  45. package/dist/{cli-c952c7ac.js → cli-e1595bgx.js} +3 -3
  46. package/dist/{cli-621ce7kw.js → cli-f12mr20z.js} +3 -3
  47. package/dist/{cli-dqygeeh5.js → cli-f1e0ptjj.js} +10 -1
  48. package/dist/{cli-xzfq4at3.js → cli-f4dbh7a5.js} +16 -12
  49. package/dist/{cli-ha9rmwgq.js → cli-fb8m699c.js} +2 -2
  50. package/dist/{cli-5bnsc0e1.js → cli-gsfk58nw.js} +5 -5
  51. package/dist/cli-gt4h8cxw.js +895 -0
  52. package/dist/{cli-nwyw3zhg.js → cli-gxee0bqa.js} +1 -1
  53. package/dist/{cli-25n3mv5d.js → cli-h5jeg46p.js} +6 -6
  54. package/dist/{cli-gn5af0x5.js → cli-he9gbdx0.js} +62 -12
  55. package/dist/{cli-gbfxp48k.js → cli-j0ppywdr.js} +42 -35
  56. package/dist/{cli-k4tjsdx9.js → cli-j3y0x2n4.js} +277 -199
  57. package/dist/{cli-7p4jxgtt.js → cli-j9bpmykh.js} +75 -27
  58. package/dist/{cli-dabehxhp.js → cli-kt907da6.js} +1 -1
  59. package/dist/{cli-wx2nj19m.js → cli-kv7sbc2c.js} +6 -6
  60. package/dist/{cli-abs8ndtr.js → cli-m30k9777.js} +26 -26
  61. package/dist/{cli-cz5nmdxt.js → cli-m5e802r7.js} +2 -2
  62. package/dist/{cli-0shx9j93.js → cli-pfk92eqz.js} +2 -2
  63. package/dist/{cli-xtrz6bzs.js → cli-ptfq202x.js} +5 -3
  64. package/dist/{cli-6n8axjv1.js → cli-qj442fws.js} +2 -2
  65. package/dist/{cli-c61mbdds.js → cli-rd5m70gj.js} +4 -4
  66. package/dist/{cli-eftzyh9y.js → cli-s09j9vp5.js} +2 -2
  67. package/dist/{cli-m4ecdxvz.js → cli-s0mgx214.js} +7 -7
  68. package/dist/{cli-wghjkr0e.js → cli-sgqm2582.js} +10 -7
  69. package/dist/{cli-2655k16q.js → cli-sjctjr48.js} +2 -2
  70. package/dist/{cli-nzfnt2gs.js → cli-tf1n0r0b.js} +1 -1
  71. package/dist/{cli-x0f2v69c.js → cli-twctnkng.js} +12 -7
  72. package/dist/{cli-j6rktc91.js → cli-v6shg7na.js} +1 -1
  73. package/dist/{cli-gx4rm0qx.js → cli-vz78s5wm.js} +1 -1
  74. package/dist/{cli-gwhjmh8g.js → cli-wqr7cea4.js} +5 -5
  75. package/dist/{cli-e42vss7b.js → cli-x37r7qwf.js} +16 -8
  76. package/dist/{cli-4b2z6n0m.js → cli-yd2nreqw.js} +17 -8
  77. package/dist/{cli-64mh9mf3.js → cli-ymjpkxgn.js} +69 -17
  78. package/dist/{cli-5kqnemen.js → cli-yvy48785.js} +1 -1
  79. package/dist/{cli-9j129brq.js → cli-yxbsmx56.js} +6 -6
  80. package/dist/cli.js +22 -20
  81. package/dist/{codex-adapter-wch12q99.js → codex-adapter-zk59x2zh.js} +7 -7
  82. package/dist/{codex-hook-brpnycn5.js → codex-hook-xyjhvxwd.js} +1 -1
  83. package/dist/{codex-login-rn7c5gxt.js → codex-login-8cg0vhf2.js} +1 -1
  84. package/dist/{codex-session-runner-41pfcr95.js → codex-session-runner-kh6s4j3n.js} +8 -8
  85. package/dist/{commands-9849dkj6.js → commands-68et1qv2.js} +2 -2
  86. package/dist/{controller-ja0sd95p.js → controller-fyrbnzg4.js} +4 -4
  87. package/dist/{controllers-connect-xp05dj6x.js → controllers-connect-nep9h2xv.js} +23 -23
  88. package/dist/{cursor-adapter-623qxqew.js → cursor-adapter-yss3pg7t.js} +2 -2
  89. package/dist/{db-w759qmx0.js → db-28gjyvxj.js} +4 -2
  90. package/dist/{db-47vj40dy.js → db-8ftwvzba.js} +3 -3
  91. package/dist/{devin-adapter-4ycy4vrf.js → devin-adapter-byen2cxt.js} +1 -1
  92. package/dist/{dispatcher-7b8e4z2j.js → dispatcher-hayexmkr.js} +20 -20
  93. package/dist/{docker-s84hjs91.js → docker-fdzyewpz.js} +2 -2
  94. package/dist/{dsh-adapter-2ezw3xea.js → dsh-adapter-46wawff9.js} +2 -2
  95. package/dist/{e2b-9gkdhm3c.js → e2b-222hsajc.js} +2 -2
  96. package/dist/{e2b-cjtf8k1z.js → e2b-fn3zacvt.js} +2 -2
  97. package/dist/{easter-egg-3d-e4qv8dxj.js → easter-egg-3d-8d1e0h4m.js} +1 -1
  98. package/dist/{event-bus-3z78ep5n.js → event-bus-pghg8hqy.js} +1 -1
  99. package/dist/{execute-0xpz8950.js → execute-cjrfxfnm.js} +1 -1
  100. package/dist/{external-channel-e2s028da.js → external-channel-xbmxcg09.js} +2 -2
  101. package/dist/{grok-adapter-ys7gaqee.js → grok-adapter-ag4wks2a.js} +10 -10
  102. package/dist/handlers-tzmevqb5.js +41 -0
  103. package/dist/{heartbeat-w3mntghj.js → heartbeat-psfvcgwp.js} +23 -23
  104. package/dist/{hook-g7kvv6x9.js → hook-8pz04697.js} +6 -6
  105. package/dist/{hook-3jjywnpb.js → hook-cyq4mryn.js} +8 -8
  106. package/dist/{http-tkp7d4kb.js → http-haj1nd6k.js} +921 -169
  107. package/dist/{index-q7m26kvs.js → index-jwt2bj6f.js} +265 -97
  108. package/dist/{index-3n8m9ea2.js → index-q52f47pe.js} +1 -1
  109. package/dist/{index-31ceyf42.js → index-syvhnvtw.js} +1 -1
  110. package/dist/index-y1ajx04a.js +43 -0
  111. package/dist/{index-1ef1myq2.js → index-z5h22pes.js} +20 -20
  112. package/dist/{index-xdaahthb.js → index-zyq8ct52.js} +20 -20
  113. package/dist/{keepalive-v4ta8wgg.js → keepalive-xdvsamvj.js} +4 -4
  114. package/dist/lead-hqg57kdp.js +79 -0
  115. package/dist/{maintenance-84n1ex21.js → maintenance-4y2xzh3a.js} +1 -1
  116. package/dist/{oauth-refresh-sweep-vjnnandw.js → oauth-refresh-sweep-jd4r7zbs.js} +9 -4
  117. package/dist/{onboard-sfmpe4np.js → onboard-18447q9q.js} +6 -6
  118. package/dist/{opencode-adapter-jmpst4q4.js → opencode-adapter-26b2frdf.js} +7 -7
  119. package/dist/{otel-impl-v6cc0zve.js → otel-impl-f9tgr8yw.js} +2 -2
  120. package/dist/{pi-mono-adapter-fk8fkcf6.js → pi-mono-adapter-qn6qbga1.js} +38 -17
  121. package/dist/{pricing-refresh-fsk1e1zc.js → pricing-refresh-dw2tgb53.js} +3 -3
  122. package/dist/{rbac-roles-prmkdnkw.js → rbac-roles-aygm2760.js} +2 -2
  123. package/dist/{rbac-roles-zxmxqa39.js → rbac-roles-s4nwj2m3.js} +1 -1
  124. package/dist/render-v2-yfvdfr0x.js +16 -0
  125. package/dist/{resume-frdtgfa0.js → resume-1bmvjbf3.js} +9 -9
  126. package/dist/{runtime-bt5pkpm1.js → runtime-f7tpzedy.js} +1 -1
  127. package/dist/{session-access-fpez4y5q.js → session-access-r5garej6.js} +4 -4
  128. package/dist/session-source-backfill-424wzv0r.js +110 -0
  129. package/dist/{setup-mq6m3m8q.js → setup-8tygvxkq.js} +1 -1
  130. package/dist/{virtual-modules-mnfbh5mr.js → virtual-modules-xsw323b3.js} +2 -2
  131. package/dist/{work-objects-kfahhjsq.js → work-objects-ggnghpgf.js} +23 -23
  132. package/dist/worker-sqjd1nqf.js +79 -0
  133. package/dist/{x-pbpezr0s.js → x-wxrzvzx8.js} +2 -2
  134. package/openapi.json +1324 -14
  135. package/package.json +5 -3
  136. package/src/assets/source-key.ts +95 -0
  137. package/src/be/automation-preflight.ts +2 -0
  138. package/src/be/db/api-keys.ts +89 -0
  139. package/src/be/db/session-labels.ts +28 -0
  140. package/src/be/db/session-moves.ts +5 -6
  141. package/src/be/db/session-tree.ts +153 -14
  142. package/src/be/db/tasks/read.ts +11 -0
  143. package/src/be/db/tasks/write.ts +6 -1
  144. package/src/be/db/workflow-runs.ts +219 -6
  145. package/src/be/db-retention.ts +61 -6
  146. package/src/be/db.ts +135 -132
  147. package/src/be/memory/providers/openai-embedding.ts +5 -1
  148. package/src/be/memory/task-learning.ts +155 -0
  149. package/src/be/migrations/244_api_key_validation.sql +4 -0
  150. package/src/be/migrations/245_script_executor.sql +3 -0
  151. package/src/be/oauth-refresh-sweep.ts +2 -1
  152. package/src/be/onboarding.ts +4 -0
  153. package/src/be/scripts/boot-reembed.ts +2 -0
  154. package/src/be/scripts/db.ts +14 -4
  155. package/src/be/scripts/embeddings.ts +2 -0
  156. package/src/be/scripts/run-saved.ts +1 -0
  157. package/src/be/scripts/typecheck.ts +10 -4
  158. package/src/be/seed-skills/index.ts +7 -0
  159. package/src/be/session-source-backfill.ts +163 -0
  160. package/src/be/swarm-config-guard.ts +1 -0
  161. package/src/comb/review-batch.ts +28 -1
  162. package/src/comb/templates.ts +7 -2
  163. package/src/commands/claude-managed-setup.ts +7 -1
  164. package/src/commands/credential-refresh.ts +76 -1
  165. package/src/commands/credential-wait.ts +2 -2
  166. package/src/commands/onboard/compose-generator.ts +5 -3
  167. package/src/commands/provider-credentials.ts +212 -171
  168. package/src/commands/runner.ts +25 -4
  169. package/src/extensions/apply-task-create.ts +8 -1
  170. package/src/extensions/contract-types.generated.ts +1 -1
  171. package/src/extensions/contract.ts +5 -0
  172. package/src/heartbeat/heartbeat.ts +14 -1
  173. package/src/heartbeat/script-runner-probe.ts +47 -0
  174. package/src/http/all-routes.ts +2 -0
  175. package/src/http/api-keys.ts +77 -9
  176. package/src/http/assets.ts +15 -0
  177. package/src/http/codex-oauth-accounts.ts +191 -0
  178. package/src/http/comb.ts +15 -2
  179. package/src/http/config.ts +1 -1
  180. package/src/http/core.ts +6 -0
  181. package/src/http/fleet-benchmark.ts +5 -2
  182. package/src/http/idempotency-key.ts +1 -1
  183. package/src/http/index.ts +14 -3
  184. package/src/http/integrations.ts +5 -1
  185. package/src/http/kv.ts +8 -3
  186. package/src/http/onboarding.ts +6 -1
  187. package/src/http/pages-public.ts +38 -5
  188. package/src/http/pool-keys.ts +354 -0
  189. package/src/http/scripts.ts +27 -0
  190. package/src/http/sessions.ts +37 -12
  191. package/src/http/stats.ts +1 -0
  192. package/src/http/status.ts +26 -12
  193. package/src/http/tasks.ts +3 -0
  194. package/src/http/used-by.ts +7 -2
  195. package/src/http/workflows.ts +88 -9
  196. package/src/http/x.ts +5 -0
  197. package/src/integrations/chat-sdk/inbound.ts +27 -0
  198. package/src/kv-reserved-namespaces.ts +19 -0
  199. package/src/prompts/base-prompt.ts +21 -1
  200. package/src/prompts/session-templates.ts +17 -3
  201. package/src/providers/acp-targets.ts +1 -1
  202. package/src/providers/amp-adapter.ts +1 -1
  203. package/src/providers/claude-adapter.ts +5 -1
  204. package/src/providers/claude-managed-adapter.ts +16 -9
  205. package/src/providers/codex-adapter.ts +71 -11
  206. package/src/providers/dsh-adapter.ts +1 -1
  207. package/src/providers/pi-mono-adapter.ts +41 -11
  208. package/src/scheduler/scheduler.ts +5 -3
  209. package/src/scripts-runtime/eval-harness.ts +8 -1
  210. package/src/scripts-runtime/executors/native.ts +22 -16
  211. package/src/scripts-runtime/executors/quickjs-prelude.ts +103 -1
  212. package/src/scripts-runtime/executors/quickjs-runner.ts +2 -1
  213. package/src/scripts-runtime/executors/quickjs.ts +7 -0
  214. package/src/scripts-runtime/executors/registry.ts +32 -0
  215. package/src/scripts-runtime/executors/types.ts +7 -0
  216. package/src/scripts-runtime/host-validated-args.ts +133 -0
  217. package/src/scripts-runtime/loader.ts +30 -3
  218. package/src/scripts-runtime/swarm-sdk.ts +16 -3
  219. package/src/scripts-runtime/types/stdlib.d.ts +10 -2
  220. package/src/scripts-runtime/types/swarm-extension.d.ts +17 -8
  221. package/src/scripts-runtime/types/swarm-sdk.d.ts +10 -2
  222. package/src/slack/assistant.ts +7 -0
  223. package/src/slack/channel-labels.ts +74 -0
  224. package/src/slack/handlers.ts +8 -0
  225. package/src/slack/inbound-files.ts +5 -2
  226. package/src/slack/thread-buffer.ts +4 -0
  227. package/src/tasks/sibling-awareness.ts +12 -3
  228. package/src/telemetry-context.ts +5 -6
  229. package/src/telemetry-identity.ts +3 -3
  230. package/src/tests/api-key-status-by-suffix.test.ts +242 -0
  231. package/src/tests/api-keys-validation.test.ts +170 -0
  232. package/src/tests/base-prompt.test.ts +70 -2
  233. package/src/tests/claude-managed-adapter.test.ts +46 -0
  234. package/src/tests/claude-routes-e2e.test.ts +7 -2
  235. package/src/tests/codex-credential-pools.test.ts +120 -0
  236. package/src/tests/codex-gateway-route.test.ts +165 -0
  237. package/src/tests/codex-oauth-accounts.test.ts +357 -0
  238. package/src/tests/comb-review-batch.test.ts +63 -2
  239. package/src/tests/community-linear-delivery-loop.test.ts +2099 -0
  240. package/src/tests/config-pool-keys.test.ts +533 -0
  241. package/src/tests/credential-check.test.ts +294 -4
  242. package/src/tests/credential-refresh.test.ts +172 -1
  243. package/src/tests/db-retention.test.ts +9 -3
  244. package/src/tests/db-task-read-contract.test.ts +1 -0
  245. package/src/tests/db-task-write-contract.test.ts +1 -3
  246. package/src/tests/docs-index-footprint.test.js +3 -3
  247. package/src/tests/docs-index.test.ts +135 -0
  248. package/src/tests/dsh-adapter.test.ts +1 -1
  249. package/src/tests/extensions-pre-task.test.ts +53 -1
  250. package/src/tests/fetch-resolved-env.test.ts +16 -2
  251. package/src/tests/fixtures/extensions/record-task-create/hooks.ts +9 -0
  252. package/src/tests/fixtures/extensions/record-task-create/manifest.json +7 -0
  253. package/src/tests/fleet-benchmark.test.ts +8 -0
  254. package/src/tests/get-workflow-run-tool.test.ts +98 -0
  255. package/src/tests/heartbeat-checklist.test.ts +89 -0
  256. package/src/tests/heartbeat-script-runner-probe.test.ts +83 -0
  257. package/src/tests/integrations-http.test.ts +32 -2
  258. package/src/tests/internal-ai/complete-structured.test.ts +37 -1
  259. package/src/tests/internal-ai/credentials.test.ts +23 -0
  260. package/src/tests/kv-http.test.ts +32 -0
  261. package/src/tests/kv-tool.test.ts +25 -0
  262. package/src/tests/multi-runtime-registration.test.ts +12 -12
  263. package/src/tests/oauth-refresh-sweep.test.ts +34 -1
  264. package/src/tests/onboard-compose.test.ts +20 -0
  265. package/src/tests/onboarding.test.ts +108 -1
  266. package/src/tests/openai-embedding-credentials.test.ts +35 -0
  267. package/src/tests/opencode-adapter.test.ts +2 -1
  268. package/src/tests/openrouter-attribution-call-sites.test.ts +2 -2
  269. package/src/tests/org-website.test.ts +36 -0
  270. package/src/tests/pages-frame-ancestors.test.ts +162 -0
  271. package/src/tests/pi-codemode.test.ts +2 -1
  272. package/src/tests/pi-mono-adapter.test.ts +54 -0
  273. package/src/tests/pi-native-mcp.test.ts +13 -1
  274. package/src/tests/pi-tool-exposure.test.ts +8 -8
  275. package/src/tests/probe-tool-schema-tokens.test.ts +144 -0
  276. package/src/tests/prompt-template-session.test.ts +4 -3
  277. package/src/tests/provider-key-probes.test.ts +181 -0
  278. package/src/tests/rbac-e2e-helpers.test.ts +88 -1
  279. package/src/tests/rbac-e2e-helpers.ts +36 -11
  280. package/src/tests/request-composition-derive.test.ts +329 -0
  281. package/src/tests/script-connections.test.ts +4 -0
  282. package/src/tests/script-executor-conformance.test.ts +36 -1
  283. package/src/tests/script-executor-quickjs-pool.test.ts +50 -0
  284. package/src/tests/script-executor-quickjs.test.ts +140 -0
  285. package/src/tests/script-host-validated-args-native.test.ts +196 -0
  286. package/src/tests/script-per-script-executor.test.ts +415 -0
  287. package/src/tests/script-runs-retention.test.ts +212 -0
  288. package/src/tests/scripts-external-api.test.ts +45 -0
  289. package/src/tests/sdk-allowlist.test.ts +42 -2
  290. package/src/tests/self-improvement.test.ts +4 -4
  291. package/src/tests/session-source-backfill.test.ts +429 -0
  292. package/src/tests/session-source-keys.test.ts +236 -0
  293. package/src/tests/session-source-labels.test.ts +419 -0
  294. package/src/tests/sessions-detail-batch.test.ts +2 -0
  295. package/src/tests/sessions-move.test.ts +131 -0
  296. package/src/tests/sessions-tree.test.ts +299 -2
  297. package/src/tests/slack-inbound-files.test.ts +103 -23
  298. package/src/tests/status.test.ts +101 -18
  299. package/src/tests/store-progress-learning.test.ts +288 -0
  300. package/src/tests/tasks-workflow-run-filter.test.ts +146 -0
  301. package/src/tests/test-net.ts +31 -0
  302. package/src/tests/transcription.test.ts +8 -0
  303. package/src/tests/ui-logs-parser.test.ts +180 -0
  304. package/src/tests/used-by-route.test.ts +11 -0
  305. package/src/tests/workflow-cascade-cancel.test.ts +172 -0
  306. package/src/tests/workflow-engine-v2.test.ts +116 -1
  307. package/src/tests/workflow-http-v2.test.ts +131 -0
  308. package/src/tests/workflow-mcp-trigger-schema.test.ts +209 -0
  309. package/src/tests/workflow-run-finished-event.test.ts +263 -0
  310. package/src/tests/workflow-step-order.test.ts +92 -0
  311. package/src/tests/workflow-sub-workflow.test.ts +30 -0
  312. package/src/tests/workflow-swarm-script.test.ts +34 -0
  313. package/src/tools/get-tasks.ts +8 -0
  314. package/src/tools/kv/kv-read-auth.ts +4 -2
  315. package/src/tools/script-common.ts +1 -0
  316. package/src/tools/script-run.ts +7 -0
  317. package/src/tools/script-upsert.ts +7 -0
  318. package/src/tools/store-progress.ts +67 -1
  319. package/src/tools/task-action.ts +1 -1
  320. package/src/tools/workflows/get-workflow-run.ts +14 -4
  321. package/src/tools/workflows/trigger-workflow.ts +104 -10
  322. package/src/types.ts +36 -1
  323. package/src/utils/company-profile.ts +51 -1
  324. package/src/utils/credentials.ts +10 -4
  325. package/src/utils/internal-ai/complete-structured.ts +5 -0
  326. package/src/utils/internal-ai/credentials.ts +4 -3
  327. package/src/utils/provider-key-probes.ts +328 -0
  328. package/src/utils/transcription.ts +4 -1
  329. package/src/workflows/engine.ts +18 -16
  330. package/src/workflows/executors/sub-workflow.ts +8 -1
  331. package/src/workflows/executors/swarm-script.ts +5 -0
  332. package/src/workflows/foreach-driver.ts +5 -11
  333. package/src/workflows/node-timeout.ts +30 -0
  334. package/templates/ai-toolbox.manifest.json +2 -2
  335. package/templates/community/linear-delivery-loop/PLAYBOOK.md +323 -0
  336. package/templates/community/linear-delivery-loop/README.md +38 -0
  337. package/templates/community/linear-delivery-loop/config.example.json +31 -0
  338. package/templates/community/linear-delivery-loop/lead-prompt.md +34 -0
  339. package/templates/community/linear-delivery-loop/schedule.json +10 -0
  340. package/templates/community/linear-delivery-loop/scripts/linear-delivery-setup.ts +148 -0
  341. package/templates/community/linear-delivery-loop/scripts/linear-delivery-tick.ts +1781 -0
  342. package/templates/skills/researching/content.md +2 -0
  343. package/templates/skills/swarm-docs/config.json +14 -0
  344. package/templates/skills/swarm-docs/content.md +144 -0
  345. package/templates/skills/swarm-scripts/SKILL.md +1 -0
  346. package/templates/skills/swarm-scripts/content.md +1 -0
  347. package/dist/cli-7421jzsw.js +0 -84
  348. package/dist/cli-e45e4m6b.js +0 -413
  349. package/dist/cli-zw88zjba.js +0 -307
  350. package/dist/handlers-wy1dr6ja.js +0 -41
  351. package/dist/index-nmm79qgr.js +0 -43
  352. package/dist/lead-sawnyqxk.js +0 -79
  353. package/dist/render-v2-pjwxvnkw.js +0 -12
  354. package/dist/worker-v5e5rxj3.js +0 -79
@@ -13,7 +13,7 @@
13
13
  import { getDbClient } from "./be/db/runtime";
14
14
  import { setTelemetryOrgDomain } from "./telemetry";
15
15
  import { emailDomain, type TelemetryUserRole } from "./telemetry-context";
16
- import { USER_FUNCTIONS } from "./utils/company-profile";
16
+ import { reportedUserFunction } from "./utils/company-profile";
17
17
 
18
18
  const ROLE_TTL_MS = 60 * 60_000;
19
19
  const ROLE_CACHE_LIMIT = 1_000;
@@ -21,7 +21,7 @@ const DOMAIN_RECOMPUTE_DEBOUNCE_MS = 2_000;
21
21
 
22
22
  interface UserRoleEntry {
23
23
  role: TelemetryUserRole;
24
- userFunction?: (typeof USER_FUNCTIONS)[number];
24
+ userFunction?: ReturnType<typeof reportedUserFunction>;
25
25
  at: number;
26
26
  }
27
27
 
@@ -40,7 +40,7 @@ async function loadUserRole(userId: string): Promise<UserRoleEntry | undefined>
40
40
  );
41
41
  return {
42
42
  role,
43
- userFunction: USER_FUNCTIONS.find((value) => value === user?.jobFunction),
43
+ userFunction: reportedUserFunction(user?.jobFunction),
44
44
  at: Date.now(),
45
45
  };
46
46
  } catch {
@@ -0,0 +1,242 @@
1
+ /**
2
+ * Key status is matched by key suffix when the caller sends its pool
3
+ * (`GET /api/keys/available?keySuffixes=`), so two agent-scoped pools with
4
+ * different keys at the same index do not bench each other (issue #2425).
5
+ */
6
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
7
+ import { unlink } from "node:fs/promises";
8
+ import { createServer as createHttpServer, type Server } from "node:http";
9
+ import {
10
+ closeDb,
11
+ getAvailableKeyIndices,
12
+ initDb,
13
+ markKeyRateLimited,
14
+ recordKeyRateLimitWindows,
15
+ } from "../be/db";
16
+ import { recordKeyValidation } from "../be/db/api-keys";
17
+ import { handleApiKeys } from "../http/api-keys";
18
+ import { resolveCredentialPools } from "../utils/credentials";
19
+ import { listenOnFreePort } from "./test-net";
20
+
21
+ const TEST_DB = `./test-api-key-status-by-suffix-${Date.now()}.sqlite`;
22
+ const FUTURE = () => new Date(Date.now() + 3_600_000).toISOString();
23
+
24
+ // Agent A and agent B each pool two keys; index 1 differs between them.
25
+ const POOL_A = ["example-key-shared", "example-key-aaaa1"];
26
+ const POOL_B = ["example-key-shared", "example-key-bbbb2"];
27
+ const suffixes = (pool: string[]) => pool.map((key) => key.slice(-5));
28
+
29
+ describe("key status by suffix", () => {
30
+ let server: Server;
31
+ let baseUrl = "";
32
+
33
+ const getAvailable = async (query: string) => {
34
+ const res = await fetch(`${baseUrl}/api/keys/available?${query}`);
35
+ return { status: res.status, body: (await res.json()) as Record<string, unknown> };
36
+ };
37
+
38
+ beforeAll(async () => {
39
+ process.env.DB_PATH = TEST_DB;
40
+ initDb(TEST_DB);
41
+ server = createHttpServer(async (req, res) => {
42
+ const url = new URL(req.url || "/", "http://localhost");
43
+ const pathSegments = url.pathname.split("/").filter(Boolean);
44
+ const handled = await handleApiKeys(req, res, pathSegments, url.searchParams);
45
+ if (!handled) {
46
+ res.writeHead(404);
47
+ res.end();
48
+ }
49
+ });
50
+ const port = await listenOnFreePort(server);
51
+ baseUrl = `http://127.0.0.1:${port}`;
52
+ });
53
+
54
+ afterAll(async () => {
55
+ await new Promise<void>((resolve) => server.close(() => resolve()));
56
+ closeDb();
57
+ await unlink(TEST_DB).catch(() => {});
58
+ await unlink(`${TEST_DB}-wal`).catch(() => {});
59
+ await unlink(`${TEST_DB}-shm`).catch(() => {});
60
+ });
61
+
62
+ test("a rate limit on agent A's key at index 1 does not bench agent B's key at index 1", async () => {
63
+ // Workers report at global scope, by suffix and index.
64
+ await markKeyRateLimited("OPENAI_API_KEY", "aaaa1", 1, FUTURE());
65
+
66
+ const a = await getAvailableKeyIndices("OPENAI_API_KEY", 2, "global", null, undefined, [
67
+ ...suffixes(POOL_A),
68
+ ]);
69
+ const b = await getAvailableKeyIndices("OPENAI_API_KEY", 2, "global", null, undefined, [
70
+ ...suffixes(POOL_B),
71
+ ]);
72
+ expect(a.availableIndices).toEqual([0]);
73
+ expect(b.availableIndices).toEqual([0, 1]);
74
+
75
+ // Control: the index read (no suffixes) still benches index 1 for everyone.
76
+ const legacy = await getAvailableKeyIndices("OPENAI_API_KEY", 2);
77
+ expect(legacy.availableIndices).toEqual([0]);
78
+ });
79
+
80
+ test("a rejection on agent A's key does not bench agent B's key at the same index", async () => {
81
+ await recordKeyValidation(
82
+ "GEMINI_API_KEY",
83
+ [{ keyIndex: 1, keySuffix: "aaaa1", ok: false, httpStatus: 401 }],
84
+ "global",
85
+ null,
86
+ { retireAbsent: false },
87
+ );
88
+
89
+ const a = await getAvailable(
90
+ `keyType=GEMINI_API_KEY&totalKeys=2&keySuffixes=${suffixes(POOL_A).join(",")}`,
91
+ );
92
+ const b = await getAvailable(
93
+ `keyType=GEMINI_API_KEY&totalKeys=2&keySuffixes=${suffixes(POOL_B).join(",")}`,
94
+ );
95
+ expect(a.body).toMatchObject({ availableIndices: [0], rejectedIndices: [1] });
96
+ expect(b.body).toMatchObject({ availableIndices: [0, 1], rejectedIndices: [] });
97
+ });
98
+
99
+ test("one key in two scopes has one state, at each pool's own position", async () => {
100
+ // The shared key sits at index 0 for A and B, and at index 2 in a third pool.
101
+ await markKeyRateLimited("ANTHROPIC_API_KEY", "hared", 0, FUTURE());
102
+ // A row of the same key in an agent scope (for example a dashboard check) also counts.
103
+ await recordKeyValidation(
104
+ "ANTHROPIC_API_KEY",
105
+ [{ keyIndex: 0, keySuffix: "hared", ok: false, httpStatus: 401 }],
106
+ "agent",
107
+ "agent-b",
108
+ { retireAbsent: false },
109
+ );
110
+
111
+ const b = await getAvailableKeyIndices("ANTHROPIC_API_KEY", 2, "global", null, undefined, [
112
+ ...suffixes(POOL_B),
113
+ ]);
114
+ expect(b.availableIndices).toEqual([1]);
115
+ expect(b.rejectedIndices).toEqual([0]);
116
+
117
+ const reordered = ["example-key-ccccc", "example-key-ddddd", "example-key-shared"];
118
+ const c = await getAvailableKeyIndices("ANTHROPIC_API_KEY", 3, "global", null, undefined, [
119
+ ...suffixes(reordered),
120
+ ]);
121
+ expect(c.availableIndices).toEqual([0, 1]);
122
+ expect(c.rejectedIndices).toEqual([2]);
123
+ });
124
+
125
+ test("model window blocks map to the caller's positions", async () => {
126
+ const resetsAt = Math.floor(Date.now() / 1000) + 3600;
127
+ await recordKeyRateLimitWindows("CLAUDE_CODE_OAUTH_TOKEN", "aaaa1", 1, {
128
+ seven_day_opus: { status: "rejected", resetsAt, lastSeenAt: new Date().toISOString() },
129
+ });
130
+
131
+ const a = await getAvailableKeyIndices(
132
+ "CLAUDE_CODE_OAUTH_TOKEN",
133
+ 2,
134
+ "global",
135
+ null,
136
+ "opus",
137
+ suffixes(POOL_A),
138
+ );
139
+ const b = await getAvailableKeyIndices(
140
+ "CLAUDE_CODE_OAUTH_TOKEN",
141
+ 2,
142
+ "global",
143
+ null,
144
+ "opus",
145
+ suffixes(POOL_B),
146
+ );
147
+ expect(a.availableIndices).toEqual([0]);
148
+ expect(a.modelBlockedIndices).toEqual([1]);
149
+ expect(b.availableIndices).toEqual([0, 1]);
150
+ expect(b.modelBlockedIndices).toEqual([]);
151
+ });
152
+
153
+ test("two pool keys with the same suffix keep the index read: one bench does not bench both", async () => {
154
+ // a-same1 and b-same1 share one status row; a rate limit at index 0 must leave index 1.
155
+ await markKeyRateLimited("MISTRAL_API_KEY", "same1", 0, FUTURE());
156
+
157
+ const res = await getAvailable("keyType=MISTRAL_API_KEY&totalKeys=2&keySuffixes=same1,same1");
158
+ expect(res.body).toMatchObject({ availableIndices: [1] });
159
+ // Control: the index read gives the same answer.
160
+ const legacy = await getAvailableKeyIndices("MISTRAL_API_KEY", 2);
161
+ expect(legacy.availableIndices).toEqual([1]);
162
+ });
163
+
164
+ test("validation success and a rate-limit clear at global scope reach the key's agent-scoped rows", async () => {
165
+ const post = (path: string, body: unknown) =>
166
+ fetch(`${baseUrl}/api/keys/${path}`, {
167
+ method: "POST",
168
+ headers: { "Content-Type": "application/json" },
169
+ body: JSON.stringify(body),
170
+ });
171
+ const available = () =>
172
+ getAvailable(
173
+ `keyType=DEEPSEEK_API_KEY&totalKeys=2&keySuffixes=${suffixes(POOL_A).join(",")}`,
174
+ );
175
+
176
+ // 401 for aaaa1 at agent scope, then OK at global scope.
177
+ const keys = (ok: boolean) => [
178
+ { keyIndex: 1, keySuffix: "aaaa1", ok, ...(ok ? {} : { httpStatus: 401 }) },
179
+ ];
180
+ expect(
181
+ (
182
+ await post("report-validation", {
183
+ keyType: "DEEPSEEK_API_KEY",
184
+ keys: keys(false),
185
+ scope: "agent",
186
+ scopeId: "agent-a",
187
+ })
188
+ ).status,
189
+ ).toBe(200);
190
+ expect((await available()).body).toMatchObject({ rejectedIndices: [1] });
191
+ expect(
192
+ (await post("report-validation", { keyType: "DEEPSEEK_API_KEY", keys: keys(true) })).status,
193
+ ).toBe(200);
194
+ expect((await available()).body).toMatchObject({
195
+ availableIndices: [0, 1],
196
+ rejectedIndices: [],
197
+ });
198
+
199
+ // A rate limit on the agent-scoped row, cleared by the worker's global-scope recheck.
200
+ await markKeyRateLimited("DEEPSEEK_API_KEY", "aaaa1", 1, FUTURE(), "agent", "agent-a");
201
+ expect((await available()).body).toMatchObject({ availableIndices: [0] });
202
+ const clear = await post("clear-rate-limit", {
203
+ keyType: "DEEPSEEK_API_KEY",
204
+ keySuffix: "aaaa1",
205
+ });
206
+ expect(((await clear.json()) as { cleared: boolean }).cleared).toBe(true);
207
+ expect((await available()).body).toMatchObject({ availableIndices: [0, 1] });
208
+ });
209
+
210
+ test("the route rejects a suffix list that does not match totalKeys", async () => {
211
+ const res = await getAvailable("keyType=OPENAI_API_KEY&totalKeys=3&keySuffixes=hared,aaaa1");
212
+ expect(res.status).toBe(400);
213
+ });
214
+
215
+ test("worker key selection skips its own benched key and keeps the other pool's", async () => {
216
+ await markKeyRateLimited("OPENROUTER_API_KEY", "aaaa1", 1, FUTURE());
217
+ await markKeyRateLimited("OPENROUTER_API_KEY", "hared", 0, FUTURE());
218
+
219
+ for (let i = 0; i < 10; i++) {
220
+ const envA: Record<string, string | undefined> = { OPENROUTER_API_KEY: POOL_A.join(",") };
221
+ const [selA] = await resolveCredentialPools(envA, {
222
+ apiUrl: baseUrl,
223
+ apiKey: "test",
224
+ provider: "opencode",
225
+ model: "google/gemini-3-flash-preview",
226
+ });
227
+ // Both of A's keys are benched: best-effort fallback.
228
+ expect(selA!.isRateLimitFallback).toBe(true);
229
+
230
+ const envB: Record<string, string | undefined> = { OPENROUTER_API_KEY: POOL_B.join(",") };
231
+ const [selB] = await resolveCredentialPools(envB, {
232
+ apiUrl: baseUrl,
233
+ apiKey: "test",
234
+ provider: "opencode",
235
+ model: "google/gemini-3-flash-preview",
236
+ });
237
+ expect(selB!.isRateLimitFallback).toBe(false);
238
+ expect(selB!.index).toBe(1);
239
+ expect(envB.OPENROUTER_API_KEY).toBe("example-key-bbbb2");
240
+ }
241
+ });
242
+ });
@@ -0,0 +1,170 @@
1
+ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
2
+ import { Readable } from "node:stream";
3
+ import {
4
+ closeDb,
5
+ getAvailableKeyIndices,
6
+ getDbClient,
7
+ getKeyStatuses,
8
+ initDb,
9
+ recordKeyValidation,
10
+ } from "../be/db";
11
+ import { handleApiKeys } from "../http/api-keys";
12
+ import { getPathSegments, parseQueryParams } from "../http/utils";
13
+
14
+ const TEST_DB = "./test-api-keys-validation.sqlite";
15
+ const keyType = "ANTHROPIC_API_KEY";
16
+ const rejected = { keyIndex: 0, keySuffix: "aaa11", ok: false, httpStatus: 401 };
17
+
18
+ async function call(method: string, url: string, body?: unknown) {
19
+ const req = Readable.from(body === undefined ? [] : [Buffer.from(JSON.stringify(body))]);
20
+ Object.assign(req, { method, url, headers: { "content-type": "application/json" } });
21
+ let statusCode = 200;
22
+ let responseBody = "";
23
+ const res = {
24
+ setHeader: () => {},
25
+ writeHead: (code: number) => {
26
+ statusCode = code;
27
+ },
28
+ end: (chunk?: string) => {
29
+ responseBody += chunk ?? "";
30
+ },
31
+ };
32
+ const handled = await handleApiKeys(
33
+ req as never,
34
+ res as never,
35
+ getPathSegments(url),
36
+ parseQueryParams(url),
37
+ );
38
+ return { handled, statusCode, body: JSON.parse(responseBody) };
39
+ }
40
+
41
+ describe("pooled key validation", () => {
42
+ beforeAll(() => {
43
+ initDb(TEST_DB);
44
+ });
45
+ beforeEach(async () => {
46
+ await getDbClient().run("DELETE FROM api_key_status");
47
+ });
48
+ afterAll(async () => {
49
+ closeDb();
50
+ for (const suffix of ["", "-wal", "-shm"]) {
51
+ await Bun.file(TEST_DB + suffix)
52
+ .delete()
53
+ .catch(() => {});
54
+ }
55
+ });
56
+
57
+ test("HTTP 401 marks the key as rejected", async () => {
58
+ await recordKeyValidation(keyType, [{ ...rejected, httpStatus: 401 }]);
59
+ const [row] = await getKeyStatuses(keyType);
60
+ expect(row).toMatchObject({
61
+ keyIndex: 0,
62
+ keySuffix: "aaa11",
63
+ provider: "claude",
64
+ lastValidationError: "HTTP 401",
65
+ });
66
+ expect(row!.lastValidationFailedAt).not.toBeNull();
67
+ });
68
+
69
+ test("a pass clears rejection and updates the index", async () => {
70
+ await recordKeyValidation(keyType, [rejected]);
71
+ await recordKeyValidation(keyType, [{ ...rejected, keyIndex: 2, ok: true }]);
72
+ expect((await getKeyStatuses(keyType))[0]).toMatchObject({
73
+ keyIndex: 2,
74
+ lastValidationError: null,
75
+ lastValidationFailedAt: null,
76
+ });
77
+ });
78
+
79
+ // 403 included: a valid key can lack the scope of the list endpoint.
80
+ test.each([
81
+ 403,
82
+ 500,
83
+ 429,
84
+ undefined,
85
+ ])("failure with status %s preserves existing state", async (httpStatus) => {
86
+ await recordKeyValidation(keyType, [rejected]);
87
+ const before = await getKeyStatuses(keyType);
88
+ await recordKeyValidation(keyType, [{ ...rejected, keyIndex: 3, httpStatus }]);
89
+ expect(await getKeyStatuses(keyType)).toEqual(before);
90
+ await recordKeyValidation("OPENAI_API_KEY", [{ ...rejected, httpStatus }]);
91
+ expect(await getKeyStatuses("OPENAI_API_KEY")).toEqual([]);
92
+ });
93
+
94
+ test("a removed suffix cannot block a replacement at the same index", async () => {
95
+ await recordKeyValidation(keyType, [rejected]);
96
+ await recordKeyValidation(keyType, [{ keyIndex: 0, keySuffix: "bbb22", ok: true }]);
97
+ const rows = await getKeyStatuses(keyType);
98
+ expect(rows.find((row) => row.keySuffix === "aaa11")).toMatchObject({
99
+ lastValidationError: null,
100
+ lastValidationFailedAt: null,
101
+ });
102
+ expect((await getAvailableKeyIndices(keyType, 1)).availableIndices).toEqual([0]);
103
+ });
104
+
105
+ test("rejections and removed-suffix cleanup stay within their scope and key type", async () => {
106
+ await recordKeyValidation(keyType, [rejected], "agent", "agent-a");
107
+ await recordKeyValidation(keyType, [rejected], "agent", "agent-b");
108
+ await recordKeyValidation("OPENAI_API_KEY", [rejected], "agent", "agent-a");
109
+ await recordKeyValidation(
110
+ keyType,
111
+ [{ keyIndex: 0, keySuffix: "bbb22", ok: true }],
112
+ "agent",
113
+ "agent-a",
114
+ );
115
+ expect((await getAvailableKeyIndices(keyType, 1, "agent", "agent-a")).availableIndices).toEqual(
116
+ [0],
117
+ );
118
+ expect((await getAvailableKeyIndices(keyType, 1, "agent", "agent-b")).availableIndices).toEqual(
119
+ [],
120
+ );
121
+ expect(
122
+ (await getAvailableKeyIndices("OPENAI_API_KEY", 1, "agent", "agent-a")).availableIndices,
123
+ ).toEqual([]);
124
+ });
125
+
126
+ test("availability excludes rejected keys and reports their indices", async () => {
127
+ await recordKeyValidation(keyType, [rejected, { keyIndex: 1, keySuffix: "bbb22", ok: true }]);
128
+ expect(await getAvailableKeyIndices(keyType, 3)).toMatchObject({
129
+ availableIndices: [1, 2],
130
+ rejectedIndices: [0],
131
+ });
132
+ });
133
+
134
+ test("availability is empty when every key is rejected", async () => {
135
+ await recordKeyValidation(keyType, [
136
+ rejected,
137
+ { ...rejected, keyIndex: 1, keySuffix: "bbb22" },
138
+ ]);
139
+ expect(await getAvailableKeyIndices(keyType, 2)).toMatchObject({
140
+ availableIndices: [],
141
+ rejectedIndices: [0, 1],
142
+ });
143
+ });
144
+
145
+ test("the report handler records validation and exposes it in reads", async () => {
146
+ expect(
147
+ await call("POST", "/api/keys/report-validation", { keyType, keys: [rejected] }),
148
+ ).toEqual({
149
+ handled: true,
150
+ statusCode: 200,
151
+ body: { success: true, message: "Key validation recorded" },
152
+ });
153
+ expect(
154
+ (await call("GET", `/api/keys/available?keyType=${keyType}&totalKeys=2`)).body,
155
+ ).toMatchObject({
156
+ availableIndices: [1],
157
+ rejectedIndices: [0],
158
+ });
159
+ const status = await call("GET", `/api/keys/status?keyType=${keyType}`);
160
+ expect(status.body.keys[0]).toMatchObject({ lastValidationError: "HTTP 401" });
161
+ expect(status.body.keys[0].lastValidationFailedAt).not.toBeNull();
162
+ });
163
+
164
+ test("the report handler rejects an empty pool", async () => {
165
+ expect(
166
+ (await call("POST", "/api/keys/report-validation", { keyType, keys: [] })).statusCode,
167
+ ).toBe(400);
168
+ expect(await getKeyStatuses(keyType)).toEqual([]);
169
+ });
170
+ });
@@ -787,12 +787,78 @@ describe("getBasePrompt: tools and skills section", () => {
787
787
  );
788
788
  });
789
789
 
790
+ test("pi with deferral points installed MCP servers at tool search", async () => {
791
+ const result = await getBasePrompt({
792
+ ...minimalArgs,
793
+ provider: "pi",
794
+ traits: { ...localTraits, hasToolSearch: true },
795
+ mcpServers: ["linear"],
796
+ });
797
+ expect(result).toContain(
798
+ "Connected MCP servers: linear. Load their tools with your harness tool search.",
799
+ );
800
+ expect(result).not.toContain("Their tools are in your tool list.");
801
+ });
802
+
803
+ test("pi without deferral keeps installed MCP servers in the tool list", async () => {
804
+ const result = await getBasePrompt({
805
+ ...minimalArgs,
806
+ provider: "pi",
807
+ traits: { ...localTraits, hasToolSearch: false },
808
+ mcpServers: ["linear"],
809
+ });
810
+ expect(result).toContain("Connected MCP servers: linear. Their tools are in your tool list.");
811
+ });
812
+
790
813
  test("omits the MCP server line for an empty list", async () => {
791
814
  const result = await getBasePrompt({ ...minimalArgs, mcpServers: [] });
792
815
  expect(result).not.toContain("Connected MCP servers");
793
816
  });
794
817
  });
795
818
 
819
+ describe("getBasePrompt: lead docs pointer", () => {
820
+ const DOCS_LINE =
821
+ "Load the `swarm-docs` skill only when a person asks for something no skill, workflow, schedule or memory of yours covers, or asks where something is documented. Then follow its offer rules.";
822
+ const withDocs = [
823
+ { name: "commit", description: "Create a commit" },
824
+ { name: "swarm-docs", description: "docs index" },
825
+ ];
826
+
827
+ test("renders for a lead that has the swarm-docs skill", async () => {
828
+ const result = await getBasePrompt({
829
+ ...minimalArgs,
830
+ role: "lead",
831
+ traits: localTraits,
832
+ skillsSummary: withDocs,
833
+ });
834
+ expect(result).toContain(DOCS_LINE);
835
+ });
836
+
837
+ test("drops the line when swarm-docs is disabled for the lead", async () => {
838
+ const result = await getBasePrompt({
839
+ ...minimalArgs,
840
+ role: "lead",
841
+ traits: localTraits,
842
+ skillsSummary: [{ name: "commit", description: "Create a commit" }],
843
+ });
844
+ expect(result).not.toContain(DOCS_LINE);
845
+ });
846
+
847
+ test("renders when the skill list is unknown (claude-managed)", async () => {
848
+ const result = await getBasePrompt({ ...minimalArgs, role: "lead", traits: managedTraits });
849
+ expect(result).toContain(DOCS_LINE);
850
+ });
851
+
852
+ test("never renders for a worker", async () => {
853
+ const result = await getBasePrompt({
854
+ ...minimalArgs,
855
+ traits: localTraits,
856
+ skillsSummary: withDocs,
857
+ });
858
+ expect(result).not.toContain(DOCS_LINE);
859
+ });
860
+ });
861
+
796
862
  // ---------------------------------------------------------------------------
797
863
  // 10. Repository
798
864
  // ---------------------------------------------------------------------------
@@ -1015,14 +1081,16 @@ describe("getBasePrompt: size budget", () => {
1015
1081
  expect(result.length).toBeLessThan(5_750);
1016
1082
  });
1017
1083
 
1018
- test("a fresh claude lead stays under 5,900 characters", async () => {
1084
+ // The lead ceiling includes the swarm-docs pointer (~190 chars), which renders
1085
+ // while the skill list is still unknown.
1086
+ test("a fresh claude lead stays under 6,100 characters", async () => {
1019
1087
  const result = await getBasePrompt({
1020
1088
  ...minimalArgs,
1021
1089
  role: "lead",
1022
1090
  name: "Cora",
1023
1091
  traits: localTraits,
1024
1092
  });
1025
- expect(result.length).toBeLessThan(5_900);
1093
+ expect(result.length).toBeLessThan(6_100);
1026
1094
  });
1027
1095
 
1028
1096
  test("Picateclas spawn-OOM hardening: the kitchen sink stays below MAX_ARG_STRLEN", async () => {
@@ -59,6 +59,52 @@ describe("ClaudeManagedAdapter (Phase 1 skeleton)", () => {
59
59
  expect(adapter.formatCommand("plan")).toBe("/plan");
60
60
  });
61
61
 
62
+ for (const source of ["container", "task", "task-pool"] as const) {
63
+ test(`uses one Anthropic key from ${source}`, async () => {
64
+ const originalFetch = globalThis.fetch;
65
+ const originalKey = process.env.ANTHROPIC_API_KEY;
66
+ let apiKey: string | null = null;
67
+ process.env.ANTHROPIC_API_KEY = "example-container-first,example-container-second";
68
+ globalThis.fetch = (async (input, init) => {
69
+ apiKey =
70
+ new Request(input, init).headers.get("authorization")?.slice("Bearer ".length) ?? null;
71
+ return Response.json(
72
+ { error: { type: "authentication_error", message: "test rejection" } },
73
+ { status: 401 },
74
+ );
75
+ }) as typeof fetch;
76
+ try {
77
+ const adapter = new ClaudeManagedAdapter();
78
+ await expect(
79
+ adapter.createSession(
80
+ tConfig({
81
+ env:
82
+ source === "container"
83
+ ? {}
84
+ : {
85
+ ANTHROPIC_API_KEY:
86
+ source === "task"
87
+ ? "example-task"
88
+ : "example-task-first,example-task-second",
89
+ },
90
+ }),
91
+ ),
92
+ ).rejects.toThrow("test rejection");
93
+ expect(
94
+ source === "container"
95
+ ? ["example-container-first", "example-container-second"]
96
+ : source === "task"
97
+ ? ["example-task"]
98
+ : ["example-task-first", "example-task-second"],
99
+ ).toContain(apiKey);
100
+ } finally {
101
+ globalThis.fetch = originalFetch;
102
+ if (originalKey === undefined) delete process.env.ANTHROPIC_API_KEY;
103
+ else process.env.ANTHROPIC_API_KEY = originalKey;
104
+ }
105
+ });
106
+ }
107
+
62
108
  test("ctor throws when MANAGED_AGENT_ID is missing", () => {
63
109
  const saved = process.env.MANAGED_AGENT_ID;
64
110
  delete process.env.MANAGED_AGENT_ID;
@@ -10,10 +10,10 @@
10
10
  * auth redacted.
11
11
  */
12
12
  import { afterAll, beforeAll, describe, expect, test } from "bun:test";
13
- import { existsSync, mkdtempSync, rmSync } from "node:fs";
13
+ import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
14
14
  import { createRequire } from "node:module";
15
15
  import { tmpdir } from "node:os";
16
- import { dirname, join } from "node:path";
16
+ import { basename, dirname, join } from "node:path";
17
17
  import { deriveDefaultRoute } from "@desplega/model-routing";
18
18
  import { closeDb, createAgent, getAgentById, initDb, updateAgentCredentialState } from "../be/db";
19
19
  import { awaitCredentials } from "../commands/credential-wait";
@@ -240,6 +240,11 @@ describe.skipIf(!CLAUDE_BINARY)("claude routes e2e (real CLI, fake endpoints)",
240
240
 
241
241
  expect(result).toMatchObject({ isError: false, exitCode: 0 });
242
242
  expect(result.output).toContain(REPLY);
243
+ // Trust lands in the session's HOME, never in the host's ~/.claude.json.
244
+ const claudeJson = JSON.parse(readFileSync(join(env.HOME ?? "", ".claude.json"), "utf8"));
245
+ expect(
246
+ Object.keys(claudeJson.projects ?? {}).some((dir) => dir.endsWith(basename(cwd))),
247
+ ).toBe(true);
243
248
 
244
249
  const turn = fake.requests.find(
245
250
  (req) => req.method === "POST" && route.turnPath.test(req.path),