@desplega.ai/agent-swarm 2.5.3 → 2.6.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 (349) hide show
  1. package/README.md +2 -2
  2. package/dist/{acp-adapter-gr2fcyyj.js → acp-adapter-dgfakfpw.js} +9 -9
  3. package/dist/{actions-wv766ghf.js → actions-fbqma8aa.js} +21 -21
  4. package/dist/{amp-adapter-speb906t.js → amp-adapter-en0jy6hs.js} +4 -4
  5. package/dist/{app-ag29n14f.js → app-k8a5cgpp.js} +3 -3
  6. package/dist/{assistant-3x8bz3eg.js → assistant-a16433gr.js} +30 -26
  7. package/dist/{boot-reembed-jsmfrpzf.js → boot-reembed-6c1sjfnr.js} +1 -1
  8. package/dist/{boot-reembed-5b3d2eea.js → boot-reembed-f4nn561n.js} +1 -1
  9. package/dist/{boot-scrub-sweep-s73gerxy.js → boot-scrub-sweep-d4w9gajf.js} +4 -4
  10. package/dist/{boot-seeds-5zmax2wr.js → boot-seeds-9bmkspyw.js} +4 -4
  11. package/dist/{claude-adapter-hswdnm3t.js → claude-adapter-nys1kjxa.js} +9 -9
  12. package/dist/{claude-managed-adapter-cvbzj1h6.js → claude-managed-adapter-55207973.js} +3 -3
  13. package/dist/{claude-managed-setup-3k8m3hhc.js → claude-managed-setup-3cm35gey.js} +11 -4
  14. package/dist/{claude-sdk-session-fkbyk66x.js → claude-sdk-session-nw5yd8cp.js} +9313 -9260
  15. package/dist/{cli-210xd1f4.js → cli-03gg9pd1.js} +4 -4
  16. package/dist/{cli-kxpfx2xr.js → cli-0z1xjzwa.js} +2 -2
  17. package/dist/cli-1tem6ns9.js +894 -0
  18. package/dist/{cli-25j01dpj.js → cli-24j1jeeb.js} +3 -3
  19. package/dist/{cli-ta8bd7th.js → cli-2sjhpsns.js} +2 -2
  20. package/dist/{cli-p859hcc8.js → cli-39aet655.js} +2 -2
  21. package/dist/{cli-g1cw7xtt.js → cli-39rrw8yp.js} +1 -1
  22. package/dist/{cli-e2ar4bp1.js → cli-3aqd4jaa.js} +121 -47
  23. package/dist/{cli-jjznk3f7.js → cli-3fwqy71j.js} +69 -35
  24. package/dist/{cli-p9rafdrb.js → cli-3gfvmzht.js} +7 -7
  25. package/dist/{cli-03vpq45j.js → cli-3ndmpxxv.js} +2 -2
  26. package/dist/{cli-aecdcr77.js → cli-4hxrf0p4.js} +228 -74
  27. package/dist/{cli-mz9afxxf.js → cli-4rf0f5vg.js} +15 -3
  28. package/dist/{cli-qzme5949.js → cli-5rj83bam.js} +284 -202
  29. package/dist/{cli-fsdprwrg.js → cli-5sckpqcx.js} +2 -2
  30. package/dist/{cli-cz5nmdxt.js → cli-683y0b3a.js} +2 -2
  31. package/dist/{cli-3ngdk5te.js → cli-6y51ryva.js} +2 -2
  32. package/dist/{cli-n4c8e3s5.js → cli-708wdy32.js} +1 -1
  33. package/dist/{cli-pasyvjmm.js → cli-737pyqxr.js} +1 -1
  34. package/dist/{cli-gx4rm0qx.js → cli-7enjwya9.js} +1 -1
  35. package/dist/{cli-6x9ma5w5.js → cli-803188pb.js} +1 -1
  36. package/dist/{cli-avdvgcay.js → cli-9abdgn5t.js} +28 -9
  37. package/dist/{cli-99xgw26r.js → cli-a2rr0fmg.js} +1 -1
  38. package/dist/{cli-e5bxnjps.js → cli-a34pr3w1.js} +7 -7
  39. package/dist/{cli-502eqv93.js → cli-abkm71vr.js} +1 -1
  40. package/dist/{cli-mmhj8vd7.js → cli-ag2yrnt5.js} +11 -2
  41. package/dist/{cli-n45mr7zh.js → cli-aj8djyqt.js} +5 -5
  42. package/dist/{cli-1fc85mvt.js → cli-art7c1zh.js} +5 -5
  43. package/dist/{cli-40jsnrjm.js → cli-az25fn5f.js} +10 -8
  44. package/dist/{cli-9s05422w.js → cli-bqztfyve.js} +1 -1
  45. package/dist/{cli-bn0rewsy.js → cli-c081bj3b.js} +2 -2
  46. package/dist/{cli-svrj71pg.js → cli-c2habvsf.js} +12 -12
  47. package/dist/{cli-s49hpn37.js → cli-ca6banwz.js} +85 -19
  48. package/dist/{cli-hhxjnjzs.js → cli-cz37jqss.js} +12 -10
  49. package/dist/{cli-6n8axjv1.js → cli-d2dpf95n.js} +2 -2
  50. package/dist/{cli-tx00c0h6.js → cli-dqygeeh5.js} +17 -1
  51. package/dist/{cli-z21whrrg.js → cli-emhjcj2y.js} +17 -17
  52. package/dist/{cli-e8jyq7d8.js → cli-esakngh8.js} +7 -3
  53. package/dist/{cli-ha9rmwgq.js → cli-fb8m699c.js} +2 -2
  54. package/dist/{cli-35gtk1p6.js → cli-ffvsj2qw.js} +44 -13
  55. package/dist/{cli-ce0ak9ms.js → cli-g20jjwt4.js} +26 -26
  56. package/dist/{cli-kxawjrxg.js → cli-g8ka5j4j.js} +772 -194
  57. package/dist/{cli-vwn81jbs.js → cli-gf95q3bm.js} +5 -5
  58. package/dist/{cli-s53vp709.js → cli-gnsb1zxa.js} +2 -2
  59. package/dist/{cli-nwyw3zhg.js → cli-gxee0bqa.js} +1 -1
  60. package/dist/{cli-gpv60278.js → cli-hfeb68z4.js} +7 -7
  61. package/dist/{cli-0ebey9hg.js → cli-j6rktc91.js} +1 -1
  62. package/dist/{cli-ewm3pwbq.js → cli-k21gx5ht.js} +46 -23
  63. package/dist/{cli-egjggehm.js → cli-kf4jree9.js} +2 -2
  64. package/dist/{cli-dabehxhp.js → cli-kt907da6.js} +1 -1
  65. package/dist/{cli-p0rzjkya.js → cli-m5a9m0ak.js} +32 -13
  66. package/dist/{cli-fzcn3d87.js → cli-m70bajzr.js} +3 -3
  67. package/dist/{cli-w1zwctj2.js → cli-mtnj4fwa.js} +11 -5
  68. package/dist/{cli-gpsz7g88.js → cli-mvjz5an3.js} +16 -12
  69. package/dist/{cli-hcpj2xqz.js → cli-n7asmce6.js} +225 -3
  70. package/dist/{cli-y561h5a9.js → cli-nn6vx2mq.js} +5 -5
  71. package/dist/cli-nq677csy.js +140 -0
  72. package/dist/{cli-a5ebc95n.js → cli-pfk92eqz.js} +18 -15
  73. package/dist/{cli-p6zs9h4y.js → cli-rr7zx19j.js} +767 -249
  74. package/dist/{cli-qgf8emrn.js → cli-sqma6vw1.js} +10 -7
  75. package/dist/{cli-0xk505e8.js → cli-sxgtf5fw.js} +1 -1
  76. package/dist/{cli-nzfnt2gs.js → cli-tf1n0r0b.js} +1 -1
  77. package/dist/{cli-qqzmjxkf.js → cli-v49fkghp.js} +11 -6
  78. package/dist/{cli-x77gen2p.js → cli-w2656ka1.js} +7 -7
  79. package/dist/{cli-5kqnemen.js → cli-yvy48785.js} +1 -1
  80. package/dist/{cli-8s5wx3nz.js → cli-zqne1qcj.js} +44 -17
  81. package/dist/cli.js +22 -22
  82. package/dist/{codex-adapter-n8ka66vf.js → codex-adapter-d95q4qsn.js} +7 -7
  83. package/dist/{codex-hook-e2he7aca.js → codex-hook-brpnycn5.js} +1 -1
  84. package/dist/{codex-login-rn7c5gxt.js → codex-login-8cg0vhf2.js} +1 -1
  85. package/dist/{codex-session-runner-fsebts37.js → codex-session-runner-gdwd8h0c.js} +8 -8
  86. package/dist/{commands-t04rt2pc.js → commands-jqmgk54b.js} +2 -2
  87. package/dist/{controller-rgna04r2.js → controller-z3jq589f.js} +30 -4
  88. package/dist/{controllers-connect-bqm1rtft.js → controllers-connect-r2za39ef.js} +23 -23
  89. package/dist/{cursor-adapter-swr2k95y.js → cursor-adapter-4mr4jgkw.js} +2 -3
  90. package/dist/{db-a4m7haw0.js → db-ppv2tze7.js} +3 -3
  91. package/dist/{db-24ck09xy.js → db-w36wwb5g.js} +4 -2
  92. package/dist/{devin-adapter-4ycy4vrf.js → devin-adapter-byen2cxt.js} +1 -1
  93. package/dist/{dispatcher-nwf9ss0q.js → dispatcher-pv5h77vf.js} +20 -20
  94. package/dist/{docker-nfy2gc8t.js → docker-fdzyewpz.js} +4 -4
  95. package/dist/{dsh-adapter-kpdnhjst.js → dsh-adapter-d4pdpb3r.js} +2 -3
  96. package/dist/{e2b-9gkdhm3c.js → e2b-222hsajc.js} +2 -2
  97. package/dist/{e2b-cjtf8k1z.js → e2b-fn3zacvt.js} +2 -2
  98. package/dist/{easter-egg-3d-e4qv8dxj.js → easter-egg-3d-4sp4dvmc.js} +1 -1
  99. package/dist/{event-bus-3z78ep5n.js → event-bus-pghg8hqy.js} +1 -1
  100. package/dist/{execute-0xpz8950.js → execute-jev839y1.js} +1 -1
  101. package/dist/{external-channel-j1e2p34p.js → external-channel-y6nkzhnn.js} +2 -2
  102. package/dist/{grok-adapter-qtjz0whk.js → grok-adapter-t08b9ntm.js} +10 -10
  103. package/dist/handlers-eqsnmv95.js +41 -0
  104. package/dist/{heartbeat-vtesda5d.js → heartbeat-vn31xwdk.js} +23 -23
  105. package/dist/{hook-b2hhj6rd.js → hook-6d0zbvvt.js} +6 -5
  106. package/dist/{hook-8wqaszda.js → hook-cpjs8q51.js} +13 -7
  107. package/dist/{http-mxcqhhcs.js → http-9c6medda.js} +1085 -136
  108. package/dist/{index-spwxjgay.js → index-38t35h0n.js} +20 -20
  109. package/dist/{index-1r8wfpzs.js → index-g6k9tctj.js} +1 -1
  110. package/dist/{index-srg522m6.js → index-k0yt1tvq.js} +20 -20
  111. package/dist/{index-31ceyf42.js → index-pkma1yf8.js} +1 -1
  112. package/dist/{index-n1vbvta9.js → index-qhjf2htm.js} +29 -24
  113. package/dist/index-yqgbfgaj.js +43 -0
  114. package/dist/{keepalive-dsd8rtgx.js → keepalive-em0117bp.js} +4 -4
  115. package/dist/lead-23e7z59t.js +79 -0
  116. package/dist/{maintenance-jqmqwnet.js → maintenance-kbme3qy9.js} +1 -1
  117. package/dist/{oauth-refresh-sweep-q2c3bnvm.js → oauth-refresh-sweep-xysj4yse.js} +3 -3
  118. package/dist/{onboard-w4yd7s8t.js → onboard-p6q0vz97.js} +3 -3
  119. package/dist/{opencode-adapter-9tppvxar.js → opencode-adapter-esv3k9yn.js} +7 -7
  120. package/dist/{otel-impl-f36byzbd.js → otel-impl-54qhqh1z.js} +2 -2
  121. package/dist/{pi-mono-adapter-tqsge1ay.js → pi-mono-adapter-mymbwhj0.js} +41 -14
  122. package/dist/{pricing-refresh-xvak5p4v.js → pricing-refresh-yb75wv33.js} +3 -3
  123. package/dist/{rbac-roles-gfn30fb8.js → rbac-roles-1vp2mw4p.js} +2 -2
  124. package/dist/{rbac-roles-682k2mt5.js → rbac-roles-dexhc9x8.js} +1 -1
  125. package/dist/render-v2-tjay5hhf.js +16 -0
  126. package/dist/{resume-2dvq348g.js → resume-8ayf37g5.js} +11 -9
  127. package/dist/{runtime-bt5pkpm1.js → runtime-hqe5cqcd.js} +1 -1
  128. package/dist/{session-access-7f21ekq9.js → session-access-fdhp987s.js} +4 -4
  129. package/dist/session-source-backfill-qgtc86z1.js +110 -0
  130. package/dist/{setup-epffxtbk.js → setup-4rrmqcq3.js} +1 -1
  131. package/dist/{virtual-modules-mnfbh5mr.js → virtual-modules-52km00d6.js} +2 -2
  132. package/dist/{work-objects-xz1a5rqk.js → work-objects-p28fgd70.js} +23 -23
  133. package/dist/worker-e3w7vye1.js +79 -0
  134. package/dist/{x-pbpezr0s.js → x-wxrzvzx8.js} +2 -2
  135. package/openapi.json +1323 -16
  136. package/package.json +5 -5
  137. package/plugin/commands/create-pr.md +2 -2
  138. package/plugin/pi-skills/create-pr/SKILL.md +2 -2
  139. package/src/assets/source-key.ts +95 -0
  140. package/src/be/db/api-keys.ts +80 -0
  141. package/src/be/db/approvals.ts +48 -0
  142. package/src/be/db/session-labels.ts +28 -0
  143. package/src/be/db/session-moves.ts +5 -6
  144. package/src/be/db/session-tree.ts +153 -14
  145. package/src/be/db/tasks/read.ts +11 -0
  146. package/src/be/db/tasks/write.ts +21 -6
  147. package/src/be/db/workflow-idempotency.ts +183 -0
  148. package/src/be/db/workflow-runs.ts +500 -7
  149. package/src/be/db-retention.ts +61 -6
  150. package/src/be/db.ts +126 -131
  151. package/src/be/memory/providers/openai-embedding.ts +5 -1
  152. package/src/be/migrations/242_workflow_idempotency_keys.sql +40 -0
  153. package/src/be/migrations/244_api_key_validation.sql +4 -0
  154. package/src/be/onboarding.ts +4 -0
  155. package/src/be/scripts/typecheck.ts +1 -1
  156. package/src/be/sealed-json.ts +1 -0
  157. package/src/be/seed-scripts/catalog/compound-insights.inline.ts +2 -2
  158. package/src/be/seed-scripts/catalog/compound-insights.ts +2 -2
  159. package/src/be/seed-skills/bundled-files.generated.json +1 -1
  160. package/src/be/session-source-backfill.ts +163 -0
  161. package/src/be/sql-connections.ts +7 -1
  162. package/src/be/swarm-config-guard.ts +3 -0
  163. package/src/be/users.ts +9 -0
  164. package/src/comb/review-batch.ts +28 -1
  165. package/src/comb/templates.ts +7 -2
  166. package/src/commands/claude-managed-setup.ts +7 -1
  167. package/src/commands/controller.ts +33 -0
  168. package/src/commands/credential-refresh.ts +76 -1
  169. package/src/commands/credential-wait.ts +2 -2
  170. package/src/commands/provider-credentials.ts +210 -170
  171. package/src/commands/runner.ts +31 -6
  172. package/src/extensions/apply-task-create.ts +64 -11
  173. package/src/extensions/contract-types.generated.ts +1 -1
  174. package/src/extensions/contract.ts +30 -0
  175. package/src/extensions/dispatcher.ts +4 -0
  176. package/src/extensions/post-bridge.ts +8 -0
  177. package/src/hooks/context-bound-note.ts +196 -0
  178. package/src/hooks/hook.ts +12 -0
  179. package/src/http/all-routes.ts +2 -0
  180. package/src/http/api-keys.ts +53 -0
  181. package/src/http/assets.ts +15 -0
  182. package/src/http/codex-oauth-accounts.ts +191 -0
  183. package/src/http/comb.ts +15 -2
  184. package/src/http/config.ts +49 -17
  185. package/src/http/core.ts +6 -0
  186. package/src/http/fleet-benchmark.ts +5 -2
  187. package/src/http/idempotency-key.ts +117 -0
  188. package/src/http/index.ts +11 -0
  189. package/src/http/integrations.ts +5 -1
  190. package/src/http/kv.ts +8 -3
  191. package/src/http/onboarding.ts +6 -1
  192. package/src/http/pool-keys.ts +354 -0
  193. package/src/http/sessions.ts +37 -12
  194. package/src/http/stats.ts +1 -0
  195. package/src/http/status.ts +26 -12
  196. package/src/http/tasks.ts +3 -0
  197. package/src/http/used-by.ts +7 -2
  198. package/src/http/workflows.ts +207 -33
  199. package/src/integrations/chat-sdk/inbound.ts +27 -0
  200. package/src/kv-reserved-namespaces.ts +19 -0
  201. package/src/prompts/context-bound-note.ts +48 -0
  202. package/src/prompts/session-templates.ts +25 -1
  203. package/src/providers/acp-targets.ts +1 -1
  204. package/src/providers/amp-adapter.ts +30 -8
  205. package/src/providers/claude-adapter.ts +34 -5
  206. package/src/providers/claude-managed-adapter.ts +42 -9
  207. package/src/providers/claude-sdk-session.ts +10 -0
  208. package/src/providers/claude-session-events.ts +13 -0
  209. package/src/providers/codex-adapter.ts +83 -11
  210. package/src/providers/opencode-adapter.ts +18 -0
  211. package/src/providers/pi-mono-adapter.ts +35 -0
  212. package/src/runtime-providers/docker.ts +6 -2
  213. package/src/scheduler/scheduler.ts +3 -2
  214. package/src/scripts-runtime/swarm-sdk.ts +7 -1
  215. package/src/scripts-runtime/types/stdlib.d.ts +1 -1
  216. package/src/scripts-runtime/types/swarm-extension.d.ts +52 -8
  217. package/src/scripts-runtime/types/swarm-sdk.d.ts +1 -1
  218. package/src/slack/assistant.ts +7 -0
  219. package/src/slack/channel-labels.ts +74 -0
  220. package/src/slack/handlers.ts +8 -0
  221. package/src/slack/inbound-files.ts +5 -2
  222. package/src/slack/thread-buffer.ts +4 -0
  223. package/src/tasks/sibling-awareness.ts +12 -3
  224. package/src/telemetry-context.ts +5 -6
  225. package/src/telemetry-identity.ts +3 -3
  226. package/src/tests/amp-adapter.test.ts +97 -0
  227. package/src/tests/api-keys-validation.test.ts +170 -0
  228. package/src/tests/approval-requests.test.ts +10 -7
  229. package/src/tests/approval-sweeps.test.ts +12 -6
  230. package/src/tests/base-prompt.test.ts +1 -1
  231. package/src/tests/claude-hook-output.test.ts +79 -1
  232. package/src/tests/claude-managed-adapter.test.ts +321 -0
  233. package/src/tests/claude-routes-e2e.test.ts +7 -2
  234. package/src/tests/codex-adapter.test.ts +163 -0
  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/config-pool-keys.test.ts +533 -0
  240. package/src/tests/context-bound-note.test.ts +269 -0
  241. package/src/tests/credential-check.test.ts +290 -2
  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/extensions-pre-task.test.ts +325 -0
  248. package/src/tests/fetch-resolved-env.test.ts +17 -0
  249. package/src/tests/fixtures/extensions/record-blocks/hooks.ts +10 -0
  250. package/src/tests/fixtures/extensions/record-blocks/manifest.json +7 -0
  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/fixtures/fake-amp.ts +22 -8
  254. package/src/tests/fixtures/held-template-render.ts +42 -0
  255. package/src/tests/fixtures/workflow-idempotency-trigger.child.ts +73 -0
  256. package/src/tests/fleet-benchmark.test.ts +8 -0
  257. package/src/tests/get-workflow-run-tool.test.ts +98 -0
  258. package/src/tests/integrations-http.test.ts +32 -2
  259. package/src/tests/internal-ai/complete-structured.test.ts +37 -1
  260. package/src/tests/internal-ai/credentials.test.ts +23 -0
  261. package/src/tests/kv-http.test.ts +32 -0
  262. package/src/tests/kv-tool.test.ts +25 -0
  263. package/src/tests/multi-runtime-registration.test.ts +12 -12
  264. package/src/tests/onboarding.test.ts +108 -1
  265. package/src/tests/openai-embedding-credentials.test.ts +35 -0
  266. package/src/tests/openapi-route-headers.test.ts +2 -0
  267. package/src/tests/opencode-adapter.test.ts +134 -0
  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/pi-mono-adapter.test.ts +105 -0
  271. package/src/tests/prompt-template-session.test.ts +4 -3
  272. package/src/tests/provider-key-probes.test.ts +181 -0
  273. package/src/tests/rbac-e2e-helpers.test.ts +88 -1
  274. package/src/tests/rbac-e2e-helpers.ts +36 -11
  275. package/src/tests/runner-push-delivery.test.ts +25 -0
  276. package/src/tests/runtime-controller-liveness.test.ts +133 -0
  277. package/src/tests/runtime-providers-docker.test.ts +2 -0
  278. package/src/tests/script-connections.test.ts +5 -0
  279. package/src/tests/script-run-config-scope.test.ts +91 -3
  280. package/src/tests/script-runs-retention.test.ts +212 -0
  281. package/src/tests/sdk-allowlist.test.ts +39 -0
  282. package/src/tests/session-logs.test.ts +70 -0
  283. package/src/tests/session-source-backfill.test.ts +429 -0
  284. package/src/tests/session-source-keys.test.ts +236 -0
  285. package/src/tests/session-source-labels.test.ts +419 -0
  286. package/src/tests/sessions-detail-batch.test.ts +2 -0
  287. package/src/tests/sessions-move.test.ts +131 -0
  288. package/src/tests/sessions-tree.test.ts +336 -2
  289. package/src/tests/slack-inbound-files.test.ts +103 -23
  290. package/src/tests/status.test.ts +101 -18
  291. package/src/tests/tasks-workflow-run-filter.test.ts +146 -0
  292. package/src/tests/test-net.ts +31 -0
  293. package/src/tests/transcription.test.ts +8 -0
  294. package/src/tests/used-by-route.test.ts +11 -0
  295. package/src/tests/workflow-cascade-cancel.test.ts +1341 -0
  296. package/src/tests/workflow-foreach-supersede-retry.test.ts +488 -0
  297. package/src/tests/workflow-http-v2.test.ts +131 -0
  298. package/src/tests/workflow-idempotency.test.ts +915 -0
  299. package/src/tests/workflow-step-order.test.ts +92 -0
  300. package/src/tests/workflow-sub-workflow.test.ts +30 -0
  301. package/src/tools/get-tasks.ts +8 -0
  302. package/src/tools/kv/kv-read-auth.ts +4 -2
  303. package/src/tools/task-action.ts +1 -1
  304. package/src/tools/workflows/cancel-workflow-run.ts +1 -1
  305. package/src/tools/workflows/get-workflow-run.ts +14 -4
  306. package/src/types.ts +24 -0
  307. package/src/utils/company-profile.ts +51 -1
  308. package/src/utils/context-window.ts +8 -6
  309. package/src/utils/credentials.ts +5 -2
  310. package/src/utils/internal-ai/complete-structured.ts +5 -0
  311. package/src/utils/internal-ai/credentials.ts +4 -3
  312. package/src/utils/provider-key-probes.ts +328 -0
  313. package/src/utils/transcription.ts +4 -1
  314. package/src/workflows/approval-cancel.ts +15 -13
  315. package/src/workflows/approval-notifications.ts +5 -1
  316. package/src/workflows/checkpoint.ts +6 -3
  317. package/src/workflows/engine.ts +30 -8
  318. package/src/workflows/executors/foreach.ts +2 -2
  319. package/src/workflows/executors/sub-workflow.ts +8 -1
  320. package/src/workflows/foreach-driver.ts +10 -3
  321. package/src/workflows/foreach-join.ts +29 -6
  322. package/src/workflows/recovery.ts +6 -1
  323. package/src/workflows/resume.ts +168 -56
  324. package/src/workflows/retry-poller.ts +49 -3
  325. package/src/workflows/task-step-routing.ts +29 -7
  326. package/templates/ai-toolbox.manifest.json +8 -8
  327. package/templates/community/open-prompt-showreel/PLAYBOOK.md +5 -6
  328. package/templates/community/open-prompt-showreel/lead-prompt.md +1 -1
  329. package/templates/community/open-prompt-showreel/skills/open-prompt-showreel/SKILL.md +4 -4
  330. package/templates/skills/agent-browser/SKILL.md +4 -4
  331. package/templates/skills/agent-browser/content.md +4 -4
  332. package/templates/skills/code-quality/config.json +1 -1
  333. package/templates/skills/code-quality/content.md +2 -2
  334. package/templates/skills/delegate-work/content.md +29 -25
  335. package/templates/skills/delegate-work/files/scripts/codex-exec.sh +5 -5
  336. package/templates/skills/qa/content.md +2 -2
  337. package/templates/skills/researching/content.md +4 -0
  338. package/templates/skills/swarm-scripts/SKILL.md +1 -0
  339. package/templates/skills/swarm-scripts/content.md +1 -0
  340. package/templates/skills/work-on-task/SKILL.md +1 -1
  341. package/templates/skills/work-on-task/content.md +1 -1
  342. package/dist/cli-7421jzsw.js +0 -84
  343. package/dist/cli-e45e4m6b.js +0 -413
  344. package/dist/cli-zw88zjba.js +0 -307
  345. package/dist/handlers-19xr6dnz.js +0 -41
  346. package/dist/index-kw2mmat7.js +0 -43
  347. package/dist/lead-zyerk1x2.js +0 -79
  348. package/dist/render-v2-hgytz8nn.js +0 -12
  349. package/dist/worker-42t8emjt.js +0 -79
@@ -2,9 +2,14 @@ import { afterEach, beforeEach, describe, expect, mock, spyOn, test } from "bun:
2
2
  import {
3
3
  CREDENTIAL_RETRY_INTERVAL_MS,
4
4
  type CredentialRefreshState,
5
+ credentialFingerprint,
6
+ KEY_RECHECK_INTERVAL_MS,
7
+ recheckableFailures,
5
8
  refreshCredentialStatus,
9
+ reportKeyValidation,
6
10
  } from "../commands/credential-refresh";
7
11
  import * as credentials from "../commands/provider-credentials";
12
+ import { fetchResolvedEnv } from "../commands/runner";
8
13
  import type { AgentCredStatus } from "../types";
9
14
 
10
15
  const api = { apiUrl: "http://unused.test", apiKey: "test", agentId: crypto.randomUUID() };
@@ -27,7 +32,14 @@ describe("runner credential refresh", () => {
27
32
  let send: ReturnType<typeof spyOn<typeof credentials, "sendCredStatusReport">>;
28
33
 
29
34
  beforeEach(() => {
30
- state = { harnessProvider: "claude", ready: true, lastRefreshAt: 0, inFlight: false };
35
+ state = {
36
+ harnessProvider: "claude",
37
+ ready: true,
38
+ lastRefreshAt: 0,
39
+ inFlight: false,
40
+ fingerprint: credentialFingerprint({}),
41
+ rejectedKeys: 0,
42
+ };
31
43
  build = spyOn(credentials, "buildCredStatusReport").mockResolvedValue(ready);
32
44
  send = spyOn(credentials, "sendCredStatusReport").mockResolvedValue(undefined);
33
45
  });
@@ -43,6 +55,136 @@ describe("runner credential refresh", () => {
43
55
  expect(state.ready).toBe(true);
44
56
  });
45
57
 
58
+ test("a changed credential fingerprint refreshes a ready worker immediately", async () => {
59
+ const env = { ANTHROPIC_API_KEY: "test-pool-a,test-pool-b" };
60
+ await refreshCredentialStatus(api, state, "claude", env, 1);
61
+ expect(build).toHaveBeenCalledWith("claude", env, {}, "post_task");
62
+ expect(state.fingerprint).toBe(credentialFingerprint(env));
63
+ await refreshCredentialStatus(api, state, "claude", env, 2);
64
+ expect(build).toHaveBeenCalledTimes(1);
65
+ await refreshCredentialStatus(api, state, "claude", {}, 3);
66
+ expect(build).toHaveBeenCalledTimes(2);
67
+ });
68
+
69
+ test("an unchanged fingerprint does not refresh a ready worker", async () => {
70
+ await refreshCredentialStatus(api, state, "claude", {}, KEY_RECHECK_INTERVAL_MS);
71
+ expect(build).not.toHaveBeenCalled();
72
+ });
73
+
74
+ test("fingerprints ignore environment order and unrelated values", () => {
75
+ expect(credentialFingerprint({ ANTHROPIC_API_KEY: "test-a", PATH: "a" })).toBe(
76
+ credentialFingerprint({ PATH: "b", ANTHROPIC_API_KEY: "test-a" }),
77
+ );
78
+ for (const key of ["ANTHROPIC_AUTH_TOKEN", "ANTHROPIC_BASE_URL", "OPENAI_API_KEY"]) {
79
+ expect(credentialFingerprint({ [key]: "changed" })).not.toBe(credentialFingerprint({}));
80
+ }
81
+ });
82
+
83
+ test("resolved credential env preserves the full pool before selection", async () => {
84
+ const originalFetch = globalThis.fetch;
85
+ globalThis.fetch = (async (url) => {
86
+ if (String(url).includes("/api/config/resolved")) {
87
+ return Response.json({
88
+ configs: [{ key: "ANTHROPIC_API_KEY", value: "test-aaa11,test-bbb22" }],
89
+ });
90
+ }
91
+ return Response.json({ availableIndices: [1], rejectedIndices: [0] });
92
+ }) as typeof fetch;
93
+ try {
94
+ const result = await fetchResolvedEnv(api.apiUrl, api.apiKey, api.agentId, {
95
+ HARNESS_PROVIDER: "claude",
96
+ });
97
+ expect(result.credentialEnv.ANTHROPIC_API_KEY).toBe("test-aaa11,test-bbb22");
98
+ expect(result.env.ANTHROPIC_API_KEY).toBe("test-bbb22");
99
+ expect(result.credentialEnv).not.toBe(result.env);
100
+ } finally {
101
+ globalThis.fetch = originalFetch;
102
+ }
103
+ });
104
+
105
+ test("rejected keys refresh after ten minutes, even when the worker is ready", async () => {
106
+ state.rejectedKeys = 1;
107
+ await refreshCredentialStatus(api, state, "claude", {}, KEY_RECHECK_INTERVAL_MS - 1);
108
+ expect(build).not.toHaveBeenCalled();
109
+ await refreshCredentialStatus(api, state, "claude", {}, KEY_RECHECK_INTERVAL_MS);
110
+ expect(build).toHaveBeenCalledTimes(1);
111
+ expect(state.rejectedKeys).toBe(0);
112
+ });
113
+
114
+ test("a failed live test without per-key results remains eligible for recheck", async () => {
115
+ build.mockResolvedValueOnce({
116
+ ...ready,
117
+ liveTest: { ok: false, error: "timeout", latency_ms: 1 },
118
+ });
119
+ await refreshCredentialStatus(api, state, "pi", {}, 1);
120
+ expect(state.rejectedKeys).toBe(1);
121
+ });
122
+
123
+ test("refresh reports per-key results once per credential type", async () => {
124
+ const keys = [
125
+ {
126
+ keyType: "ANTHROPIC_API_KEY",
127
+ index: 0,
128
+ suffix: "aaa11",
129
+ ok: false,
130
+ httpStatus: 401,
131
+ latency_ms: 1,
132
+ },
133
+ { keyType: "ANTHROPIC_API_KEY", index: 1, suffix: "bbb22", ok: true, latency_ms: 1 },
134
+ { keyType: "OPENAI_API_KEY", index: 0, suffix: "ccc33", ok: true, latency_ms: 1 },
135
+ ];
136
+ build.mockResolvedValueOnce({ ...ready, liveTest: { ok: false, latency_ms: 1, keys } });
137
+ const requests: Array<{ url: string; init?: RequestInit }> = [];
138
+ const originalFetch = globalThis.fetch;
139
+ globalThis.fetch = (async (url, init) => {
140
+ requests.push({ url: String(url), init });
141
+ return Response.json({ success: true });
142
+ }) as typeof fetch;
143
+ try {
144
+ await refreshCredentialStatus(api, state, "pi", {}, 1);
145
+ expect(state.rejectedKeys).toBe(1);
146
+ expect(requests).toHaveLength(2);
147
+ expect(requests[0]!.url).toBe(`${api.apiUrl}/api/keys/report-validation`);
148
+ expect(requests[0]!.init?.headers).toMatchObject({
149
+ Authorization: `Bearer ${api.apiKey}`,
150
+ "X-Agent-ID": api.agentId,
151
+ });
152
+ expect(JSON.parse(String(requests[0]!.init?.body))).toEqual({
153
+ keyType: "ANTHROPIC_API_KEY",
154
+ keys: [
155
+ { keyIndex: 0, keySuffix: "aaa11", ok: false, httpStatus: 401 },
156
+ { keyIndex: 1, keySuffix: "bbb22", ok: true },
157
+ ],
158
+ });
159
+ expect(JSON.parse(String(requests[1]!.init?.body))).toEqual({
160
+ keyType: "OPENAI_API_KEY",
161
+ keys: [{ keyIndex: 0, keySuffix: "ccc33", ok: true }],
162
+ });
163
+ expect(requests[0]!.init?.signal).toBeInstanceOf(AbortSignal);
164
+ } finally {
165
+ globalThis.fetch = originalFetch;
166
+ }
167
+ });
168
+
169
+ test("validation reporting contains HTTP and network failures", async () => {
170
+ const originalFetch = globalThis.fetch;
171
+ const warn = spyOn(console, "warn").mockImplementation(() => {});
172
+ const fetchMock = mock<typeof fetch>()
173
+ .mockResolvedValueOnce(new Response(null, { status: 503 }))
174
+ .mockRejectedValueOnce(new Error("network unavailable"));
175
+ globalThis.fetch = fetchMock as typeof fetch;
176
+ try {
177
+ await reportKeyValidation(api, [
178
+ { keyType: "ANTHROPIC_API_KEY", index: 0, suffix: "aaa11", ok: true, latency_ms: 1 },
179
+ { keyType: "OPENAI_API_KEY", index: 0, suffix: "bbb22", ok: true, latency_ms: 1 },
180
+ ]);
181
+ expect(fetchMock).toHaveBeenCalledTimes(2);
182
+ expect(warn).toHaveBeenCalledTimes(2);
183
+ } finally {
184
+ globalThis.fetch = originalFetch;
185
+ }
186
+ });
187
+
46
188
  test("a failed readiness write is throttled and retried even when the snapshot is ready", async () => {
47
189
  send.mockRejectedValueOnce(new Error("HTTP 503"));
48
190
  await expect(refreshCredentialStatus(api, state, "pi", {}, 1)).rejects.toThrow("HTTP 503");
@@ -113,3 +255,32 @@ describe("runner credential refresh", () => {
113
255
  expect(build).toHaveBeenCalledTimes(1);
114
256
  });
115
257
  });
258
+
259
+ describe("recheckableFailures", () => {
260
+ const key = (keyType: string, ok: boolean) => ({
261
+ keyType,
262
+ index: 0,
263
+ suffix: "abcde",
264
+ ok,
265
+ latency_ms: 1,
266
+ });
267
+
268
+ test("a Claude subscription token never joins the periodic recheck", () => {
269
+ const liveTest = {
270
+ ok: false,
271
+ error: "rejected",
272
+ latency_ms: 1,
273
+ testedAt: 1,
274
+ keys: [key("CLAUDE_CODE_OAUTH_TOKEN", false), key("ANTHROPIC_API_KEY", false)],
275
+ };
276
+ expect(recheckableFailures(liveTest)).toBe(1);
277
+ expect(
278
+ recheckableFailures({ ...liveTest, keys: [key("CLAUDE_CODE_OAUTH_TOKEN", false)] }),
279
+ ).toBe(0);
280
+ });
281
+
282
+ test("a failed test without per-key results counts once", () => {
283
+ expect(recheckableFailures({ ok: false, error: "x", latency_ms: 1, testedAt: 1 })).toBe(1);
284
+ expect(recheckableFailures(null)).toBe(0);
285
+ });
286
+ });
@@ -19,8 +19,13 @@ import {
19
19
  const TEST_DB_PATH = "./test-db-retention.sqlite";
20
20
  const NOW = new Date("2026-08-23T12:00:00.000Z");
21
21
  const RETENTION_KEYS = DB_RETENTION_TABLES.map((table) => table.envKey);
22
- /** Time-horizon tables. Count-based context_versions has its own suite. */
23
- const AGE_TABLES = DB_RETENTION_TABLES.filter((table) => table.policy === "age");
22
+ /**
23
+ * Plain time-horizon tables. Count-based context_versions and status-filtered
24
+ * script_runs have their own suites.
25
+ */
26
+ const AGE_TABLES = DB_RETENTION_TABLES.filter(
27
+ (table) => table.policy === "age" && !("terminalStatuses" in table),
28
+ );
24
29
  type AgeTable = (typeof AGE_TABLES)[number];
25
30
  const TUNING_KEYS = [
26
31
  "DB_RETENTION_TICK_BUDGET_MS",
@@ -135,12 +140,13 @@ afterEach(async () => {
135
140
  });
136
141
 
137
142
  describe("DB retention", () => {
138
- test("keeps the closed allowlist limited to the four approved tables", () => {
143
+ test("keeps the closed allowlist limited to the five approved tables", () => {
139
144
  expect(DB_RETENTION_TABLES.map((table) => table.table)).toEqual([
140
145
  "session_logs",
141
146
  "agent_log",
142
147
  "events",
143
148
  "context_versions",
149
+ "script_runs",
144
150
  ]);
145
151
  });
146
152
 
@@ -529,6 +529,7 @@ value:recordKeyAuthFailure
529
529
  value:recordKeyRateLimitWindows
530
530
  value:recordKeySeatMismatch
531
531
  value:recordKeyUsage
532
+ value:recordKeyValidation
532
533
  value:recordSlackMessage
533
534
  value:recordTaskProviderIfUnset
534
535
  value:recordTaskPullRequestAttachments
@@ -552,6 +552,7 @@ value:recordKeyAuthFailure
552
552
  value:recordKeyRateLimitWindows
553
553
  value:recordKeySeatMismatch
554
554
  value:recordKeyUsage
555
+ value:recordKeyValidation
555
556
  value:recordSlackMessage
556
557
  value:recordTaskProviderIfUnset
557
558
  value:recordTaskPullRequestAttachments
@@ -684,7 +685,6 @@ STEERING_MESSAGE_SELECT
684
685
  UNATTRIBUTED_USER_ID
685
686
  VERSIONABLE_FIELDS
686
687
  WEEK_SECONDS
687
- WORKFLOW_RUN_SUMMARY_COLUMNS
688
688
  abandonSlackOutcomeDelivery
689
689
  acceptTask
690
690
  assertSkillFileLimits
@@ -1097,8 +1097,6 @@ rowToUser
1097
1097
  rowToUserFavorite
1098
1098
  rowToWaitState
1099
1099
  rowToWorkflow
1100
- rowToWorkflowRun
1101
- rowToWorkflowRunStep
1102
1100
  rowToWorkflowSummary
1103
1101
  rowToWorkflowVersion
1104
1102
  scriptRunUpdateSets
@@ -78,7 +78,7 @@ describe("docs API index footprint", () => {
78
78
  expect(tasks).toContain('"path":"/api/task-templates","method":"get"');
79
79
  });
80
80
 
81
- test("OpenAPI and generated pages keep all 449 operations", async () => {
81
+ test("OpenAPI and generated pages keep all 456 operations", async () => {
82
82
  const spec = await Bun.file(resolve(repoRoot, "openapi.json")).json();
83
83
  const operationKeys = [];
84
84
  const tags = new Set();
@@ -103,8 +103,8 @@ describe("docs API index footprint", () => {
103
103
  expect(spec.paths["/api/agents/{id}/runtime"]?.get).toBeDefined();
104
104
 
105
105
  const generatedKeys = await generatedOperationKeys();
106
- expect(operationKeys).toHaveLength(449);
107
- expect(generatedKeys).toHaveLength(449);
106
+ expect(operationKeys).toHaveLength(456);
107
+ expect(generatedKeys).toHaveLength(456);
108
108
  expect(new Set(generatedKeys)).toEqual(new Set(operationKeys));
109
109
  });
110
110
 
@@ -14,6 +14,7 @@ import {
14
14
  createWorkflowRunStep,
15
15
  getDbClient,
16
16
  getKv,
17
+ getMostRecentTaskInThread,
17
18
  getTaskByWorkflowRunStepId,
18
19
  initDb,
19
20
  startTask,
@@ -22,9 +23,12 @@ import * as dbClient from "../be/db-client";
22
23
  import type { InstallExtensionArgs as ExtensionInstallBody } from "../be/extensions/db";
23
24
  import { installExtension, listExtensionRuns } from "../be/extensions/db";
24
25
  import { getCatalogEntry } from "../extensions/catalog";
26
+ import type { TaskBlockedEvent, TaskCreateEvent } from "../extensions/contract";
25
27
  import { disableExtension, enableExtension, stopExtensionRuntime } from "../extensions/lifecycle";
28
+ import { initExtensionPostBridge, teardownExtensionPostBridge } from "../extensions/post-bridge";
26
29
  import { handleTasks } from "../http/tasks";
27
30
  import { dispatchScheduleTarget } from "../scheduler/scheduler";
31
+ import { bufferThreadMessage, instantFlush } from "../slack/thread-buffer";
28
32
  import { createTaskWithSiblingAwareness } from "../tasks/sibling-awareness";
29
33
  import { createWorkerTaskFollowUp } from "../tasks/worker-follow-up";
30
34
  import { registerSendTaskTool, sendTaskHandler } from "../tools/send-task";
@@ -145,6 +149,276 @@ describe("extension task boundaries", () => {
145
149
  await client.run("DELETE FROM agents");
146
150
  });
147
151
 
152
+ describe("post.task.blocked", () => {
153
+ const events: unknown[] = [];
154
+ const capture = (event: unknown) => {
155
+ events.push(event);
156
+ };
157
+
158
+ beforeAll(() => {
159
+ initExtensionPostBridge();
160
+ workflowEventBus.on("task.create_blocked", capture);
161
+ });
162
+
163
+ afterAll(() => {
164
+ workflowEventBus.off("task.create_blocked", capture);
165
+ teardownExtensionPostBridge();
166
+ });
167
+
168
+ beforeEach(() => {
169
+ events.length = 0;
170
+ });
171
+
172
+ async function waitForCounter(
173
+ extension: Awaited<ReturnType<typeof installAndEnable>>,
174
+ key: string,
175
+ count = 1,
176
+ ): Promise<void> {
177
+ const deadline = Date.now() + 3_000;
178
+ while (Date.now() < deadline) {
179
+ const entry = await getKv(`task:agent:${extension.agentId}`, key);
180
+ if (entry?.value === count) return;
181
+ await Bun.sleep(10);
182
+ }
183
+ throw new Error(`post.task.blocked was not recorded ${count} time(s)`);
184
+ }
185
+
186
+ async function recordedBlock(
187
+ extension: Awaited<ReturnType<typeof installAndEnable>>,
188
+ count = 1,
189
+ ): Promise<TaskBlockedEvent> {
190
+ await waitForCounter(extension, "ext:record-blocks:count", count);
191
+ return (await getKv(`task:agent:${extension.agentId}`, "ext:record-blocks:last"))
192
+ ?.value as TaskBlockedEvent;
193
+ }
194
+
195
+ test.each([
196
+ "rest",
197
+ "mcp",
198
+ "slack",
199
+ "schedule",
200
+ ] as const)("%s delivers one blocked event with its source and requester", async (origin) => {
201
+ const blocker = await installAndEnable("block-tasks-from-source", { source: origin });
202
+ const recorder = await installAndEnable("record-blocks");
203
+ let requester: TaskBlockedEvent["requester"] = {};
204
+ let scheduleId: string | undefined;
205
+
206
+ if (origin === "rest") {
207
+ const user = await createUser({ name: "Blocked REST requester" });
208
+ requester = { userId: user.id };
209
+ expect(
210
+ (await postTask({ task: "blocked REST task", requestedByUserId: user.id })).status,
211
+ ).toBe(422);
212
+ } else if (origin === "mcp") {
213
+ const caller = await createAgent({ name: "blocked-mcp-caller", status: "idle" });
214
+ requester = { agentId: caller.id };
215
+ const server = new McpServer({ name: "blocked-event-send", version: "1.0.0" });
216
+ registerSendTaskTool(server);
217
+ const result = await registeredSendTask(server).handler(sendTaskArgs("blocked MCP task"), {
218
+ sessionId: "blocked-event",
219
+ requestInfo: { headers: { "x-agent-id": caller.id } },
220
+ });
221
+ expect(result.isError).toBe(true);
222
+ } else if (origin === "slack") {
223
+ requester = { slackUserId: "U_BLOCKED_EVENT" };
224
+ await createAgent({ name: "blocked-slack-lead", isLead: true, status: "idle" });
225
+ bufferThreadMessage(
226
+ "C_BLOCKED_EVENT",
227
+ "1000.0001",
228
+ "blocked Slack task",
229
+ "U_BLOCKED_EVENT",
230
+ "1000.0002",
231
+ );
232
+ await instantFlush("C_BLOCKED_EVENT:1000.0001");
233
+ } else {
234
+ const schedule = await createScheduledTask({
235
+ name: "blocked-event-schedule",
236
+ taskTemplate: "blocked schedule task",
237
+ intervalMs: 60_000,
238
+ });
239
+ scheduleId = schedule.id;
240
+ await expect(dispatchScheduleTarget(schedule)).rejects.toThrow(
241
+ "Task source schedule is blocked",
242
+ );
243
+ }
244
+
245
+ const event = await recordedBlock(recorder);
246
+ expect(events).toHaveLength(1);
247
+ expect(event).toMatchObject({
248
+ extension: { id: blocker.id, name: "block-tasks-from-source" },
249
+ origin,
250
+ source: origin === "rest" ? "api" : origin,
251
+ requester,
252
+ });
253
+ expect(event.requester).toEqual(requester);
254
+ if (scheduleId) expect(event.context?.scheduleId).toBe(scheduleId);
255
+ expect(event).not.toHaveProperty("task");
256
+ expect(event).not.toHaveProperty("taskId");
257
+ });
258
+
259
+ test("scrubs and bounds the payload, preserving correlation on each retry", async () => {
260
+ const secret = `ghp_${"Z".repeat(36)}`;
261
+ await installSource(
262
+ "secret-blocker",
263
+ `
264
+ import { block, type SwarmExtension } from "swarm-extension";
265
+ const extension: SwarmExtension = (api) => {
266
+ api.on("pre.task.create", () => block(${JSON.stringify(`Ticket required: ${secret}`)}));
267
+ };
268
+ export default extension;
269
+ `,
270
+ );
271
+ const recorder = await installAndEnable("record-blocks");
272
+ const context = {
273
+ contextKey: "github:acme/project:7",
274
+ scheduleId: crypto.randomUUID(),
275
+ workflowRunId: crypto.randomUUID(),
276
+ workflowRunStepId: crypto.randomUUID(),
277
+ parentTaskId: crypto.randomUUID(),
278
+ vcsRepo: "acme/project",
279
+ vcsNumber: 7,
280
+ slackChannelId: "C_CORRELATED_BLOCK",
281
+ slackThreadTs: "2000.0001",
282
+ };
283
+ const requester = {
284
+ userId: crypto.randomUUID(),
285
+ agentId: crypto.randomUUID(),
286
+ slackUserId: "U_CORRELATED_BLOCK",
287
+ };
288
+ for (const attempt of [1, 2]) {
289
+ await expect(
290
+ createTaskWithSiblingAwareness(
291
+ `${"x".repeat(480)} ${secret}`,
292
+ {
293
+ ...context,
294
+ source: "github",
295
+ requestedByUserId: requester.userId,
296
+ creatorAgentId: requester.agentId,
297
+ slackUserId: requester.slackUserId,
298
+ },
299
+ { origin: "webhook" },
300
+ ),
301
+ ).rejects.toThrow("Ticket required:");
302
+ const event = await recordedBlock(recorder, attempt);
303
+ expect(events).toHaveLength(attempt);
304
+ expect(event.reason).toContain("[REDACTED:");
305
+ expect(event.reason).not.toContain(secret);
306
+ expect(event.description).toHaveLength(500);
307
+ expect(event.description).not.toContain("ghp_");
308
+ expect(event.context).toEqual(context);
309
+ expect(event.requester).toEqual(requester);
310
+ }
311
+ });
312
+
313
+ test("a rejected model rewrite emits a block attributed to its editor", async () => {
314
+ const codex = await createAgent({
315
+ name: "blocked-rewrite-codex",
316
+ harnessProvider: "codex",
317
+ status: "idle",
318
+ });
319
+ const editor = await installSource(
320
+ "incompatible-model",
321
+ `
322
+ import { modify, type SwarmExtension } from "swarm-extension";
323
+ const extension: SwarmExtension = (api) => {
324
+ api.on("pre.task.create", () => modify({ model: "claude-opus-5-5" }));
325
+ };
326
+ export default extension;
327
+ `,
328
+ );
329
+ const recorder = await installAndEnable("record-blocks");
330
+ const result = await postTask({
331
+ task: "incompatible rewrite",
332
+ agentId: codex.id,
333
+ routingReason: "human_pinned",
334
+ });
335
+ expect(result.status).toBe(422);
336
+ const event = await recordedBlock(recorder);
337
+ expect(events).toHaveLength(1);
338
+ expect(event.extension).toEqual({ id: editor.id, name: "incompatible-model" });
339
+ expect(event.reason).toContain("rewrite");
340
+ expect(event.reason).toContain("codex");
341
+ });
342
+
343
+ test("the creator is skipped while the blocking extension receives its own block", async () => {
344
+ const creator = await installAndEnable("record-blocks");
345
+ const blocker = await installSource(
346
+ "blocking-recorder",
347
+ `
348
+ import { block, type SwarmExtension } from "swarm-extension";
349
+ const extension: SwarmExtension = (api) => {
350
+ api.on("pre.task.create", () => block("blocked by recorder"));
351
+ api.on("post.task.blocked", async (_event, ctx) => { await ctx.state.incr("count"); });
352
+ };
353
+ export default extension;
354
+ `,
355
+ );
356
+ const observerBundle = await loadBundleFixture("record-blocks");
357
+ const installedObserver = await installExtension({
358
+ ...observerBundle,
359
+ manifest: { ...observerBundle.manifest, name: "last-block-observer" },
360
+ priority: 1_000,
361
+ });
362
+ const observer = await enableExtension(installedObserver.extension.id);
363
+ const server = new McpServer({ name: "extension-blocked-event", version: "1.0.0" });
364
+ registerSendTaskTool(server);
365
+ const result = await registeredSendTask(server).handler(
366
+ sendTaskArgs("extension task"),
367
+ markExtensionRequestOrigin({
368
+ sessionId: "blocked-extension",
369
+ requestInfo: { headers: { "x-agent-id": creator.agentId! } },
370
+ }),
371
+ );
372
+ expect(result.isError).toBe(true);
373
+ // The final subscriber acknowledges only after every earlier recipient has finished.
374
+ await waitForCounter(observer, "ext:last-block-observer:count");
375
+ expect(
376
+ (await getKv(`task:agent:${blocker.agentId}`, "ext:blocking-recorder:count"))?.value,
377
+ ).toBe(1);
378
+ expect(events).toHaveLength(1);
379
+ expect(events[0]).toMatchObject({ origin: "extension:record-blocks" });
380
+ expect(await getKv(`task:agent:${creator.agentId}`, "ext:record-blocks:count")).toBeNull();
381
+ expect(await listExtensionRuns(creator.id)).toEqual([]);
382
+ const runs = await listExtensionRuns(blocker.id);
383
+ expect(runs).toContainEqual(
384
+ expect.objectContaining({
385
+ event: "post.task.blocked",
386
+ agentId: creator.agentId,
387
+ subject: "extension:record-blocks: blocking-recorder",
388
+ }),
389
+ );
390
+ });
391
+
392
+ test("continue and valid rewrites emit no blocked event", async () => {
393
+ const recorder = await installAndEnable("record-blocks");
394
+ expect((await postTask({ task: "continued task" })).status).toBe(201);
395
+ await installAndEnable("rewrite-task-priority");
396
+ const modified = await postTask({ task: "rewritten task" });
397
+ expect(modified.status).toBe(201);
398
+ expect(modified.body.priority).toBe(1);
399
+ expect(events).toEqual([]);
400
+ expect(await getKv(`task:agent:${recorder.agentId}`, "ext:record-blocks:count")).toBeNull();
401
+ });
402
+
403
+ test("a throwing bus observer does not replace the blocked response", async () => {
404
+ await installAndEnable("block-tasks-from-source", { source: "rest" });
405
+ let calls = 0;
406
+ const observer = () => {
407
+ calls++;
408
+ throw new Error("broken block observer");
409
+ };
410
+ const error = spyOn(console, "error").mockImplementation(() => {});
411
+ workflowEventBus.on("task.create_blocked", observer);
412
+ try {
413
+ expect((await postTask({ task: "still blocked" })).status).toBe(422);
414
+ expect(calls).toBe(1);
415
+ } finally {
416
+ workflowEventBus.off("task.create_blocked", observer);
417
+ error.mockRestore();
418
+ }
419
+ });
420
+ });
421
+
148
422
  test("REST task creation applies rewrites and reports blocks as 422", async () => {
149
423
  const rewrite = await installAndEnable("rewrite-task-priority");
150
424
  const created = await postTask({ task: "rewrite this task" });
@@ -278,6 +552,57 @@ describe("extension task boundaries", () => {
278
552
  );
279
553
  });
280
554
 
555
+ test("Slack event metadata retains mixed-sender batches and rejects metadata rewrites", async () => {
556
+ await createAgent({ name: "slack-metadata-lead", isLead: true, status: "idle" });
557
+ await installSource(
558
+ "a-rewrite-slack-metadata",
559
+ `import type { SwarmExtension } from "swarm-extension";
560
+ const extension: SwarmExtension = (api) => {
561
+ api.on("pre.task.create", () => ({ action: "modify", data: {
562
+ priority: 7, slack: { trigger: "mention", messages: [] }
563
+ } as never }));
564
+ };
565
+ export default extension;
566
+ `,
567
+ );
568
+ const recorder = await installAndEnable("record-task-create");
569
+ const channelId = "C_EVENT_METADATA";
570
+ const threadTs = "3000.0001";
571
+ bufferThreadMessage(channelId, threadTs, "first request", "U_FIRST", "3000.0002");
572
+ bufferThreadMessage(channelId, threadTs, "second request", "U_SECOND", "3000.0003");
573
+
574
+ await instantFlush(`${channelId}:${threadTs}`);
575
+
576
+ const event = (await getKv(`task:agent:${recorder.agentId}`, "ext:record-task-create:last"))
577
+ ?.value as TaskCreateEvent & { slack?: unknown };
578
+ expect(event.slack).toEqual({
579
+ trigger: "thread-buffer",
580
+ messages: [
581
+ { ts: "3000.0002", userId: "U_FIRST", text: "first request" },
582
+ { ts: "3000.0003", userId: "U_SECOND", text: "second request" },
583
+ ],
584
+ });
585
+ expect(event.origin).toBe("slack");
586
+ expect(event.options).toMatchObject({
587
+ slackTriggerMessageTs: "3000.0003",
588
+ slackUserId: "U_FIRST",
589
+ priority: 7,
590
+ });
591
+ expect(event.options).not.toHaveProperty("slack");
592
+ const task = await getMostRecentTaskInThread(channelId, threadTs);
593
+ expect(task).toMatchObject({
594
+ priority: 7,
595
+ slackTriggerMessageTs: "3000.0003",
596
+ slackUserId: "U_FIRST",
597
+ });
598
+ expect(task).not.toHaveProperty("slack");
599
+
600
+ await createTaskWithSiblingAwareness("ordinary API task", { source: "api" });
601
+ const apiEvent = (await getKv(`task:agent:${recorder.agentId}`, "ext:record-task-create:last"))
602
+ ?.value as TaskCreateEvent & { slack?: unknown };
603
+ expect(apiEvent).not.toHaveProperty("slack");
604
+ });
605
+
281
606
  test("scheduler and workflow executor provide their task creation origins", async () => {
282
607
  const extension = await installSource(
283
608
  "record-task-origin",
@@ -14,6 +14,7 @@ import {
14
14
  fetchResolvedEnv,
15
15
  RELOADABLE_ENV_KEYS,
16
16
  } from "../commands/runner";
17
+ import { contextBoundNoteThreshold } from "../hooks/context-bound-note";
17
18
  import { checkCodexCredentials } from "../providers/codex-adapter";
18
19
  import { MASKED_SECRET_VALUE } from "../utils/config-secrets";
19
20
  import { registerVolatileSecret } from "../utils/secret-scrubber";
@@ -795,6 +796,22 @@ describe("agent-scoped PI_CODEMODE_MODELS reaches the worker env", () => {
795
796
  });
796
797
  });
797
798
 
799
+ describe("agent-scoped CONTEXT_BOUND_NOTE_TOKENS turns the note on for one agent", () => {
800
+ // Each session's env is this agent's /api/config/resolved result (global,
801
+ // then agent, then repo rows), and every adapter reads the threshold from it.
802
+ test("the agent with the row gets the threshold; an agent without it stays off", async () => {
803
+ mockResponsesByAgentId.set("bound-note-pilot", {
804
+ status: 200,
805
+ body: { configs: [{ key: "CONTEXT_BOUND_NOTE_TOKENS", value: "150000" }] },
806
+ });
807
+ const baseEnv: Record<string, string | undefined> = {};
808
+ const pilot = await fetchResolvedEnv(testUrl, "key", "bound-note-pilot", baseEnv);
809
+ const other = await fetchResolvedEnv(testUrl, "key", "bound-note-other", baseEnv);
810
+ expect(contextBoundNoteThreshold(pilot.env)).toBe(150_000);
811
+ expect(contextBoundNoteThreshold(other.env)).toBe(0);
812
+ });
813
+ });
814
+
798
815
  describe("deleting a reloadable config row", () => {
799
816
  // One key per test: the runner remembers each overridden key's boot value
800
817
  // for the life of the process.
@@ -0,0 +1,10 @@
1
+ import type { SwarmExtension } from "swarm-extension";
2
+
3
+ const extension: SwarmExtension = (api) => {
4
+ api.on("post.task.blocked", async (event, ctx) => {
5
+ await ctx.state.set("last", event);
6
+ await ctx.state.incr("count");
7
+ });
8
+ };
9
+
10
+ export default extension;