@desplega.ai/agent-swarm 1.149.1 → 1.151.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 (538) hide show
  1. package/README.md +18 -3
  2. package/dist/{acp-adapter-vhkzfpjg.js → acp-adapter-ex8hzjfc.js} +5 -5
  3. package/dist/{actions-9d2694yk.js → actions-2bgpyhww.js} +39 -11
  4. package/dist/{app-fq2ggynt.js → app-1706zhr1.js} +7 -7
  5. package/dist/{artifact-gj9arwrr.js → artifact-td4q4dch.js} +1 -1
  6. package/dist/{assistant-edb6n9nn.js → assistant-bavke5p7.js} +44 -20
  7. package/dist/{azure-openai-responses-cqar64rc.js → azure-openai-responses-f38x2avq.js} +13 -13
  8. package/dist/{boot-reembed-msweabbp.js → boot-reembed-3cmrrmpa.js} +9 -9
  9. package/dist/{boot-reembed-q5wd3tha.js → boot-reembed-nafcmmmc.js} +14 -16
  10. package/dist/{boot-scrub-logs-3g1az933.js → boot-scrub-logs-cd6a2sq0.js} +5 -5
  11. package/dist/{claude-adapter-v3rnzsdc.js → claude-adapter-wfp2gzvz.js} +15 -16
  12. package/dist/{claude-managed-adapter-qvsmh6mv.js → claude-managed-adapter-dsmkcrjy.js} +3 -3
  13. package/dist/{claude-sdk-session-qxrpsna1.js → claude-sdk-session-bht9jjak.js} +15 -16
  14. package/dist/{cli-5m0qmvdp.js → cli-0qb1z6s2.js} +1 -1
  15. package/dist/{cli-9d8ntetj.js → cli-2658f6at.js} +1 -1
  16. package/dist/{cli-7evafqvy.js → cli-2tbvf1z1.js} +85 -69
  17. package/dist/{cli-k351hv5k.js → cli-3v9xhjcn.js} +24 -12
  18. package/dist/{cli-1ebpar1q.js → cli-4wppfkcm.js} +350 -20
  19. package/dist/{cli-4260r1tg.js → cli-5jd2z7av.js} +97 -84
  20. package/dist/{cli-cv7s5zdq.js → cli-5tpgq60f.js} +9865 -715
  21. package/dist/{cli-6skz3rk8.js → cli-7tgcstn2.js} +9 -4
  22. package/dist/{cli-01g3x1ca.js → cli-9004vfdf.js} +108 -56
  23. package/dist/{cli-1c94wvcw.js → cli-a1z3h107.js} +124 -99
  24. package/dist/{cli-9br8ypfv.js → cli-czjtgfys.js} +2 -2
  25. package/dist/{cli-5my3bjsd.js → cli-d3p82erb.js} +7 -7
  26. package/dist/{cli-e29gmdmw.js → cli-dv6cdjs2.js} +2 -1
  27. package/dist/{cli-wby1xnnb.js → cli-f1wky6zp.js} +1 -1
  28. package/dist/{cli-zvz62chv.js → cli-fm4ytsdk.js} +4 -4
  29. package/dist/{cli-tg8qj8ee.js → cli-frcta2w4.js} +6 -6
  30. package/dist/{cli-7xx10h0a.js → cli-g69fdc9g.js} +2 -2
  31. package/dist/{cli-cmp8g3sw.js → cli-gbfwcd3k.js} +44 -9
  32. package/dist/{handlers-nsg0bcye.js → cli-gjyvhmmh.js} +97 -45
  33. package/dist/{cli-08b07b4r.js → cli-ht5rs63n.js} +1 -1
  34. package/dist/{cli-jecc01cb.js → cli-j4nv2wqy.js} +2 -4
  35. package/dist/{cli-cd60fv6j.js → cli-j9xczhaq.js} +1 -1
  36. package/dist/cli-pf9q3696.js +172 -0
  37. package/dist/{cli-xm87hzbw.js → cli-pgj4a5fb.js} +1 -1
  38. package/dist/{cli-jezx9b9c.js → cli-ppm4975n.js} +9 -3
  39. package/dist/{cli-3z634pqe.js → cli-q88st9aj.js} +2 -2
  40. package/dist/{cli-3sysyjve.js → cli-r2q6pevn.js} +142 -49
  41. package/dist/{cli-07jd68a1.js → cli-re8z5p8p.js} +1 -1
  42. package/dist/{cli-qgd4sqz5.js → cli-s14agm5s.js} +121 -14
  43. package/dist/{cli-svbrpf5q.js → cli-sjaae6ff.js} +9872 -424
  44. package/dist/{cli-73gq0zxn.js → cli-vwm416m6.js} +13 -13
  45. package/dist/{cli-4wtq5jjv.js → cli-wbyk465z.js} +1 -1
  46. package/dist/{cli-3apbzgyk.js → cli-z61ykvam.js} +19 -14
  47. package/dist/{cli-e06a8a7y.js → cli-zkxymas5.js} +4 -3
  48. package/dist/cli.js +26 -25
  49. package/dist/{codex-adapter-jyw7a91z.js → codex-adapter-jjjk8w4m.js} +8 -9
  50. package/dist/{codex-hook-hfrt9shr.js → codex-hook-8jexc34x.js} +3 -3
  51. package/dist/{codex-session-runner-bmngj0s5.js → codex-session-runner-nd6ayxkx.js} +8 -9
  52. package/dist/{commands-n755g391.js → commands-9v4psyq1.js} +5 -5
  53. package/dist/{db-s1cgh5gv.js → db-wnt58tpe.js} +5 -5
  54. package/dist/dispatcher-05jh1aqk.js +74 -0
  55. package/dist/{e2b-p04vmtx0.js → e2b-413b8t4v.js} +1 -1
  56. package/dist/{google-generative-ai-tsskx7te.js → google-generative-ai-ndjmj5fa.js} +7 -7
  57. package/dist/{google-vertex-ppw8ydpa.js → google-vertex-fr665tqs.js} +10 -10
  58. package/dist/handlers-tw90pkh5.js +65 -0
  59. package/dist/{hook-jf0xe8zh.js → hook-123e2scb.js} +11 -12
  60. package/dist/{hook-h9cqrm8q.js → hook-sxe3r349.js} +11 -12
  61. package/dist/{http-nc6pydw5.js → http-ecsar8ez.js} +1442 -581
  62. package/dist/{index-ezxkds2n.js → index-16js521z.js} +335 -107
  63. package/dist/{index-769nc012.js → index-hw26vj40.js} +25 -19
  64. package/dist/{index-0wdy6gwc.js → index-nnrbpcjp.js} +25 -17
  65. package/dist/index-ymn7em0p.js +25 -0
  66. package/dist/{index-x1zcj09z.js → index-zj518gf9.js} +26 -20
  67. package/dist/{keepalive-nesm3xb6.js → keepalive-asqtcm4y.js} +7 -7
  68. package/dist/{lead-bwy6t2za.js → lead-xbfr7fvt.js} +40 -48
  69. package/dist/{maintenance-8w393zf0.js → maintenance-qma55qvw.js} +9 -9
  70. package/dist/{oauth-refresh-sweep-sh8xjy08.js → oauth-refresh-sweep-3s04c6qw.js} +6 -6
  71. package/dist/{onboard-25ps29h1.js → onboard-drskd386.js} +13 -4
  72. package/dist/{openai-codex-responses-g7w48y30.js → openai-codex-responses-wy6a43wd.js} +13 -13
  73. package/dist/{openai-responses-nmeyptke.js → openai-responses-6xwf7bzg.js} +19 -19
  74. package/dist/{opencode-adapter-dce4wskd.js → opencode-adapter-28axqa09.js} +4 -4
  75. package/dist/{otel-impl-b93gxnz5.js → otel-impl-943c96er.js} +1 -1
  76. package/dist/{pi-mono-adapter-zvshk5cj.js → pi-mono-adapter-jcqt71cg.js} +17 -19
  77. package/dist/{pricing-refresh-5v56x7gb.js → pricing-refresh-mx51az05.js} +7 -7
  78. package/dist/{rbac-roles-x55cnezn.js → rbac-roles-78baecqd.js} +13 -9
  79. package/dist/{rbac-roles-0p0s6pbj.js → rbac-roles-wcmrg6sv.js} +7 -7
  80. package/dist/{render-v2-hyd45w8b.js → render-v2-8zjntgf5.js} +7 -7
  81. package/dist/{seed-pricing-49gbgn96.js → seed-pricing-jqjs6cws.js} +6 -6
  82. package/dist/{setup-qvq64vzq.js → setup-4pks0vfk.js} +2 -2
  83. package/dist/work-objects-wangt792.js +165 -0
  84. package/dist/{worker-scj60cxg.js → worker-p2mr422x.js} +40 -48
  85. package/openapi.json +1365 -268
  86. package/package.json +4 -3
  87. package/src/agentmail/handlers.ts +95 -66
  88. package/src/be/budget-refusal-notify.ts +1 -0
  89. package/src/be/db/tasks/read.ts +3 -0
  90. package/src/be/db/tasks/write.ts +13 -1
  91. package/src/be/db-client.ts +5 -0
  92. package/src/be/db.ts +15 -5
  93. package/src/be/extensions/db.ts +461 -0
  94. package/src/be/extensions/validate.ts +107 -0
  95. package/src/be/memory/boot-reembed.ts +17 -12
  96. package/src/be/memory/index.ts +12 -0
  97. package/src/be/memory/providers/openai-embedding.ts +4 -0
  98. package/src/be/memory/types.ts +2 -0
  99. package/src/be/migrations/153_deferred_task_wait_members.sql +25 -0
  100. package/src/be/migrations/154_routing_source.sql +3 -0
  101. package/src/be/migrations/155_extensions.sql +66 -0
  102. package/src/be/migrations/156_inbox_item_state_read_at.sql +7 -0
  103. package/src/be/rbac-roles.ts +3 -2
  104. package/src/be/scripts/typecheck.ts +59 -41
  105. package/src/be/seed/schedules-seeder.ts +25 -2
  106. package/src/be/seed/workflows-seeder.ts +17 -0
  107. package/src/be/seed-scripts/catalog/delegate.ts +3 -1
  108. package/src/be/seed-skills/bundled-files.generated.json +1 -1
  109. package/src/be/seed-skills/index.ts +7 -0
  110. package/src/be/steering.ts +1 -0
  111. package/src/be/swarm-config-guard.ts +3 -1
  112. package/src/commands/credential-refresh.ts +70 -0
  113. package/src/commands/onboard/compose-generator.ts +14 -2
  114. package/src/commands/onboard/env-generator.ts +7 -0
  115. package/src/commands/provider-credentials.ts +2 -25
  116. package/src/commands/runner.ts +24 -57
  117. package/src/extensions/apply-task-create.ts +135 -0
  118. package/src/extensions/bundle-path.ts +10 -0
  119. package/src/extensions/contract-runtime.ts +7 -0
  120. package/src/extensions/contract-types.generated.ts +4 -0
  121. package/src/extensions/contract.ts +270 -0
  122. package/src/extensions/ctx.ts +186 -0
  123. package/src/extensions/dispatcher.ts +479 -0
  124. package/src/extensions/identity.ts +44 -0
  125. package/src/extensions/lifecycle.ts +281 -0
  126. package/src/extensions/loader.ts +207 -0
  127. package/src/extensions/post-bridge.ts +161 -0
  128. package/src/github/handlers.ts +185 -140
  129. package/src/gitlab/handlers.ts +97 -77
  130. package/src/heartbeat/heartbeat.ts +155 -83
  131. package/src/http/agents.ts +1 -1
  132. package/src/http/all-routes.ts +1 -0
  133. package/src/http/approval-requests.ts +1 -0
  134. package/src/http/apps.ts +14 -9
  135. package/src/http/config.ts +4 -1
  136. package/src/http/core.ts +38 -2
  137. package/src/http/extensions.ts +547 -0
  138. package/src/http/inbox-state.ts +2 -0
  139. package/src/http/index.ts +41 -3
  140. package/src/http/mcp-bridge.ts +96 -62
  141. package/src/http/mcp.ts +1 -1
  142. package/src/http/script-runs.ts +5 -0
  143. package/src/http/status.ts +6 -4
  144. package/src/http/tasks.ts +51 -30
  145. package/src/integrations/kapso/inbound.ts +15 -10
  146. package/src/jira/sync.ts +26 -16
  147. package/src/linear/sync.ts +26 -16
  148. package/src/prompts/session-templates.ts +18 -13
  149. package/src/providers/claude-adapter.ts +21 -8
  150. package/src/rbac/admission.ts +1 -1
  151. package/src/rbac/elevated-agents.ts +24 -0
  152. package/src/rbac/legacy-policy.ts +35 -6
  153. package/src/rbac/permissions.ts +8 -0
  154. package/src/scheduler/deferred-task-waits.ts +138 -27
  155. package/src/scheduler/schedule-task.ts +54 -24
  156. package/src/scheduler/scheduler.ts +108 -125
  157. package/src/scripts-runtime/executors/native.ts +5 -1
  158. package/src/scripts-runtime/executors/types.ts +5 -0
  159. package/src/scripts-runtime/import-allowlist.ts +79 -19
  160. package/src/scripts-runtime/sdk-allowlist.ts +4 -0
  161. package/src/scripts-runtime/swarm-config.ts +8 -0
  162. package/src/scripts-runtime/swarm-sdk.ts +3 -0
  163. package/src/scripts-runtime/types/stdlib.d.ts +16 -3
  164. package/src/scripts-runtime/types/swarm-extension.d.ts +562 -0
  165. package/src/scripts-runtime/types/swarm-sdk.d.ts +16 -3
  166. package/src/server.ts +6 -0
  167. package/src/slack/HEURISTICS.md +3 -1
  168. package/src/slack/actions.ts +21 -16
  169. package/src/slack/app.ts +2 -0
  170. package/src/slack/assistant.ts +4 -12
  171. package/src/slack/blocks.ts +1 -1
  172. package/src/slack/handlers.ts +107 -19
  173. package/src/slack/inbound-files.ts +9 -5
  174. package/src/slack/render-v2.ts +188 -38
  175. package/src/slack/router.ts +13 -3
  176. package/src/slack/steering.ts +3 -3
  177. package/src/slack/thread-buffer.ts +66 -23
  178. package/src/slack/work-objects.test.ts +252 -0
  179. package/src/slack/work-objects.ts +118 -0
  180. package/src/tasks/errors.ts +9 -0
  181. package/src/tasks/sibling-awareness.ts +35 -1
  182. package/src/tasks/worker-follow-up.ts +67 -11
  183. package/src/tests/acp-adapter.test.ts +10 -10
  184. package/src/tests/agent-fs-live-url.test.ts +40 -0
  185. package/src/tests/agent-fs-provision-seeder.test.ts +12 -12
  186. package/src/tests/api-key-tracking.test.ts +18 -18
  187. package/src/tests/api-key.test.ts +4 -2
  188. package/src/tests/apps-rbac.test.ts +1 -1
  189. package/src/tests/apps-spike5.test.ts +1 -1
  190. package/src/tests/apps-sync-engine.test.ts +8 -8
  191. package/src/tests/apps-user-config.test.ts +1 -1
  192. package/src/tests/artifact-sdk.test.ts +8 -8
  193. package/src/tests/asset-key-mcp.test.ts +2 -0
  194. package/src/tests/automation-preflight-alert.test.ts +1 -1
  195. package/src/tests/base-prompt.test.ts +7 -6
  196. package/src/tests/budget-user-scope.test.ts +1 -1
  197. package/src/tests/budgets-routes.test.ts +1 -1
  198. package/src/tests/claude-adapter-binary.test.ts +22 -40
  199. package/src/tests/claude-adapter-otel.test.ts +13 -7
  200. package/src/tests/claude-adapter.test.ts +8 -6
  201. package/src/tests/claude-hook-credentials.test.ts +3 -3
  202. package/src/tests/claude-managed-adapter.test.ts +4 -4
  203. package/src/tests/claude-managed-setup.test.ts +3 -3
  204. package/src/tests/claude-sdk-lifecycle.test.ts +1 -1
  205. package/src/tests/claude-sdk-transport.test.ts +6 -6
  206. package/src/tests/claude-stop-hook.test.ts +8 -8
  207. package/src/tests/claude-worker-parity.test.ts +3 -3
  208. package/src/tests/codex-adapter-pool-auth-revalidate.test.ts +5 -5
  209. package/src/tests/codex-adapter.test.ts +7 -5
  210. package/src/tests/codex-app-server.test.ts +2 -2
  211. package/src/tests/codex-login.test.ts +15 -15
  212. package/src/tests/codex-oauth-credential-info.test.ts +4 -4
  213. package/src/tests/codex-oauth-keep-warm.test.ts +4 -4
  214. package/src/tests/codex-oauth-refresh-lock.test.ts +1 -1
  215. package/src/tests/codex-oauth-storage.test.ts +7 -7
  216. package/src/tests/codex-pool.test.ts +1 -1
  217. package/src/tests/codex-steering-hook.test.ts +1 -1
  218. package/src/tests/codex-swarm-events.test.ts +1 -1
  219. package/src/tests/config-api-only-keys.test.ts +1 -1
  220. package/src/tests/config-session-auth.test.ts +2 -2
  221. package/src/tests/connection-embedded-auth.test.ts +20 -18
  222. package/src/tests/core-auth.test.ts +1 -1
  223. package/src/tests/create-page-tool.test.ts +3 -3
  224. package/src/tests/credential-refresh.test.ts +115 -0
  225. package/src/tests/credential-wait.test.ts +2 -2
  226. package/src/tests/credentials.test.ts +6 -4
  227. package/src/tests/db-queries-oauth.test.ts +40 -40
  228. package/src/tests/defer-task.test.ts +73 -8
  229. package/src/tests/deferred-task-wait-migration.test.ts +64 -0
  230. package/src/tests/deferred-task-wake.test.ts +349 -8
  231. package/src/tests/devin-adapter.test.ts +1 -1
  232. package/src/tests/devin-api.test.ts +1 -1
  233. package/src/tests/docs-index-footprint.test.js +3 -3
  234. package/src/tests/e2b-dispatch.test.ts +21 -19
  235. package/src/tests/e2e-claude-transport.test.ts +3 -3
  236. package/src/tests/e2e-redact.test.ts +6 -6
  237. package/src/tests/ensure-token.test.ts +68 -68
  238. package/src/tests/entrypoint-api-readiness.test.ts +1 -1
  239. package/src/tests/entrypoint-codex-oauth-seed.test.ts +13 -13
  240. package/src/tests/env-flag.test.ts +1 -0
  241. package/src/tests/events-http.test.ts +1 -1
  242. package/src/tests/extensions-db.test.ts +216 -0
  243. package/src/tests/extensions-dispatcher.test.ts +275 -0
  244. package/src/tests/extensions-example-heartbeat-remediate.test.ts +69 -0
  245. package/src/tests/extensions-example-mcp-install.test.ts +63 -0
  246. package/src/tests/extensions-example-no-exclamation-marks.test.ts +72 -0
  247. package/src/tests/extensions-example-rewrite-and-suppress.test.ts +70 -0
  248. package/src/tests/extensions-example-route-slack-channel.test.ts +217 -0
  249. package/src/tests/extensions-heartbeat.test.ts +291 -0
  250. package/src/tests/extensions-http.test.ts +431 -0
  251. package/src/tests/extensions-lead-equivalence.test.ts +49 -0
  252. package/src/tests/extensions-lifecycle.test.ts +209 -0
  253. package/src/tests/extensions-loader.test.ts +95 -0
  254. package/src/tests/extensions-mcp-tools.test.ts +245 -0
  255. package/src/tests/extensions-pre-task.test.ts +487 -0
  256. package/src/tests/extensions-run-context.test.ts +41 -0
  257. package/src/tests/extensions-tool-call.test.ts +288 -0
  258. package/src/tests/extensions-validate.test.ts +196 -0
  259. package/src/tests/fixtures/extensions/bad-import/hooks.ts +8 -0
  260. package/src/tests/fixtures/extensions/bad-import/manifest.json +9 -0
  261. package/src/tests/fixtures/extensions/bad-return-shape/hooks.ts +10 -0
  262. package/src/tests/fixtures/extensions/bad-return-shape/manifest.json +9 -0
  263. package/src/tests/fixtures/extensions/block-tasks-from-source/hooks.ts +22 -0
  264. package/src/tests/fixtures/extensions/block-tasks-from-source/manifest.json +9 -0
  265. package/src/tests/fixtures/extensions/ignore-channel/hooks.ts +22 -0
  266. package/src/tests/fixtures/extensions/ignore-channel/manifest.json +9 -0
  267. package/src/tests/fixtures/extensions/load.ts +10 -0
  268. package/src/tests/fixtures/extensions/minimal/hooks.ts +7 -0
  269. package/src/tests/fixtures/extensions/minimal/manifest.json +9 -0
  270. package/src/tests/fixtures/extensions/never-fail-long-tasks/hooks.ts +11 -0
  271. package/src/tests/fixtures/extensions/never-fail-long-tasks/manifest.json +9 -0
  272. package/src/tests/fixtures/extensions/no-exclamation-marks/hooks.ts +23 -0
  273. package/src/tests/fixtures/extensions/no-exclamation-marks/manifest.json +9 -0
  274. package/src/tests/fixtures/extensions/post-logger/hooks.ts +9 -0
  275. package/src/tests/fixtures/extensions/post-logger/manifest.json +9 -0
  276. package/src/tests/fixtures/extensions/priority-a/hooks.ts +7 -0
  277. package/src/tests/fixtures/extensions/priority-a/manifest.json +9 -0
  278. package/src/tests/fixtures/extensions/priority-b/hooks.ts +7 -0
  279. package/src/tests/fixtures/extensions/priority-b/manifest.json +9 -0
  280. package/src/tests/fixtures/extensions/record-only-on-tag/hooks.ts +16 -0
  281. package/src/tests/fixtures/extensions/record-only-on-tag/manifest.json +9 -0
  282. package/src/tests/fixtures/extensions/reserved-assets/hooks.ts +5 -0
  283. package/src/tests/fixtures/extensions/reserved-assets/manifest.json +10 -0
  284. package/src/tests/fixtures/extensions/rewrite-progress-prefix/hooks.ts +22 -0
  285. package/src/tests/fixtures/extensions/rewrite-progress-prefix/manifest.json +9 -0
  286. package/src/tests/fixtures/extensions/rewrite-task-priority/hooks.ts +7 -0
  287. package/src/tests/fixtures/extensions/rewrite-task-priority/manifest.json +9 -0
  288. package/src/tests/fixtures/extensions/route-channel-to-agent/hooks.ts +26 -0
  289. package/src/tests/fixtures/extensions/route-channel-to-agent/manifest.json +9 -0
  290. package/src/tests/fixtures/extensions/slow/hooks.ts +10 -0
  291. package/src/tests/fixtures/extensions/slow/manifest.json +9 -0
  292. package/src/tests/fixtures/extensions/suppress-lead-follow-up/hooks.ts +12 -0
  293. package/src/tests/fixtures/extensions/suppress-lead-follow-up/manifest.json +9 -0
  294. package/src/tests/fixtures/extensions/throws/hooks.ts +12 -0
  295. package/src/tests/fixtures/extensions/throws/manifest.json +9 -0
  296. package/src/tests/fixtures/extensions/worker-runtime/hooks.ts +5 -0
  297. package/src/tests/fixtures/extensions/worker-runtime/manifest.json +9 -0
  298. package/src/tests/fs-routes.test.ts +1 -1
  299. package/src/tests/gitlab-auth.test.ts +10 -10
  300. package/src/tests/harness-provider-resolution.test.ts +8 -5
  301. package/src/tests/heartbeat-reroute-decision.test.ts +1 -0
  302. package/src/tests/helper-credentials.test.ts +137 -0
  303. package/src/tests/http/context-routes.test.ts +2 -2
  304. package/src/tests/http-api-integration.test.ts +5 -2
  305. package/src/tests/http-users.test.ts +1 -1
  306. package/src/tests/identity-refresh.test.ts +2 -2
  307. package/src/tests/internal-ai/complete-structured.test.ts +2 -2
  308. package/src/tests/internal-ai/credentials.test.ts +19 -17
  309. package/src/tests/jira-metadata.test.ts +1 -1
  310. package/src/tests/jira-oauth.test.ts +5 -5
  311. package/src/tests/jira-sync.test.ts +1 -1
  312. package/src/tests/jira-webhook-lifecycle.test.ts +3 -3
  313. package/src/tests/jira-webhook.test.ts +2 -2
  314. package/src/tests/kapso-inbound.test.ts +2 -2
  315. package/src/tests/kv-http.test.ts +1 -1
  316. package/src/tests/kv-namespace-resolution.test.ts +1 -1
  317. package/src/tests/kv-page-proxy.test.ts +2 -2
  318. package/src/tests/launch-password-rejection.test.ts +2 -2
  319. package/src/tests/linear-webhook.test.ts +1 -1
  320. package/src/tests/mcp-oauth-dcr-reuse.test.ts +6 -6
  321. package/src/tests/mcp-oauth-ensure-token.test.ts +28 -22
  322. package/src/tests/mcp-oauth-manual-client.test.ts +16 -12
  323. package/src/tests/mcp-oauth-queries.test.ts +35 -32
  324. package/src/tests/mcp-oauth-resolve-secrets.test.ts +8 -8
  325. package/src/tests/mcp-oauth-wrapper.test.ts +20 -20
  326. package/src/tests/mcp-user-route.test.ts +1 -1
  327. package/src/tests/memory-edges.test.ts +1 -1
  328. package/src/tests/memory-edit-authorization.test.ts +32 -0
  329. package/src/tests/memory-rate-endpoint.test.ts +1 -1
  330. package/src/tests/memory-rate-tool.test.ts +2 -2
  331. package/src/tests/memory-rater-e2e.test.ts +1 -1
  332. package/src/tests/memory-rater-llm-summarizer.test.ts +3 -3
  333. package/src/tests/memory-rater-llm.test.ts +1 -1
  334. package/src/tests/model-control.test.ts +3 -0
  335. package/src/tests/multi-runtime-enabled-stats.test.ts +2 -2
  336. package/src/tests/multi-runtime-registration.test.ts +111 -17
  337. package/src/tests/oauth-access-token-tool.test.ts +18 -18
  338. package/src/tests/oauth-callback-flow.test.ts +8 -8
  339. package/src/tests/oauth-credential-bindings.test.ts +29 -27
  340. package/src/tests/oauth-keepalive.test.ts +10 -10
  341. package/src/tests/oauth-presets.test.ts +13 -5
  342. package/src/tests/oauth-refresh-failure.test.ts +9 -9
  343. package/src/tests/oauth-refresh-sweep.test.ts +20 -13
  344. package/src/tests/oauth-wrapper.test.ts +1 -1
  345. package/src/tests/onboard-compose.test.ts +47 -3
  346. package/src/tests/onboard-env.test.ts +26 -26
  347. package/src/tests/onboard-non-interactive.test.ts +4 -4
  348. package/src/tests/opencode-adapter.test.ts +1 -1
  349. package/src/tests/opencode-plugin.test.ts +2 -2
  350. package/src/tests/otel-session-cost-metrics.test.ts +1 -1
  351. package/src/tests/page-proxy-authed.test.ts +2 -2
  352. package/src/tests/page-proxy.test.ts +2 -2
  353. package/src/tests/page-session.test.ts +7 -7
  354. package/src/tests/page-viewer-auth.test.ts +2 -2
  355. package/src/tests/pages-authed-mode.test.ts +1 -1
  356. package/src/tests/pages-http.test.ts +1 -1
  357. package/src/tests/pages-password-mode.test.ts +1 -1
  358. package/src/tests/pages-public-authed-401.test.ts +1 -1
  359. package/src/tests/pages-public-html.test.ts +2 -2
  360. package/src/tests/pages-view-count.test.ts +1 -1
  361. package/src/tests/pi-mono-adapter.test.ts +10 -10
  362. package/src/tests/pi-mono-extension.test.ts +2 -2
  363. package/src/tests/preload.ts +5 -0
  364. package/src/tests/pricing-routes.test.ts +1 -1
  365. package/src/tests/profile-sync-warning.test.ts +8 -8
  366. package/src/tests/profile-sync.test.ts +8 -8
  367. package/src/tests/prompt-template-session.test.ts +36 -5
  368. package/src/tests/provider-process-group.test.ts +95 -4
  369. package/src/tests/providers/pi-cost.test.ts +1 -1
  370. package/src/tests/queue-stall-alarm.test.ts +1 -1
  371. package/src/tests/rbac-admission.test.ts +19 -4
  372. package/src/tests/rbac-charact-http.test.ts +1 -1
  373. package/src/tests/rbac-e2e-helpers.ts +1 -1
  374. package/src/tests/rbac-engine.test.ts +39 -0
  375. package/src/tests/realtime-room-auth.test.ts +1 -1
  376. package/src/tests/realtime-room-surfaces.test.ts +1 -1
  377. package/src/tests/reload-config.test.ts +45 -1
  378. package/src/tests/routing-affinity-inventory.test.ts +1 -1
  379. package/src/tests/routing-reason-contract.test.ts +76 -3
  380. package/src/tests/routing-reason-inventory.test.ts +11 -6
  381. package/src/tests/routing-source.test.ts +53 -0
  382. package/src/tests/runner-agent-fs-provision-retry.test.ts +1 -1
  383. package/src/tests/runner-context-preamble.test.ts +1 -1
  384. package/src/tests/runner-deployment-resume.test.ts +1 -1
  385. package/src/tests/runner-fallback-output.test.ts +1 -1
  386. package/src/tests/runner-identity-refresh.test.ts +1 -1
  387. package/src/tests/runner-skills-refresh.test.ts +1 -1
  388. package/src/tests/schedule-target-type.test.ts +3 -3
  389. package/src/tests/scheduled-tasks.test.ts +14 -5
  390. package/src/tests/scheduler-occurrence.test.ts +347 -0
  391. package/src/tests/script-apis-mcp.test.ts +1 -1
  392. package/src/tests/script-connections-http.test.ts +61 -58
  393. package/src/tests/script-connections-mcp.test.ts +1 -1
  394. package/src/tests/script-connections.test.ts +3 -1
  395. package/src/tests/script-runs-http.test.ts +15 -7
  396. package/src/tests/script-workflows-runtime-e2e.test.ts +1 -1
  397. package/src/tests/scripts-boot-reembed.test.ts +4 -0
  398. package/src/tests/scripts-embeddings.test.ts +4 -0
  399. package/src/tests/scripts-external-api.test.ts +1 -1
  400. package/src/tests/scripts-http.test.ts +1 -1
  401. package/src/tests/scripts-import-allowlist.test.ts +44 -1
  402. package/src/tests/scripts-mcp-e2e.test.ts +81 -1
  403. package/src/tests/scripts-only-gating.test.ts +4 -2
  404. package/src/tests/scripts-retention.test.ts +1 -1
  405. package/src/tests/scripts-runtime-identity.test.ts +2 -2
  406. package/src/tests/scripts-runtime-secret-egress.test.ts +1 -1
  407. package/src/tests/scripts-runtime.test.ts +6 -4
  408. package/src/tests/scripts-typecheck.test.ts +21 -0
  409. package/src/tests/sdk-allowlist.test.ts +1 -1
  410. package/src/tests/secret-scrubber.test.ts +38 -31
  411. package/src/tests/seed-registry.test.ts +46 -8
  412. package/src/tests/seed-schedules.test.ts +44 -9
  413. package/src/tests/seed-workflows.test.ts +21 -9
  414. package/src/tests/send-task-output-schema.test.ts +9 -1
  415. package/src/tests/send-task-requested-by.test.ts +1 -0
  416. package/src/tests/send-task-slack-routing-guard.test.ts +2 -0
  417. package/src/tests/session-costs-codex-recompute.test.ts +1 -1
  418. package/src/tests/session-costs-golden.test.ts +2 -2
  419. package/src/tests/session-costs-model-key-normalize.test.ts +2 -2
  420. package/src/tests/session-costs-recompute-all-providers.test.ts +1 -1
  421. package/src/tests/session-tokens.test.ts +1 -1
  422. package/src/tests/shutdown-error-scrubbing.test.ts +1 -1
  423. package/src/tests/slack-blocks.test.ts +12 -1
  424. package/src/tests/slack-file-tools-attachments.test.ts +1 -1
  425. package/src/tests/slack-inbound-files.test.ts +222 -86
  426. package/src/tests/slack-reaction-config.test.ts +5 -4
  427. package/src/tests/slack-read-boundaries.test.ts +1 -1
  428. package/src/tests/slack-render-v2.test.ts +196 -4
  429. package/src/tests/slack-router.test.ts +17 -2
  430. package/src/tests/slack-steering.test.ts +19 -0
  431. package/src/tests/status.test.ts +19 -7
  432. package/src/tests/steering-core.test.ts +1 -1
  433. package/src/tests/steering-enabled.test.ts +6 -8
  434. package/src/tests/steering-transport.test.ts +4 -4
  435. package/src/tests/store-progress-output-guidance.test.ts +34 -0
  436. package/src/tests/swarm-tool-result-gate.test.ts +11 -11
  437. package/src/tests/swarm-x-tool.test.ts +7 -5
  438. package/src/tests/sync-ai-toolbox-skills.test.ts +19 -0
  439. package/src/tests/task-supersede-resume.test.ts +1 -1
  440. package/src/tests/task-tool-preload-mcp.test.ts +4 -1
  441. package/src/tests/task-tools-ctx.test.ts +2 -0
  442. package/src/tests/tasks-requested-by-filter.test.ts +2 -2
  443. package/src/tests/telemetry-init.test.ts +11 -3
  444. package/src/tests/tool-annotations.test.ts +1 -1
  445. package/src/tests/tool-output-agent-id.test.ts +1 -0
  446. package/src/tests/tool-registrar-no-input.test.ts +42 -1
  447. package/src/tests/tracker-fold.test.ts +7 -7
  448. package/src/tests/tracker-tools.test.ts +3 -3
  449. package/src/tests/ui-followup-lead-delegation.test.ts +1 -0
  450. package/src/tests/unified-oauth-migration.test.ts +5 -5
  451. package/src/tests/user-token-rest-auth.test.ts +1 -1
  452. package/src/tests/user-token-routes.test.ts +1 -1
  453. package/src/tests/whoami-route.test.ts +1 -1
  454. package/src/tests/workflow-agent-task.test.ts +15 -13
  455. package/src/tests/workflow-ctx.test.ts +1 -1
  456. package/src/tests/workflow-e2e.test.ts +1 -1
  457. package/src/tests/workflow-engine-v2.test.ts +3 -1
  458. package/src/tests/workflow-executors.test.ts +2 -2
  459. package/src/tests/workflow-http-v2.test.ts +8 -6
  460. package/src/tests/workflow-input-redaction.test.ts +7 -5
  461. package/src/tests/workflow-integration-io.test.ts +2 -2
  462. package/src/tests/workflow-llm.test.ts +5 -5
  463. package/src/tests/workflow-registry.test.ts +1 -1
  464. package/src/tests/workflow-swarm-script.test.ts +1 -1
  465. package/src/tests/workflow-triggers-v2.test.ts +23 -21
  466. package/src/tests/x-composio.test.ts +9 -7
  467. package/src/tests/x402-client.test.ts +1 -1
  468. package/src/tests/x402-config.test.ts +28 -41
  469. package/src/tools/defer-task.ts +156 -24
  470. package/src/tools/extension-common.ts +84 -0
  471. package/src/tools/extension-install.ts +65 -0
  472. package/src/tools/extension-list.ts +66 -0
  473. package/src/tools/script-common.ts +43 -18
  474. package/src/tools/send-task.ts +87 -105
  475. package/src/tools/skills/skill-publish.ts +1 -0
  476. package/src/tools/store-progress.ts +1 -1
  477. package/src/tools/task-action.ts +43 -22
  478. package/src/tools/task-tool-ctx.ts +2 -0
  479. package/src/tools/templates.ts +2 -1
  480. package/src/tools/tool-config.ts +3 -1
  481. package/src/tools/utils.ts +140 -16
  482. package/src/types.ts +144 -0
  483. package/src/utils/constants.ts +7 -1
  484. package/src/utils/credentials.test.ts +16 -16
  485. package/src/utils/multi-runtime.ts +2 -2
  486. package/src/utils/steering-enabled.ts +4 -4
  487. package/src/workflows/executors/agent-task.ts +18 -3
  488. package/templates/ai-toolbox.manifest.json +7 -1
  489. package/templates/extensions/README.md +17 -0
  490. package/templates/extensions/notify-on-complete/README.md +11 -0
  491. package/templates/extensions/notify-on-complete/hooks.ts +61 -0
  492. package/templates/extensions/notify-on-complete/manifest.json +7 -0
  493. package/templates/extensions/require-ticket-ref/README.md +11 -0
  494. package/templates/extensions/require-ticket-ref/hooks.ts +30 -0
  495. package/templates/extensions/require-ticket-ref/manifest.json +7 -0
  496. package/templates/extensions/require-verification-note/README.md +11 -0
  497. package/templates/extensions/require-verification-note/hooks.ts +32 -0
  498. package/templates/extensions/require-verification-note/manifest.json +7 -0
  499. package/templates/schedules/daily-hn-briefing/config.json +1 -1
  500. package/templates/schedules/gtm-weekly-review/config.json +1 -1
  501. package/templates/schedules/weekly-code-health-reports/config.json +1 -1
  502. package/templates/schedules/weekly-dependabot-triage/config.json +1 -1
  503. package/templates/schedules/weekly-dora-metrics/config.json +1 -1
  504. package/templates/schedules/weekly-harness-upgrade-check/config.json +1 -1
  505. package/templates/skills/delegate-work/files/scripts/codex-exec.sh +1 -1
  506. package/templates/skills/scheduling/content.md +2 -0
  507. package/templates/skills/serply-search/SKILL.md +95 -0
  508. package/templates/skills/serply-search/config.json +13 -0
  509. package/templates/skills/serply-search/content.md +90 -0
  510. package/templates/skills/swarm-extensions/config.json +14 -0
  511. package/templates/skills/swarm-extensions/content.md +120 -0
  512. package/templates/skills/swarm-scripts/SKILL.md +2 -2
  513. package/templates/skills/swarm-scripts/content.md +2 -2
  514. package/templates/workflows/claude-code-changelog-watch/config.json +1 -1
  515. package/templates/workflows/gsc-topic-miner/config.json +1 -1
  516. package/templates/workflows/linear-drain-loop/config.json +1 -1
  517. package/templates/workflows/llm-safe-release-context/config.json +1 -1
  518. package/templates/workflows/ralph-loop/config.json +1 -1
  519. package/dist/cli-7fehefdf.js +0 -562
  520. package/dist/cli-ayvy5kde.js +0 -164
  521. package/dist/cli-ddfw1mc4.js +0 -4322
  522. package/dist/cli-hzqmgcm2.js +0 -3066
  523. package/dist/cli-j4d4f6hv.js +0 -338
  524. package/dist/cli-qgm2pfpq.js +0 -6377
  525. package/dist/cli-sdzq01t9.js +0 -823
  526. package/dist/cli-xvkmedgn.js +0 -332
  527. package/dist/cli-y3pq4v9g.js +0 -664
  528. package/dist/{anthropic-messages-vs2rabzp.js → anthropic-messages-2y8nktar.js} +19 -19
  529. package/dist/{claude-managed-setup-f5pmsyrp.js → claude-managed-setup-48dwg9bv.js} +3 -3
  530. package/dist/{cli-81r683bv.js → cli-ch38q5z4.js} +0 -0
  531. package/dist/{cli-6664w7y4.js → cli-kj76p1ez.js} +8 -8
  532. package/dist/{cli-c71mg51n.js → cli-r2fpe7cx.js} +5 -5
  533. package/dist/{cli-4xz3pmrq.js → cli-y6x0gqtg.js} +16 -16
  534. package/dist/{index-aa3sa6s8.js → index-2wsdsnmm.js} +1 -1
  535. package/dist/{mistral-conversations-4ryk7h83.js → mistral-conversations-qhr84ph3.js} +9 -9
  536. package/dist/{openai-completions-0bqmsajw.js → openai-completions-32rdwmze.js} +19 -19
  537. package/dist/{openrouter-images-qx18fyp8.js → openrouter-images-9f9ek4rw.js} +3 -3
  538. package/dist/{pi-messages-79jvzz1d.js → pi-messages-b1mbr6gp.js} +4 -4
@@ -40,7 +40,7 @@ function callSendTask(
40
40
  function structuredOf(result: CallToolResult) {
41
41
  return result.structuredContent as {
42
42
  success: boolean;
43
- task?: { id: string; outputSchema?: Record<string, unknown> };
43
+ task?: { id: string; outputSchema?: Record<string, unknown>; routingSource?: string };
44
44
  message: string;
45
45
  };
46
46
  }
@@ -168,6 +168,7 @@ describe("send-task: outputSchema propagation", () => {
168
168
  const created = await getTaskById(s.task!.id);
169
169
  expect(created?.outputSchema).toEqual(schema);
170
170
  expect(created?.routingReason).toBeUndefined();
171
+ expect(created?.routingSource).toBeUndefined();
171
172
  });
172
173
 
173
174
  test("a provided outputSchema is persisted when the task is offered to an agent", async () => {
@@ -184,6 +185,7 @@ describe("send-task: outputSchema propagation", () => {
184
185
  task: "offered task with a schema",
185
186
  agentId: worker.id,
186
187
  routingReason: "human_pinned",
188
+ routingNote: "This test explicitly selects the target worker",
187
189
  offerMode: true,
188
190
  outputSchema: schema,
189
191
  allowDuplicate: true,
@@ -214,6 +216,7 @@ describe("send-task: outputSchema propagation", () => {
214
216
  task: "directly assigned task with a schema",
215
217
  agentId: worker.id,
216
218
  routingReason: "human_pinned",
219
+ routingNote: "This test explicitly selects the target worker",
217
220
  outputSchema: schema,
218
221
  allowDuplicate: true,
219
222
  },
@@ -224,6 +227,8 @@ describe("send-task: outputSchema propagation", () => {
224
227
  const created = await getTaskById(s.task!.id);
225
228
  expect(created?.outputSchema).toEqual(schema);
226
229
  expect(created?.routingReason).toBe("human_pinned");
230
+ expect(created?.routingSource).toBe("declared");
231
+ expect(s.task?.routingSource).toBe("declared");
227
232
  });
228
233
 
229
234
  test("implicit parent routing stamps continuity while pool creation omits a reason", async () => {
@@ -249,6 +254,8 @@ describe("send-task: outputSchema propagation", () => {
249
254
  const continuedTask = await getTaskById(structuredOf(continued).task!.id);
250
255
  expect(continuedTask?.agentId).toBe(worker.id);
251
256
  expect(continuedTask?.routingReason).toBe("continuity");
257
+ expect(continuedTask?.routingSource).toBe("engine_default");
258
+ expect(structuredOf(continued).task?.routingSource).toBe("engine_default");
252
259
 
253
260
  const pooled = await callSendTask(
254
261
  server,
@@ -278,6 +285,7 @@ describe("send-task: outputSchema propagation", () => {
278
285
  );
279
286
  const created = await getTaskById(structuredOf(result).task!.id);
280
287
  expect(created?.routingNote).toBe("matches the required runtime specialization");
288
+ expect(created?.routingSource).toBe("declared");
281
289
  });
282
290
  });
283
291
 
@@ -152,6 +152,7 @@ describe("send-task: requestedByUserId inheritance", () => {
152
152
  task: "worker direct assign — inherit user",
153
153
  agentId: WORKER_ID,
154
154
  routingReason: "human_pinned",
155
+ routingNote: "This test explicitly selects the target worker",
155
156
  allowDuplicate: true,
156
157
  },
157
158
  LEAD_ID,
@@ -276,6 +276,7 @@ describe("send-task: Slack-routing coherence guard", () => {
276
276
  task: "direct assign with wrong channel",
277
277
  agentId: WORKER_ID,
278
278
  routingReason: "human_pinned",
279
+ routingNote: "This test explicitly selects the target worker",
279
280
  slackChannelId: "C_WRONG_DIRECT",
280
281
  slackThreadTs: "555.555",
281
282
  allowDuplicate: true,
@@ -301,6 +302,7 @@ describe("send-task: Slack-routing coherence guard", () => {
301
302
  task: "offer with wrong channel",
302
303
  agentId: WORKER_ID,
303
304
  routingReason: "human_pinned",
305
+ routingNote: "This test explicitly selects the target worker",
304
306
  offerMode: true,
305
307
  slackChannelId: "C_WRONG_OFFER",
306
308
  slackThreadTs: "666.666",
@@ -22,7 +22,7 @@ import { getPathSegments, parseQueryParams } from "../http/utils";
22
22
  import { listenOnFreePort } from "./test-net";
23
23
 
24
24
  const TEST_DB_PATH = "./test-session-costs-codex-recompute.sqlite";
25
- const API_KEY = "test-codex-recompute-secret";
25
+ const API_KEY = "example-test-codex-recompute-secret";
26
26
 
27
27
  async function removeDbFiles(path: string): Promise<void> {
28
28
  for (const suffix of ["", "-wal", "-shm"]) {
@@ -21,8 +21,8 @@ import { getPathSegments, parseQueryParams } from "../http/utils";
21
21
  import type { PricingProvider, PricingTokenClass } from "../types";
22
22
  import { listenOnFreePort } from "./test-net";
23
23
 
24
- const TEST_DB_PATH = "./test-session-costs-golden.sqlite";
25
- const API_KEY = "test-session-costs-golden";
24
+ const TEST_DB_PATH = "./example-test-session-costs-golden.sqlite";
25
+ const API_KEY = "example-test-session-costs-golden";
26
26
  const RATE_EFFECTIVE_AT = 1_800_000_000_000;
27
27
  const FIXTURE_CREATED_AT = RATE_EFFECTIVE_AT + 1;
28
28
  const EPSILON = 1e-9;
@@ -19,8 +19,8 @@ import { handleSessionData } from "../http/session-data";
19
19
  import { getPathSegments, parseQueryParams } from "../http/utils";
20
20
  import { listenOnFreePort } from "./test-net";
21
21
 
22
- const TEST_DB_PATH = "./test-model-key-normalize.sqlite";
23
- const API_KEY = "test-model-key-normalize";
22
+ const TEST_DB_PATH = "./example-test-model-key-normalize.sqlite";
23
+ const API_KEY = "example-test-model-key-normalize";
24
24
 
25
25
  async function removeDbFiles(path: string): Promise<void> {
26
26
  for (const suffix of ["", "-wal", "-shm"]) {
@@ -17,7 +17,7 @@ import { getPathSegments, parseQueryParams } from "../http/utils";
17
17
  import { listenOnFreePort } from "./test-net";
18
18
 
19
19
  const TEST_DB_PATH = "./test-recompute-all-providers.sqlite";
20
- const API_KEY = "test-recompute-all";
20
+ const API_KEY = "example-test-recompute-all";
21
21
 
22
22
  async function removeDbFiles(path: string): Promise<void> {
23
23
  for (const suffix of ["", "-wal", "-shm"]) {
@@ -23,7 +23,7 @@ import { handleSessions } from "../http/sessions";
23
23
  import { getPathSegments, parseQueryParams } from "../http/utils";
24
24
  import { listenOnFreePort } from "./test-net";
25
25
 
26
- const API_KEY = "test-session-token-key";
26
+ const API_KEY = "example-test-session-token-key";
27
27
 
28
28
  function createTestServer(apiKey: string): Server {
29
29
  return createHttpServer(async (req: IncomingMessage, res: ServerResponse) => {
@@ -18,7 +18,7 @@ import {
18
18
  * rather than executed.
19
19
  */
20
20
 
21
- const SECRET = "xoxb-9911-shutdown-token-value";
21
+ const SECRET = "example-shutdown-token-value";
22
22
 
23
23
  const shutdownSourcePath = `${import.meta.dir}/../http/index.ts`;
24
24
  const shutdownSource = await Bun.file(shutdownSourcePath).text();
@@ -298,8 +298,10 @@ describe("buildAssignmentSummaryBlocks", () => {
298
298
  failed: [{ agentName: "Delta", reason: "error" }],
299
299
  });
300
300
 
301
+ expect(blocks).toHaveLength(1);
302
+ expect(blocks[0].text.text).not.toBe("");
301
303
  expect(blocks[0].text.text).toContain("⚠️");
302
- expect(blocks[0].text.text).toContain("Could not assign");
304
+ expect(blocks[0].text.text).toContain("Could not assign to:");
303
305
  });
304
306
  });
305
307
 
@@ -463,6 +465,15 @@ function makeTaskId(prefix: string): string {
463
465
  }
464
466
 
465
467
  describe("buildTreeBlocks", () => {
468
+ test("empty tree omits the section with empty text", () => {
469
+ const blocks = buildTreeBlocks([]);
470
+
471
+ expect(blocks).toHaveLength(0);
472
+ expect(blocks.every((block) => block.type !== "section" || block.text.text.length > 0)).toBe(
473
+ true,
474
+ );
475
+ });
476
+
466
477
  test("single root, no children", () => {
467
478
  const root: TreeNode = {
468
479
  taskId: makeTaskId("aaaa0001"),
@@ -21,7 +21,7 @@ import type { SlackFile } from "../slack/files";
21
21
  import { createSlackTaskWithFiles, fetchSlackFiles } from "../slack/inbound-files";
22
22
 
23
23
  const TEST_DB_PATH = "./test-slack-file-tools-attachments.sqlite";
24
- const BOT_TOKEN = "xoxb-file-tools-test";
24
+ const BOT_TOKEN = "example-slack-file-tools-token";
25
25
  const AGENT_ID = "aaaaaaaa-0000-4000-8000-00000000f001";
26
26
  const OTHER_ID = "aaaaaaaa-0000-4000-8000-00000000f002";
27
27
  const PNG = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 9, 9]);
@@ -6,7 +6,17 @@
6
6
  * task unclaimable until its attachments exist, and both Slack ingress
7
7
  * handlers end to end against a real DB and the local file provider.
8
8
  */
9
- import { afterAll, beforeAll, beforeEach, describe, expect, mock, spyOn, test } from "bun:test";
9
+ import {
10
+ afterAll,
11
+ afterEach,
12
+ beforeAll,
13
+ beforeEach,
14
+ describe,
15
+ expect,
16
+ mock,
17
+ spyOn,
18
+ test,
19
+ } from "bun:test";
10
20
  import { mkdtemp, rm, unlink } from "node:fs/promises";
11
21
  import { tmpdir } from "node:os";
12
22
  import { join } from "node:path";
@@ -16,13 +26,17 @@ import {
16
26
  createTaskExtended,
17
27
  getDbClient,
18
28
  getLogsByTaskId,
29
+ getSlackTasksInThread,
30
+ getSteeringMessagesForTask,
19
31
  getTaskAttachments,
20
32
  getTaskById,
21
33
  initDb,
22
34
  promoteAbandonedDraftTasks,
35
+ startTask,
23
36
  } from "../be/db";
24
37
  import { MAX_TASK_ATTACHMENT_BYTES } from "../be/task-attachment-store";
25
38
  import { getFileStorageProvider, resetFileStorageProviderForTests } from "../fs/registry";
39
+ import * as slackAppModule from "../slack/app";
26
40
  import { createAssistant } from "../slack/assistant";
27
41
  import * as slackEnrichModule from "../slack/enrich";
28
42
  import type { SlackFile } from "../slack/files";
@@ -34,10 +48,10 @@ import {
34
48
  notifySlackFileFailures,
35
49
  setDraftLeaseRefreshMsForTests,
36
50
  } from "../slack/inbound-files";
37
- import { instantFlush } from "../slack/thread-buffer";
51
+ import { getBufferMessageCount, instantFlush } from "../slack/thread-buffer";
38
52
 
39
53
  const TEST_DB_PATH = "./test-slack-inbound-files.sqlite";
40
- const BOT_TOKEN = "xoxb-inbound-files-test";
54
+ const BOT_TOKEN = "example-slack-inbound-files-token";
41
55
  const PNG_BYTES = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 1, 2, 3]);
42
56
 
43
57
  let slackFiles: ReturnType<typeof Bun.serve>;
@@ -51,6 +65,7 @@ const previousEnv = {
51
65
  SLACK_RENDER_V2: process.env.SLACK_RENDER_V2,
52
66
  STEERING_ENABLED: process.env.STEERING_ENABLED,
53
67
  SLACK_THREAD_STEERING: process.env.SLACK_THREAD_STEERING,
68
+ SLACK_THREAD_FOLLOWUP_REQUIRE_MENTION: process.env.SLACK_THREAD_FOLLOWUP_REQUIRE_MENTION,
54
69
  };
55
70
 
56
71
  const HTML_FILE = new TextEncoder().encode("<html><body>quarterly report</body></html>");
@@ -708,113 +723,234 @@ describe("Slack ingress with files", () => {
708
723
  expect(await getTaskAttachments(followUp!.id)).toHaveLength(1);
709
724
  });
710
725
 
711
- test("a follow-up queued by ADDITIVE_SLACK says its file was not attached", async () => {
712
- await createAgent({ name: "lead", isLead: true, status: "busy" });
713
- const { task: first } = await sendAssistantDm({ subtype: undefined, text: "start" });
714
- await getDbClient().run("UPDATE agent_tasks SET status = 'in_progress' WHERE id = ?", [
715
- first!.id,
716
- ]);
717
- process.env.ADDITIVE_SLACK = "true";
718
- // instantFlush steers into the running session when steering is on; this
719
- // asserts the task-creating path, so keep steering off whatever the env says.
720
- process.env.STEERING_ENABLED = "false";
721
- delete process.env.SLACK_THREAD_STEERING;
722
- const client = slackClient();
726
+ describe("buffered follow-ups with files", () => {
727
+ let earlier: Awaited<ReturnType<typeof createTaskExtended>>;
728
+ let client: ReturnType<typeof slackClient>;
729
+ const getApp = spyOn(slackAppModule, "getSlackApp");
730
+ const channelId = "C0BUFFERFILES";
731
+ const threadTs = "1950000000.000001";
732
+ const key = `${channelId}:${threadTs}`;
733
+
734
+ beforeEach(async () => {
735
+ const lead = await createAgent({
736
+ name: "lead",
737
+ isLead: true,
738
+ status: "busy",
739
+ harnessProvider: "pi",
740
+ });
741
+ earlier = await createTaskExtended("earlier work", {
742
+ agentId: lead.id,
743
+ source: "slack",
744
+ slackChannelId: channelId,
745
+ slackThreadTs: threadTs,
746
+ });
747
+ await startTask(earlier.id);
748
+ process.env.ADDITIVE_SLACK = "true";
749
+ process.env.STEERING_ENABLED = "true";
750
+ process.env.SLACK_THREAD_STEERING = "lead";
751
+ process.env.SLACK_THREAD_FOLLOWUP_REQUIRE_MENTION = "false";
752
+ client = slackClient();
753
+ getApp.mockReturnValue({ client } as never);
754
+ });
723
755
 
724
- try {
756
+ afterEach(async () => {
757
+ await instantFlush(key);
758
+ process.env.ADDITIVE_SLACK = "false";
759
+ restoreSteeringEnv();
760
+ const requireMention = previousEnv.SLACK_THREAD_FOLLOWUP_REQUIRE_MENTION;
761
+ if (requireMention === undefined) delete process.env.SLACK_THREAD_FOLLOWUP_REQUIRE_MENTION;
762
+ else process.env.SLACK_THREAD_FOLLOWUP_REQUIRE_MENTION = requireMention;
763
+ getApp.mockReturnValue(null);
764
+ });
765
+
766
+ afterAll(() => getApp.mockRestore());
767
+
768
+ async function sendBuffered(text: string, files: SlackFile[] = []) {
725
769
  seq += 1;
726
- await assistantMessage({
727
- message: {
728
- channel: first!.slackChannelId,
729
- thread_ts: first!.slackThreadTs,
730
- ts: `1800000000.${String(seq).padStart(6, "0")}`,
770
+ const ts = `1950000001.${String(seq).padStart(6, "0")}`;
771
+ await channelMessage({
772
+ event: {
773
+ type: "message",
774
+ subtype: files.length ? "file_share" : undefined,
775
+ channel: channelId,
776
+ thread_ts: threadTs,
777
+ ts,
731
778
  user: "U_HUMAN",
732
- subtype: "file_share",
733
- text: "",
734
- files: [slackFile()],
779
+ text,
780
+ files,
735
781
  },
736
782
  body: { event_id: `evt_inbound_files_${seq}` },
737
783
  client,
738
784
  say: mock(async () => ({})),
739
- setStatus: mock(async () => {}),
740
- setTitle: mock(async () => {}),
741
- getThreadContext: mock(async () => ({})),
742
785
  });
743
- } finally {
744
- process.env.ADDITIVE_SLACK = "false";
786
+ return ts;
745
787
  }
746
788
 
747
- try {
748
- await assertQueuedFollowUp();
749
- } finally {
750
- restoreSteeringEnv();
751
- }
752
-
753
- async function assertQueuedFollowUp() {
754
- expect(fileRequests).toEqual([]);
755
- expect(client.chat.postMessage).toHaveBeenCalledTimes(1);
756
- const [args] = client.chat.postMessage.mock.calls[0] as unknown as [{ text: string }];
757
- expect(args.text).toContain("`screenshot.png` (follow-ups queued by ADDITIVE_SLACK");
758
-
759
- // Drain the debounce buffer now, while the DB is still open.
760
- await instantFlush(`${first!.slackChannelId}:${first!.slackThreadTs}`);
761
- const queued = await getDbClient().get<{ task: string }>(
762
- "SELECT task FROM agent_tasks WHERE slackThreadTs = ? AND id != ? ORDER BY createdAt DESC",
763
- [first!.slackThreadTs, first!.id],
789
+ async function followUp() {
790
+ const tasks = (await getSlackTasksInThread(channelId, threadTs)).filter(
791
+ (task) => task.id !== earlier.id,
764
792
  );
765
- expect(queued?.task).toContain("(not attached: follow-ups queued by ADDITIVE_SLACK");
793
+ expect(tasks).toHaveLength(1);
794
+ return tasks[0]!;
766
795
  }
767
- });
768
796
 
769
- test("a !now follow-up with a file says the file was not attached", async () => {
770
- const lead = await createAgent({ name: "lead", isLead: true, status: "busy" });
771
- const threadTs = "1950000000.000001";
772
- const earlier = await createTaskExtended("earlier work", {
773
- agentId: lead.id,
774
- source: "slack",
775
- slackChannelId: "C0NOWTEST1",
776
- slackThreadTs: threadTs,
777
- });
778
- await getDbClient().run("UPDATE agent_tasks SET status = 'in_progress' WHERE id = ?", [
779
- earlier.id,
780
- ]);
781
- process.env.ADDITIVE_SLACK = "true";
782
- process.env.STEERING_ENABLED = "false";
783
- delete process.env.SLACK_THREAD_STEERING;
784
- const client = slackClient();
785
- seq += 1;
797
+ function failureNotices() {
798
+ return (client.chat.postMessage.mock.calls as unknown as [{ text: string }][])
799
+ .map(([args]) => args.text)
800
+ .filter((text) => text.includes("Couldn't attach"));
801
+ }
786
802
 
787
- try {
788
- await channelMessage({
789
- event: {
790
- type: "message",
791
- subtype: "file_share",
792
- channel: "C0NOWTEST1",
803
+ test("assistant follow-ups retain files until flush and resolve id-only metadata", async () => {
804
+ client.files.info.mockResolvedValue({ ok: true, file: slackFile() } as never);
805
+ seq += 1;
806
+ await assistantMessage({
807
+ message: {
808
+ channel: channelId,
793
809
  thread_ts: threadTs,
794
810
  ts: `1950000001.${String(seq).padStart(6, "0")}`,
795
811
  user: "U_HUMAN",
796
- text: "!now here it is",
797
- files: [slackFile()],
812
+ subtype: "file_share",
813
+ text: "",
814
+ files: [{ id: "F0SHOT0001" }],
798
815
  },
799
816
  body: { event_id: `evt_inbound_files_${seq}` },
800
817
  client,
801
818
  say: mock(async () => ({})),
819
+ setStatus: mock(async () => {}),
820
+ setTitle: mock(async () => {}),
821
+ getThreadContext: mock(async () => ({})),
822
+ });
823
+ expect(fileRequests).toEqual([]);
824
+ expect(client.files.info).not.toHaveBeenCalled();
825
+ expect(failureNotices()).toEqual([]);
826
+
827
+ await instantFlush(key);
828
+ const task = await followUp();
829
+ expect(await readStoredBytes(task.id)).toEqual([PNG_BYTES]);
830
+ expect(task.task).toContain("[File: screenshot.png");
831
+ expect(task.task).not.toContain("not attached");
832
+ expect(await wasDraft(task.id)).toBe(true);
833
+ expect(await getSteeringMessagesForTask(earlier.id)).toEqual([]);
834
+ expect(failureNotices()).toEqual([]);
835
+ });
836
+
837
+ for (const steering of ["off", "lead"]) {
838
+ test(`timed flush attaches every message's files with steering ${steering}`, async () => {
839
+ process.env.SLACK_THREAD_STEERING = steering;
840
+ const nativeSetTimeout = globalThis.setTimeout;
841
+ const bufferMs = Number(process.env.ADDITIVE_SLACK_BUFFER_MS) || 10_000;
842
+ const timer = spyOn(globalThis, "setTimeout").mockImplementation((fn, ms, ...args) =>
843
+ nativeSetTimeout(fn, ms === bufferMs ? 250 : ms, ...args),
844
+ );
845
+ const otherBytes = new Uint8Array([...PNG_BYTES, 4]);
846
+ bytesById.set("F0OTHER0001", otherBytes);
847
+ let lastTs: string;
848
+ try {
849
+ await sendBuffered("", [slackFile()]);
850
+ lastTs = await sendBuffered("compare these", [
851
+ slackFile({ id: "F0OTHER0001", name: "other.png", size: otherBytes.length }),
852
+ ]);
853
+ expect(getBufferMessageCount(key)).toBe(2);
854
+ expect(fileRequests).toEqual([]);
855
+ expect(failureNotices()).toEqual([]);
856
+ } finally {
857
+ timer.mockRestore();
858
+ }
859
+ // Wait for the real timer callback and draft promotion, not instantFlush.
860
+ for (let i = 0; i < 200; i++) {
861
+ const tasks = await getSlackTasksInThread(channelId, threadTs);
862
+ if (tasks.some((task) => task.id !== earlier.id && task.status === "pending")) break;
863
+ await Bun.sleep(10);
864
+ }
865
+ const task = await followUp();
866
+ expect(task.status).toBe("pending");
867
+ expect(task.dependsOn).toEqual([earlier.id]);
868
+ expect(task.slackTriggerMessageTs).toBe(lastTs!);
869
+ expect(task.task).toContain("2 message(s) buffered");
870
+ expect(task.task).toContain("compare these");
871
+ expect(task.task).not.toContain("not attached");
872
+ expect(await readStoredBytes(task.id)).toEqual([PNG_BYTES, otherBytes]);
873
+ expect(await wasDraft(task.id)).toBe(true);
874
+ expect(await getSteeringMessagesForTask(earlier.id)).toEqual([]);
875
+ expect(failureNotices()).toEqual([]);
876
+ });
877
+
878
+ test(`!now with files flushes existing files without dependency with steering ${steering}`, async () => {
879
+ process.env.SLACK_THREAD_STEERING = steering;
880
+ await sendBuffered("earlier screenshot", [slackFile()]);
881
+ expect(fileRequests).toEqual([]);
882
+ const otherBytes = new Uint8Array([...PNG_BYTES, 4]);
883
+ bytesById.set("F0OTHER0001", otherBytes);
884
+ await sendBuffered("!now", [
885
+ slackFile({ id: "F0OTHER0001", name: "other.png", size: otherBytes.length }),
886
+ ]);
887
+ const task = await followUp();
888
+ expect(task.task).toContain("earlier screenshot");
889
+ expect(task.task).not.toContain("!now");
890
+ expect(task.dependsOn).toEqual([]);
891
+ expect(await getTaskAttachments(task.id)).toHaveLength(2);
892
+ expect(await readStoredBytes(task.id)).toEqual([PNG_BYTES, otherBytes]);
893
+ expect(await getSteeringMessagesForTask(earlier.id)).toEqual([]);
894
+ expect(failureNotices()).toEqual([]);
802
895
  });
803
- } finally {
804
- process.env.ADDITIVE_SLACK = "false";
805
- restoreSteeringEnv();
806
896
  }
807
897
 
808
- expect(fileRequests).toEqual([]);
809
- expect(client.chat.postMessage).toHaveBeenCalledTimes(1);
810
- const [args] = client.chat.postMessage.mock.calls[0] as unknown as [{ text: string }];
811
- expect(args.text).toContain("`screenshot.png`");
812
- const flushed = await getDbClient().get<{ task: string }>(
813
- "SELECT task FROM agent_tasks WHERE slackThreadTs = ? AND task != 'earlier work'",
814
- [threadTs],
815
- );
816
- expect(flushed?.task).toContain("here it is");
817
- expect(flushed?.task).toContain("(not attached: follow-ups queued by ADDITIVE_SLACK");
898
+ test("text-only follow-ups still steer the running task", async () => {
899
+ await sendBuffered("!now use the safer approach");
900
+ const steering = await getSteeringMessagesForTask(earlier.id);
901
+ expect(steering).toHaveLength(1);
902
+ expect(steering[0]!.body).toContain("use the safer approach");
903
+ expect(await getSlackTasksInThread(channelId, threadTs)).toHaveLength(1);
904
+ expect(fileRequests).toEqual([]);
905
+ });
906
+
907
+ test("download failures and oversize files are reported at flush while good files attach", async () => {
908
+ await sendBuffered("inspect these", [
909
+ slackFile(),
910
+ slackFile({
911
+ id: "F0DENIED001",
912
+ name: "denied.png",
913
+ url_private_download: `${slackFiles.url}forbidden`,
914
+ }),
915
+ slackFile({ id: "F0HUGE0001", name: "huge.png", size: HUGE_BYTES }),
916
+ ]);
917
+ expect(failureNotices()).toEqual([]);
918
+ await instantFlush(key);
919
+ const task = await followUp();
920
+ expect(await readStoredBytes(task.id)).toEqual([PNG_BYTES]);
921
+ expect(task.task).toContain("not attached: download failed (HTTP 403)");
922
+ expect(task.task).not.toContain("follow-ups queued by ADDITIVE_SLACK");
923
+ expect(failureNotices()).toHaveLength(1);
924
+ expect(failureNotices()[0]).toContain("`denied.png` (download failed (HTTP 403))");
925
+ expect(failureNotices()[0]).toContain("`huge.png`");
926
+ expect(failureNotices()[0]).not.toContain("`screenshot.png`");
927
+ });
928
+
929
+ test("storage failures are reported after flush", async () => {
930
+ const upload = spyOn(getFileStorageProvider(), "upload").mockRejectedValue(
931
+ new Error("storage unavailable"),
932
+ );
933
+ try {
934
+ await sendBuffered("!now inspect this", [slackFile()]);
935
+ const task = await followUp();
936
+ expect(task.status).toBe("pending");
937
+ expect(await getTaskAttachments(task.id)).toEqual([]);
938
+ expect(failureNotices()).toHaveLength(1);
939
+ expect(failureNotices()[0]).toContain("storage unavailable");
940
+ } finally {
941
+ upload.mockRestore();
942
+ }
943
+ });
944
+
945
+ test("an unavailable Slack app retains the buffered file failure fallback", async () => {
946
+ getApp.mockReturnValue(null);
947
+ await sendBuffered("!now inspect this", [slackFile()]);
948
+ const task = await followUp();
949
+ expect(await getTaskAttachments(task.id)).toEqual([]);
950
+ expect(task.task).toContain("[File: screenshot.png");
951
+ expect(task.task).toContain("not attached: follow-ups queued by ADDITIVE_SLACK");
952
+ expect(await getSteeringMessagesForTask(earlier.id)).toEqual([]);
953
+ });
818
954
  });
819
955
 
820
956
  test("a channel message that mentions the bot with a file attaches it", async () => {
@@ -20,6 +20,7 @@ const ALL_EVENTS: SlackReactionEvent[] = [
20
20
 
21
21
  /** The four acceptance-stage names `main` removed as a fixed list at finalize. */
22
22
  const DEFAULT_ACCEPTANCE_NAMES = ["eyes", "heavy_plus_sign", "zap", "speech_balloon"];
23
+ const SECRET_SHAPED_GITHUB_PAT = `github_pat_${"a".repeat(32)}`;
23
24
 
24
25
  function clearReactionEnv() {
25
26
  for (const key of Object.values(SLACK_REACTION_CONFIG_KEYS)) delete process.env[key];
@@ -320,7 +321,7 @@ describe("reaction-shortcode.ts", () => {
320
321
  test("a generic (non-invalid_name) add failure redacts a secret-shaped error message in the emitted log", async () => {
321
322
  const logSpy = spyOn(console, "log");
322
323
  logSpy.mockClear();
323
- const leaked = "github_pat_11B4WKYAA0Qe95fajGmt3o_ABCDEF1234567890abcdef";
324
+ const leaked = SECRET_SHAPED_GITHUB_PAT;
324
325
  const add = async () => {
325
326
  throw new Error(`rate_limited: ${leaked}`);
326
327
  };
@@ -339,7 +340,7 @@ describe("reaction-shortcode.ts", () => {
339
340
  test("a generic (non-invalid_name/no_reaction) remove failure redacts a secret-shaped error message in the emitted log", async () => {
340
341
  const logSpy = spyOn(console, "log");
341
342
  logSpy.mockClear();
342
- const leaked = "github_pat_11B4WKYAA0Qe95fajGmt3o_ABCDEF1234567890abcdef";
343
+ const leaked = SECRET_SHAPED_GITHUB_PAT;
343
344
  const remove = async () => {
344
345
  const error = new Error(`rate_limited: ${leaked}`) as Error & { data: unknown };
345
346
  error.data = { error: "rate_limited" };
@@ -361,7 +362,7 @@ describe("reaction-shortcode.ts", () => {
361
362
  test("an invalid_name add failure redacts a secret-shaped reaction name in the emitted log", async () => {
362
363
  const errorSpy = spyOn(console, "error");
363
364
  errorSpy.mockClear();
364
- const leaked = "github_pat_11B4WKYAA0Qe95fajGmt3o_ABCDEF1234567890abcdef";
365
+ const leaked = SECRET_SHAPED_GITHUB_PAT;
365
366
  const add = async () => {
366
367
  throw { data: { error: "invalid_name" } };
367
368
  };
@@ -380,7 +381,7 @@ describe("reaction-shortcode.ts", () => {
380
381
  test("a fallback add failure after invalid_name redacts a secret-shaped error message in the emitted log", async () => {
381
382
  const logSpy = spyOn(console, "log");
382
383
  logSpy.mockClear();
383
- const leaked = "github_pat_11B4WKYAA0Qe95fajGmt3o_ABCDEF1234567890abcdef";
384
+ const leaked = SECRET_SHAPED_GITHUB_PAT;
384
385
  const add = async ({ name }: { name: string }) => {
385
386
  if (name === "not_a_real_emoji") throw { data: { error: "invalid_name" } };
386
387
  throw new Error(`rate_limited: ${leaked}`);
@@ -9,7 +9,7 @@ import { mcpOverflowNamespace } from "../tools/utils";
9
9
  import { SKIP_SANDBOX_SPAWN_TESTS } from "./sandbox-spawn-test-helpers";
10
10
 
11
11
  const TEST_DB_PATH = "./test-slack-read-boundaries.sqlite";
12
- const API_KEY = "test-slack-read-boundaries-key-1234567890";
12
+ const API_KEY = "example-test-slack-read-boundaries-key-1234567890";
13
13
  const AGENT_ID = "aaaaaaaa-0000-4000-8000-000000000001";
14
14
  const CHANNEL_ID = "C_BUSY_BOUNDARY";
15
15
  const FULL_MESSAGE_COUNT = 20;